Current section
Files
Jump to
Current section
Files
lib/toon/decoder/event_builder.ex
defmodule ExToon.Decoder.EventBuilder do
@moduledoc false
# Consumes a flat list of stream events produced by Parser and assembles
# the final json_value() tree.
#
# Event types consumed:
# %{type: :primitive, value: v}
# %{type: :start_object} ... %{type: :key, key: k} VALUE ... %{type: :end_object}
# %{type: :start_array, length: n} ... VALUE* ... %{type: :end_array}
alias ExToon.DecodeError
# Build a json_value() from a list of stream events.
# Raises ExToon.DecodeError on incomplete or malformed event streams.
@spec build([map()]) :: term()
def build(events) do
case do_build(events) do
{value, []} ->
value
{_value, remaining} when remaining != [] ->
raise %DecodeError{
reason: :incomplete_stream,
message: "Unexpected events after root value: #{inspect(hd(remaining))}"
}
end
end
# Build while preserving document order for objects.
# Objects are returned as {:toon_ordered_object, [{key_or_literal_tag, value}, ...]}
# where key_or_literal_tag is {:toon_literal, key} for quoted keys or just key (string)
# for unquoted keys. Used by the expand pathway to enable correct LWW semantics.
@spec build_for_expand([map()]) :: term()
def build_for_expand(events) do
case do_build_for_expand(events) do
{value, []} ->
value
{_value, remaining} when remaining != [] ->
raise %DecodeError{
reason: :incomplete_stream,
message: "Unexpected events after root value: #{inspect(hd(remaining))}"
}
end
end
# ---------------------------------------------------------------------------
# Order-preserving builder (for expand pathway)
# ---------------------------------------------------------------------------
defp do_build_for_expand([]) do
raise %DecodeError{
reason: :incomplete_stream,
message: "Incomplete event stream: no events to build from"
}
end
defp do_build_for_expand([%{type: :primitive, value: v} | rest]) do
{v, rest}
end
defp do_build_for_expand([%{type: :start_object} | rest]) do
{pairs, remaining} = collect_object_ordered(rest, [])
{{:toon_ordered_object, pairs}, remaining}
end
defp do_build_for_expand([%{type: :start_array} | rest]) do
{items, remaining} = collect_array_ordered(rest, [])
{items, remaining}
end
defp do_build_for_expand([event | _]) do
raise %DecodeError{
reason: :incomplete_stream,
message: "Unexpected event: #{inspect(event)}"
}
end
defp collect_object_ordered([%{type: :end_object} | rest], acc) do
{Enum.reverse(acc), rest}
end
defp collect_object_ordered([%{type: :key, key: k} = event | rest], acc) do
literal = Map.get(event, :literal, false)
tagged_key = if literal, do: {:toon_literal, k}, else: k
{value, remaining} = do_build_for_expand(rest)
collect_object_ordered(remaining, [{tagged_key, value} | acc])
end
defp collect_object_ordered([], _acc) do
raise %DecodeError{
reason: :incomplete_stream,
message: "Incomplete event stream: missing :end_object"
}
end
defp collect_object_ordered([event | _], _acc) do
raise %DecodeError{
reason: :incomplete_stream,
message: "Expected :key or :end_object, got: #{inspect(event)}"
}
end
defp collect_array_ordered([%{type: :end_array} | rest], acc) do
{Enum.reverse(acc), rest}
end
defp collect_array_ordered([] = _events, _acc) do
raise %DecodeError{
reason: :incomplete_stream,
message: "Incomplete event stream: missing :end_array"
}
end
defp collect_array_ordered(events, acc) do
{value, remaining} = do_build_for_expand(events)
collect_array_ordered(remaining, [value | acc])
end
# ---------------------------------------------------------------------------
# Internal recursive builder — returns {value, remaining_events}
# ---------------------------------------------------------------------------
defp do_build([]) do
raise %DecodeError{
reason: :incomplete_stream,
message: "Incomplete event stream: no events to build from"
}
end
defp do_build([%{type: :primitive, value: v} | rest]) do
{v, rest}
end
defp do_build([%{type: :start_object} | rest]) do
{pairs, remaining} = collect_object(rest, [])
{Map.new(pairs), remaining}
end
defp do_build([%{type: :start_array} | rest]) do
{items, remaining} = collect_array(rest, [])
{items, remaining}
end
defp do_build([event | _]) do
raise %DecodeError{
reason: :incomplete_stream,
message: "Unexpected event: #{inspect(event)}"
}
end
# ---------------------------------------------------------------------------
# Object collector — reads key/value pairs until :end_object
# ---------------------------------------------------------------------------
defp collect_object([%{type: :end_object} | rest], acc) do
{Enum.reverse(acc), rest}
end
defp collect_object([%{type: :key, key: k} | rest], acc) do
{value, remaining} = do_build(rest)
collect_object(remaining, [{k, value} | acc])
end
defp collect_object([], _acc) do
raise %DecodeError{
reason: :incomplete_stream,
message: "Incomplete event stream: missing :end_object"
}
end
defp collect_object([event | _], _acc) do
raise %DecodeError{
reason: :incomplete_stream,
message: "Expected :key or :end_object, got: #{inspect(event)}"
}
end
# ---------------------------------------------------------------------------
# Array collector — reads values until :end_array
# ---------------------------------------------------------------------------
defp collect_array([%{type: :end_array} | rest], acc) do
{Enum.reverse(acc), rest}
end
defp collect_array([] = _events, _acc) do
raise %DecodeError{
reason: :incomplete_stream,
message: "Incomplete event stream: missing :end_array"
}
end
defp collect_array(events, acc) do
{value, remaining} = do_build(events)
collect_array(remaining, [value | acc])
end
end