Yaml
:= [
Null ,
Bool (Bool ),
Int (I64 ),
Float (F64 ),
Text (Str ),
Sequence (List (Yaml )),
Mapping (List ({ key : Str , value : Yaml })),
]
A practical YAML configuration parser.
This module implements a YAML 1.2 subset aimed at configuration files and
Markdown frontmatter. It supports a single document (optionally between
--- and ... markers), block mappings and sequences (including compact and
indentless sequences), single-line flow collections, comments, literal and
folded block scalars, single-line quoted scalars with all YAML escapes, and
plain scalars resolved with the YAML 1.2 core schema (null, booleans, decimal,
octal and hexadecimal integers, floats, .inf and .nan).
Mapping keys are their source text, so 1 and 01 are different keys.
Nesting is limited to 100 levels. Anchors, aliases, tags, directives,
complex keys, multi-line flow collections, multi-line quoted or plain
scalars, and multi-document streams are rejected with a parse error rather
than misread.
For source-preserving syntax highlighting, Yaml.tokenize_str and
Yaml.tokenize_bytes return original byte spans and diagnostics even for
incomplete snippets. Yaml.highlight_category provides theme categories.
There are two ways to read a document:
A Yaml tree value is one of:
Null for null, ~, an empty value or an empty document;
Bool, Int (an I64; larger integers are an error) and Float for plain scalars the core schema resolves;
Text for quoted scalars, block scalars and any other plain scalar;
Sequence and Mapping for collections. Mapping entries keep their source order, and a duplicate key is an error.
tokenize_str : Str -> Tokenized
Tokenize source text for highlighting, including incomplete YAML snippets.
Slicing each token interval from source and concatenating them reproduces
the input exactly, including comments, whitespace, CRLF, and Unicode.
Anchors, aliases, tags, directives and document markers are recognized
lexically even when Yaml.parse_str rejects the document.
Tokenize arbitrary bytes without discarding invalid UTF-8. Diagnostics
report malformed encoding; tokens still retain every original byte.
highlight_category : Token -> HighlightCategory
Map a token to a theme category without rendering or changing its text.
Mapping keys use PropertyName; scalar values use their core-schema
category. Quoted values and block-scalar scripts remain StringText.
Compare two parsed YAML values structurally.
Hash a parsed YAML value, so trees can be Dict keys or Set members.
from_quote : Str -> Try (Yaml , [BadQuotedBytes (Str )])
Read a quoted YAML tree during compilation, rejecting invalid input and
unquoted legacy boolean values such as yes and off.
validate : ValidationProps -> Try (Yaml , [BadQuotedBytes (Str )])
Validate one complete document, reporting its source name and position.
Unlike Yaml.parse_str , this also rejects enabled lint checks. Legacy
boolean checks inspect unquoted values, never mapping keys or quoted text.
validate_as : DecodeValidationProps -> Try (a , [BadQuotedBytes (Str )]) where [a .Parseable ([InvalidYaml (Error ), ..errs ])]
Validate and decode one document into the inferred Roc type. All existing
decoder types, including U128 and strings such as 1.10, are preserved.
Use unknown_keys: Reject to reject fields the result type does not have.
parse_str : Str -> Try (Yaml , [InvalidYaml (Error )])
Parse one YAML configuration document from a string.
Empty input produces Null. Invalid input returns InvalidYaml with a
one-based line and column. See the module description for the supported subset.
expect Yaml.parse_str("draft: false") == Ok(Mapping([{ key: "draft", value: Bool(False) }]))
parser : Parser (Bytes , Yaml )
A Parser that reads the rest of its input as one YAML document, for
composing YAML with other parsers.
It always consumes all of its input. A failure is a ParseError whose
offset is the byte offset of the reported line and column.
expect Utf8.parse_str(Yaml.parser, "a: 1") == Ok(Mapping([{ key: "a", value: Int(1) }]))
get : Yaml , Str -> Try (Yaml , [Missing ])
The value stored under key in a mapping. Anything else, or a mapping
without that key, is Err(Missing).
expect Yaml.parse_str("title: Post").map_ok(|doc| doc.get("title")) == Ok(Ok(Text("Post")))
at : Yaml , U64 -> Try (Yaml , [Missing ])
The item at a zero-based index of a sequence. Anything else, or an
index past the end, is Err(Missing).
expect Yaml.parse_str("[a, b]").map_ok(|doc| doc.at(1)) == Ok(Ok(Text("b")))
get_path : Yaml , List (Str ) -> Try (Yaml , [Missing ])
Follow a path of mapping keys and sequence indexes. A segment applied
to a sequence must be a decimal index such as "0".
expect {
doc = Yaml.parse_str("jobs:\n build:\n steps: [checkout, test]\n")?
doc.get_path(["jobs", "build", "steps", "1"]) == Ok(Text("test"))
}
as_str : Yaml -> Try (Str , [WrongType ])
The text of a Text value. Other values, including numbers and
booleans, are Err(WrongType); use Yaml.decode to read a plain
scalar such as 1.10 as text.
as_i64 : Yaml -> Try (I64 , [WrongType ])
The integer of an Int value, or Err(WrongType).
as_bool : Yaml -> Try (Bool , [WrongType ])
The boolean of a Bool value, or Err(WrongType).
as_list : Yaml -> Try (List (Yaml ), [WrongType ])
The items of a Sequence value, or Err(WrongType).
expect Yaml.parse_str("[1, 2]").map_ok(|doc| doc.as_list()) == Ok(Ok([Int(1), Int(2)]))
Render a parsed YAML value in Roc source-like notation for inspection.
The output is meant for debugging and test messages, not for writing
YAML. Control characters in text and keys are escaped, so the output is
always printable on one line.
expect Yaml.to_inspect(Sequence([Int(1), Text("a")])) == "Sequence([Int(1), Text(\"a\")])"
decode : Str -> Try (a , [InvalidYaml (Error ), ..errs ]) where [a .Parseable ([InvalidYaml (Error ), ..errs ])]
Decode one YAML document straight into a Roc type, chosen by type
inference.
A mapping decodes into a record (by field name), or a Dict with Str or integer keys. Unknown keys are skipped; use Yaml.decoder with unknown_keys: Reject to make them an error.
A sequence decodes into a List or, when it has exactly the right number of items, a tuple.
A scalar is resolved for the type that asks for it, using the YAML 1.2 core schema: Str takes any quoted or block scalar and any plain scalar except a null, so version: 1.10 stays "1.10"; Bool, the integer types, F32, F64 and Dec take the matching core-schema forms; a tag union without payloads takes the tag's name.
A null (null, ~ or an empty value) decodes as an empty list, dict or record, and fills a Try(_, [Null]) field with Err(Null).
A Try(_, [Missing]) field is Err(Missing) when its key is absent. Any other absent field fails with MissingRequiredField(name), a tag the compiler adds to the error type.
Invalid YAML and values that do not fit the type both fail with
InvalidYaml, at the line and column of the offending value.
Config : { name : Str, version : Str, port : U16, debug : Try(Bool, [Missing]) }
expect {
config : Try(Config, [InvalidYaml(Yaml.Error), MissingRequiredField(Str)])
config = Yaml.decode("name: app\nversion: 1.10\nport: 8080\n")
config == Ok({ name: "app", version: "1.10", port: 8080, debug: Err(Missing) })
}
decoder : DecodeOptions -> (Str -> Try (a , [InvalidYaml (Error ), ..errs ])) where [a .Parseable ([InvalidYaml (Error ), ..errs ])]
Build a decoder like Yaml.decode with other conventions.
keys says how YAML keys spell Roc's snake_case field names: SnakeCase as written, KebabCase with dashes (user-id) or CamelCase (userId).
unknown_keys is Skip to ignore keys the record does not have, or Reject to fail with InvalidYaml at the first one.
Build the decoder once, for example as a top-level constant, and call it
for each document.
decode_strict = Yaml.decoder({ keys: KebabCase, unknown_keys: Reject })
expect {
result : Try({ user_id : U64 }, [InvalidYaml(Yaml.Error), MissingRequiredField(Str)])
result = decode_strict("user-id: 7\n")
result == Ok({ user_id: 7 })
}
Error : { line : U64 , column : U64 , message : Str }
Location and explanation of invalid YAML input. Lines and columns are
one-based; a column counts bytes from the start of its line.
Span : { start : U64 , end : U64 }
Half-open byte interval in the original source. Offsets count UTF-8 bytes,
not characters, and preserve CRLF without normalization.
TokenKind : [Whitespace , Comment , PlainScalar , SingleQuotedScalar , DoubleQuotedScalar , MappingValue , SequenceEntry , ExplicitKey , FlowSequenceStart , FlowSequenceEnd , FlowMappingStart , FlowMappingEnd , FlowEntry , BlockScalarHeader , BlockScalarContent , Anchor , Alias , Tag , Directive , DocumentStart , DocumentEnd , Invalid ]
Stable lexical kinds; punctuation spelling is available in the source span.
Quotes include their delimiters. Block scalar content includes its original
indentation and line endings. Whitespace and comments are retained.
TokenRole : [Key , Value , Other ]
A lexical mapping-key role, a scalar value, or punctuation/trivia.
Roles describe source context, not the validity or decoded type of a node.
ScalarCategory : [Text , Boolean , Null , Number ]
YAML 1.2 core-schema category of a plain scalar. Quoted and block scalars
are Text. Numeric classification does not depend on machine integer range.
Token : { kind : TokenKind , start : U64 , end : U64 , role : TokenRole , scalar : ScalarCategory }
A nonempty original-source interval. Tokens form a contiguous partition of
the entire input: no gaps, overlap, synthetic tokens, or end-of-file token.
DiagnosticCode : [UnclosedQuote , InvalidEscape , UnclosedCollection , UnexpectedDelimiter , InvalidIndentation , InvalidBlockHeader , InvalidCharacter , InvalidUtf8 , ResourceLimit ]
Lexical problems found while preserving displayable source.
Diagnostic : { code : DiagnosticCode , start : U64 , end : U64 }
A lexical issue located by its original-source byte interval.
TokenStatus : [Complete , Recovered , Limited ]
Complete means no lexical issue was detected; it does not establish YAML
semantic validity. Recovered preserves malformed source with diagnostics.
Limited preserves the input after reaching 256 open flow collections;
the remaining bytes become one Invalid token with a ResourceLimit issue.
Tokenized : { source : List (U8 ), tokens : List (Token ), diagnostics : List (Diagnostic ), status : TokenStatus }
Source retained once, its lossless token partition, and located diagnostics.
Tokenization is independent of the semantic parser's supported subset.
HighlightCategory : [Trivia , CommentText , PropertyName , StringText , NumericLiteral , BooleanLiteral , NullLiteral , Symbol , Name , TypeName , InvalidText ]
Theme-independent categories, usable by HTML, PDF, or terminal renderers.
:= { legacy_booleans : Bool ?? True }
Checks for accepted YAML that is commonly mistaken for another value.
Set a check to False to accept that spelling deliberately.
:= { source : Str , name : Str ?? "<literal>", lints : Lints ?? {} }
Checked tree parsing. Omitted fields use the documented defaults.
:= {
source : Str ,
name : Str ?? "<literal>",
lints : Lints ?? {},
keys : [SnakeCase , KebabCase , CamelCase ] ?? SnakeCase,
unknown_keys : [Skip , Reject ] ?? Skip,
}
Checked type-directed decoding, with the same key conventions as Yaml.decode .
:= { value : a }
A typed YAML quotation. Access the decoded application value with .value.
from_quote : Str -> Try (Decoded (a ), [BadQuotedBytes (Str )]) where [a .Parseable ([InvalidYaml (Error ), ..errs ])]
Decode quoted source with the defaults of Yaml.validate_as .
DecodeOptions : { keys : [SnakeCase , KebabCase , CamelCase ], unknown_keys : [Skip , Reject ] }
Options for Yaml.decoder .
Parseable : a
where [
a .parser_for : Format -> (Cursor -> Try ({ value : a , rest : Cursor }, errs )),
]
Names the requirement that a type can be decoded from YAML, so a
generic function can say "YAML-decodable" without naming the format and
cursor types:
load : Str -> Try(a, [InvalidYaml(Yaml.Error), ..errs]) where [a.Yaml.Parseable([InvalidYaml(Yaml.Error), ..errs])]
load = |text| Yaml.decode(text)
Yaml.Format :: # (opaque)
The decoding format that Yaml.decode passes to a type's parser_for.
It implements the builtin parsing protocol; you do not call its methods
yourself.
Spell a Roc field name as a YAML key, following the decoder's keys option.
parse_str : Format , Cursor -> Try ({ value : Str , rest : Cursor }, [InvalidYaml (Error )])
Read a string: any quoted or block scalar, or a plain scalar other than null.
parse_bool : Format , Cursor -> Try ({ value : Bool , rest : Cursor }, [InvalidYaml (Error )])
Read a core-schema boolean such as true or FALSE.
parse_u8 : Format , Cursor -> Try ({ value : U8 , rest : Cursor }, [InvalidYaml (Error )])
Read a core-schema integer (decimal, 0o octal or 0x hexadecimal) that fits the type.
parse_i8 : Format , Cursor -> Try ({ value : I8 , rest : Cursor }, [InvalidYaml (Error )])
Read a core-schema integer (decimal, 0o octal or 0x hexadecimal) that fits the type.
parse_u16 : Format , Cursor -> Try ({ value : U16 , rest : Cursor }, [InvalidYaml (Error )])
Read a core-schema integer (decimal, 0o octal or 0x hexadecimal) that fits the type.
parse_i16 : Format , Cursor -> Try ({ value : I16 , rest : Cursor }, [InvalidYaml (Error )])
Read a core-schema integer (decimal, 0o octal or 0x hexadecimal) that fits the type.
parse_u32 : Format , Cursor -> Try ({ value : U32 , rest : Cursor }, [InvalidYaml (Error )])
Read a core-schema integer (decimal, 0o octal or 0x hexadecimal) that fits the type.
parse_i32 : Format , Cursor -> Try ({ value : I32 , rest : Cursor }, [InvalidYaml (Error )])
Read a core-schema integer (decimal, 0o octal or 0x hexadecimal) that fits the type.
parse_u64 : Format , Cursor -> Try ({ value : U64 , rest : Cursor }, [InvalidYaml (Error )])
Read a core-schema integer (decimal, 0o octal or 0x hexadecimal) that fits the type.
parse_i64 : Format , Cursor -> Try ({ value : I64 , rest : Cursor }, [InvalidYaml (Error )])
Read a core-schema integer (decimal, 0o octal or 0x hexadecimal) that fits the type.
parse_u128 : Format , Cursor -> Try ({ value : U128 , rest : Cursor }, [InvalidYaml (Error )])
Read a core-schema integer (decimal, 0o octal or 0x hexadecimal) that fits the type.
parse_i128 : Format , Cursor -> Try ({ value : I128 , rest : Cursor }, [InvalidYaml (Error )])
Read a core-schema integer (decimal, 0o octal or 0x hexadecimal) that fits the type.
parse_dec : Format , Cursor -> Try ({ value : Dec , rest : Cursor }, [InvalidYaml (Error )])
Read a core-schema integer or float; .inf and .nan only where the type has them.
parse_f32 : Format , Cursor -> Try ({ value : F32 , rest : Cursor }, [InvalidYaml (Error )])
Read a core-schema integer or float; .inf and .nan only where the type has them.
parse_f64 : Format , Cursor -> Try ({ value : F64 , rest : Cursor }, [InvalidYaml (Error )])
Read a core-schema integer or float; .inf and .nan only where the type has them.
parse_null : Format , Cursor -> Try (Cursor , [InvalidYaml (Error )])
Succeeds only on a null, so a Try(_, [Null]) field falls back to
its value parser otherwise.
parse_tag_union : Format , ParseTagUnionSpec (a ), Cursor -> Try ({ value : a , rest : Cursor }, [InvalidYaml (Error )])
Read a tag without payloads from a scalar holding its name.
parse_list_start : Format , Cursor -> Try ([Counted ({ len : U64 , rest : Cursor }), Uncounted (Cursor )], [InvalidYaml (Error )])
Start a list at a sequence (or a null, as an empty list); sequences are always counted.
parse_list_next : Format , Cursor -> Try ([Item (Cursor ), Done (Cursor )], [InvalidYaml (Error )])
Sequences are always counted, so the driver never asks for the next item.
parse_list_after_item : Format , Cursor -> Try ([Continue (Cursor ), Done (Cursor )], [InvalidYaml (Error )])
Protocol step that counted YAML collections never need; it does nothing.
parse_tuple_start : Format , Cursor , U64 -> Try (Cursor , [InvalidYaml (Error )])
Tuple protocol step; a tuple needs a sequence of exactly its length.
parse_tuple_next : Format , Cursor , U64 , U64 -> Try (Cursor , [InvalidYaml (Error )])
Tuple protocol step; a tuple needs a sequence of exactly its length.
parse_tuple_end : Format , Cursor , U64 -> Try (Cursor , [InvalidYaml (Error )])
Tuple protocol step; a tuple needs a sequence of exactly its length.
parse_record_start : Format , Cursor -> Try ([Counted ({ len : U64 , rest : Cursor }), Uncounted (Cursor )], [InvalidYaml (Error )])
Start a record or dict at a mapping (or a null, as an empty one); mappings are always counted.
parse_record_field : Format , FieldNames (_shape ), Cursor -> Try (
[
Field ({ field : FieldName (_shape ), rest : Cursor }),
TryField ({ name : Str , rest : Cursor }),
TryFieldCaseless ({ name : Str , rest : Cursor }),
Continue (Cursor ),
Done (Cursor ),
],
[InvalidYaml (Error )],
)
Read the next mapping key for the generated record parser to match.
parse_record_after_field : Format , Cursor -> Try ([Continue (Cursor ), Done (Cursor )], [InvalidYaml (Error )])
Mappings are always counted, so the driver never asks whether another
field follows.
skip_record_field : Format , Cursor -> Try (Cursor , [InvalidYaml (Error )])
Skip the value of a key the record lacks, or reject it when unknown_keys is Reject.
parse_dict_start : Format , Cursor -> Try ([Counted ({ len : U64 , rest : Cursor }), Uncounted (Cursor )], [InvalidYaml (Error )])
Start a record or dict at a mapping (or a null, as an empty one); mappings are always counted.
parse_dict_next : Format , Cursor -> Try ([Entry (Cursor ), Done (Cursor )], [InvalidYaml (Error )])
Protocol step that counted YAML collections never need; it does nothing.
parse_dict_after_key : Format , Cursor -> Try (Cursor , [InvalidYaml (Error )])
Protocol step that counted YAML collections never need; it does nothing.
parse_dict_after_entry : Format , Cursor -> Try ([Continue (Cursor ), Done (Cursor )], [InvalidYaml (Error )])
Protocol step that counted YAML collections never need; it does nothing.
invalid_value : Format , Cursor -> [InvalidYaml (Error )]
The error for a value the generated parser cannot use.
parse_key_str : Format , Cursor -> Try ({ value : Str , rest : Cursor }, [InvalidYaml (Error )])
Read a mapping key as a Dict key.
parse_key_u8 : Format , Cursor -> Try ({ value : U8 , rest : Cursor }, [InvalidYaml (Error )])
Read a mapping key as a Dict key.
parse_key_i8 : Format , Cursor -> Try ({ value : I8 , rest : Cursor }, [InvalidYaml (Error )])
Read a mapping key as a Dict key.
parse_key_u16 : Format , Cursor -> Try ({ value : U16 , rest : Cursor }, [InvalidYaml (Error )])
Read a mapping key as a Dict key.
parse_key_i16 : Format , Cursor -> Try ({ value : I16 , rest : Cursor }, [InvalidYaml (Error )])
Read a mapping key as a Dict key.
parse_key_u32 : Format , Cursor -> Try ({ value : U32 , rest : Cursor }, [InvalidYaml (Error )])
Read a mapping key as a Dict key.
parse_key_i32 : Format , Cursor -> Try ({ value : I32 , rest : Cursor }, [InvalidYaml (Error )])
Read a mapping key as a Dict key.
parse_key_u64 : Format , Cursor -> Try ({ value : U64 , rest : Cursor }, [InvalidYaml (Error )])
Read a mapping key as a Dict key.
parse_key_i64 : Format , Cursor -> Try ({ value : I64 , rest : Cursor }, [InvalidYaml (Error )])
Read a mapping key as a Dict key.
Yaml.Cursor :: # (opaque)
The read position that Yaml.decode threads through a type's parser:
the document as a flat list of events and an index into it.