packages feed

oughta-0.1.0.0: src/Oughta/Result.hs

{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE OverloadedStrings #-}

-- | The result of running a Oughta Lua program
module Oughta.Result
  ( Match(..)
  , Progress(..)
  , newProgress
  , updateProgress
  , progressToSuccess
  , Failure(..)
  , Success(..)
  , Result(..)
  , resultNull
  , printResult
  ) where

import Control.Exception qualified as X
import Data.ByteString (ByteString)
import Data.Foldable (toList)
import Data.Sequence (Seq)
import Data.Sequence qualified as Seq
import Data.Text (Text)
import Data.Text qualified as Text
import Data.Text.Encoding qualified as Text
import Oughta.Pos (Loc, Span)
import Oughta.Pos qualified as OP
import Oughta.Traceback (Traceback)
import Oughta.Traceback qualified as OT

-- | A successful match of an API call against some text
data Match
  = Match
    { -- | The 'Span' of the match
      matchSpan :: {-# UNPACK #-} !Span
      -- | The 'Text' that was matched
    , matchText :: !ByteString
      -- | 'Traceback' at the time of the match
    , matchTraceback :: !Traceback
      -- | The rest of the 'Text' after the match
    , matchRemainder :: !ByteString
    }

indent :: Text
indent = "  "

indentLines :: Text -> Text
indentLines = Text.unlines . map (indent <>) . Text.lines

printMatch :: Match -> Text
printMatch m =
  Text.unlines
  [ Text.unwords
    [ "✔️ match at"
    , OP.printSpan (matchSpan m) <> ":"
    ]
  , indentLines (Text.decodeUtf8Lenient (matchText m))
  , OT.printTraceback (matchTraceback m)
  ]

-- | A sequence of successful matches of API calls against some text
data Progress
 = Progress
   { -- | t'Loc' after the last match
     progressLoc :: {-# UNPACK #-} !Loc
     -- | Successful 'Match'es
   , progressMatches :: Seq Match
     -- | Remaining text after the last match
   , progressRemainder :: !ByteString
   }

printProgress :: Progress -> Text
printProgress p = Text.unlines (map printMatch (toList (progressMatches p)))

-- | Create a new 'Progress' starting at position @'OP.Pos' 1 1@.
newProgress :: FilePath -> ByteString -> Progress
newProgress path txt =
  let loc0 = OP.Loc (Just path) (OP.Pos 1 1) in
  Progress loc0 Seq.empty txt

-- | Update 'Progress' with a new 'Match'
updateProgress :: Match -> Progress -> Progress
updateProgress m p =
  Progress
  { progressLoc = (progressLoc p) { OP.pos = OP.spanEnd (matchSpan m) }
  , progressMatches = progressMatches p Seq.:|> m
  , progressRemainder = matchRemainder m
  }

-- | Helper, not exported
progressToSuccess :: Progress -> Success
progressToSuccess (Progress loc matches remainder) =
  Success loc matches remainder

-- | Failure to match a program against some text.
data Failure
 = Failure
   { failureProgress :: Progress
   , failureTraceback :: !Traceback
   }

trunc :: Text -> Text
trunc txt =
  let ls = Text.lines txt in
  if length ls > 3
  then Text.unlines (take 3 ls) <> "\n..."
  else txt

instance Show Failure where
  show f =
    Text.unpack $
      Text.unlines
      [ ""  -- a leading newline makes the output of Tasty more readable
      , "Check failed! Passing checks:"
      , printProgress (failureProgress f)
      , "Failing check:"
      , Text.unwords
        [ "❌ no match at"
        , OP.printLoc (progressLoc (failureProgress f)) <> ":"
        ]
      , indentLines (trunc (Text.decodeUtf8Lenient (progressRemainder (failureProgress f))))
      , OT.printTraceback (failureTraceback f)
      ]

instance X.Exception Failure

-- | The result of matching a program against some text.
data Success
 = Success
   { -- | t'Loc' after the last match
     successLoc :: {-# UNPACK #-} !Loc
     -- | Successful 'Match'es
   , successMatches :: Seq Match
     -- | Remaining text after the last match
   , successRemainder :: !ByteString
   }

-- | The result of running a Oughta Lua program
newtype Result = Result (Either Failure Success)

-- | Does this 'Rusult' reflect running zero checks?
resultNull :: Result -> Bool
resultNull =
  \case
    Result (Left {}) -> False
    Result (Right s) -> null (successMatches s)

-- | Display a 'Result' in human-readable 'Text'
printResult :: Result -> Text
printResult =
  \case
    Result (Left f) -> Text.pack (show f)
    Result (Right (Success loc matches remainder)) ->
      printProgress (Progress loc matches remainder)