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