packages feed

bookhound-0.1.26.0: test/Bookhound/ParserSpec.hs

module Bookhound.ParserSpec where

import Test.Hspec
import Test.Hspec.QuickCheck
import Test.QuickCheck.Instances.Text ()

import Bookhound.Utils.List 
import Bookhound.Parser
import Data.Char
import Data.Text                (pack, unpack)
import Test.QuickCheck.Property ((===))



spec :: Spec
spec = do

  describe "Functor laws" $ do

    prop "Identity" $
      \x -> parse (id <$> anyChar) x
           ===
           parse anyChar x

    prop "Composition" $
      \x -> parse ((isLower . toUpper) <$> anyChar) x
           ===
           parse ((isLower <$>) . (toUpper <$>) $ anyChar) x


  describe "Applicative laws" $ do

    prop "Identity" $
      \x -> parse (pure id <*> anyChar) x
           ===
           parse anyChar x

    prop "Homomorphism" $
      \x (y :: [Int]) -> parse (pure sum <*> pure y) x
           ===
           parse (pure (sum y)) x

    prop "Interchange" $
      \x (y :: [Int])  -> parse (pure length <*> pure y) x
           ===
           parse (pure ($ y) <*> pure length) x


  describe "Monad laws" $ do

    prop "Left identity" $
      \x -> parse (pure 'a' >>= isMatch (==) anyChar) x
           ===
           parse (isMatch (==) anyChar 'a') x

    prop "Right identity" $
      \x -> parse (anyChar >>= pure) x
           ===
           parse anyChar x

    prop "Associativity" $
      \x -> parse
           (anyChar >>= (\y -> isMatch (<) anyChar y >>= isMatch (>) anyChar)) x
           ===
           parse ((anyChar >>= isMatch (<) anyChar) >>= isMatch (>) anyChar) x

  describe "anyChar" $

    prop "parses a single anyChar" $
      \x -> parse anyChar x
           ===
           case unpack x of
             (ch : rest) -> Result (pack rest) ch
             []          -> Error UnexpectedEof

  describe "isMatch" $ do

    prop "works for ==" $
      \x y -> parse (isMatch (==) anyChar y) x
             ===
             case unpack x of
               (ch : rest)
                 | ch == y    -> Result (pack rest) ch
                 | hasSome rest -> Error $ ExpectedEof $ pack rest
               _            -> Error UnexpectedEof

    prop "works for /=" $
      \x y -> parse (isMatch (/=) anyChar y) x
             ===
             case unpack x of
               (ch : rest)
                 | ch /= y    -> Result (pack rest) ch
                 | hasSome rest -> Error $ ExpectedEof $ pack rest
               _            -> Error UnexpectedEof

  describe "satisfy" $

    prop "performs a check on the parse result" $
      \x -> parse (satisfy isDigit anyChar) x
           ===
           case unpack x of
             (ch : rest)
               | isDigit ch -> Result (pack rest) ch
               | hasSome rest -> Error $ ExpectedEof $ pack rest
             _             -> Error UnexpectedEof

  describe "except" $

    context "when the second parser fails" $
      prop "the first parser then runs" $
        \x -> parse (except anyChar (anyChar *> anyChar)) x
             ===
             case unpack x of
               [ch]    -> Result (pack "") ch
               (_ : _) -> Error $ ExpectedEof x
               []      -> Error UnexpectedEof

  describe "anyOf" $

    prop "returns first parser success or last parser error if all fail" $
      \x -> parse (anyOf [throwError $ ErrorAt "firstError",
                         "firstSuccess"  <$ anyChar,
                         "secondSuccess" <$ anyChar,
                         throwError $ ErrorAt "secondError",
                         throwError $ ErrorAt "lastError"]) x
           ===
           case unpack x of
             (_ : rest) -> Result (pack rest) "firstSuccess"
             []         -> Error $ ErrorAt "lastError"

  describe "allOf" $ do

    context "when any parser fails" $
      prop "returns first parser error" $
        \x -> parse (allOf ["firstSuccess"  <$ anyChar,
                           "secondSuccess" <$ anyChar,
                           throwError $ ErrorAt "firstError"]) x
             ===
             case unpack x of
               (_ : _) -> Error $ ErrorAt "firstError"
               []      -> Error UnexpectedEof

    context "when all parsers succeed" $
      prop "returns last parser success" $
        \x -> parse (allOf ["firstSuccess"  <$ anyChar,
                           "secondSuccess" <$ anyChar,
                           "thirdSuccess"  <$ anyChar]) x
             ===
             case unpack x of
               (_ : rest) -> Result (pack rest) "thirdSuccess"
               []         -> Error UnexpectedEof

  describe "exactly" $

    prop "returns an error when some chars remain after parsing" $
      \x -> parse (exactly anyChar) x
           ===
           case unpack x of
             [ch]       -> Result (pack []) ch
             (_ : rest) -> Error $ ExpectedEof (pack rest)
             []         -> Error UnexpectedEof

  describe "runParser" $

    prop "parses exactly and wraps the result into an either" $
      \x -> runParser anyChar x
           ===
           toEither (parse (exactly anyChar) x)


  describe "withError" $

    prop "includes error labels in order when running the parser" $
      \x -> runParser ((,) <$> withErrorN 2 "firstErr" anyChar
                          <*> withErrorN 1 "secondErr" anyChar) x
           ===
           case unpack x of
             [ch1, ch2] -> Right (ch1, ch2)
             (_ : _ : rest)   -> Left [ErrorAt "firstErr",
                                      ErrorAt "secondErr",
                                      ExpectedEof (pack rest)]
             _                -> Left [ErrorAt "firstErr",
                                      ErrorAt "secondErr",
                                      UnexpectedEof]

  describe "withTransform" $

    prop "transforms current parser with provided fn" $
      \x -> parse (withTransform (\p -> anyChar *> p <* anyChar) anyChar) x
           ===
           parse (anyChar *> anyChar <* anyChar) x


isMatch :: (a -> a -> Bool) -> Parser a -> a -> Parser a
isMatch cond ma c1 = satisfy (cond c1) ma

toEither :: ParseResult a -> Either [ParseError] a
toEither = \case
  Result _ a -> Right a
  Error pe   -> Left [pe]