packages feed

bookhound 0.1.24.0 → 0.1.25.0

raw patch · 16 files changed

+648/−72 lines, 16 filesdep +QuickCheckdep +bookhounddep +hspecdep ~textPVP: major bump suggested

API removals or changes: PVP suggests a major version bump

Dependencies added: QuickCheck, bookhound, hspec, quickcheck-instances

Dependency ranges changed: text

API changes (from Hackage documentation)

- Bookhound.Parsers.Char: char :: Parser Char
+ Bookhound.Parser: Error :: ParseError -> ParseResult a
+ Bookhound.Parser: Result :: Input -> a -> ParseResult a
+ Bookhound.ParserCombinators: sepByOps :: Parser a -> Parser b -> Parser ([a], [b])
+ Bookhound.Utils.List: headSafe :: [a] -> Maybe a
- Bookhound.ParserCombinators: (<#>) :: Parser a -> Integer -> Parser [a]
+ Bookhound.ParserCombinators: (<#>) :: Parser a -> Int -> Parser [a]
- Bookhound.ParserCombinators: times :: Integer -> Parser a -> Parser [a]
+ Bookhound.ParserCombinators: times :: Int -> Parser a -> Parser [a]

Files

bookhound.cabal view
@@ -5,7 +5,7 @@ -- see: https://github.com/sol/hpack  name:           bookhound-version:        0.1.24.0+version:        0.1.25.0 synopsis:       Simple Parser Combinators description:    Please see the README on GitHub at <https://github.com/albertprz/bookhound#readme> category:       Parser Combinators@@ -36,6 +36,7 @@       Bookhound.Utils.Applicative       Bookhound.Utils.DateTime       Bookhound.Utils.Foldable+      Bookhound.Utils.List       Bookhound.Utils.Map       Bookhound.Utils.String   other-modules:@@ -78,6 +79,64 @@   build-depends:       base >=4.7 && <5     , containers >=0.6 && <1-    , text >=2.0 && <3+    , text+    , time >=1.9 && <2+  default-language: Haskell2010++test-suite test+  type: exitcode-stdio-1.0+  main-is: Spec.hs+  other-modules:+      Bookhound.ParserCombinatorsSpec+      Bookhound.Parsers.CollectionsSpec+      Bookhound.Parsers.DateTimeSpec+      Bookhound.Parsers.NumberSpec+      Bookhound.ParserSpec+      Paths_bookhound+  hs-source-dirs:+      test+  default-extensions:+      LambdaCase+      MultiWayIf+      TupleSections+      PostfixOperators+      BlockArguments+      RankNTypes+      ExplicitForAll+      ScopedTypeVariables+      LiberalTypeSynonyms+      InstanceSigs+      FunctionalDependencies+      DuplicateRecordFields+      NamedFieldPuns+      RecordWildCards+      ConstrainedClassMethods+      MultiParamTypeClasses+      FlexibleContexts+      FlexibleInstances+      ApplicativeDo+      ParallelListComp+      MonadComprehensions+      GADTs+      TypeFamilies+      TypeFamilyDependencies+      DataKinds+      PolyKinds+      DeriveFunctor+      DeriveFoldable+      DeriveTraversable+      DeriveGeneric+      GeneralizedNewtypeDeriving+  ghc-options: -Wall -Wcompat -Widentities -Wincomplete-record-updates -Wpartial-fields -Wincomplete-uni-patterns -Wredundant-constraints -Wno-orphans+  build-tool-depends:+      hspec-discover:hspec-discover+  build-depends:+      QuickCheck+    , base >=4.7 && <5+    , bookhound+    , containers >=0.6 && <1+    , hspec+    , quickcheck-instances+    , text     , time >=1.9 && <2   default-language: Haskell2010
src/Bookhound/Parser.hs view
@@ -1,4 +1,4 @@-module Bookhound.Parser (Parser, ParseResult, ParseError(..), runParser, errorParser,+module Bookhound.Parser (Parser(parse), ParseResult(..), ParseError(..), runParser, errorParser,                andThen, exactly, isMatch, check, anyOf, allOf, char,                withTransform, withError, withErrorN, except) where @@ -33,26 +33,9 @@   deriving (Eq, Ord)  -instance Show a => Show (ParseResult a) where-  show (Result i a) = "Pending: " <> " >" <> unpack i <> "< "-                                  <> "\n\nResult: \n" <> show a-  show (Error err)  = show err--instance Show ParseError where-  show UnexpectedEof        = "Unexpected end of stream"-  show (ExpectedEof i)      = "Expected end of stream, but got "-                               <> ">" <> unpack i <> "<"-  show (UnexpectedChar c)   = "Unexpected char: "-                               <> "[" <> show c <> "]"-  show (UnexpectedString s) = "Unexpected string: "-                               <> "[" <> s <> "]"-  show (NoMatch s)          = "Did not match condition: " <> s-  show (ErrorAt s)          = "Error at " <> s-- instance Functor ParseResult where   fmap f (Result i a) = Result i (f a)-  fmap _ (Error pe)   = (Error pe)+  fmap _ (Error pe)   = Error pe   instance Functor Parser where@@ -64,7 +47,7 @@   liftA2 f (P p t e) mb@(P _ t' e') =     applyTransformsErrors [t, t'] [e, e'] $ mkParser (\x ->       case p x of-        Result i a -> parse ((f a) <$> mb) i+        Result i a -> parse (f a <$> mb) i         Error pe   -> Error pe     ) @@ -81,20 +64,16 @@   where     toEither = \case       Result _ a -> Right a-      Error pe   -> Left $ filter (hasPriorityError)   [pe]   <>+      Error pe   -> Left $ filter hasPriorityError       [pe] <>                           (snd <$> reverse (Set.toList e))   <>                           filter (not . hasPriorityError) [pe] -hasPriorityError :: ParseError -> Bool-hasPriorityError (ErrorAt _) = True-hasPriorityError _           = False- errorParser :: ParseError -> Parser a errorParser = mkParser . const . Error  andThen :: Parser String -> Parser a -> Parser a andThen p1 p2@(P _ t e) = applyTransformError t e $-  P (\i -> parse p2 $ fromRight i $ pack <$> runParser p1 i) t e+  mkParser (\i -> parse p2 $ fromRight i $ pack <$> runParser p1 i)  char :: Parser Char char = mkParser $@@ -179,8 +158,25 @@ withTransform :: (forall b. Parser b -> Parser b) -> Parser a -> Parser a withTransform t = applyTransform $ Just t +instance Show a => Show (ParseResult a) where+  show (Result i a) = "Pending: " <> " >" <> unpack i <> "< "+                                  <> "\n\nResult: \n" <> show a+  show (Error err)  = show err +instance Show ParseError where+  show UnexpectedEof        = "Unexpected end of stream"+  show (ExpectedEof i)      = "Expected end of stream, but got "+                               <> ">" <> unpack i <> "<"+  show (UnexpectedChar c)   = "Unexpected char: "+                               <> "[" <> show c <> "]"+  show (UnexpectedString s) = "Unexpected string: "+                               <> "[" <> s <> "]"+  show (NoMatch s)          = "Did not match condition: " <> s+  show (ErrorAt s)          = "Error at " <> s +++ applyTransformsErrors :: (forall b. [Maybe (Parser b -> Parser b)])                       -> [Set (Int, ParseError)]                       -> Parser a@@ -205,3 +201,8 @@  mkParser :: (Input -> ParseResult a) -> Parser a mkParser p = P {parse = p, transform = Nothing, errors = Set.empty}+++hasPriorityError :: ParseError -> Bool+hasPriorityError (ErrorAt _) = True+hasPriorityError _           = False
src/Bookhound/ParserCombinators.hs view
@@ -1,10 +1,12 @@ {-# LANGUAGE UndecidableInstances #-}+{-# OPTIONS_GHC -Wno-unrecognised-pragmas #-}+{-# HLINT ignore "Use optional" #-}  module Bookhound.ParserCombinators (IsMatch(..), satisfies, contains, notContains,                           containsAnyOf, containsNoneOf,                           times, maybeTimes, anyTimes, someTimes, multipleTimes,                           within, maybeWithin, withinBoth, maybeWithinBoth,-                          anySepBy, someSepBy, multipleSepBy, sepByOp,+                          anySepBy, someSepBy, multipleSepBy, sepByOps, sepByOp,                           (<|>), (<?>), (<#>), (->>-), (|?), (|*), (|+), (|++))  where  import Bookhound.Parser            (Parser, allOf, anyOf, char, check, except,@@ -13,8 +15,7 @@ import Bookhound.Utils.Foldable    (hasMultiple, hasSome) import Bookhound.Utils.String      (ToString (..)) -import Data.List  (isInfixOf)-import Data.Maybe (listToMaybe)+import Data.List (isInfixOf)  import           Data.Bifunctor (Bifunctor (first)) import qualified Data.Foldable  as Foldable@@ -49,13 +50,13 @@  -- Condition combinators satisfies :: (a -> Bool) -> Parser a -> Parser a-satisfies cond p = check "satisfies" cond p+satisfies = check "satisfies"  contains :: Eq a => [a] -> Parser [a] -> Parser [a]-contains val p = check "contains" (isInfixOf val) p+contains val = check "contains" (isInfixOf val)  notContains :: Eq a => [a] -> Parser [a] -> Parser [a]-notContains val p = check "notContains" (isInfixOf val) p+notContains val = check "notContains" (isInfixOf val)  containsAnyOf :: (Foldable t, Eq a) => t [a] -> Parser [a] -> Parser [a] containsAnyOf x y = foldr contains y x@@ -65,15 +66,15 @@    -- Frequency combinators-times :: Integer -> Parser a  -> Parser [a]+times :: Int -> Parser a  -> Parser [a] times n p = sequence $ p <$ [1 .. n]   maybeTimes :: Parser a -> Parser (Maybe a)-maybeTimes = (listToMaybe <$>) . check "maybeTimes" (not . hasMultiple) . anyTimes+maybeTimes p = Just <$> p <|> pure Nothing  anyTimes :: Parser a -> Parser [a]-anyTimes parser = (parser >>= \x -> (x :) <$> anyTimes parser) <|> pure []+anyTimes p = (p >>= \x -> (x :) <$> anyTimes p) <|> pure []  someTimes :: Parser a -> Parser [a] someTimes = check "someTimes" hasSome . anyTimes@@ -83,19 +84,19 @@   -- Within combinators-within :: Parser a -> Parser b -> Parser b-within p = extract p p--maybeWithin :: Parser a -> Parser b -> Parser b-maybeWithin p = within (p |?)- withinBoth :: Parser a -> Parser b -> Parser c -> Parser c withinBoth = extract  maybeWithinBoth :: Parser a -> Parser b -> Parser c -> Parser c-maybeWithinBoth p1 p2 = extract (p1 |?) (p2 |?)+maybeWithinBoth p1 p2 = withinBoth (p1 |?) (p2 |?) +within :: Parser a -> Parser b -> Parser b+within p = withinBoth p p +maybeWithin :: Parser a -> Parser b -> Parser b+maybeWithin p = within (p |?)++ -- Separated by combinators sepBy :: (Parser b -> Parser (Maybe b)) -> (Parser b -> Parser [b])                 -> Parser a -> Parser b -> Parser [b]@@ -114,7 +115,7 @@ sepByOps :: Parser a -> Parser b -> Parser ([a], [b]) sepByOps sep p = do x <-  p                     y <- (((,) <$> sep <*> p) |+)-                    pure $ (fst <$> y, x : (snd <$> y))+                    pure (fst <$> y, x : (snd <$> y))  sepByOp :: Parser a -> Parser b -> Parser (a, [b]) sepByOp sep p = first head <$> sepByOps sep p@@ -126,7 +127,7 @@ (<|>) p1 p2 = anyOf [p1, p2]  infixl 6 <#>-(<#>) :: Parser a -> Integer -> Parser [a]+(<#>) :: Parser a -> Int -> Parser [a] (<#>) = flip times  infixl 6 <?>
src/Bookhound/Parsers/Char.hs view
@@ -2,12 +2,8 @@  import Bookhound.ParserCombinators (IsMatch (..), (<|>)) -import           Bookhound.Parser (Parser)-import qualified Bookhound.Parser as Parser-+import Bookhound.Parser (Parser) -char :: Parser Char-char = Parser.char  digit :: Parser Char digit = oneOf ['0' .. '9']
src/Bookhound/Parsers/Collections.hs view
@@ -6,8 +6,9 @@                                     openCurly, openParens, openSquare) import Bookhound.Parsers.String    (spacing) -import           Data.Map (Map)-import qualified Data.Map as Map+import           Bookhound.Utils.Foldable (hasMultiple)+import           Data.Map                 (Map)+import qualified Data.Map                 as Map   collOf :: Parser a -> Parser b -> Parser c -> Parser d -> Parser [d]@@ -24,7 +25,7 @@  tupleOf :: Parser a -> Parser [a] tupleOf = withErrorN (-1) "Tuple"-  . satisfies ((>= 2) . length)+  . satisfies hasMultiple   . collOf openParens closeParens comma  
src/Bookhound/Parsers/DateTime.hs view
@@ -3,13 +3,14 @@ import Bookhound.Parser            (Parser, check, withErrorN) import Bookhound.ParserCombinators (IsMatch (..), within, (<#>), (<|>), (|+),                                     (|?))-import Bookhound.Parsers.Char      (colon, dash, digit, plus)+import Bookhound.Parsers.Char      (colon, dash, digit, dot, plus)  import Data.Maybe (fromMaybe) import Data.Time  (Day, LocalTime (..), TimeOfDay (..), TimeZone,                    ZonedTime (..), fromGregorian, minutesToTimeZone)  + date :: Parser Day date = withErrorN (-1) "Date" $   fromGregorian <$> year <*> within dash month <*> day@@ -20,26 +21,26 @@   do h <- hour      m <- colon *> minute      s <- colon *> second-     decimals <- fromMaybe 0 <$> ((colon *> secondDecimals) |?)-     pure $ TimeOfDay h m $ read (show s <> "." <> show decimals)+     decimals <- ((dot *> secondDecimals) |?)+     pure $ TimeOfDay h m $ read (show s <> foldMap ("." ++) decimals)   timeZoneOffset :: Parser TimeZone timeZoneOffset = withErrorN (-1) "Timezone Offset" $   do pos <- (True <$ plus) <|> (False <$ dash)      h <- hour-     m <- fromMaybe 0 <$> ((colon *> minute) |?)+     m <- fromMaybe 0 <$> (minute |?)      pure $ minutesToTimeZone $ (if pos then 1 else (-1)) * (h * 60 + m)   localDateTime :: Parser LocalTime localDateTime = withErrorN (-1) "Local DateTime" $-  LocalTime <$> (date <* oneOf ['T', 't']) <*> time+  LocalTime <$> (date <* oneOf ['T', 't', ' ']) <*> time   offsetDateTime :: Parser ZonedTime offsetDateTime = withErrorN (-1) "Offset DateTime" $-  ZonedTime <$> localDateTime <*> timeZoneOffset+  ZonedTime <$> localDateTime <*> (is ' ' *> timeZoneOffset)   dateTime :: Parser ZonedTime@@ -67,8 +68,8 @@ second :: Parser Int second = check "Second" (range 0 59) $ read <$> digit <#> 2 -secondDecimals :: Parser Integer-secondDecimals = read <$> check "Pico Seconds" ((<= 12) . length) (digit |+)+secondDecimals :: Parser String+secondDecimals = check "Pico Seconds" ((<= 12) . length) (digit |+)   
src/Bookhound/Parsers/Number.hs view
@@ -21,14 +21,15 @@  posInt :: Parser Integer posInt = withErrorN (-1) "Positive Int"-  $ read <$> (plus |?) ->>- (digit |+)+  $ read <$> ((plus |?) *> (digit |+))  negInt :: Parser Integer negInt = withErrorN (-1) "Negative Int"   $ read <$> dash ->>- (digit |+)  int :: Parser Integer-int = withErrorN (-1) "Int" $ negInt <|> posInt+int = withErrorN (-1) "Int"+  $ negInt <|> posInt  intLike :: Parser Integer intLike = parser <|> int@@ -45,7 +46,7 @@  double :: Parser Double double = withErrorN (-1) "Double"-  $ read <$> int ->>- (decimals |?) ->>- (expn |?) where+  $ read <$> (dash |?) ->>- posInt ->>- (decimals |?) ->>- (expn |?) where -  decimals = dot ->>- unsignedInt+  decimals = dot ->>- (digit |+)   expn      = oneOf ['e', 'E'] ->>- int
src/Bookhound/Parsers/String.hs view
@@ -1,10 +1,11 @@ module Bookhound.Parsers.String where -import Bookhound.Parser            (Parser)+import Bookhound.Parser            (Parser, char) import Bookhound.ParserCombinators (IsMatch (..), maybeWithin, maybeWithinBoth,-                                    within, withinBoth, (->>-), (|*), (|+), (|?))-import Bookhound.Parsers.Char      (alpha, alphaNum, char, closeAngle,-                                    closeCurly, closeParens, closeSquare, digit,+                                    within, withinBoth, (->>-), (|*), (|+),+                                    (|?))+import Bookhound.Parsers.Char      (alpha, alphaNum, closeAngle, closeCurly,+                                    closeParens, closeSquare, digit,                                     doubleQuote, letter, lower, newLine,                                     openAngle, openCurly, openParens,                                     openSquare, quote, space, spaceOrTab, tab,
src/Bookhound/Utils/Foldable.hs view
@@ -14,10 +14,10 @@ hasSome = not . hasNone  hasMultiple :: Foldable m => m a -> Bool-hasMultiple xs = all hasSome $ [id, tail] <*> [Foldable.toList xs]+hasMultiple xs = all hasSome $ [id, tail] <*> [toList xs]  -stringify :: (Foldable m) => String -> String -> String -> Int -> m String -> String+stringify :: Foldable m => String -> String -> String -> Int -> m String -> String stringify sep start end n xs = start <> indent n str <> end   where     str = intercalate sep list
+ src/Bookhound/Utils/List.hs view
@@ -0,0 +1,6 @@+module Bookhound.Utils.List where+++headSafe :: [a] -> Maybe a+headSafe (x: _) = Just x+headSafe _      = Nothing
+ test/Bookhound/ParserCombinatorsSpec.hs view
@@ -0,0 +1,130 @@+module Bookhound.ParserCombinatorsSpec where++import Test.Hspec+import Test.Hspec.QuickCheck+import Test.QuickCheck.Instances.Text ()++import           Bookhound.Parser+import           Bookhound.ParserCombinators+import           Bookhound.Utils.List        (headSafe)+import qualified Data.Foldable               as Foldable+import           Data.Text                   (Text, unpack)+import qualified Data.Text                   as Text++spec :: Spec+spec = do++  describe "times" $++    prop "applies a parser n times sequentially" $+      \x n -> parse (times n char) x+          `shouldBe`+            if length (unpack x) >= n then+               Result (Text.drop n x) (unpack $ Text.take n x)+            else+              Error UnexpectedEof++  describe "maybeTimes" $++    prop "applies a parser 1 or 0 times" $+      \x -> parse (maybeTimes char) x+          `shouldBe`+           headSafe <$> parseTimes char [0, 1] x++  describe "anyTimes" $++    prop "applies a parser any number of times" $+      \x -> parse (anyTimes char) x+          `shouldBe`+           parseTimes char [0 .. Text.length x] x+++  describe "someTimes" $++    prop "applies a parser at least once" $+      \x -> parse (someTimes char) x+          `shouldBe`+           replaceError "someTimes"+                        (parseTimes char [1 .. Text.length x] x)++  describe "multipleTimes" $++    prop "applies a parser at least twice" $+      \x -> parse (multipleTimes char) x+          `shouldBe`+           replaceError "multipleTimes"+                        (parseTimes char [2 .. Text.length x] x)++  describe "withinBoth" $++    prop "applies a parser surrounded by 2 parsers" $+      \x (y :: Char) (z :: Char) ->+        parse (withinBoth (is y) (is z) char) x+       `shouldBe`+        parse (is y *> char <* is z) x++  describe "maybeWithinBoth" $++    prop "applies a parser surrounded by 2 optional parsers" $+      \x (y :: Char) (z :: Char) ->+        parse (maybeWithinBoth (is y) (is z) char) x+       `shouldBe`+        parse ((is y |?) *> char <* (is z |?)) x++  describe "within" $++    prop "applies a parser surrounded by a parser" $+      \x (y :: Char) ->+        parse (within (is y) char) x+       `shouldBe`+        parse (is y *> char <* is y) x++  describe "maybeWithin" $++    prop "applies a parser surrounded by a optional parsers" $+      \x (y :: Char) ->+        parse (maybeWithin (is y) char) x+       `shouldBe`+        parse ((is y |?) *> char <* (is y |?)) x++  describe "anySepBy" $++    prop "applies a parser separated by a parser any number of times" $+      \x (y :: Char) ->+        parse (anySepBy (is y) char) x+       `shouldBe`+        parse ((<>) <$> (Foldable.toList <$> (char |?))+                    <*> ((is y *> char) |*)) x++  describe "someSepBy" $++    prop "applies a parser separated by a parser at least once" $+      \x (y :: Char) ->+        parse (someSepBy (is y) char) x+       `shouldBe`+        parse ((:) <$> char <*> ((is y *> char) |*)) x++  describe "multipleSepBy" $++    prop "applies a parser separated by a parser at least twice" $+      \x (y :: Char) ->+        parse (multipleSepBy (is y) char) x+       `shouldBe`+        parse ((:) <$> char <*> ((is y *> char) |+)) x++  describe "->>-" $++    prop "concats results of 2 parsers that can be converted to Strings" $+      \x (y :: Char) (z :: Char) ->+        parse (is y ->>- is z) x+       `shouldBe`+        parse (is [y, z]) x+++parseTimes :: Parser a -> [Int] -> Text -> ParseResult [a]+parseTimes p ns = parse $ anyOf ((`times` p) <$> reverse ns)+++replaceError :: String -> ParseResult a -> ParseResult a+replaceError err (Error (NoMatch _)) = Error $ NoMatch err+replaceError _ result                = result
+ test/Bookhound/ParserSpec.hs view
@@ -0,0 +1,197 @@+module Bookhound.ParserSpec where++import Test.Hspec+import Test.Hspec.QuickCheck+import Test.QuickCheck.Instances.Text ()++import Bookhound.Parser+import Data.Char+import Data.Text        (pack, unpack)++++spec :: Spec+spec = do++  describe "Functor laws" $ do++    prop "Identity" $+      \x -> parse (id <$> char) x+          `shouldBe`+           parse char x++    prop "Composition" $+      \x -> parse ((isLower . toUpper) <$> char) x+          `shouldBe`+           parse ((isLower <$>) . (toUpper <$>) $ char) x+++  describe "Applicative laws" $ do++    prop "Identity" $+      \x -> parse (pure id <*> char) x+          `shouldBe`+           parse char x++    prop "Homomorphism" $+      \x (y :: [Int]) -> parse (pure sum <*> pure y) x+          `shouldBe`+           parse (pure (sum y)) x++    prop "Interchange" $+      \x (y :: [Int])  -> parse (pure length <*> pure y) x+          `shouldBe`+           parse (pure ($ y) <*> pure length) x+++  describe "Monad laws" $ do++    prop "Left identity" $+      \x -> parse (pure 'a' >>= isMatch (==) char) x+          `shouldBe`+           parse (isMatch (==) char 'a') x++    prop "Right identity" $+      \x -> parse (char >>= pure) x+          `shouldBe`+           parse char x++    prop "Associativity" $+      \x -> parse+           (char >>= (\y -> isMatch (<) char y >>= isMatch (>) char)) x+          `shouldBe`+           parse ((char >>= isMatch (<) char) >>= isMatch (>) char) x++  describe "char" $++    prop "parses a single char" $+      \x -> parse char x+          `shouldBe`+           case unpack x of+             (ch : rest) -> Result (pack rest) ch+             []          -> Error UnexpectedEof++  describe "isMatch" $ do++    prop "works for ==" $+      \x y -> parse (isMatch (==) char y) x+            `shouldBe`+             case unpack x of+               (ch : rest)+                 | ch == y    -> Result (pack rest) ch+                 | otherwise -> Error $ UnexpectedChar ch+               []            -> Error UnexpectedEof++    prop "works for /=" $+      \x y -> parse (isMatch (/=) char y) x+            `shouldBe`+             case unpack x of+               (ch : rest)+                 | ch /= y    -> Result (pack rest) ch+                 | otherwise -> Error $ UnexpectedChar ch+               []            -> Error UnexpectedEof++  describe "check" $++    prop "performs a check on the parse result" $+      \x -> parse (check "digit" isDigit char) x+          `shouldBe`+           case unpack x of+             (ch : rest)+               | isDigit ch -> Result (pack rest) ch+               | otherwise  -> Error $ NoMatch "digit"+             []             -> Error UnexpectedEof++  describe "except" $++    context "when the second parser fails" $+      prop "the first parser then runs" $+        \x -> parse (except char (char *> char)) x+            `shouldBe`+             case unpack x of+               [ch]    -> Result (pack "") ch+               (_ : _) -> Error $ NoMatch "except"+               []      -> Error UnexpectedEof++  describe "anyOf" $++    prop "returns first parser success or last parser error if all fail" $+      \x -> parse (anyOf [errorParser $ ErrorAt "firstError",+                         "firstSuccess"  <$ char,+                         "secondSuccess" <$ char,+                         errorParser $ ErrorAt "secondError",+                         errorParser $ ErrorAt "lastError"]) x+          `shouldBe`+           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"  <$ char,+                           "secondSuccess" <$ char,+                           errorParser $ ErrorAt "firstError"]) x+            `shouldBe`+             case unpack x of+               (_ : _) -> Error $ ErrorAt "firstError"+               []      -> Error UnexpectedEof++    context "when all parsers succeed" $+      prop "returns last parser success" $+        \x -> parse (allOf ["firstSuccess"  <$ char,+                           "secondSuccess" <$ char,+                           "thirdSuccess"  <$ char]) x+            `shouldBe`+             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 char) x+          `shouldBe`+           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 char x+          `shouldBe`+           toEither (parse (exactly char) x)+++  describe "withError" $++    prop "includes error labels in order when running the parser" $+      \x -> runParser ((,) <$> withErrorN 2 "firstErr" char+                          <*> withErrorN 1 "secondErr" char) x+          `shouldBe`+             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 -> char *> p <* char) char) x+          `shouldBe`+           parse (char *> char <* char) x+++++toEither :: ParseResult a -> Either [ParseError] a+toEither = \case+  Result _ a -> Right a+  Error pe   -> Left [pe]
+ test/Bookhound/Parsers/CollectionsSpec.hs view
@@ -0,0 +1,64 @@+module Bookhound.Parsers.CollectionsSpec where++import Test.Hspec+import Test.Hspec.QuickCheck+import Test.QuickCheck.Instances.Text ()++import           Bookhound.Parser+import           Bookhound.ParserCombinators   (IsMatch (is))+import           Bookhound.Parsers.Char        (alpha)+import           Bookhound.Parsers.Collections+import           Data.Char                     (isAlpha, isAscii)+import           Data.List                     (intercalate)+import qualified Data.Map                      as Map+import           Data.Text                     (pack)+import           Test.QuickCheck               ((==>))++++spec :: Spec+spec = do++  describe "listOf" $++    prop "parses a list provided the element parser" $+      \(x :: [Char]) ->+        runParser (listOf alpha)+        (pack ("[" <>+               intercalate ", " (pure <$> filter isAlpha' x)+               <> "]"))+       `shouldBe`+        Right (filter isAlpha' x)++  describe "tupleOf" $++    prop "parses a tuple provided the element parser" $+      \(x :: [Char]) -> length (filter isAlpha' x) >= 2 ==>+        runParser (tupleOf alpha)+        (pack ("(" <>+               intercalate ", " (pure <$> filter isAlpha' x)+               <> ")"))+       `shouldBe`+        Right (filter isAlpha' x)++  describe "mapOf" $++    prop "parses a map provided the key and value parsers" $+      \(x :: [(Char, Char)]) ->+        runParser (mapOf (is "->") alpha alpha)+        (pack ("{" <>+               intercalate ", " (showMapEntry <$> filter areAlpha' x)+               <> "}"))+       `shouldBe`+        Right (Map.fromList $ filter areAlpha' x)++++showMapEntry :: (Char, Char) -> String+showMapEntry (x, y) = pure x <> " -> " <> pure y++areAlpha' :: (Char, Char) -> Bool+areAlpha' (x, y) = isAlpha' x && isAlpha' y++isAlpha' :: Char -> Bool+isAlpha' x = isAlpha x && isAscii x
+ test/Bookhound/Parsers/DateTimeSpec.hs view
@@ -0,0 +1,68 @@+module Bookhound.Parsers.DateTimeSpec where++import Test.Hspec+import Test.Hspec.QuickCheck+import Test.QuickCheck.Instances.Text ()+import Test.QuickCheck.Instances.Time ()++import Bookhound.Parser+import Bookhound.Parsers.DateTime+import Data.Text                  (pack)++import Data.Time (Day, LocalTime, TimeOfDay, TimeZone (..), ZonedTime (..),+                  zonedTimeToUTC)+++spec :: Spec+spec = do++  describe "date" $++    prop "parses a date" $+      \(x :: Day) ->+        runParser date (pack $ show x)+       `shouldBe`+        Right x++  describe "time" $++    prop "parses a time" $+      \(x :: TimeOfDay) ->+        runParser time (pack $ show x)+       `shouldBe`+        Right x++  describe "timeZoneOffset" $++    prop "parses a timezone offset" $+      \(x :: TimeZone) ->+        runParser timeZoneOffset (pack $ show $ normalizeTimeZone x)+       `shouldBe`+        Right (normalizeTimeZone x)++  describe "localDateTime" $++    prop "parses a local datetime" $+      \(x :: LocalTime) ->+        runParser localDateTime (pack $ show x)+       `shouldBe`+        Right x++  describe "DateTime" $++    prop "parses a datetime" $+      \(x :: ZonedTime) ->+        (zonedTimeToUTC <$> runParser dateTime+         (pack $ show $ normalizeZonedTime x))+       `shouldBe`+        Right (zonedTimeToUTC $ normalizeZonedTime x)++++normalizeTimeZone :: TimeZone -> TimeZone+normalizeTimeZone z =+  z {timeZoneName = "", timeZoneSummerOnly = False}++normalizeZonedTime :: ZonedTime -> ZonedTime+normalizeZonedTime t@(ZonedTime {zonedTimeZone = x}) =+  t {zonedTimeZone = normalizeTimeZone x}
+ test/Bookhound/Parsers/NumberSpec.hs view
@@ -0,0 +1,49 @@+module Bookhound.Parsers.NumberSpec where++import Test.Hspec+import Test.Hspec.QuickCheck++import Test.QuickCheck                ((==>))+import Test.QuickCheck.Instances.Text ()++import Bookhound.Parser+import Bookhound.Parsers.Number+import Data.Text                (pack)++++spec :: Spec+spec = do++  describe "posInt" $++    prop "parses a positive Int" $+      \(x :: Integer) -> x > 0 ==>+        runParser posInt (pack $ show x)+       `shouldBe`+        Right x++  describe "negInt" $++    prop "parses a negative Int" $+      \(x :: Integer) -> x < 0 ==>+        runParser negInt (pack $ show x)+       `shouldBe`+        Right x++  describe "int" $++    prop "parses an Int" $+      \(x :: Integer) ->+        runParser int (pack $ show x)+       `shouldBe`+        Right x+++  describe "double" $++    prop "parses a Double" $+      \(x :: Double) ->+        runParser double (pack $ show x)+       `shouldBe`+        Right x
+ test/Spec.hs view
@@ -0,0 +1,1 @@+{-# OPTIONS_GHC -F -pgmF hspec-discover #-}