-- | A YAML 1.2.2 library.
--
-- The library handles two typical use cases well:
--
-- 1. Decoding a document that describes a configuration:
--
--     >>> :{
--     data Config = Config
--       { name :: T.Text
--       , paths :: [FilePath]
--       }
--       deriving stock (Generic, Show)
--       deriving (FromYaml) via GenericYaml Config
--     instance GenericYamlOptions Config where
--       yamlDefault = Just Config {name = requiredField, paths = ["."]}
--     :}
--
--     >>> input = "name: app\n"
--
--     >>> T.putStr input
--     name: app
--
--     >>> either printErrors print (decodeText @Config input)
--     Config {name = "app", paths = ["."]}
--
--     When a document fails to decode, you get multiple errors pointing at what
--     failed and why:
--
--     >>> input = "paths:\n- src\n- 42\nport: 80\n"
--
--     >>> T.putStr input
--     paths:
--     - src
--     - 42
--     port: 80
--
--     >>> either printErrors print (decodeText @Config input)
--     input.yaml:1:1: missing key "name"
--       |
--     1 | paths:
--       | ^
--     input.yaml:3:3: paths[1]: expected a string, but got an integer, quote the value, e.g. '42'
--       |
--     3 | - 42
--       |   ^
--     input.yaml:4:1: unknown key "port", expected one of: name, paths
--       |
--     4 | port: 80
--       | ^
--
-- 2. Decoding a document into a Haskell type and encoding it back. A type
--    can keep the comments of a value with t'Yamlet.Commented', and a part
--    of the document as it was written with t'Yamlet.Node':
--
--     >>> :{
--     data Workflow = Workflow
--       { name :: Commented T.Text
--       , matrix :: Node
--       }
--       deriving stock (Generic)
--       deriving anyclass (GenericYamlOptions)
--       deriving (FromYaml, ToYaml) via GenericYaml Workflow
--     :}
--
--     >>> input = "# The name in the UI.\nname: build # short\nmatrix:\n  # Each system runs the jobs.\n  os: [linux, macos]\n  ghc: ['9.10', '9.12']\n"
--
--     >>> T.putStr input
--     # The name in the UI.
--     name: build # short
--     matrix:
--       # Each system runs the jobs.
--       os: [linux, macos]
--       ghc: ['9.10', '9.12']
--
--     >>> Right workflow = decodeText @Workflow input
--
--     The decoded name keeps the comment above its key and the comment after
--     its value:
--
--     >>> print workflow.name
--     Commented {value = "build", comments = Comments {before = [Comment "The name in the UI."], inline = Just "short", after = []}}
--
--     >>> T.putStr (encodeText workflow)
--     # The name in the UI.
--     name: build # short
--     matrix:
--       # Each system runs the jobs.
--       os: [linux, macos]
--       ghc: ['9.10', '9.12']
--
-- The record types of the library, e.g. t'Error' and t'YamlOptions', have no
-- field selectors. Read a field with the @OverloadedRecordDot@ extension,
-- e.g. @err.message@, and set one with the record syntax, e.g.
-- @defaultYamlOptions {fieldLabelModifier = snakeCase}@, or with the generic
-- optics of <https://hackage.haskell.org/package/optics-core optics-core>,
-- e.g. @defaultYamlOptions & #fieldLabelModifier .~ snakeCase@.
module Yamlet
  ( -- * Decoding
    decode
  , decodeAll
  , decodeText
  , decodeAllText
  , decodeInput
  , decodeFile
  , decodeAllFile

    -- * Syntax trees
  , decodeWithDocument
  , decodeDocument
  , decodeDocuments

    -- * Encoding
  , encode
  , encodeAll
  , encodeText
  , encodeAllText
  , encodeFile
  , encodeAllFile

    -- * Nodes
  , S.Node
  , S.Offset (..)
  , S.noOffset
  , S.Located (..)

    -- * Comments
  , S.Commented (..)
  , S.Comments (..)
  , S.noComments
  , S.Line (..)

    -- * Values
  , module Yamlet.Value

    -- * Conversion from nodes
  , module Yamlet.Decode

    -- * Conversion to nodes
  , module Yamlet.Encode

    -- * Generic instances
  , module Yamlet.Generic

    -- * Errors
  , module Yamlet.Error
  ) where

import Control.Monad
import Data.Bifunctor
import Data.ByteString qualified as BS
import Data.List.NonEmpty qualified as NE
import Data.Maybe
import Data.Text qualified as T
import Data.Text.Encoding qualified as T

import Yamlet.Decode
import Yamlet.Encode
import Yamlet.Error
import Yamlet.Generic
import Yamlet.Internal.Compose
import Yamlet.Internal.Encoder
import Yamlet.Internal.FromYaml
import Yamlet.Internal.Input
import Yamlet.Internal.Parser
import Yamlet.Internal.Syntax qualified as S
import Yamlet.Internal.Utils
import Yamlet.Value

-- | Decode a stream with one document. An empty stream is null.
--
-- If the input has a syntax error or fails a check that 'decodeDocument'
-- describes, the result has only that error, with its notes, e.g. the first
-- key of a duplicate key. Otherwise the result has every error that the
-- 'Parser' collects, in the order of their positions.
--
-- >>> decode @[Int] "- 1\n- 2\n"
-- Right [1,2]
--
-- >>> decode @(Maybe Int) ""
-- Right Nothing
--
-- >>> either printErrors print (decode @[Int] "- 1\n- x\n- true\n")
-- input.yaml:2:3: [1]: expected an integer, but got a string
--   |
-- 2 | - x
--   |   ^
-- input.yaml:3:3: [2]: expected an integer, but got a boolean
--   |
-- 3 | - true
--   |   ^
decode :: FromYaml a => BS.ByteString -> Either (NE.NonEmpty Error) a
decode :: forall a. FromYaml a => ByteString -> Either (NonEmpty Error) a
decode ByteString
bs = Either Error Text -> Either (NonEmpty Error) Text
forall a. Either Error a -> Either (NonEmpty Error) a
single (ByteString -> Either Error Text
decodeInput ByteString
bs) Either (NonEmpty Error) Text
-> (Text -> Either (NonEmpty Error) a) -> Either (NonEmpty Error) a
forall a b.
Either (NonEmpty Error) a
-> (a -> Either (NonEmpty Error) b) -> Either (NonEmpty Error) b
forall (m :: * -> *) a b. Monad m => m a -> (a -> m b) -> m b
>>= Text -> Either (NonEmpty Error) a
forall a. FromYaml a => Text -> Either (NonEmpty Error) a
decodeText

-- | Decode every document of a stream. The errors are as for 'decode', from
-- the first document that fails. The parser reads the whole stream first, so
-- a syntax error in any document comes before the errors of the others.
--
-- >>> decodeAll @Int "1\n---\n2\n"
-- Right [1,2]
decodeAll :: FromYaml a => BS.ByteString -> Either (NE.NonEmpty Error) [a]
decodeAll :: forall a. FromYaml a => ByteString -> Either (NonEmpty Error) [a]
decodeAll ByteString
bs = Either Error Text -> Either (NonEmpty Error) Text
forall a. Either Error a -> Either (NonEmpty Error) a
single (ByteString -> Either Error Text
decodeInput ByteString
bs) Either (NonEmpty Error) Text
-> (Text -> Either (NonEmpty Error) [a])
-> Either (NonEmpty Error) [a]
forall a b.
Either (NonEmpty Error) a
-> (a -> Either (NonEmpty Error) b) -> Either (NonEmpty Error) b
forall (m :: * -> *) a b. Monad m => m a -> (a -> m b) -> m b
>>= Text -> Either (NonEmpty Error) [a]
forall a. FromYaml a => Text -> Either (NonEmpty Error) [a]
decodeAllText

-- | Decode a stream with one document. An empty stream is null. The errors
-- are as for 'decode'.
--
-- >>> decodeText @Value "ports: [80, 443]\nenabled: yes\n"
-- Right (Mapping [(String "ports",Sequence [Int 80,Int 443]),(String "enabled",String "yes")])
decodeText :: FromYaml a => T.Text -> Either (NE.NonEmpty Error) a
decodeText :: forall a. FromYaml a => Text -> Either (NonEmpty Error) a
decodeText Text
input = do
  (a, _) <- Text -> Either (NonEmpty Error) (a, Document)
forall a.
FromYaml a =>
Text -> Either (NonEmpty Error) (a, Document)
decodeWithDocument Text
input
  pure a

-- | Decode a stream with one document as 'decodeText' does, and give the
-- document too, e.g. for 'documentErrors' or to write the file back with its
-- comments. An empty stream is a document with null. A stream of only
-- comments is empty too, so the document does not keep them, as the section
-- [Comments]("Yamlet.Syntax#comments") says.
decodeWithDocument :: FromYaml a => T.Text -> Either (NE.NonEmpty Error) (a, S.Document)
decodeWithDocument :: forall a.
FromYaml a =>
Text -> Either (NonEmpty Error) (a, Document)
decodeWithDocument Text
input =
  Either Error [Document] -> Either (NonEmpty Error) [Document]
forall a. Either Error a -> Either (NonEmpty Error) a
single (Text -> Either Error [Document]
parseStream Text
input) Either (NonEmpty Error) [Document]
-> ([Document] -> Either (NonEmpty Error) (a, Document))
-> Either (NonEmpty Error) (a, Document)
forall a b.
Either (NonEmpty Error) a
-> (a -> Either (NonEmpty Error) b) -> Either (NonEmpty Error) b
forall (m :: * -> *) a b. Monad m => m a -> (a -> m b) -> m b
>>= \case
    [] ->
      Document -> Either (NonEmpty Error) (a, Document)
forall a.
FromYaml a =>
Document -> Either (NonEmpty Error) (a, Document)
withDocument (Document -> Either (NonEmpty Error) (a, Document))
-> (Node -> Document)
-> Node
-> Either (NonEmpty Error) (a, Document)
forall b c a. (b -> c) -> (a -> b) -> a -> c
. Node -> Document
S.document (Node -> Either (NonEmpty Error) (a, Document))
-> Node -> Either (NonEmpty Error) (a, Document)
forall a b. (a -> b) -> a -> b
$
        S.Node
          { offset :: Offset
S.offset = Int -> Offset
S.Offset Int
0
          , endOffset :: Offset
S.endOffset = Int -> Offset
S.Offset Int
0
          , props :: Props
S.props = Props
S.noProps
          , comments :: Comments
S.comments = Comments
S.noComments
          , content :: Content
S.content = ScalarStyle -> Text -> Content
S.ScalarContent ScalarStyle
S.Plain Text
""
          }
    [Document
doc] -> Document -> Either (NonEmpty Error) (a, Document)
forall a.
FromYaml a =>
Document -> Either (NonEmpty Error) (a, Document)
withDocument Document
doc
    docs :: [Document]
docs@(Document
_ : Document
doc : [Document]
_) -> do
      let limit :: Int
limit = [Node] -> Int
aliasLimit ((Document -> Node) -> [Document] -> [Node]
forall a b. (a -> b) -> [a] -> [b]
map (.root) [Document]
docs)
          check :: Int -> S.Document -> Either (NE.NonEmpty Error) Int
          check :: Int -> Document -> Either (NonEmpty Error) Int
check Int
added Document
d =
            (Node, Int) -> Int
forall a b. (a, b) -> b
snd ((Node, Int) -> Int)
-> Either (NonEmpty Error) (Node, Int)
-> Either (NonEmpty Error) Int
forall (f :: * -> *) a b. Functor f => (a -> b) -> f a -> f b
<$> (NonEmpty (Offset, String) -> NonEmpty Error)
-> Either (NonEmpty (Offset, String)) (Node, Int)
-> Either (NonEmpty Error) (Node, Int)
forall a b c. (a -> b) -> Either a c -> Either b c
forall (p :: * -> * -> *) a b c.
Bifunctor p =>
(a -> b) -> p a c -> p b c
first (Text -> Document -> NonEmpty (Offset, String) -> NonEmpty Error
decoderErrors Text
input Document
d) (Int
-> Int -> Node -> Either (NonEmpty (Offset, String)) (Node, Int)
prepareWithin Int
limit Int
added Document
d.root)
      (Int -> Document -> Either (NonEmpty Error) Int)
-> Int -> [Document] -> Either (NonEmpty Error) ()
forall (t :: * -> *) (m :: * -> *) b a.
(Foldable t, Monad m) =>
(b -> a -> m b) -> b -> t a -> m ()
foldM_ Int -> Document -> Either (NonEmpty Error) Int
check Int
0 [Document]
docs
      Either Error (a, Document) -> Either (NonEmpty Error) (a, Document)
forall a. Either Error a -> Either (NonEmpty Error) a
single (Either Error (a, Document)
 -> Either (NonEmpty Error) (a, Document))
-> (Error -> Either Error (a, Document))
-> Error
-> Either (NonEmpty Error) (a, Document)
forall b c a. (b -> c) -> (a -> b) -> a -> c
. Error -> Either Error (a, Document)
forall a b. a -> Either a b
Left (Error -> Either (NonEmpty Error) (a, Document))
-> Error -> Either (NonEmpty Error) (a, Document)
forall a b. (a -> b) -> a -> b
$
        Text -> Offset -> String -> Error
errorAt Text
input Document
doc.root.offset String
"expected a single document, but got a second one"
  where
    withDocument :: FromYaml a => S.Document -> Either (NE.NonEmpty Error) (a, S.Document)
    withDocument :: forall a.
FromYaml a =>
Document -> Either (NonEmpty Error) (a, Document)
withDocument Document
doc = do
      a <- Text -> Document -> Either (NonEmpty Error) a
forall a.
FromYaml a =>
Text -> Document -> Either (NonEmpty Error) a
decodeDocument Text
input Document
doc
      pure (a, doc)

-- | Decode every document of a stream. The errors are as for 'decodeAll'.
decodeAllText :: FromYaml a => T.Text -> Either (NE.NonEmpty Error) [a]
decodeAllText :: forall a. FromYaml a => Text -> Either (NonEmpty Error) [a]
decodeAllText Text
input = Either Error [Document] -> Either (NonEmpty Error) [Document]
forall a. Either Error a -> Either (NonEmpty Error) a
single (Text -> Either Error [Document]
parseStream Text
input) Either (NonEmpty Error) [Document]
-> ([Document] -> Either (NonEmpty Error) [a])
-> Either (NonEmpty Error) [a]
forall a b.
Either (NonEmpty Error) a
-> (a -> Either (NonEmpty Error) b) -> Either (NonEmpty Error) b
forall (m :: * -> *) a b. Monad m => m a -> (a -> m b) -> m b
>>= Text -> [Document] -> Either (NonEmpty Error) [a]
forall a.
FromYaml a =>
Text -> [Document] -> Either (NonEmpty Error) [a]
decodeDocuments Text
input

single :: Either Error a -> Either (NE.NonEmpty Error) a
single :: forall a. Either Error a -> Either (NonEmpty Error) a
single = (Error -> NonEmpty Error)
-> Either Error a -> Either (NonEmpty Error) a
forall a b c. (a -> b) -> Either a c -> Either b c
forall (p :: * -> * -> *) a b c.
Bifunctor p =>
(a -> b) -> p a c -> p b c
first (Error -> [Error] -> NonEmpty Error
forall a. a -> [a] -> NonEmpty a
NE.:| [])

-- | Decode a document of a syntax tree, e.g. to read the values of a file and
-- keep its comments from one parse.
--
-- As for a parsed input, the decoder checks the document first. The check
-- fails for:
--
-- * a duplicate key,
--
-- * an undefined alias,
--
-- * an alias inside the node that it refers to, e.g. @&a [*a]@,
--
-- * aliases beyond the limit in "Yamlet.Value",
--
-- * a value that is not valid for its tag,
--
-- * a float whose exponent in scientific notation is beyond the range
--   from -1000 to 1000.
--
-- The text is the input of the document. An error takes its line from the
-- text. For a document that the program built, the text can be empty. If
-- such a document contains the nodes of a parsed input, pass that input, so
-- that their errors get their lines. The errors are as for 'decode'.
--
-- The document has the limit of the aliases to itself. For the documents of
-- a stream, use 'decodeDocuments', so that they share the limit.
decodeDocument :: FromYaml a => T.Text -> S.Document -> Either (NE.NonEmpty Error) a
decodeDocument :: forall a.
FromYaml a =>
Text -> Document -> Either (NonEmpty Error) a
decodeDocument Text
input Document
doc =
  Either (NonEmpty Error) (a, Int) -> Either (NonEmpty Error) a
forall e a b. Either e (a, b) -> Either e a
firstOfResult (Either (NonEmpty Error) (a, Int) -> Either (NonEmpty Error) a)
-> Either (NonEmpty Error) (a, Int) -> Either (NonEmpty Error) a
forall a b. (a -> b) -> a -> b
$ Int -> Int -> Text -> Document -> Either (NonEmpty Error) (a, Int)
forall a.
FromYaml a =>
Int -> Int -> Text -> Document -> Either (NonEmpty Error) (a, Int)
decodeDocumentWithin ([Node] -> Int
aliasLimit [Document
doc.root]) Int
0 Text
input Document
doc

-- | Decode the documents of a syntax tree as 'decodeDocument' does, e.g. the
-- documents of a stream from 'Yamlet.Syntax.parseDocumentsText'. The
-- documents share the limit of the aliases, as the documents of a stream do.
-- The errors are as for 'decodeAll'.
decodeDocuments :: FromYaml a => T.Text -> [S.Document] -> Either (NE.NonEmpty Error) [a]
decodeDocuments :: forall a.
FromYaml a =>
Text -> [Document] -> Either (NonEmpty Error) [a]
decodeDocuments Text
input [Document]
docs = Int -> [Document] -> Either (NonEmpty Error) [a]
forall a.
FromYaml a =>
Int -> [Document] -> Either (NonEmpty Error) [a]
go Int
0 [Document]
docs
  where
    limit :: Int
    limit :: Int
limit = [Node] -> Int
aliasLimit ((Document -> Node) -> [Document] -> [Node]
forall a b. (a -> b) -> [a] -> [b]
map (.root) [Document]
docs)

    go :: FromYaml a => Int -> [S.Document] -> Either (NE.NonEmpty Error) [a]
    go :: forall a.
FromYaml a =>
Int -> [Document] -> Either (NonEmpty Error) [a]
go Int
added = \case
      [] -> [a] -> Either (NonEmpty Error) [a]
forall a b. b -> Either a b
Right []
      Document
d : [Document]
ds -> do
        (a, added') <- Int -> Int -> Text -> Document -> Either (NonEmpty Error) (a, Int)
forall a.
FromYaml a =>
Int -> Int -> Text -> Document -> Either (NonEmpty Error) (a, Int)
decodeDocumentWithin Int
limit Int
added Text
input Document
d
        (a :) <$> go added' ds

-- | 'decodeDocument' with the visits of the aliases as for 'prepareWithin'.
decodeDocumentWithin
  :: FromYaml a
  => Int -> Int -> T.Text -> S.Document -> Either (NE.NonEmpty Error) (a, Int)
decodeDocumentWithin :: forall a.
FromYaml a =>
Int -> Int -> Text -> Document -> Either (NonEmpty Error) (a, Int)
decodeDocumentWithin Int
limit Int
added Text
input Document
doc =
  (NonEmpty (Offset, String) -> NonEmpty Error)
-> Either (NonEmpty (Offset, String)) (a, Int)
-> Either (NonEmpty Error) (a, Int)
forall a b c. (a -> b) -> Either a c -> Either b c
forall (p :: * -> * -> *) a b c.
Bifunctor p =>
(a -> b) -> p a c -> p b c
first (Text -> Document -> NonEmpty (Offset, String) -> NonEmpty Error
decoderErrors Text
input Document
doc) (Int
-> Int
-> (Node -> Parser a)
-> Node
-> Either (NonEmpty (Offset, String)) (a, Int)
forall a.
Int
-> Int
-> (Node -> Parser a)
-> Node
-> Either (NonEmpty (Offset, String)) (a, Int)
runParserWithin Int
limit Int
added Node -> Parser a
forall a. FromYaml a => Node -> Parser a
parseYaml Node
root)
  where
    -- The root with the lines of the document, e.g. the lines above a @---@
    -- marker and below a @...@ marker, so that a decoder can keep them. The
    -- renderer writes them at the same places. The comment on the line of the
    -- marker becomes a line above the root.
    root :: S.Node
    root :: Node
root
      | [Line] -> Bool
forall a. [a] -> Bool
forall (t :: * -> *) a. Foldable t => t a -> Bool
null Comments
dc.before Bool -> Bool -> Bool
&& Maybe Text -> Bool
forall a. Maybe a -> Bool
isNothing Comments
dc.inline Bool -> Bool -> Bool
&& [Line] -> Bool
forall a. [a] -> Bool
forall (t :: * -> *) a. Foldable t => t a -> Bool
null Comments
dc.after = Node
r
      | Bool
otherwise = Comments -> Node -> Node
S.withComments Comments
comments Node
r

    dc :: S.Comments
    dc :: Comments
dc = Document
doc.docComments

    r :: S.Node
    r :: Node
r = Document
doc.root

    comments :: S.Comments
    comments :: Comments
comments =
      S.Comments
        { before :: [Line]
S.before =
            Comments
dc.before [Line] -> [Line] -> [Line]
forall a. [a] -> [a] -> [a]
++ [Text -> Line
S.Comment Text
c | Just Text
c <- [Comments
dc.inline]] [Line] -> [Line] -> [Line]
forall a. [a] -> [a] -> [a]
++ Node
r.comments.before
        , inline :: Maybe Text
S.inline = Node
r.comments.inline
        , after :: [Line]
S.after = Node
r.comments.after [Line] -> [Line] -> [Line]
forall a. [a] -> [a] -> [a]
++ Comments
dc.after
        }

-- | The errors of the decoder in the document, with their paths.
decoderErrors
  :: T.Text -> S.Document -> NE.NonEmpty (S.Offset, String) -> NE.NonEmpty Error
decoderErrors :: Text -> Document -> NonEmpty (Offset, String) -> NonEmpty Error
decoderErrors Text
input Document
doc = [Error] -> NonEmpty Error
forall a. HasCallStack => [a] -> NonEmpty a
NE.fromList ([Error] -> NonEmpty Error)
-> (NonEmpty (Offset, String) -> [Error])
-> NonEmpty (Offset, String)
-> NonEmpty Error
forall b c a. (b -> c) -> (a -> b) -> a -> c
. Text -> Document -> [(Offset, String)] -> [Error]
documentErrors Text
input Document
doc ([(Offset, String)] -> [Error])
-> (NonEmpty (Offset, String) -> [(Offset, String)])
-> NonEmpty (Offset, String)
-> [Error]
forall b c a. (b -> c) -> (a -> b) -> a -> c
. NonEmpty (Offset, String) -> [(Offset, String)]
forall a. NonEmpty a -> [a]
NE.toList

-- | Encode a value as a document.
--
-- >>> encode [1, 2 :: Int]
-- "- 1\n- 2\n"
encode :: ToYaml a => a -> BS.ByteString
encode :: forall a. ToYaml a => a -> ByteString
encode = Text -> ByteString
T.encodeUtf8 (Text -> ByteString) -> (a -> Text) -> a -> ByteString
forall b c a. (b -> c) -> (a -> b) -> a -> c
. a -> Text
forall a. ToYaml a => a -> Text
encodeText

-- | Encode values as a stream of documents.
encodeAll :: ToYaml a => [a] -> BS.ByteString
encodeAll :: forall a. ToYaml a => [a] -> ByteString
encodeAll = Text -> ByteString
T.encodeUtf8 (Text -> ByteString) -> ([a] -> Text) -> [a] -> ByteString
forall b c a. (b -> c) -> (a -> b) -> a -> c
. [a] -> Text
forall a. ToYaml a => [a] -> Text
encodeAllText

-- | Encode a value as a document.
--
-- >>> T.putStr (encodeText (mapping ["name" .= ("app" :: T.Text), "ports" .= [80, 443 :: Int]]))
-- name: app
-- ports:
-- - 80
-- - 443
encodeText :: ToYaml a => a -> T.Text
encodeText :: forall a. ToYaml a => a -> Text
encodeText a
a = [Node] -> Text
renderDocuments [a -> Node
forall a. ToYaml a => a -> Node
toYaml a
a]

-- | Encode values as a stream of documents.
--
-- >>> T.putStr (encodeAllText [1, 2 :: Int])
-- 1
-- ---
-- 2
encodeAllText :: ToYaml a => [a] -> T.Text
encodeAllText :: forall a. ToYaml a => [a] -> Text
encodeAllText = [Node] -> Text
renderDocuments ([Node] -> Text) -> ([a] -> [Node]) -> [a] -> Text
forall b c a. (b -> c) -> (a -> b) -> a -> c
. (a -> Node) -> [a] -> [Node]
forall a b. (a -> b) -> [a] -> [b]
map a -> Node
forall a. ToYaml a => a -> Node
toYaml

-- | Decode the file as 'decode' does. The file is read as bytes, so the
-- encoding does not depend on the locale. It is UTF-8, UTF-16 or UTF-32,
-- detected as the YAML specification describes.
--
-- For the errors, give the same path to 'prettyError':
--
-- @
-- decodeFile \@Config path >>= \\case
--   Left errs -> mapM_ (putStrLn . prettyError path) errs
--   Right config -> ...
-- @
decodeFile :: FromYaml a => FilePath -> IO (Either (NE.NonEmpty Error) a)
decodeFile :: forall a. FromYaml a => String -> IO (Either (NonEmpty Error) a)
decodeFile String
path = ByteString -> Either (NonEmpty Error) a
forall a. FromYaml a => ByteString -> Either (NonEmpty Error) a
decode (ByteString -> Either (NonEmpty Error) a)
-> IO ByteString -> IO (Either (NonEmpty Error) a)
forall (f :: * -> *) a b. Functor f => (a -> b) -> f a -> f b
<$> String -> IO ByteString
BS.readFile String
path

-- | Decode every document of the file as 'decodeAll' does, with the encoding
-- of 'decodeFile'.
decodeAllFile :: FromYaml a => FilePath -> IO (Either (NE.NonEmpty Error) [a])
decodeAllFile :: forall a. FromYaml a => String -> IO (Either (NonEmpty Error) [a])
decodeAllFile String
path = ByteString -> Either (NonEmpty Error) [a]
forall a. FromYaml a => ByteString -> Either (NonEmpty Error) [a]
decodeAll (ByteString -> Either (NonEmpty Error) [a])
-> IO ByteString -> IO (Either (NonEmpty Error) [a])
forall (f :: * -> *) a b. Functor f => (a -> b) -> f a -> f b
<$> String -> IO ByteString
BS.readFile String
path

-- | Encode a value as a document in the file, in UTF-8. The file is written
-- as bytes, so the encoding does not depend on the locale.
encodeFile :: ToYaml a => FilePath -> a -> IO ()
encodeFile :: forall a. ToYaml a => String -> a -> IO ()
encodeFile String
path = String -> ByteString -> IO ()
BS.writeFile String
path (ByteString -> IO ()) -> (a -> ByteString) -> a -> IO ()
forall b c a. (b -> c) -> (a -> b) -> a -> c
. a -> ByteString
forall a. ToYaml a => a -> ByteString
encode

-- | Encode values as a stream of documents in the file, as 'encodeFile'
-- does.
encodeAllFile :: ToYaml a => FilePath -> [a] -> IO ()
encodeAllFile :: forall a. ToYaml a => String -> [a] -> IO ()
encodeAllFile String
path = String -> ByteString -> IO ()
BS.writeFile String
path (ByteString -> IO ()) -> ([a] -> ByteString) -> [a] -> IO ()
forall b c a. (b -> c) -> (a -> b) -> a -> c
. [a] -> ByteString
forall a. ToYaml a => [a] -> ByteString
encodeAll

-- $setup
-- >>> import Data.Text qualified as T
-- >>> import Data.Text.IO qualified as T
-- >>> import Yamlet
-- >>> printErrors = mapM_ (putStrLn . prettyError "input.yaml")