yaml-streamly-0.12.5: src/Data/Yaml/Include.hs
{-# LANGUAGE CPP #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE FlexibleContexts #-}
module Data.Yaml.Include (
decodeFile
, decodeFileEither
, decodeFileWithWarnings
) where
#if !MIN_VERSION_directory(1, 2, 3)
import Control.Exception (handleJust)
import Control.Monad (guard)
import System.IO.Error (ioeGetFileName, ioeGetLocation, isDoesNotExistError)
#endif
import Control.Monad (when)
import Control.Monad.IO.Class (MonadIO(..))
import Data.Aeson (FromJSON)
import Data.Text (unpack)
import Data.Text.Encoding (decodeUtf8)
import System.Directory
import System.FilePath
import Data.Yaml.Internal (ParseException(..), Warning(..), decodeHelper_, decodeHelper)
import Text.Libyaml hiding (decodeFile)
import qualified Text.Libyaml as Y
import Control.Exception.Safe
import Streamly.Data.Stream (Stream)
import Streamly.Data.StreamK (StreamK)
import qualified Streamly.Data.StreamK as K
import qualified Streamly.Data.Stream as S
eventsFromFile
:: (MonadCatch m, MonadIO m, MonadMask m)
=> FilePath
-> Stream m Event
eventsFromFile = K.toStream . go []
where
go :: (MonadCatch m, MonadIO m, MonadMask m) => [FilePath] -> FilePath -> StreamK m Event
go seen fp = K.concatEffect $ do
cfp <- liftIO $ handleNotFound $ canonicalizePath fp
when (cfp `elem` seen) $ throwIO CyclicIncludes
pure $ K.concatMapWith K.append (go1 seen cfp) (K.fromStream (Y.decodeFile cfp))
go1 seen cfp event =
case event of
EventScalar f (UriTag "!include") _ _ ->
let includeFile =
takeDirectory cfp </> unpack (decodeUtf8 f)
in K.fromStream
$ S.filter (`notElem` irrelevantEvents)
$ K.toStream
$ go (cfp : seen) includeFile
_ -> K.fromPure event
irrelevantEvents = [EventStreamStart, EventDocumentStart, EventDocumentEnd, EventStreamEnd]
#if !MIN_VERSION_directory(1, 2, 3)
handleNotFound = handleJust
(\e -> do
guard (isDoesNotExistError e)
guard (ioeGetLocation e == "canonicalizePath")
ioeGetFileName e)
(throwIO . YamlException . ("Yaml file not found: " ++))
#else
handleNotFound = id
#endif
-- | Like `Data.Yaml.decodeFile` but with support for relative and absolute
-- includes.
--
-- The syntax for includes follows the form:
--
-- > somekey: !include ./somefile.yaml
decodeFile
:: FromJSON a
=> FilePath
-> IO (Maybe a)
decodeFile fp = (fmap snd <$> decodeHelper (eventsFromFile fp)) >>= either throwIO (return . either (const Nothing) id)
-- | Like `Data.Yaml.decodeFileEither` but with support for relative and
-- absolute includes.
--
-- The syntax for includes follows the form:
--
-- > somekey: !include ./somefile.yaml
decodeFileEither
:: FromJSON a
=> FilePath
-> IO (Either ParseException a)
decodeFileEither = fmap (fmap snd) . decodeFileWithWarnings
-- | A version of `decodeFileEither` that returns warnings along with the parse
-- result.
--
-- @since 0.10.0
decodeFileWithWarnings
:: FromJSON a
=> FilePath
-> IO (Either ParseException ([Warning], a))
decodeFileWithWarnings = decodeHelper_ . eventsFromFile