defmodule ExToon.Decoder.Core do @moduledoc false # Pipeline orchestrator for the TOON decoder. # # decode_lines/2 runs the full decode pipeline: # 1. Scanner.scan_lines/1 — [String.t()] → [%Scanner{}] + indent_size # 2. StrictMode.validate_lines/2 (if strict: true) — validates indentation # 3. Parser.parse/3 — [%Scanner{}] → [event()] # 4. EventBuilder.build/1 — [event()] → json_value() # 5. Expand.expand/1 (if expand_paths: :safe) — expands dotted keys alias ExToon.DecodeError alias ExToon.Decoder.{Scanner, Parser, EventBuilder, StrictMode, Expand} @spec decode_lines(Enumerable.t(), keyword()) :: {:ok, term()} | {:error, DecodeError.t()} def decode_lines(lines, opts) do strict = Keyword.get(opts, :strict, true) expand_paths = Keyword.get(opts, :expand_paths, :off) indent_override = Keyword.get(opts, :indent) try do lines_list = Enum.to_list(lines) # Step 1: scan into ParsedLine structs {:ok, parsed_lines, detected_indent_size} = Scanner.scan_lines(lines_list) # Use the caller-supplied indent size when provided; otherwise use auto-detected value. indent_size = indent_override || detected_indent_size # Re-scan with the correct indent_size when the caller overrides auto-detection. parsed_lines = if indent_override && indent_override != detected_indent_size do {:ok, rescanned, _} = Scanner.scan_lines(lines_list, indent_override) rescanned else parsed_lines end # Step 2: strict-mode indentation validation if strict do case StrictMode.validate_lines(parsed_lines, indent_size) do :ok -> :ok {:error, e} -> throw({:decode_error, e}) end end # Step 3: parse events from the line stream # Parser.parse/3 returns {:ok, events}; errors surface as thrown DecodeError # or matched by the rescue clause below. {:ok, events} = Parser.parse(parsed_lines, indent_size, strict) # Step 4 + 5: build the value tree and optionally expand dotted keys. # When expanding, use the order-preserving builder so LWW semantics are correct. result = if expand_paths == :safe do ordered_value = EventBuilder.build_for_expand(events) Expand.expand_ordered(ordered_value, Keyword.take(opts, [:strict])) else EventBuilder.build(events) end {:ok, result} rescue e in DecodeError -> {:error, e} catch {:decode_error, e} -> {:error, e} end end end