refactor(users/Profpatsch/jbovlaste): factor Parse into own module
We want to use this quite generic parser type for other things as well. Change-Id: I890b43c58e479bdf2d179a724280ef1d8748fafa Reviewed-on: https://cl.tvl.fyi/c/depot/+/9742 Autosubmit: Profpatsch <mail@profpatsch.de> Tested-by: BuildkiteCI Reviewed-by: Profpatsch <mail@profpatsch.de>
This commit is contained in:
parent
c79c2f3557
commit
df43454dc5
7 changed files with 176 additions and 167 deletions
|
@ -5,34 +5,26 @@ module Main where
|
||||||
|
|
||||||
import Conduit ((.|))
|
import Conduit ((.|))
|
||||||
import Conduit qualified as Cond
|
import Conduit qualified as Cond
|
||||||
import Control.Category qualified
|
|
||||||
import Control.Category qualified as Cat
|
import Control.Category qualified as Cat
|
||||||
import Control.Foldl qualified as Fold
|
import Control.Foldl qualified as Fold
|
||||||
import Control.Selective (Selective)
|
|
||||||
import Data.ByteString.Internal qualified as Bytes
|
import Data.ByteString.Internal qualified as Bytes
|
||||||
import Data.Error.Tree
|
import Data.Error.Tree
|
||||||
import Data.Functor.Compose
|
|
||||||
import Data.Int (Int64)
|
import Data.Int (Int64)
|
||||||
import Data.List qualified as List
|
import Data.List qualified as List
|
||||||
import Data.Map.Strict qualified as Map
|
import Data.Map.Strict qualified as Map
|
||||||
import Data.Maybe (catMaybes)
|
import Data.Maybe (catMaybes)
|
||||||
import Data.Monoid (First (..))
|
|
||||||
import Data.Semigroup.Traversable
|
|
||||||
import Data.Semigroupoid qualified as Semigroupoid
|
|
||||||
import Data.Text qualified as Text
|
import Data.Text qualified as Text
|
||||||
import Data.Text.IO qualified as Text
|
import Data.Text.IO qualified as Text
|
||||||
import Database.SQLite.Simple qualified as Sqlite
|
import Database.SQLite.Simple qualified as Sqlite
|
||||||
import Database.SQLite.Simple.FromField qualified as Sqlite
|
import Database.SQLite.Simple.FromField qualified as Sqlite
|
||||||
import Database.SQLite.Simple.QQ qualified as Sqlite
|
import Database.SQLite.Simple.QQ qualified as Sqlite
|
||||||
import FieldParser (FieldParser)
|
|
||||||
import FieldParser qualified as Field
|
import FieldParser qualified as Field
|
||||||
import Label
|
import Label
|
||||||
|
import Parse
|
||||||
import PossehlAnalyticsPrelude
|
import PossehlAnalyticsPrelude
|
||||||
import Text.XML (def)
|
import Text.XML (def)
|
||||||
import Text.XML qualified as Xml
|
import Text.XML qualified as Xml
|
||||||
import Validation (partitionValidations)
|
|
||||||
import Prelude hiding (init, maybe)
|
import Prelude hiding (init, maybe)
|
||||||
import Prelude qualified
|
|
||||||
|
|
||||||
main :: IO ()
|
main :: IO ()
|
||||||
main = do
|
main = do
|
||||||
|
@ -131,7 +123,7 @@ insertValsi env vs = do
|
||||||
":sense" Sqlite.:= g.sense
|
":sense" Sqlite.:= g.sense
|
||||||
]
|
]
|
||||||
|
|
||||||
migrate :: HasField "envData" p Sqlite.Connection => p -> IO ()
|
migrate :: (HasField "envData" p Sqlite.Connection) => p -> IO ()
|
||||||
migrate env = do
|
migrate env = do
|
||||||
let x q = Sqlite.execute env.envData q ()
|
let x q = Sqlite.execute env.envData q ()
|
||||||
x
|
x
|
||||||
|
@ -181,7 +173,7 @@ withSqlite fileName inner = Sqlite.withConnection fileName $ \conn -> do
|
||||||
Sqlite.execute conn [Sqlite.sql|PRAGMA foreign_keys = ON|] ()
|
Sqlite.execute conn [Sqlite.sql|PRAGMA foreign_keys = ON|] ()
|
||||||
inner conn
|
inner conn
|
||||||
|
|
||||||
parseJbovlasteXml :: HasField "documentRoot" r Xml.Element => r -> Either ErrorTree [Valsi]
|
parseJbovlasteXml :: (HasField "documentRoot" r Xml.Element) => r -> Either ErrorTree [Valsi]
|
||||||
parseJbovlasteXml xml =
|
parseJbovlasteXml xml =
|
||||||
xml.documentRoot
|
xml.documentRoot
|
||||||
& runParse
|
& runParse
|
||||||
|
@ -338,55 +330,6 @@ nodeElementMay = \case
|
||||||
Xml.NodeElement el -> Just el
|
Xml.NodeElement el -> Just el
|
||||||
_ -> Nothing
|
_ -> Nothing
|
||||||
|
|
||||||
-- | A generic applicative “vertical” parser.
|
|
||||||
-- Similar to `FieldParser`, but made for parsing whole structures and collect all errors in an `ErrorTree`.
|
|
||||||
newtype Parse from to = Parse ((Context, from) -> Validation (NonEmpty ErrorTree) (Context, to))
|
|
||||||
deriving
|
|
||||||
(Functor, Applicative, Selective)
|
|
||||||
via ( Compose
|
|
||||||
( Compose
|
|
||||||
((->) (Context, from))
|
|
||||||
(Validation (NonEmpty ErrorTree))
|
|
||||||
)
|
|
||||||
((,) Context)
|
|
||||||
)
|
|
||||||
|
|
||||||
-- | Every parser can add to the context, like e.g. an element parser will add the name of the element it should be parsing.
|
|
||||||
-- This should be added to the error message of each parser, with `showContext`.
|
|
||||||
newtype Context = Context (Maybe [Text])
|
|
||||||
deriving stock (Show)
|
|
||||||
deriving (Semigroup, Monoid) via (First [Text])
|
|
||||||
|
|
||||||
instance Semigroupoid Parse where
|
|
||||||
o p2 p1 = Parse $ \from -> case runParse' p1 from of
|
|
||||||
Failure err -> Failure err
|
|
||||||
Success to1 -> runParse' p2 to1
|
|
||||||
|
|
||||||
instance Category Parse where
|
|
||||||
(.) = Semigroupoid.o
|
|
||||||
id = Parse $ \t -> Success t
|
|
||||||
|
|
||||||
instance Profunctor Parse where
|
|
||||||
lmap f (Parse p) = Parse $ lmap (second f) p
|
|
||||||
rmap = (<$>)
|
|
||||||
|
|
||||||
runParse :: Error -> Parse from to -> from -> Either ErrorTree to
|
|
||||||
runParse errMsg parser t =
|
|
||||||
(Context (Just ["$"]), t)
|
|
||||||
& runParse' parser
|
|
||||||
<&> snd
|
|
||||||
& first (nestedMultiError errMsg)
|
|
||||||
& validationToEither
|
|
||||||
|
|
||||||
runParse' :: Parse from to -> (Context, from) -> Validation (NonEmpty ErrorTree) (Context, to)
|
|
||||||
runParse' (Parse f) from = f from
|
|
||||||
|
|
||||||
showContext :: Context -> Text
|
|
||||||
showContext (Context context) = context & fromMaybe [] & List.reverse & Text.intercalate "."
|
|
||||||
|
|
||||||
addContext :: Text -> Context -> Context
|
|
||||||
addContext x (Context mxs) = Context (Just $ x : (mxs & fromMaybe []))
|
|
||||||
|
|
||||||
element :: Text -> Parse Xml.Element Xml.Element
|
element :: Text -> Parse Xml.Element Xml.Element
|
||||||
element name = Parse $ \(ctx, el) ->
|
element name = Parse $ \(ctx, el) ->
|
||||||
if el.elementName.nameLocalName == name
|
if el.elementName.nameLocalName == name
|
||||||
|
@ -417,96 +360,6 @@ attributeMay name = Parse $ \(ctx, el) ->
|
||||||
Just a -> Success (ctx & addContext [fmt|{{attr:{name}}}|], Just a)
|
Just a -> Success (ctx & addContext [fmt|{{attr:{name}}}|], Just a)
|
||||||
Nothing -> Success (ctx, Nothing)
|
Nothing -> Success (ctx, Nothing)
|
||||||
|
|
||||||
-- | Accept only exactly the given value
|
|
||||||
exactly :: Eq from => (from -> Text) -> from -> Parse from from
|
|
||||||
exactly errDisplay from = Parse $ \(ctx, from') ->
|
|
||||||
if from == from'
|
|
||||||
then Success (ctx, from')
|
|
||||||
else Failure $ singleton [fmt|Field has to be exactly {errDisplay from}, was: {errDisplay from'} at {showContext ctx}|]
|
|
||||||
|
|
||||||
-- | Make a parser to parse the whole list
|
|
||||||
multiple :: Parse a1 a2 -> Parse [a1] [a2]
|
|
||||||
multiple inner = dimap nonEmpty (Prelude.maybe [] toList) (maybe $ multipleNE inner)
|
|
||||||
|
|
||||||
-- | Make a parser to parse the whole non-empty list
|
|
||||||
multipleNE :: Parse from to -> Parse (NonEmpty from) (NonEmpty to)
|
|
||||||
multipleNE inner = Parse $ \(ctx, from) ->
|
|
||||||
from
|
|
||||||
& zipIndex
|
|
||||||
& traverse (\(idx, f) -> runParse' inner (ctx, f) & first (singleton . nestedMultiError [fmt|{idx}|]))
|
|
||||||
-- we assume that, since the same parser is used everywhere, the context will be the same as well (TODO: correct?)
|
|
||||||
& second (\((ctx', y) :| ys) -> (ctx', y :| (snd <$> ys)))
|
|
||||||
|
|
||||||
-- | Lift a parser into an optional value
|
|
||||||
maybe :: Parse from to -> Parse (Maybe from) (Maybe to)
|
|
||||||
maybe inner = Parse $ \(ctx, m) -> case m of
|
|
||||||
Nothing -> Success (ctx, Nothing)
|
|
||||||
Just a -> runParse' inner (ctx, a) & second (fmap Just)
|
|
||||||
|
|
||||||
-- | Assert that there is exactly one element in the list
|
|
||||||
exactlyOne :: Parse [from] from
|
|
||||||
exactlyOne = Parse $ \(ctx, xs) -> case xs of
|
|
||||||
[] -> Failure $ singleton [fmt|Expected exactly 1 element, but got 0, at {ctx & showContext}|]
|
|
||||||
[one] -> Success (ctx, one)
|
|
||||||
_more -> Failure $ singleton [fmt|Expected exactly 1 element, but got 2, at {ctx & showContext}|]
|
|
||||||
|
|
||||||
-- | Assert that there is exactly zero or one element in the list
|
|
||||||
zeroOrOne :: Parse [from] (Maybe from)
|
|
||||||
zeroOrOne = Parse $ \(ctx, xs) -> case xs of
|
|
||||||
[] -> Success (ctx, Nothing)
|
|
||||||
[one] -> Success (ctx, Just one)
|
|
||||||
_more -> Failure $ singleton [fmt|Expected exactly 1 element, but got 2, at {ctx & showContext}|]
|
|
||||||
|
|
||||||
-- | Find the first element on which the sub-parser succeeds; if there was no match, return all error messages.
|
|
||||||
find :: Parse from to -> Parse [from] to
|
|
||||||
find inner = Parse $ \(ctx, xs) -> case xs of
|
|
||||||
[] -> failure [fmt|Wanted to get the first sub-parser that succeeds, but there were no elements in the list, at {ctx & showContext}|]
|
|
||||||
(y : ys) -> runParse' (findNE' inner) (ctx, y :| ys)
|
|
||||||
|
|
||||||
-- | Find the first element on which the sub-parser succeeds; if there was no match, return all error messages.
|
|
||||||
findNE' :: Parse from to -> Parse (NonEmpty from) to
|
|
||||||
findNE' inner = Parse $ \(ctx, xs) ->
|
|
||||||
xs
|
|
||||||
<&> (\x -> runParse' inner (ctx, x))
|
|
||||||
& traverse1
|
|
||||||
( \case
|
|
||||||
Success a -> Left a
|
|
||||||
Failure e -> Right e
|
|
||||||
)
|
|
||||||
& \case
|
|
||||||
Left a -> Success a
|
|
||||||
Right errs ->
|
|
||||||
errs
|
|
||||||
& zipIndex
|
|
||||||
<&> (\(idx, errs') -> nestedMultiError [fmt|{idx}|] errs')
|
|
||||||
& nestedMultiError [fmt|None of these sub-parsers succeeded|]
|
|
||||||
& singleton
|
|
||||||
& Failure
|
|
||||||
|
|
||||||
-- | Find all elements on which the sub-parser succeeds; if there was no match, return an empty list
|
|
||||||
findAll :: Parse from to -> Parse [from] [to]
|
|
||||||
findAll inner = Parse $ \(ctx, xs) ->
|
|
||||||
xs
|
|
||||||
<&> (\x -> runParse' inner (ctx, x))
|
|
||||||
& partitionValidations
|
|
||||||
& \case
|
|
||||||
(_miss, []) ->
|
|
||||||
-- in this case we just arbitrarily forward the original context …
|
|
||||||
Success (ctx, [])
|
|
||||||
(_miss, (hitCtx, hit) : hits) -> Success (hitCtx, hit : (hits <&> snd))
|
|
||||||
|
|
||||||
-- | convert a 'FieldParser' into a 'Parse'.
|
|
||||||
fieldParser :: FieldParser from to -> Parse from to
|
|
||||||
fieldParser fp = Parse $ \(ctx, from) -> case Field.runFieldParser fp from of
|
|
||||||
Right a -> Success (ctx, a)
|
|
||||||
Left err -> Failure $ singleton (singleError err)
|
|
||||||
|
|
||||||
zipNonEmpty :: NonEmpty a -> NonEmpty b -> NonEmpty (a, b)
|
|
||||||
zipNonEmpty (x :| xs) (y :| ys) = (x, y) :| zip xs ys
|
|
||||||
|
|
||||||
zipIndex :: NonEmpty b -> NonEmpty (Natural, b)
|
|
||||||
zipIndex = zipNonEmpty (1 :| [2 :: Natural ..])
|
|
||||||
|
|
||||||
instance
|
instance
|
||||||
( Sqlite.FromField t1,
|
( Sqlite.FromField t1,
|
||||||
Sqlite.FromField t2,
|
Sqlite.FromField t2,
|
||||||
|
|
|
@ -17,6 +17,7 @@ let
|
||||||
pkgs.haskellPackages.pa-label
|
pkgs.haskellPackages.pa-label
|
||||||
pkgs.haskellPackages.pa-error-tree
|
pkgs.haskellPackages.pa-error-tree
|
||||||
pkgs.haskellPackages.pa-field-parser
|
pkgs.haskellPackages.pa-field-parser
|
||||||
|
depot.users.Profpatsch.my-prelude
|
||||||
pkgs.haskellPackages.foldl
|
pkgs.haskellPackages.foldl
|
||||||
pkgs.haskellPackages.sqlite-simple
|
pkgs.haskellPackages.sqlite-simple
|
||||||
pkgs.haskellPackages.xml-conduit
|
pkgs.haskellPackages.xml-conduit
|
||||||
|
|
|
@ -62,6 +62,7 @@ executable jbovlaste-sqlite
|
||||||
pa-label,
|
pa-label,
|
||||||
pa-error-tree,
|
pa-error-tree,
|
||||||
pa-field-parser,
|
pa-field-parser,
|
||||||
|
my-prelude,
|
||||||
containers,
|
containers,
|
||||||
selective,
|
selective,
|
||||||
semigroupoids,
|
semigroupoids,
|
||||||
|
|
|
@ -10,6 +10,7 @@ pkgs.haskellPackages.mkDerivation {
|
||||||
./src/AtLeast.hs
|
./src/AtLeast.hs
|
||||||
./src/MyPrelude.hs
|
./src/MyPrelude.hs
|
||||||
./src/Test.hs
|
./src/Test.hs
|
||||||
|
./src/Parse.hs
|
||||||
./src/Seconds.hs
|
./src/Seconds.hs
|
||||||
./src/Tool.hs
|
./src/Tool.hs
|
||||||
./src/ValidationParseT.hs
|
./src/ValidationParseT.hs
|
||||||
|
|
|
@ -62,6 +62,7 @@ library
|
||||||
Postgres.Decoder
|
Postgres.Decoder
|
||||||
Postgres.MonadPostgres
|
Postgres.MonadPostgres
|
||||||
ValidationParseT
|
ValidationParseT
|
||||||
|
Parse
|
||||||
Seconds
|
Seconds
|
||||||
Tool
|
Tool
|
||||||
|
|
||||||
|
|
|
@ -213,7 +213,7 @@ import Validation
|
||||||
)
|
)
|
||||||
|
|
||||||
-- | Forward-applying 'contramap', like '&'/'$' and '<&>'/'<$>' but for '>$<'.
|
-- | Forward-applying 'contramap', like '&'/'$' and '<&>'/'<$>' but for '>$<'.
|
||||||
(>&<) :: Contravariant f => f b -> (a -> b) -> f a
|
(>&<) :: (Contravariant f) => f b -> (a -> b) -> f a
|
||||||
(>&<) = flip contramap
|
(>&<) = flip contramap
|
||||||
|
|
||||||
infixl 5 >&<
|
infixl 5 >&<
|
||||||
|
@ -226,7 +226,7 @@ infixl 5 >&<
|
||||||
-- for functions : (a -> b) -> (b -> c) -> (a -> c)
|
-- for functions : (a -> b) -> (b -> c) -> (a -> c)
|
||||||
-- for Folds: Fold a b -> Fold b c -> Fold a c
|
-- for Folds: Fold a b -> Fold b c -> Fold a c
|
||||||
-- @@
|
-- @@
|
||||||
(&>>) :: Semigroupoid s => s a b -> s b c -> s a c
|
(&>>) :: (Semigroupoid s) => s a b -> s b c -> s a c
|
||||||
(&>>) = flip Data.Semigroupoid.o
|
(&>>) = flip Data.Semigroupoid.o
|
||||||
|
|
||||||
-- like >>>
|
-- like >>>
|
||||||
|
@ -334,7 +334,7 @@ annotate err = \case
|
||||||
Just a -> Right a
|
Just a -> Right a
|
||||||
|
|
||||||
-- | Map the same function over both sides of a Bifunctor (e.g. a tuple).
|
-- | Map the same function over both sides of a Bifunctor (e.g. a tuple).
|
||||||
both :: Bifunctor bi => (a -> b) -> bi a a -> bi b b
|
both :: (Bifunctor bi) => (a -> b) -> bi a a -> bi b b
|
||||||
both f = bimap f f
|
both f = bimap f f
|
||||||
|
|
||||||
-- | Find the first element for which pred returns `Just a`, and return the `a`.
|
-- | Find the first element for which pred returns `Just a`, and return the `a`.
|
||||||
|
@ -348,7 +348,7 @@ both f = bimap f f
|
||||||
-- Nothing
|
-- Nothing
|
||||||
-- >>> findMaybe (Text.Read.readMaybe @Int) ["foo", "34.40", "34", "abc"]
|
-- >>> findMaybe (Text.Read.readMaybe @Int) ["foo", "34.40", "34", "abc"]
|
||||||
-- Just 34
|
-- Just 34
|
||||||
findMaybe :: Foldable t => (a -> Maybe b) -> t a -> Maybe b
|
findMaybe :: (Foldable t) => (a -> Maybe b) -> t a -> Maybe b
|
||||||
findMaybe mPred list =
|
findMaybe mPred list =
|
||||||
let pred' x = Maybe.isJust $ mPred x
|
let pred' x = Maybe.isJust $ mPred x
|
||||||
in case Foldable.find pred' list of
|
in case Foldable.find pred' list of
|
||||||
|
@ -455,13 +455,13 @@ traverseFold1 f xs = fold1 <$> traverse f xs
|
||||||
--
|
--
|
||||||
-- Uses the same trick as https://hackage.haskell.org/package/protolude-0.3.0/docs/src/Protolude.Error.html#error
|
-- Uses the same trick as https://hackage.haskell.org/package/protolude-0.3.0/docs/src/Protolude.Error.html#error
|
||||||
{-# WARNING todo "'todo' (undefined code) remains in code" #-}
|
{-# WARNING todo "'todo' (undefined code) remains in code" #-}
|
||||||
todo :: forall (r :: RuntimeRep). forall (a :: TYPE r). HasCallStack => a
|
todo :: forall (r :: RuntimeRep). forall (a :: TYPE r). (HasCallStack) => a
|
||||||
todo = raise# (errorCallWithCallStackException "This code was not yet implemented: TODO" ?callStack)
|
todo = raise# (errorCallWithCallStackException "This code was not yet implemented: TODO" ?callStack)
|
||||||
|
|
||||||
-- | Convert an integer to a 'Natural' if possible
|
-- | Convert an integer to a 'Natural' if possible
|
||||||
--
|
--
|
||||||
-- Named the same as the function from "GHC.Natural", but does not crash.
|
-- Named the same as the function from "GHC.Natural", but does not crash.
|
||||||
intToNatural :: Integral a => a -> Maybe Natural
|
intToNatural :: (Integral a) => a -> Maybe Natural
|
||||||
intToNatural i =
|
intToNatural i =
|
||||||
if i < 0
|
if i < 0
|
||||||
then Nothing
|
then Nothing
|
||||||
|
@ -560,7 +560,7 @@ inverseMap f =
|
||||||
|
|
||||||
-- Sum {getSum = 6}
|
-- Sum {getSum = 6}
|
||||||
|
|
||||||
ifTrue :: Monoid m => Bool -> m -> m
|
ifTrue :: (Monoid m) => Bool -> m -> m
|
||||||
ifTrue pred' m = if pred' then m else mempty
|
ifTrue pred' m = if pred' then m else mempty
|
||||||
|
|
||||||
-- | If the given @Maybe@ is @Just@, return the @m@, else return mempty.
|
-- | If the given @Maybe@ is @Just@, return the @m@, else return mempty.
|
||||||
|
@ -570,18 +570,12 @@ ifTrue pred' m = if pred' then m else mempty
|
||||||
-- >>> import Data.Monoid (Sum(..))
|
-- >>> import Data.Monoid (Sum(..))
|
||||||
--
|
--
|
||||||
-- >>> :{ mconcat [
|
-- >>> :{ mconcat [
|
||||||
-- ifExists (Just [1]),
|
-- unknown command '{'
|
||||||
-- [2, 3, 4],
|
|
||||||
-- ifExists Nothing,
|
|
||||||
-- ]
|
|
||||||
-- :}
|
|
||||||
-- [1,2,3,4]
|
|
||||||
--
|
--
|
||||||
-- Or any other Monoid:
|
-- Or any other Monoid:
|
||||||
--
|
--
|
||||||
-- >>> mconcat [ Sum 1, ifExists (Just (Sum 2)), Sum 3 ]
|
-- >>> mconcat [ Sum 1, ifExists Sum (Just 2), Sum 3 ]
|
||||||
|
|
||||||
-- Sum {getSum = 6}
|
-- Sum {getSum = 6}
|
||||||
|
|
||||||
ifExists :: Monoid m => Maybe m -> m
|
ifExists :: (Monoid m) => (a -> m) -> Maybe a -> m
|
||||||
ifExists = fold
|
ifExists = foldMap
|
||||||
|
|
158
users/Profpatsch/my-prelude/src/Parse.hs
Normal file
158
users/Profpatsch/my-prelude/src/Parse.hs
Normal file
|
@ -0,0 +1,158 @@
|
||||||
|
{-# LANGUAGE QuasiQuotes #-}
|
||||||
|
|
||||||
|
module Parse where
|
||||||
|
|
||||||
|
import Control.Category qualified
|
||||||
|
import Control.Selective (Selective)
|
||||||
|
import Data.Error.Tree
|
||||||
|
import Data.Functor.Compose
|
||||||
|
import Data.List qualified as List
|
||||||
|
import Data.Monoid (First (..))
|
||||||
|
import Data.Semigroup.Traversable
|
||||||
|
import Data.Semigroupoid qualified as Semigroupoid
|
||||||
|
import Data.Text qualified as Text
|
||||||
|
import FieldParser (FieldParser)
|
||||||
|
import FieldParser qualified as Field
|
||||||
|
import PossehlAnalyticsPrelude
|
||||||
|
import Validation (partitionValidations)
|
||||||
|
import Prelude hiding (init, maybe)
|
||||||
|
import Prelude qualified
|
||||||
|
|
||||||
|
-- | A generic applicative “vertical” parser.
|
||||||
|
-- Similar to `FieldParser`, but made for parsing whole structures and collect all errors in an `ErrorTree`.
|
||||||
|
newtype Parse from to = Parse ((Context, from) -> Validation (NonEmpty ErrorTree) (Context, to))
|
||||||
|
deriving
|
||||||
|
(Functor, Applicative, Selective)
|
||||||
|
via ( Compose
|
||||||
|
( Compose
|
||||||
|
((->) (Context, from))
|
||||||
|
(Validation (NonEmpty ErrorTree))
|
||||||
|
)
|
||||||
|
((,) Context)
|
||||||
|
)
|
||||||
|
|
||||||
|
-- | Every parser can add to the context, like e.g. an element parser will add the name of the element it should be parsing.
|
||||||
|
-- This should be added to the error message of each parser, with `showContext`.
|
||||||
|
newtype Context = Context (Maybe [Text])
|
||||||
|
deriving stock (Show)
|
||||||
|
deriving (Semigroup, Monoid) via (First [Text])
|
||||||
|
|
||||||
|
instance Semigroupoid Parse where
|
||||||
|
o p2 p1 = Parse $ \from -> case runParse' p1 from of
|
||||||
|
Failure err -> Failure err
|
||||||
|
Success to1 -> runParse' p2 to1
|
||||||
|
|
||||||
|
instance Category Parse where
|
||||||
|
(.) = Semigroupoid.o
|
||||||
|
id = Parse $ \t -> Success t
|
||||||
|
|
||||||
|
instance Profunctor Parse where
|
||||||
|
lmap f (Parse p) = Parse $ lmap (second f) p
|
||||||
|
rmap = (<$>)
|
||||||
|
|
||||||
|
runParse :: Error -> Parse from to -> from -> Either ErrorTree to
|
||||||
|
runParse errMsg parser t =
|
||||||
|
(Context (Just ["$"]), t)
|
||||||
|
& runParse' parser
|
||||||
|
<&> snd
|
||||||
|
& first (nestedMultiError errMsg)
|
||||||
|
& validationToEither
|
||||||
|
|
||||||
|
runParse' :: Parse from to -> (Context, from) -> Validation (NonEmpty ErrorTree) (Context, to)
|
||||||
|
runParse' (Parse f) from = f from
|
||||||
|
|
||||||
|
showContext :: Context -> Text
|
||||||
|
showContext (Context context) = context & fromMaybe [] & List.reverse & Text.intercalate "."
|
||||||
|
|
||||||
|
addContext :: Text -> Context -> Context
|
||||||
|
addContext x (Context mxs) = Context (Just $ x : (mxs & fromMaybe []))
|
||||||
|
|
||||||
|
-- | Accept only exactly the given value
|
||||||
|
exactly :: (Eq from) => (from -> Text) -> from -> Parse from from
|
||||||
|
exactly errDisplay from = Parse $ \(ctx, from') ->
|
||||||
|
if from == from'
|
||||||
|
then Success (ctx, from')
|
||||||
|
else Failure $ singleton [fmt|Field has to be exactly {errDisplay from}, was: {errDisplay from'} at {showContext ctx}|]
|
||||||
|
|
||||||
|
-- | Make a parser to parse the whole list
|
||||||
|
multiple :: Parse a1 a2 -> Parse [a1] [a2]
|
||||||
|
multiple inner = dimap nonEmpty (Prelude.maybe [] toList) (maybe $ multipleNE inner)
|
||||||
|
|
||||||
|
-- | Make a parser to parse the whole non-empty list
|
||||||
|
multipleNE :: Parse from to -> Parse (NonEmpty from) (NonEmpty to)
|
||||||
|
multipleNE inner = Parse $ \(ctx, from) ->
|
||||||
|
from
|
||||||
|
& zipIndex
|
||||||
|
& traverse (\(idx, f) -> runParse' inner (ctx, f) & first (singleton . nestedMultiError [fmt|{idx}|]))
|
||||||
|
-- we assume that, since the same parser is used everywhere, the context will be the same as well (TODO: correct?)
|
||||||
|
& second (\((ctx', y) :| ys) -> (ctx', y :| (snd <$> ys)))
|
||||||
|
|
||||||
|
-- | Lift a parser into an optional value
|
||||||
|
maybe :: Parse from to -> Parse (Maybe from) (Maybe to)
|
||||||
|
maybe inner = Parse $ \(ctx, m) -> case m of
|
||||||
|
Nothing -> Success (ctx, Nothing)
|
||||||
|
Just a -> runParse' inner (ctx, a) & second (fmap Just)
|
||||||
|
|
||||||
|
-- | Assert that there is exactly one element in the list
|
||||||
|
exactlyOne :: Parse [from] from
|
||||||
|
exactlyOne = Parse $ \(ctx, xs) -> case xs of
|
||||||
|
[] -> Failure $ singleton [fmt|Expected exactly 1 element, but got 0, at {ctx & showContext}|]
|
||||||
|
[one] -> Success (ctx, one)
|
||||||
|
_more -> Failure $ singleton [fmt|Expected exactly 1 element, but got 2, at {ctx & showContext}|]
|
||||||
|
|
||||||
|
-- | Assert that there is exactly zero or one element in the list
|
||||||
|
zeroOrOne :: Parse [from] (Maybe from)
|
||||||
|
zeroOrOne = Parse $ \(ctx, xs) -> case xs of
|
||||||
|
[] -> Success (ctx, Nothing)
|
||||||
|
[one] -> Success (ctx, Just one)
|
||||||
|
_more -> Failure $ singleton [fmt|Expected exactly 1 element, but got 2, at {ctx & showContext}|]
|
||||||
|
|
||||||
|
-- | Find the first element on which the sub-parser succeeds; if there was no match, return all error messages.
|
||||||
|
find :: Parse from to -> Parse [from] to
|
||||||
|
find inner = Parse $ \(ctx, xs) -> case xs of
|
||||||
|
[] -> failure [fmt|Wanted to get the first sub-parser that succeeds, but there were no elements in the list, at {ctx & showContext}|]
|
||||||
|
(y : ys) -> runParse' (findNE' inner) (ctx, y :| ys)
|
||||||
|
|
||||||
|
-- | Find the first element on which the sub-parser succeeds; if there was no match, return all error messages.
|
||||||
|
findNE' :: Parse from to -> Parse (NonEmpty from) to
|
||||||
|
findNE' inner = Parse $ \(ctx, xs) ->
|
||||||
|
xs
|
||||||
|
<&> (\x -> runParse' inner (ctx, x))
|
||||||
|
& traverse1
|
||||||
|
( \case
|
||||||
|
Success a -> Left a
|
||||||
|
Failure e -> Right e
|
||||||
|
)
|
||||||
|
& \case
|
||||||
|
Left a -> Success a
|
||||||
|
Right errs ->
|
||||||
|
errs
|
||||||
|
& zipIndex
|
||||||
|
<&> (\(idx, errs') -> nestedMultiError [fmt|{idx}|] errs')
|
||||||
|
& nestedMultiError [fmt|None of these sub-parsers succeeded|]
|
||||||
|
& singleton
|
||||||
|
& Failure
|
||||||
|
|
||||||
|
-- | Find all elements on which the sub-parser succeeds; if there was no match, return an empty list
|
||||||
|
findAll :: Parse from to -> Parse [from] [to]
|
||||||
|
findAll inner = Parse $ \(ctx, xs) ->
|
||||||
|
xs
|
||||||
|
<&> (\x -> runParse' inner (ctx, x))
|
||||||
|
& partitionValidations
|
||||||
|
& \case
|
||||||
|
(_miss, []) ->
|
||||||
|
-- in this case we just arbitrarily forward the original context …
|
||||||
|
Success (ctx, [])
|
||||||
|
(_miss, (hitCtx, hit) : hits) -> Success (hitCtx, hit : (hits <&> snd))
|
||||||
|
|
||||||
|
-- | convert a 'FieldParser' into a 'Parse'.
|
||||||
|
fieldParser :: FieldParser from to -> Parse from to
|
||||||
|
fieldParser fp = Parse $ \(ctx, from) -> case Field.runFieldParser fp from of
|
||||||
|
Right a -> Success (ctx, a)
|
||||||
|
Left err -> Failure $ singleton (singleError err)
|
||||||
|
|
||||||
|
zipNonEmpty :: NonEmpty a -> NonEmpty b -> NonEmpty (a, b)
|
||||||
|
zipNonEmpty (x :| xs) (y :| ys) = (x, y) :| zip xs ys
|
||||||
|
|
||||||
|
zipIndex :: NonEmpty b -> NonEmpty (Natural, b)
|
||||||
|
zipIndex = zipNonEmpty (1 :| [2 :: Natural ..])
|
Loading…
Reference in a new issue