Current section
Files
Jump to
Current section
Files
src/taffy.erl
-module(taffy).
-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]).
-define(FILEPATH, "src/taffy.gleam").
-export([error_location/2, parse/1, parse_all/1, validate_unique_keys/1, get/2, get_or/3, get_path/2, index/2, as_string/1, as_int/1, as_float/1, as_bool/1, as_list/1, as_dict/1, as_pairs/1, is_null/1, to_string/1, to_yaml/1, to_json/1, to_json_string/1]).
-if(?OTP_RELEASE >= 27).
-define(MODULEDOC(Str), -moduledoc(Str)).
-define(DOC(Str), -doc(Str)).
-else.
-define(MODULEDOC(Str), -compile([])).
-define(DOC(Str), -compile([])).
-endif.
?MODULEDOC(
" A pure-Gleam YAML 1.2 parser. Passes 351/351 of the official YAML test\n"
" suite. Works on both Erlang and JavaScript targets; an optional native\n"
" backend (`taffy/native`, Erlang only) wraps `fast_yaml` for ~3-7×\n"
" speedup on large documents.\n"
"\n"
" ## Quick start\n"
"\n"
" ```gleam\n"
" import taffy\n"
"\n"
" pub fn main() {\n"
" let assert Ok(value) =\n"
" taffy.parse(\"name: John\\nage: 30\\ntags:\\n - gleam\\n - erlang\")\n"
"\n"
" let assert Ok(name) = taffy.get(value, \"name\")\n"
" let json = taffy.to_json_string(value)\n"
" }\n"
" ```\n"
"\n"
" ## Output\n"
"\n"
" - `to_json` / `to_json_string` — convert to `gleam_json` values\n"
" - `to_yaml` — emit block-style YAML, round-trips through `parse`\n"
" - `validate_unique_keys` — opt-in YAML 1.2 strict duplicate-key check\n"
).
-file("src/taffy.gleam", 63).
-spec error_location_loop(list(binary()), integer(), integer(), integer()) -> {integer(),
integer()}.
error_location_loop(Graphemes, Remaining, Line, Col) ->
case {Graphemes, Remaining} of
{_, N} when N =< 0 ->
{Line, Col};
{[], _} ->
{Line, Col};
{[<<"\n"/utf8>> | Rest], _} ->
error_location_loop(Rest, Remaining - 1, Line + 1, 1);
{[_ | Rest@1], _} ->
error_location_loop(Rest@1, Remaining - 1, Line, Col + 1)
end.
-file("src/taffy.gleam", 59).
?DOC(
" Compute `#(line, column)` (both 1-indexed) for a grapheme position\n"
" inside `input`. Out-of-range positions clamp to the last line/column.\n"
" Useful for turning `error.pos` into a user-facing location:\n"
"\n"
" ```gleam\n"
" case taffy.parse(input) {\n"
" Ok(_) -> ...\n"
" Error(err) -> {\n"
" let #(line, col) = taffy.error_location(input, err.pos)\n"
" io.println(err.message <> \" at \" <> int.to_string(line) <>\n"
" \":\" <> int.to_string(col))\n"
" }\n"
" }\n"
" ```\n"
).
-spec error_location(binary(), integer()) -> {integer(), integer()}.
error_location(Input, Pos) ->
error_location_loop(gleam@string:to_graphemes(Input), Pos, 1, 1).
-file("src/taffy.gleam", 83).
?DOC(
" Parse a single YAML document. For multi-document streams use `parse_all`.\n"
" On error the position points at the grapheme where parsing gave up.\n"
" `<<` merge keys are resolved automatically. Duplicate mapping keys are\n"
" allowed (with first-wins access) — the YAML 1.2 spec mandates uniqueness\n"
" but the official test suite expects parsers to accept them; use\n"
" `validate_unique_keys` to enforce strict uniqueness yourself.\n"
).
-spec parse(binary()) -> {ok, taffy@value:yaml_value()} |
{error, taffy@parser@types:parse_error()}.
parse(Input) ->
case taffy@lexer:tokenize(Input) of
{error, {Msg, Pos}} ->
{error, {parse_error, Msg, Pos}};
{ok, Tokens} ->
_pipe = taffy@parser:parse(Tokens),
gleam@result:map(_pipe, fun taffy@value:resolve_merges/1)
end.
-file("src/taffy.gleam", 92).
?DOC(
" Parse a YAML stream as a list of documents. Empty input yields `[]`.\n"
" Same merge-key + duplicate semantics as `parse`.\n"
).
-spec parse_all(binary()) -> {ok, list(taffy@value:yaml_value())} |
{error, taffy@parser@types:parse_error()}.
parse_all(Input) ->
case taffy@lexer:tokenize(Input) of
{error, {Msg, Pos}} ->
{error, {parse_error, Msg, Pos}};
{ok, Tokens} ->
_pipe = taffy@parser:parse_all(Tokens),
gleam@result:map(
_pipe,
fun(_capture) ->
gleam@list:map(_capture, fun taffy@value:resolve_merges/1)
end
)
end.
-file("src/taffy.gleam", 103).
?DOC(
" Reject any mapping (recursively) that contains duplicate keys. The error\n"
" message names the first offending key. Run after `parse` if your\n"
" application requires YAML 1.2's key-uniqueness invariant.\n"
).
-spec validate_unique_keys(taffy@value:yaml_value()) -> {ok,
taffy@value:yaml_value()} |
{error, taffy@parser@types:parse_error()}.
validate_unique_keys(Val) ->
case taffy@value:check_no_duplicates(Val) of
{ok, nil} ->
{ok, Val};
{error, Key} ->
{error,
{parse_error, <<"Duplicate mapping key: "/utf8, Key/binary>>, 0}}
end.
-file("src/taffy.gleam", 112).
?DOC(
" Look up a key in a mapping. Returns `Error(Nil)` when `val` is not a\n"
" mapping or the key is absent.\n"
).
-spec get(taffy@value:yaml_value(), binary()) -> {ok, taffy@value:yaml_value()} |
{error, nil}.
get(Val, Key) ->
_pipe = taffy@value:get(Val, Key),
gleam@option:to_result(_pipe, nil).
-file("src/taffy.gleam", 118).
?DOC(
" Like `get`, but returns `default` instead of an error when the key is\n"
" missing or `val` is not a mapping.\n"
).
-spec get_or(taffy@value:yaml_value(), binary(), taffy@value:yaml_value()) -> taffy@value:yaml_value().
get_or(Val, Key, Default) ->
case taffy@value:get(Val, Key) of
{some, V} ->
V;
none ->
Default
end.
-file("src/taffy.gleam", 127).
?DOC(
" Walk a chain of keys through nested mappings. An empty path returns\n"
" `val` unchanged; any missing key short-circuits to `Error(Nil)`.\n"
).
-spec get_path(taffy@value:yaml_value(), list(binary())) -> {ok,
taffy@value:yaml_value()} |
{error, nil}.
get_path(Val, Path) ->
case Path of
[] ->
{ok, Val};
[Key | Rest] ->
case taffy@value:get(Val, Key) of
{some, V} ->
get_path(V, Rest);
none ->
{error, nil}
end
end.
-file("src/taffy.gleam", 141).
?DOC(
" Get the `idx`-th item of a sequence (0-indexed). Returns `Error(Nil)` if\n"
" `val` is not a sequence or `idx` is out of range.\n"
).
-spec index(taffy@value:yaml_value(), integer()) -> {ok,
taffy@value:yaml_value()} |
{error, nil}.
index(Val, Idx) ->
_pipe = taffy@value:index(Val, Idx),
gleam@option:to_result(_pipe, nil).
-file("src/taffy.gleam", 146).
?DOC(" Pull out a string scalar; `None` for any other variant.\n").
-spec as_string(taffy@value:yaml_value()) -> gleam@option:option(binary()).
as_string(Val) ->
taffy@value:as_string(Val).
-file("src/taffy.gleam", 151).
?DOC(" Pull out an int scalar; `None` for any other variant (no float coercion).\n").
-spec as_int(taffy@value:yaml_value()) -> gleam@option:option(integer()).
as_int(Val) ->
taffy@value:as_int(Val).
-file("src/taffy.gleam", 157).
?DOC(
" Pull out a float scalar. Coerces `Int(n)` to `Float(n)` so numeric callers\n"
" don't have to branch on the parse-time tag.\n"
).
-spec as_float(taffy@value:yaml_value()) -> gleam@option:option(float()).
as_float(Val) ->
taffy@value:as_float(Val).
-file("src/taffy.gleam", 162).
?DOC(" Pull out a bool scalar; `None` for any other variant.\n").
-spec as_bool(taffy@value:yaml_value()) -> gleam@option:option(boolean()).
as_bool(Val) ->
taffy@value:as_bool(Val).
-file("src/taffy.gleam", 167).
?DOC(" Pull out a sequence as a list. Order is preserved.\n").
-spec as_list(taffy@value:yaml_value()) -> gleam@option:option(list(taffy@value:yaml_value())).
as_list(Val) ->
taffy@value:as_list(Val).
-file("src/taffy.gleam", 173).
?DOC(
" Pull out a mapping as a `Dict`. Note: this loses YAML's insertion order;\n"
" use `as_pairs` if order matters.\n"
).
-spec as_dict(taffy@value:yaml_value()) -> gleam@option:option(gleam@dict:dict(binary(), taffy@value:yaml_value())).
as_dict(Val) ->
taffy@value:as_dict(Val).
-file("src/taffy.gleam", 179).
?DOC(
" Pull out a mapping as ordered `#(key, value)` pairs, preserving YAML\n"
" insertion order.\n"
).
-spec as_pairs(taffy@value:yaml_value()) -> gleam@option:option(list({binary(),
taffy@value:yaml_value()})).
as_pairs(Val) ->
taffy@value:as_pairs(Val).
-file("src/taffy.gleam", 184).
?DOC(" True only for `Null`. Bool/Int/zero values all return False.\n").
-spec is_null(taffy@value:yaml_value()) -> boolean().
is_null(Val) ->
taffy@value:is_null(Val).
-file("src/taffy.gleam", 190).
?DOC(
" Render a value as a JSON-shaped debug string. Useful for snapshot tests;\n"
" for round-trippable YAML output use `to_yaml`.\n"
).
-spec to_string(taffy@value:yaml_value()) -> binary().
to_string(Val) ->
taffy@value:to_string(Val).
-file("src/taffy.gleam", 197).
?DOC(
" Emit a value as block-style YAML with a trailing newline. The output\n"
" round-trips through `parse` for all values that are themselves\n"
" round-trippable (taffy is lossy on tag, anchor, and comment metadata).\n"
).
-spec to_yaml(taffy@value:yaml_value()) -> binary().
to_yaml(Val) ->
taffy@value:to_yaml(Val).
-file("src/taffy.gleam", 203).
?DOC(
" Convert a parsed YAML value into a `gleam_json` `Json`. Whole-valued\n"
" floats serialize as ints to round-trip nicely with the YAML test suite.\n"
).
-spec to_json(taffy@value:yaml_value()) -> gleam@json:json().
to_json(Val) ->
case Val of
null ->
gleam@json:null();
{bool, B} ->
gleam@json:bool(B);
{int, I} ->
gleam@json:int(I);
{float, F} ->
Truncated = erlang:trunc(F),
case F =:= erlang:float(Truncated) of
true ->
gleam@json:int(Truncated);
false ->
gleam@json:float(F)
end;
{string, S} ->
gleam@json:string(S);
{sequence, Items} ->
gleam@json:array(Items, fun to_json/1);
{mapping, Pairs} ->
gleam@json:object(
begin
_pipe = Pairs,
gleam@list:map(
_pipe,
fun(Pair) ->
{erlang:element(1, Pair),
to_json(erlang:element(2, Pair))}
end
)
end
)
end.
-file("src/taffy.gleam", 226).
?DOC(" Convenience wrapper around `to_json` + `json.to_string`.\n").
-spec to_json_string(taffy@value:yaml_value()) -> binary().
to_json_string(Val) ->
_pipe = to_json(Val),
gleam@json:to_string(_pipe).