yaml-streamly-0.12.5: src/Data/Yaml/Parser.hs
{-# LANGUAGE CPP #-}
{-# LANGUAGE DeriveDataTypeable #-}
{-# LANGUAGE OverloadedStrings #-}
-- | NOTE: This module is a highly experimental preview release. It may change
-- drastically, or be entirely removed, in a future release.
module Data.Yaml.Parser where
import Control.Applicative
import Control.Monad (MonadPlus (..), liftM, ap)
import Control.Exception.Safe
import Control.Monad.IO.Class
import Control.Monad.Trans.Writer.Strict (tell, WriterT, runWriterT)
import Data.ByteString (ByteString)
import qualified Data.Map as Map
#if !MIN_VERSION_base(4,8,0)
import Data.Monoid (Monoid (..))
#endif
#if !MIN_VERSION_base(4,11,0)
import Data.Semigroup (Semigroup(..))
#endif
import Data.Yaml.Internal
import Data.Text (Text, pack, unpack)
import Data.Text.Encoding (decodeUtf8)
import Data.Text.Read (signed, decimal)
import Streamly.Data.Stream (Stream)
import Streamly.Data.ParserK (ParserK)
import qualified Streamly.Data.Stream as Stream
import qualified Streamly.Data.StreamK as StreamK
import qualified Streamly.Data.ParserK as ParserK
import Text.Libyaml
newtype YamlParser a = YamlParser
{ unYamlParser :: AnchorMap -> Either Text a
}
instance Functor YamlParser where
fmap = liftM
instance Applicative YamlParser where
pure = YamlParser . const . Right
(<*>) = ap
instance Alternative YamlParser where
empty = fail "empty"
(<|>) = mplus
instance Semigroup (YamlParser a) where
(<>) = mplus
instance Monoid (YamlParser a) where
mempty = fail "mempty"
#if !MIN_VERSION_base(4,11,0)
mappend = (<>)
#endif
instance Monad YamlParser where
return = pure
YamlParser f >>= g = YamlParser $ \am ->
case f am of
Left t -> Left t
Right x -> unYamlParser (g x) am
#if MIN_VERSION_base(4,13,0)
instance MonadFail YamlParser where
#endif
fail = YamlParser . const . Left . pack
instance MonadPlus YamlParser where
mzero = fail "mzero"
mplus a b = YamlParser $ \am ->
case unYamlParser a am of
Left _ -> unYamlParser b am
x -> x
lookupAnchor :: AnchorName -> YamlParser (Maybe YamlValue)
lookupAnchor name = YamlParser $ Right . Map.lookup name
withAnchor :: AnchorName -> Text -> (YamlValue -> YamlParser a) -> YamlParser a
withAnchor name expected f = do
mv <- lookupAnchor name
case mv of
Nothing -> fail $ unpack expected ++ ": unknown alias " ++ name
Just v -> f v
withMapping :: Text -> ([(Text, YamlValue)] -> YamlParser a) -> YamlValue -> YamlParser a
withMapping _ f (Mapping m _) = f m
withMapping expected f (Alias an) = withAnchor an expected $ withMapping expected f
withMapping expected _ v = typeMismatch expected v
withSequence :: Text -> ([YamlValue] -> YamlParser a) -> YamlValue -> YamlParser a
withSequence _ f (Sequence s _) = f s
withSequence expected f (Alias an) = withAnchor an expected $ withSequence expected f
withSequence expected _ v = typeMismatch expected v
withText :: Text -> (Text -> YamlParser a) -> YamlValue -> YamlParser a
withText _ f (Scalar s _ _ _) = f $ decodeUtf8 s
withText expected f (Alias an) = withAnchor an expected $ withText expected f
withText expected _ v = typeMismatch expected v
typeMismatch :: Text -> YamlValue -> YamlParser a
typeMismatch expected v =
fail $ concat
[ "Expected "
, unpack expected
, ", but got: "
, t
]
where
t = case v of
Mapping _ _ -> "mapping"
Sequence _ _ -> "sequence"
Scalar _ _ _ _ -> "scalar"
Alias _ -> "alias"
class FromYaml a where
fromYaml :: YamlValue -> YamlParser a
instance FromYaml YamlValue where
fromYaml = return
instance FromYaml a => FromYaml [a] where
fromYaml = withSequence "[a]" (mapM fromYaml)
instance FromYaml Text where
fromYaml = withText "Text" return
instance FromYaml Int where
fromYaml =
withText "Int" go
where
go t =
case signed decimal t of
Right (i, "") -> return i
_ -> fail $ "Invalid Int: " ++ unpack t
data YamlValue
= Mapping [(Text, YamlValue)] Anchor
| Sequence [YamlValue] Anchor
| Scalar ByteString Tag Style Anchor
| Alias AnchorName
deriving Show
type AnchorMap = Map.Map AnchorName YamlValue
data RawDoc = RawDoc YamlValue AnchorMap
deriving Show
parseRawDoc :: (FromYaml a, MonadThrow m) => RawDoc -> m a
parseRawDoc (RawDoc val am) =
case unYamlParser (fromYaml val) am of
Left t -> throwM $ FromYamlException t
Right x -> return x
(.:) :: FromYaml a => [(Text, YamlValue)] -> Text -> YamlParser a
o .: k =
case lookup k o of
Nothing -> fail $ "Key not found: " ++ unpack k
Just v -> fromYaml v
data YamlParseException
= UnexpectedEndOfEvents
| UnexpectedEvent Event
| FromYamlException Text
deriving (Show, Typeable)
instance Exception YamlParseException
{-# INLINE sinkValue #-}
sinkValue :: (MonadIO m, MonadCatch m, MonadThrow m) => ParserK Event (WriterT AnchorMap m) YamlValue
sinkValue = start
where
start = anyEvent >>= maybe (ParserK.die "Unexpected end of events") go
tell' Nothing val = return val
tell' (Just name) val = do
ParserK.fromEffect $ tell $ Map.singleton name val
return val
go EventStreamStart = start
go EventDocumentStart = start
go (EventAlias a) = return $ Alias a
go (EventScalar a b c d) = tell' d $ Scalar a b c d
go (EventSequenceStart _tag _style mname) = do
vals <- goS id
let val = Sequence vals mname
tell' mname val
go (EventMappingStart _tag _style mname) = do
pairs <- goM id
let val = Mapping pairs mname
tell' mname val
go e = missed (Just e)
goS front = do
me <- anyEvent
case me of
Nothing -> ParserK.die "Unexpected end of events"
Just EventSequenceEnd -> return $ front []
Just e -> do
val <- go e
goS (front . (val:))
goM front = do
mk <- anyEvent
case mk of
Nothing -> ParserK.die "Unexpected end of events"
Just EventMappingEnd -> return $ front []
Just (EventScalar a b c d) -> do
_ <- tell' d $ Scalar a b c d
let k = decodeUtf8 a
v <- start
goM (front . ((k, v):))
Just e -> missed (Just e)
{-# INLINE sinkRawDoc #-}
sinkRawDoc :: Stream IO Event -> IO RawDoc
sinkRawDoc src = do
(res, aMap) <- runWriterT (StreamK.parse sinkValue (StreamK.fromStream (Stream.morphInner liftIO src)))
case res of
-- XXX Is YamlException the right exception to throw here?
Left err -> throwIO $ YamlException $ displayException err
Right val -> pure $ RawDoc val aMap
readYamlFile :: FromYaml a => FilePath -> IO a
readYamlFile fp = sinkRawDoc (decodeFile fp) >>= parseRawDoc