| Safe Haskell | None |
|---|---|
| Language | GHC2021 |
Yamlet
Description
A YAML 1.2.2 library.
The library handles two typical use cases well:
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 inputname: 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 inputpaths: - 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 | ^Decoding a document into a Haskell type and encoding it back. A type can keep the comments of a value with
Commented, and a part of the document as it was written withNode:>>>:{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 inputThe decoded name keeps the comment above its key and the comment after its value:
>>>print workflow.nameCommented {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. Error and 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 optics-core,
e.g. defaultYamlOptions & #fieldLabelModifier .~ snakeCase.
Synopsis
- decode :: FromYaml a => ByteString -> Either (NonEmpty Error) a
- decodeAll :: FromYaml a => ByteString -> Either (NonEmpty Error) [a]
- decodeText :: FromYaml a => Text -> Either (NonEmpty Error) a
- decodeAllText :: FromYaml a => Text -> Either (NonEmpty Error) [a]
- decodeInput :: ByteString -> Either Error Text
- decodeFile :: FromYaml a => FilePath -> IO (Either (NonEmpty Error) a)
- decodeAllFile :: FromYaml a => FilePath -> IO (Either (NonEmpty Error) [a])
- decodeWithDocument :: FromYaml a => Text -> Either (NonEmpty Error) (a, Document)
- decodeDocument :: FromYaml a => Text -> Document -> Either (NonEmpty Error) a
- decodeDocuments :: FromYaml a => Text -> [Document] -> Either (NonEmpty Error) [a]
- encode :: ToYaml a => a -> ByteString
- encodeAll :: ToYaml a => [a] -> ByteString
- encodeText :: ToYaml a => a -> Text
- encodeAllText :: ToYaml a => [a] -> Text
- encodeFile :: ToYaml a => FilePath -> a -> IO ()
- encodeAllFile :: ToYaml a => FilePath -> [a] -> IO ()
- data Node
- newtype Offset = Offset Int
- noOffset :: Offset
- data Located a = Located {}
- data Commented a = Commented {}
- data Comments = Comments {}
- noComments :: Comments
- data Line where
- EmptyLine
- CommentLine !Int !Text
- pattern Comment :: Text -> Line
- module Yamlet.Value
- module Yamlet.Decode
- module Yamlet.Encode
- module Yamlet.Generic
- module Yamlet.Error
Decoding
decode :: FromYaml a => ByteString -> Either (NonEmpty Error) a Source #
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 | ^
decodeAll :: FromYaml a => ByteString -> Either (NonEmpty Error) [a] Source #
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]
decodeText :: FromYaml a => Text -> Either (NonEmpty Error) a Source #
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")])
decodeAllText :: FromYaml a => Text -> Either (NonEmpty Error) [a] Source #
Decode every document of a stream. The errors are as for decodeAll.
decodeInput :: ByteString -> Either Error Text Source #
Decode the bytes of a stream to text. The encoding is UTF-8, UTF-16 or UTF-32, detected as the YAML specification describes.
decodeFile :: FromYaml a => FilePath -> IO (Either (NonEmpty Error) a) Source #
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 -> ...
decodeAllFile :: FromYaml a => FilePath -> IO (Either (NonEmpty Error) [a]) Source #
Decode every document of the file as decodeAll does, with the encoding
of decodeFile.
Syntax trees
decodeWithDocument :: FromYaml a => Text -> Either (NonEmpty Error) (a, Document) Source #
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 says.
decodeDocument :: FromYaml a => Text -> Document -> Either (NonEmpty Error) a Source #
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.
decodeDocuments :: FromYaml a => Text -> [Document] -> Either (NonEmpty Error) [a] Source #
Decode the documents of a syntax tree as decodeDocument does, e.g. the
documents of a stream from parseDocumentsText. The
documents share the limit of the aliases, as the documents of a stream do.
The errors are as for decodeAll.
Encoding
encode :: ToYaml a => a -> ByteString Source #
Encode a value as a document.
>>>encode [1, 2 :: Int]"- 1\n- 2\n"
encodeText :: ToYaml a => a -> Text Source #
Encode a value as a document.
>>>T.putStr (encodeText (mapping ["name" .= ("app" :: T.Text), "ports" .= [80, 443 :: Int]]))name: app ports: - 80 - 443
encodeAllText :: ToYaml a => [a] -> Text Source #
Encode values as a stream of documents.
>>>T.putStr (encodeAllText [1, 2 :: Int])1 --- 2
encodeFile :: ToYaml a => FilePath -> a -> IO () Source #
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.
encodeAllFile :: ToYaml a => FilePath -> [a] -> IO () Source #
Encode values as a stream of documents in the file, as encodeFile
does.
Nodes
A node of a document.
Instances
| NFData Node Source # | |||||
Defined in Yamlet.Internal.Syntax | |||||
| Generic Node Source # | |||||
Defined in Yamlet.Internal.Syntax Associated Types
| |||||
| Show Node Source # | |||||
| Eq Node Source # | |||||
| FromYaml Node Source # | The node of the syntax tree, with its styles and comments, e.g. to write a part of a document back as it was written. An alias in the input gives a copy of the node that it refers to. The texts of the node are copies, so that a small part of a document does
not keep the whole input alive. For a whole document without a copy, use
| ||||
| ToYaml Node Source # | |||||
| type Rep Node Source # | |||||
Defined in Yamlet.Internal.Syntax type Rep Node = D1 ('MetaData "Node" "Yamlet.Internal.Syntax" "yamlet-1.0.0.1-2nRkbEnNUrLFBALTocq2AS" 'False) (C1 ('MetaCons "Node" 'PrefixI 'True) ((S1 ('MetaSel ('Just "offset") 'NoSourceUnpackedness 'SourceStrict 'DecidedStrict) (Rec0 Offset) :*: S1 ('MetaSel ('Just "endOffset") 'NoSourceUnpackedness 'SourceStrict 'DecidedStrict) (Rec0 Offset)) :*: (S1 ('MetaSel ('Just "props") 'NoSourceUnpackedness 'SourceStrict 'DecidedStrict) (Rec0 Props) :*: (S1 ('MetaSel ('Just "comments") 'NoSourceUnpackedness 'SourceStrict 'DecidedStrict) (Rec0 Comments) :*: S1 ('MetaSel ('Just "content") 'NoSourceUnpackedness 'SourceStrict 'DecidedStrict) (Rec0 Content))))) | |||||
The offset of a byte in the input text, in its UTF-8 encoding. For an
input in UTF-16 or UTF-32, the offset counts the bytes of the text after
decodeInput, not the bytes of the input.
Constructors
| Offset Int |
Instances
| NFData Offset Source # | |||||
Defined in Yamlet.Internal.Syntax | |||||
| Generic Offset Source # | |||||
Defined in Yamlet.Internal.Syntax Associated Types
| |||||
| Show Offset Source # | |||||
| Eq Offset Source # | |||||
| Ord Offset Source # | |||||
| type Rep Offset Source # | |||||
Defined in Yamlet.Internal.Syntax type Rep Offset = D1 ('MetaData "Offset" "Yamlet.Internal.Syntax" "yamlet-1.0.0.1-2nRkbEnNUrLFBALTocq2AS" 'True) (C1 ('MetaCons "Offset" 'PrefixI 'False) (S1 ('MetaSel ('Nothing :: Maybe Symbol) 'NoSourceUnpackedness 'NoSourceStrictness 'DecidedLazy) (Rec0 Int))) | |||||
A value with the offset of its node, e.g. for the error of a check that runs after the decode. The encoder writes only the value.
documentErrors turns the offsets into errors with lines,
columns and paths. It needs the text and the document of the decode, so
decode with decodeWithDocument. Here the check gives an offset and
a message for each problem:
>>>input = "paths:\n- src\n- /etc\n"
>>>:{case decodeWithDocument @(M.Map T.Text [Located T.Text]) input of Right (config, doc) -> let errs = [ (p.offset, "the path is outside the repository") | p <- concat (M.elems config) , "/" `T.isPrefixOf` p.value ] in printErrors (documentErrors input doc errs) Left errs -> printErrors errs :} input.yaml:3:3: paths[1]: the path is outside the repository | 3 | - /etc | ^
A value that no node gives, e.g. a value of yamlDefault,
has noOffset. Its error has no position, and prettyError
prints only the file and the message. A value inside an alias has the
offset of the alias, i.e. of the place where the document uses the value.
Two equal values at different places are not equal as Located values,
e.g. a set keeps both. The equality and the order compare the values first
and then the offsets. To compare only the values, e.g. in a test, use the
field value.
Instances
| Functor Located Source # | |||||
| Foldable Located Source # | |||||
Defined in Yamlet.Internal.Syntax Methods fold :: Monoid m => Located m -> m foldMap :: Monoid m => (a -> m) -> Located a -> m foldMap' :: Monoid m => (a -> m) -> Located a -> m foldr :: (a -> b -> b) -> b -> Located a -> b foldr' :: (a -> b -> b) -> b -> Located a -> b foldl :: (b -> a -> b) -> b -> Located a -> b foldl' :: (b -> a -> b) -> b -> Located a -> b foldr1 :: (a -> a -> a) -> Located a -> a foldl1 :: (a -> a -> a) -> Located a -> a elem :: Eq a => a -> Located a -> Bool maximum :: Ord a => Located a -> a minimum :: Ord a => Located a -> a | |||||
| Traversable Located Source # | |||||
| NFData a => NFData (Located a) Source # | |||||
Defined in Yamlet.Internal.Syntax | |||||
| Generic (Located a) Source # | |||||
Defined in Yamlet.Internal.Syntax Associated Types
| |||||
| Show a => Show (Located a) Source # | |||||
| Eq a => Eq (Located a) Source # | |||||
| Ord a => Ord (Located a) Source # | |||||
Defined in Yamlet.Internal.Syntax | |||||
| FromYaml a => FromYaml (Located a) Source # | The value with the offset of its node. The key of an entry goes to the
value inside, e.g. for a | ||||
| ToYaml a => ToYaml (Located a) Source # | The value alone. The key of an entry goes to the value inside, e.g. for a
| ||||
| type Rep (Located a) Source # | |||||
Defined in Yamlet.Internal.Syntax type Rep (Located a) = D1 ('MetaData "Located" "Yamlet.Internal.Syntax" "yamlet-1.0.0.1-2nRkbEnNUrLFBALTocq2AS" 'False) (C1 ('MetaCons "Located" 'PrefixI 'True) (S1 ('MetaSel ('Just "value") 'NoSourceUnpackedness 'SourceStrict 'DecidedStrict) (Rec0 a) :*: S1 ('MetaSel ('Just "offset") 'NoSourceUnpackedness 'SourceStrict 'DecidedStrict) (Rec0 Offset))) | |||||
Comments
A value with the comments of its mapping entry:
before: the lines above the entry,inline: the comment at the end of the line of the key, or of the line where a value on several lines ends,after: the lines after the value, e.g. after the last entry of a collection.
A Commented value of a mapping entry has the comments of the entry. A
block list or mapping under the key has its own comments: the lines below
the key up to the last empty line above its first entry. A Commented
value inside the first one keeps them, so a type that keeps both nests two
Commented values:
>>>input = "# The CI jobs.\njobs:\n # Run on every push.\n\n # Check the formatting.\n - lint\n"
>>>T.putStr input# The CI jobs. jobs: # Run on every push. # Check the formatting. - lint
>>>Right entries = decodeText @(M.Map T.Text (Commented (Commented [Commented T.Text]))) input>>>Just jobs = M.lookup "jobs" entries
>>>jobs.commentsComments {before = [Comment "The CI jobs."], inline = Nothing, after = []}
>>>jobs.value.commentsComments {before = [Comment "Run on every push.",EmptyLine], inline = Nothing, after = []}
>>>map (.comments) jobs.value.value[Comments {before = [Comment "Check the formatting."], inline = Nothing, after = []}]
A value without a key, e.g. an item of a list, has the comments of its
node. The comments of a list or a mapping stay with it, not with its first
item or key: the lines up to the last empty line above its first entry, and
the comment on its first line, e.g. after its tag. A Commented value of
the whole list or mapping keeps them.
By the rules in Comments, some lines read back with a change:
- The lines after a text of several lines, which the encoder writes as a block scalar, read back as the lines above the next entry. After the last entry, they belong to the end of the collection around the entry.
- The lines above a list or a mapping without a key can get an empty line below them, e.g. at the top level. The empty line reads back as the last of these lines.
- The lines above the first item of a list or the first key of a mapping
that end with an empty line read back as the lines of the list or the
mapping. A
Commentedlist or mapping keeps them. Under a key, the inner of two nestedCommentedvalues keeps them, as above. Otherwise they are lost.
A comment is lost if its node has no place for it, i.e. if the node does
not decode into a node or a Commented value:
- the comments of a key without a Haskell field, e.g. the tag of a constructor;
- a comment at the end of a nested mapping, unless the field that holds
the mapping is
Commented, because a record has no place for the end of its mapping; - the comments of a record's mapping above its first key, e.g. a comment at the top of a file above an empty line;
- the comments of the key for a type such as
data Name = Name (Commented Text)that derives its instances throughGeneric, because a derived instance for one constructor with one field without a name does not give the key of its entry to the value inside. Declare such a type as a newtype and derive its instances withderiving newtype, which gives the key to the value.
In a map, use Commented on the key or on the value, not on both. With
both, the decoder gives the comments of the key to both, and the encoder
writes only those of the value, so a change to the comments of the key is
lost.
A change of the value keeps the comments:
>>>input = "# The port.\nport: 80 # the default\n"
>>>:{either printErrors (T.putStr . encodeText . M.map (fmap (+ 1))) $ decodeText @(M.Map T.Text (Commented Int)) input :} # The port. port: 81 # the default
The order compares the values first and then the comments, e.g. in a set.
Instances
| Functor Commented Source # | |||||
| Foldable Commented Source # | |||||
Defined in Yamlet.Internal.Syntax Methods fold :: Monoid m => Commented m -> m foldMap :: Monoid m => (a -> m) -> Commented a -> m foldMap' :: Monoid m => (a -> m) -> Commented a -> m foldr :: (a -> b -> b) -> b -> Commented a -> b foldr' :: (a -> b -> b) -> b -> Commented a -> b foldl :: (b -> a -> b) -> b -> Commented a -> b foldl' :: (b -> a -> b) -> b -> Commented a -> b foldr1 :: (a -> a -> a) -> Commented a -> a foldl1 :: (a -> a -> a) -> Commented a -> a elem :: Eq a => a -> Commented a -> Bool maximum :: Ord a => Commented a -> a minimum :: Ord a => Commented a -> a | |||||
| Traversable Commented Source # | |||||
| NFData a => NFData (Commented a) Source # | |||||
Defined in Yamlet.Internal.Syntax | |||||
| Generic (Commented a) Source # | |||||
Defined in Yamlet.Internal.Syntax Associated Types
| |||||
| Show a => Show (Commented a) Source # | |||||
| Eq a => Eq (Commented a) Source # | |||||
| Ord a => Ord (Commented a) Source # | |||||
Defined in Yamlet.Internal.Syntax | |||||
| FromYaml a => FromYaml (Commented a) Source # | The value with the comments of its entry, or of its node if it has no key, copied like every decoded text. | ||||
| ToYaml a => ToYaml (Commented a) Source # | The value with the comments of its entry. The lines above and the comment of the first line go on the key, or on the value without a key. A key keeps its own lines above, or its own comment, if the value has none. The lines after the value replace its own. | ||||
| type Rep (Commented a) Source # | |||||
Defined in Yamlet.Internal.Syntax type Rep (Commented a) = D1 ('MetaData "Commented" "Yamlet.Internal.Syntax" "yamlet-1.0.0.1-2nRkbEnNUrLFBALTocq2AS" 'False) (C1 ('MetaCons "Commented" 'PrefixI 'True) (S1 ('MetaSel ('Just "value") 'NoSourceUnpackedness 'SourceStrict 'DecidedStrict) (Rec0 a) :*: S1 ('MetaSel ('Just "comments") 'NoSourceUnpackedness 'SourceStrict 'DecidedStrict) (Rec0 Comments))) | |||||
The comments and the empty lines that belong to a node.
Constructors
| Comments | |
Fields
| |
Instances
| NFData Comments Source # | |||||
Defined in Yamlet.Internal.Syntax | |||||
| Generic Comments Source # | |||||
Defined in Yamlet.Internal.Syntax Associated Types
| |||||
| Show Comments Source # | |||||
| Eq Comments Source # | |||||
| Ord Comments Source # | |||||
Defined in Yamlet.Internal.Syntax | |||||
| type Rep Comments Source # | |||||
Defined in Yamlet.Internal.Syntax type Rep Comments = D1 ('MetaData "Comments" "Yamlet.Internal.Syntax" "yamlet-1.0.0.1-2nRkbEnNUrLFBALTocq2AS" 'False) (C1 ('MetaCons "Comments" 'PrefixI 'True) (S1 ('MetaSel ('Just "before") 'NoSourceUnpackedness 'SourceStrict 'DecidedStrict) (Rec0 [Line]) :*: (S1 ('MetaSel ('Just "inline") 'NoSourceUnpackedness 'SourceStrict 'DecidedStrict) (Rec0 (Maybe Text)) :*: S1 ('MetaSel ('Just "after") 'NoSourceUnpackedness 'SourceStrict 'DecidedStrict) (Rec0 [Line])))) | |||||
noComments :: Comments Source #
No comments and no empty lines.
A line of comments.
Constructors
| EmptyLine | |
| CommentLine !Int !Text | The number of The comment at the end of a line in |
Bundled Patterns
| pattern Comment :: Text -> Line | A comment with one |
Instances
| NFData Line Source # | |||||
Defined in Yamlet.Internal.Syntax | |||||
| Generic Line Source # | |||||
Defined in Yamlet.Internal.Syntax Associated Types
| |||||
| Show Line Source # | |||||
| Eq Line Source # | |||||
| Ord Line Source # | |||||
| type Rep Line Source # | |||||
Defined in Yamlet.Internal.Syntax type Rep Line = D1 ('MetaData "Line" "Yamlet.Internal.Syntax" "yamlet-1.0.0.1-2nRkbEnNUrLFBALTocq2AS" 'False) (C1 ('MetaCons "EmptyLine" 'PrefixI 'False) (U1 :: Type -> Type) :+: C1 ('MetaCons "CommentLine" 'PrefixI 'False) (S1 ('MetaSel ('Nothing :: Maybe Symbol) 'NoSourceUnpackedness 'SourceStrict 'DecidedStrict) (Rec0 Int) :*: S1 ('MetaSel ('Nothing :: Maybe Symbol) 'NoSourceUnpackedness 'SourceStrict 'DecidedStrict) (Rec0 Text))) | |||||
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