Packages

Token-Oriented Object Notation (TOON) encoder/decoder for Elixir

Current section

Files

Jump to
ex_toon lib toon decoder core.ex
Raw

lib/toon/decoder/core.ex

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