{-# LANGUAGE GeneralizedNewtypeDeriving #-} {-# LANGUAGE OverloadedStrings #-} {-| Module: Data.Gedcom.ParseMonads Description: Monads for parsing GEDCOM records Copyright: (c) Callum Lowcay, 2017 License: BSD3 Maintainer: cwslowcay@gmail.com Stability: experimental Portability: GHC This module contains monads and utility functions for extracting GEDCOM records from the raw syntax tree. -} module Data.Gedcom.ParseMonads ( StructureParser, MultiMonad, runMultiMonad, parseMulti, parseOptional, parseRequired, StructureMonad, addReference, runStructure ) where import Control.Monad import Control.Monad.Except import Control.Monad.State import Data.Dynamic import Data.Either import Data.Foldable import Data.Gedcom.Common import Data.Maybe import Data.Monoid import qualified Data.Map as M import qualified Data.Text.All as T -- | A parser that extracts a GEDCOM structure from a GEDCOM subtree. type StructureParser a = GDTree -- ^ The subtree to parse. -> StructureMonad (Either GDTree a) -- ^ Either parsed structure, or the subtree itself if the subtree doesn't contain the expected GEDCOM structure. -- | A Monad for parsing GEDCOM structures out of a list of GEDCOM subtrees. newtype MultiMonad a = MultiMonad (ExceptT GDError (StateT [GDTree] StructureMonad) a) deriving (Monad, Functor, Applicative, MonadError GDError) -- | Run a 'MultiMonad' into a 'StructureMonad'. runMultiMonad :: [GDTree] -- ^ The subtrees to parse the structure from. -> MultiMonad a -- ^ The MultiMonad that does the parsing. -> StructureMonad a runMultiMonad children (MultiMonad m) = ((flip evalStateT) children.runExceptT$ m) >>= rethrowError where rethrowError x = case x of Left e -> throwError e Right v -> return v -- | Parse multiple instances of a structure parseMulti :: StructureParser a -> MultiMonad [a] parseMulti p = MultiMonad$ do ls <- lift$ get (others, vs) <- lift$ lift$ partitionEithers <$> p `traverse` ls lift$ put others return vs -- | Parse an optional instance of a structure parseOptional :: StructureParser a -> MultiMonad (Maybe a) parseOptional p = MultiMonad$ do ls <- lift$ get (mr, leftover) <- lift$ lift$ foldrM (\v (r, rest) -> if isJust r then return (r, v:rest) else pick v rest.toMaybe <$> p v) (Nothing, []) ls lift$ put leftover return mr where toMaybe (Left _) = Nothing toMaybe (Right v) = Just v pick v rest Nothing = (Nothing, v:rest) pick _ rest x = (x, rest) -- | Parse a required instance of a structure parseRequired :: GDTag -- ^ The tag that identifies the required structure. -> StructureParser a -> MultiMonad a parseRequired tag p = do r <- parseOptional p case r of Just v -> return v Nothing -> throwError.TagError$ "Could not find required " <> (T.show tag) <> " tag" -- | A monad for parsing an instance of a GEDCOM structure from a GEDCOM -- subtree. newtype StructureMonad a = StructureMonad (ExceptT GDError (State (M.Map GDXRefID Dynamic)) a) deriving (Monad, Functor, Applicative, MonadError GDError) -- | Add a reference to the cross reference table. addReference :: Typeable a => GDXRefID -- ^ The cross reference to add. -> a -- ^ The value the reference will resolve to. -> StructureMonad () addReference thisID value = StructureMonad$ do alreadySeen <- M.member thisID <$> lift get when alreadySeen$ throwError.DuplicateRef$ "Duplicate definition of " <> (T.show thisID) lift.modify$ M.insert thisID (toDyn value) return () -- | Run a 'StructureMonad', returning either an error or a value, and the -- cross reference table. runStructure :: StructureMonad a -> (Either GDError a, M.Map GDXRefID Dynamic) runStructure (StructureMonad m) = (flip runState) M.empty . runExceptT$ m