htoml-parse-0.1: src/Data/Toml/Parse.hs
----------------------------------------------------------------------------
-- |
-- Module : Data.Toml.Parse
-- Copyright : (c) Sergey Vinokurov 2018
-- License : Apache-2.0 (see LICENSE)
-- Maintainer : serg.foo@gmail.com
----------------------------------------------------------------------------
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE DeriveTraversable #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE ImportQualifiedPost #-}
{-# LANGUAGE InstanceSigs #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TypeFamilies #-}
module Data.Toml.Parse
( Node(..)
, Parser
, runParser
, mkTomlError
, AtomicTomlError(..)
, TomlError
, (<?>)
, L
, extract
, TomlParse(..)
, FromToml(..)
, Index(..)
, (.!=)
, pTable
, pKey
, pKeyMaybe
, pStr
, pStrL
, pBool
, pInt
, pIntL
, pDouble
, pDoubleL
, pDatetime
, pDatetimeL
, pTArray
, pArray
, pCases
, ppToml
) where
import Control.Applicative
import Control.Comonad
import Control.Monad.Except
import Data.Bifunctor
import Data.DList (DList)
import Data.DList qualified as DL
import Data.Foldable
import Data.HashMap.Strict qualified as HM
import Data.List.NonEmpty (NonEmpty(..))
import Data.Map.Strict (Map)
import Data.Map.Strict qualified as M
import Data.Maybe
import Data.Text (Text)
import Data.Text qualified as T
import Data.Time (UTCTime)
import Data.Time.Format.ISO8601
import Data.Traversable
import Data.Vector (Vector)
import Data.Vector qualified as V
import Data.Void (Void, vacuous)
import Prettyprinter
import Prettyprinter.Combinators
import Prettyprinter.Generics
import Text.Toml
import Unsafe.Coerce
data TomlType
= TTable
| TTArray
| TString
| TInteger
| TFloat
| TBoolean
| TDatetime
| TArray
deriving (Eq, Ord, Show, Generic)
getType :: Node -> TomlType
getType = \case
VTable{} -> TTable
VTArray{} -> TTArray
VString{} -> TString
VInteger{} -> TInteger
VFloat{} -> TFloat
VBoolean{} -> TBoolean
VDatetime{} -> TDatetime
VArray{} -> TArray
ppTomlType :: TomlType -> (Doc ann, Doc ann)
ppTomlType = \case
TTable -> ("a", "table")
TTArray -> ("a", "table array")
TString -> ("a", "string")
TInteger -> ("an", "integer")
TFloat -> ("a", "float")
TBoolean -> ("a", "boolean")
TDatetime -> ("a", "datetime")
TArray -> ("an", "array")
data TomlPath
= PathIndex !Int
| PathKey !Text
| PathOther !Text
deriving (Eq, Ord, Show, Generic)
instance Pretty TomlPath where
pretty = \case
PathIndex n -> "In array element" <+> pretty n
PathKey str -> "In table key" <+> squotes (pretty str)
PathOther thing -> "While parsing" <+> pretty thing
data AtomicTomlError
= UnexpectedType
!TomlType -- ^ Expected
Node -- ^ Got
| MissingKey !Text Table
| IndexOutOfBounds !Int Node
| OtherError (Doc Void)
deriving (Show, Generic)
-- | Prettyprint toml value.
ppToml :: Node -> Doc ann
ppToml = \case
VTable x -> ppHashMapWith pretty ppToml x
VTArray xs -> ppVectorWith (ppHashMapWith pretty ppToml) xs
VString x -> pretty x
VInteger x -> pretty x
VFloat x -> pretty x
VBoolean x -> pretty x
VDatetime x -> pretty $ iso8601Show x
VArray xs -> ppVectorWith ppToml xs
instance Pretty AtomicTomlError where
pretty = \case
UnexpectedType expected got ->
"Expected to find" <+> article <+> typ <+> "but found" <+> article' <+> typ' <> "." <+>
"Node:" ## ppToml got
where
(article, typ) = ppTomlType expected
(article', typ') = ppTomlType $ getType got
MissingKey key tab -> "Missing key" <+> squotes (pretty key) <+> "in table:" ## ppHashMapWith pretty ppToml tab
IndexOutOfBounds ix node -> "Index" <+> pretty ix <+> "is out of bounds in array:" ## ppToml node
OtherError err -> "Other error:" ## vacuous err
data TomlError
= ErrorEmpty
| ErrorAtomic !AtomicTomlError
-- | Invariant: children of ErrorAnd never share common prefix.
| ErrorAnd TomlError TomlError
-- | Invariant: children of ErrorOr never share common prefix.
| ErrorOr TomlError TomlError
| ErrorPrefix (NonEmpty TomlPath) TomlError
deriving (Show, Generic)
instance Pretty TomlError where
pretty = \case
ErrorEmpty -> "Control.Applicative.empty"
ErrorAtomic err -> pretty err
ErrorAnd x y -> "AND" ## align (vsep $ map pretty $ toList $ collectConjuctions x y)
ErrorOr x y -> "OR" ## align (vsep $ map pretty $ toList $ collectDisjunctions x y)
ErrorPrefix ps e -> foldr (\p acc -> pretty p ## acc) (pretty e) ps
where
collectConjuctions :: TomlError -> TomlError -> DList TomlError
collectConjuctions (ErrorAnd a b) (ErrorAnd c d) = collectConjuctions a b <> collectConjuctions c d
collectConjuctions (ErrorAnd a b) c = collectConjuctions a b <> DL.singleton c
collectConjuctions a (ErrorAnd c d) = DL.singleton a <> collectConjuctions c d
collectConjuctions a c = DL.fromList [a, c]
collectDisjunctions :: TomlError -> TomlError -> DList TomlError
collectDisjunctions (ErrorOr a b) (ErrorOr c d) = collectDisjunctions a b <> collectDisjunctions c d
collectDisjunctions (ErrorOr a b) c = collectDisjunctions a b <> DL.singleton c
collectDisjunctions a (ErrorOr c d) = DL.singleton a <> collectDisjunctions c d
collectDisjunctions a c = DL.fromList [a, c]
-- NB order of constructors is important
data IsCommitted = Uncommitted | Committed
deriving (Eq, Ord, Show, Enum, Bounded)
instance Semigroup IsCommitted where
{-# INLINE (<>) #-}
(<>) = max
newtype Validation a = Validation
{ unValidation :: Either (IsCommitted, TomlError) a }
deriving (Functor)
zipErrors
:: (TomlError -> TomlError -> TomlError)
-> TomlError
-> TomlError
-> TomlError
zipErrors f x y = case commonPrefix x y of
Nothing -> f x y
Just (common, x', y') ->
ErrorPrefix common (f x' y')
commonPrefix
:: TomlError
-> TomlError
-> Maybe (NonEmpty TomlPath, TomlError, TomlError)
commonPrefix x y = case (x, y) of
(ErrorPrefix px x', ErrorPrefix py y') ->
flip fmap (go px py) $ \(common, px', py') ->
let prefix [] err = err
prefix (p : ps) err = ErrorPrefix (p :| ps) err
in (common, prefix px' x', prefix py' y')
_ -> Nothing
where
go :: NonEmpty TomlPath -> NonEmpty TomlPath -> Maybe (NonEmpty TomlPath, [TomlPath], [TomlPath])
go xs ys =
case go' [] (toList xs) (toList ys) of
(c : cs, xs', ys') -> Just (c :| cs, xs', ys')
_ -> Nothing
go' :: Eq a => [a] -> [a] -> [a] -> ([a], [a], [a])
go' common (a : as) (b : bs)
| a == b = go' (a : common) as bs
go' common as bs = (reverse common, as, bs)
instance Applicative Validation where
{-# INLINE pure #-}
pure = Validation . pure
{-# NOINLINE (<*>) #-}
(<*>) vf'@(Validation vf) vx'@(Validation vx) =
case (vf, vx) of
(Left (cf, ef), Left (cx, ex)) -> Validation $ Left (cf <> cx, zipErrors ErrorAnd ef ex)
(Left _, _) -> unsafeCoerce vf'
(_, Left _) -> unsafeCoerce vx'
(Right f, Right x) -> Validation $ Right $ f x
instance Alternative Validation where
{-# INLINE empty #-}
empty = Validation $ Left (Uncommitted, ErrorEmpty)
{-# NOINLINE (<|>) #-}
(<|>) x'@(Validation x) y'@(Validation y) =
case (x, y) of
(Right _, _) -> x'
(_, Right _) -> y'
(Left (cf, ef), Left (cx, ex)) ->
case (cf, cx) of
(Committed, Uncommitted) -> x'
(Uncommitted, Committed) -> y'
_ -> Validation $ Left (cf <> cx, zipErrors ErrorOr ef ex)
instance Monad Validation where
{-# INLINE (>>=) #-}
{-# INLINE (>>) #-}
(>>=) x'@(Validation x) f =
case x of
Left _ -> unsafeCoerce x'
Right y -> commit $ f y
where
commit (Validation (Left (_, err))) = Validation $ Left (Committed, err)
commit z@(Validation (Right _)) = z
(>>) = (*>)
instance MonadPlus Validation
newtype ParseEnv = ParseEnv { unParseEnv :: [TomlPath] }
deriving (Eq, Ord, Show, Generic, Pretty)
newtype Parser a = Parser
{ unParser :: Validation a }
deriving (Functor, Applicative, Alternative, MonadPlus)
instance Monad Parser where
{-# INLINE (>>=) #-}
{-# INLINE (>>) #-}
(>>=) (Parser x) f = Parser $ do
x' <- x
unParser $ f x'
(>>) = (*>)
infixl 9 <?>
-- | Add textual annotation to the provided located thing. The annotation will
-- be shows as part of error message if the location ultimately gets passed to
-- 'throwParseError'.
(<?>) :: L a -> Text -> L a
(<?>) (L env x) y = L (inside (PathOther y) env) x
instance TomlParse Parser where
throwParseError loc err = Parser $ Validation $ Left (Uncommitted, mkTomlError' loc err)
runParser :: a -> (L a -> Parser b) -> Either (Doc Void) b
runParser x f
= first (("Error while parsing:" ##) . pretty . snd)
$ unValidation
$ unParser
$ f
$ L (ParseEnv []) x
mkTomlError :: L a -> Doc Void -> TomlError
mkTomlError loc = mkTomlError' loc . OtherError
mkTomlError' :: L a -> AtomicTomlError -> TomlError
mkTomlError' (L env _) err = case reverse $ unParseEnv env of
[] -> ErrorAtomic err
p : ps -> ErrorPrefix (p :| ps) $ ErrorAtomic err
-- | Adds to 'a' its provenance in the toml file.
data L a = L ParseEnv a
deriving (Eq, Ord, Show, Functor, Foldable, Traversable, Generic)
instance Pretty a => Pretty (L a) where pretty = ppGeneric
instance Comonad L where
{-# INLINE extract #-}
{-# INLINE duplicate #-}
extract (L _ x) = x
duplicate orig@(L env _) = L env orig
{-# INLINE inside #-}
inside :: TomlPath -> ParseEnv -> ParseEnv
inside x (ParseEnv xs) = ParseEnv (x : xs)
class (Applicative m, Alternative m) => TomlParse m where
throwParseError :: L b -> AtomicTomlError -> m a
class FromToml a b where
fromToml :: L a -> Parser b
instance FromToml a (L a) where
{-# INLINE fromToml #-}
fromToml = pure
instance FromToml a a where
{-# INLINE fromToml #-}
fromToml = pure . extract
instance FromToml Node String where
{-# INLINE fromToml #-}
fromToml = fmap T.unpack . pStr
instance FromToml Node Text where
{-# INLINE fromToml #-}
fromToml = pStr
instance FromToml Node Bool where
{-# INLINE fromToml #-}
fromToml = pBool
instance FromToml Node Int where
{-# INLINE fromToml #-}
fromToml = pInt
instance FromToml Node Double where
{-# INLINE fromToml #-}
fromToml = pDouble
instance FromToml Node UTCTime where
{-# INLINE fromToml #-}
fromToml = pDatetime
instance (Ord k, FromToml Text k, FromToml Node v) => FromToml Node (Map k v) where
fromToml = pTable >=> fromToml
instance (Ord k, FromToml Text k, FromToml Node v) => FromToml Table (Map k v) where
fromToml (L env y) = do
ys <- for (HM.toList y) $ \(k, v) ->
(,)
<$> fromToml (L env k)
<*> fromToml (L (inside (PathKey k) env) v)
pure $ M.fromList ys
instance FromToml Node a => FromToml Node (Vector a) where
fromToml = pArray >=> traverse fromToml
instance FromToml Node a => FromToml Node (NonEmpty a) where
fromToml x = do
ys <- pArray x
case toList ys of
[] -> throwParseError x $ OtherError "Expected a non-empty list"
z : zs -> (:|) <$> fromToml z <*> traverse fromToml zs
infixl 5 .:, .:?, .!=
class Index a where
(.:) :: FromToml Node b => a -> Text -> Parser b
(.:?) :: FromToml Node b => a -> Text -> Parser (Maybe b)
instance Index (L Table) where
{-# INLINE (.:) #-}
{-# INLINE (.:?) #-}
(.:) x key = pKey key x >>= fromToml
(.:?) x key = traverse fromToml $ liftMaybe $ pKeyMaybe key x
instance Index (L Node) where
{-# INLINE (.:) #-}
{-# INLINE (.:?) #-}
(.:) x key = pTable x >>= pKey key >>= fromToml
(.:?) x key = pTable x >>= traverse fromToml . liftMaybe . pKeyMaybe key
instance a ~ L Node => Index (Parser a) where
{-# INLINE (.:) #-}
{-# INLINE (.:?) #-}
(.:) x key = x >>= pTable >>= pKey key >>= fromToml
(.:?) x key = x >>= pTable >>= traverse fromToml . liftMaybe . pKeyMaybe key
-- | Assign default value to a parser that produces 'Maybe'. Typically used together with '.:?':
--
-- > foo .:? "bar" .!= 10
{-# INLINE (.!=) #-}
(.!=) :: Functor m => m (Maybe a) -> a -> m a
(.!=) action def = fromMaybe def <$> action
pTable :: TomlParse m => L Node -> m (L Table)
pTable = \case
L env (VTable x) -> pure $ L env x
other@(L _ other') -> throwParseError other $ UnexpectedType TTable other'
pKey :: TomlParse m => Text -> L Table -> m (L Node)
pKey key tab'@(L _ tab) = case liftMaybe $ pKeyMaybe key tab' of
Just x -> pure x
Nothing -> throwParseError tab' $ MissingKey key tab
pKeyMaybe :: Text -> L Table -> L (Maybe Node)
pKeyMaybe key (L env tab) = L (inside (PathKey key) env) $ HM.lookup key tab
pStr :: TomlParse m => L Node -> m Text
pStr = fmap extract . pStrL
pStrL :: TomlParse m => L Node -> m (L Text)
pStrL = \case
L env (VString x) -> pure $ L env x
other@(L _ other') -> throwParseError other $ UnexpectedType TString other'
pBool :: TomlParse m => L Node -> m Bool
pBool = \case
L _ (VBoolean x) -> pure x
other@(L _ other') -> throwParseError other $ UnexpectedType TBoolean other'
pInt :: TomlParse m => L Node -> m Int
pInt = fmap extract . pIntL
pIntL :: TomlParse m => L Node -> m (L Int)
pIntL = \case
L env (VInteger x) -> pure $ L env $ fromIntegral x
other@(L _ other') -> throwParseError other $ UnexpectedType TInteger other'
pDouble :: TomlParse m => L Node -> m Double
pDouble = fmap extract . pDoubleL
pDoubleL :: TomlParse m => L Node -> m (L Double)
pDoubleL = \case
L env (VFloat x) -> pure $ L env x
other@(L _ other') -> throwParseError other $ UnexpectedType TFloat other'
pDatetime :: TomlParse m => L Node -> m UTCTime
pDatetime = fmap extract . pDatetimeL
pDatetimeL :: TomlParse m => L Node -> m (L UTCTime)
pDatetimeL = \case
L env (VDatetime x) -> pure $ L env x
other@(L _ other') -> throwParseError other $ UnexpectedType TDatetime other'
pTArray :: TomlParse m => L Node -> m (Vector (L Table))
pTArray = \case
L env (VTArray x) -> pure $ (\(n, x') -> L (inside (PathIndex n) env) x') <$> V.indexed x
other@(L _ other') -> throwParseError other $ UnexpectedType TTArray other'
pArray :: TomlParse m => L Node -> m (Vector (L Node))
pArray = \case
L env (VArray x) -> pure $ (\(n, x') -> L (inside (PathIndex n) env) x') <$> V.indexed x
other@(L _ other') -> throwParseError other $ UnexpectedType TArray other'
{-# INLINE pCases #-}
pCases :: (Ord k, FromToml Node k, Pretty k) => Map k v -> L Node -> Parser v
pCases env = \x -> do
k <- fromToml x
case M.lookup k env of
Just v -> pure v
Nothing -> throwParseError x $ OtherError $
"Unexpected value" <+> squotes (pretty k) <> "." <+>
"Expected one of" <+> vsep (punctuate "," (map pretty (M.keys env)))
liftMaybe :: L (Maybe a) -> Maybe (L a)
liftMaybe (L env x) = L env <$> x