Current section
Files
Jump to
Current section
Files
lib/toon/encoder/folding.ex
defmodule ExToon.Encoder.Folding do
@moduledoc false
# Key folding: collapse single-key wrapper chains into dotted paths.
#
# Example with key_folding: :safe:
# "a" => [{"b", [{"c", 42}]}] → "a.b.c" => 42
#
# Folding only applies when ALL of the following conditions are met:
# - key_folding is :safe (never :off)
# - the value is an object (list of {binary_key, value} pairs)
# - that object has exactly one key
# - the child key is a safe identifier: ^[A-Za-z_][A-Za-z0-9_]*$
# - the current (parent) key has no segments that look unsafe for dotting
# - the current depth has not exceeded flatten_depth
@spec fold(String.t(), term(), :off | :safe, pos_integer() | :infinity, non_neg_integer()) ::
{String.t(), term()}
def fold(key, value, :off, _max_depth, _depth), do: {key, value}
def fold(key, value, :safe, max_depth, depth) do
do_fold(key, value, max_depth, depth)
end
# Recursively descend single-key chains, building up a dotted key.
defp do_fold(key, value, max_depth, depth) when is_list(value) do
if within_depth?(depth, max_depth) and single_key_object?(value) do
[{child_key, child_value}] = value
# Both segments must be safe identifiers for dot-joining
if safe_identifier?(child_key) and safe_all_segments?(key) do
do_fold("#{key}.#{child_key}", child_value, max_depth, depth + 1)
else
{key, value}
end
else
{key, value}
end
end
defp do_fold(key, value, _max_depth, _depth), do: {key, value}
defp within_depth?(_depth, :infinity), do: true
# flattenDepth=N means the folded key can have at most N segments.
# Starting from 1 segment (just the key), each fold step adds 1 segment.
# So at most (N-1) folds are allowed. depth=0 before first fold, 1 after first, etc.
# Condition: depth < N-1. For N<=1, this is never true (no folding).
defp within_depth?(_depth, max_depth) when is_integer(max_depth) and max_depth <= 1, do: false
defp within_depth?(depth, max_depth) when is_integer(max_depth), do: depth < max_depth - 1
# Object with exactly one entry (list of exactly one {binary_key, _} pair)
defp single_key_object?([{k, _}]) when is_binary(k), do: true
defp single_key_object?(_), do: false
# Child key segment: no dots allowed (^[A-Za-z_][A-Za-z0-9_]*$)
defp safe_identifier?(str), do: Regex.match?(~r/^[A-Za-z_][A-Za-z0-9_]*$/, str)
# Parent key may already be dotted (from a previous fold step); each segment
# must satisfy the identifier pattern. Accepts already-dotted keys like "a.b".
defp safe_all_segments?(str) do
Regex.match?(~r/^[A-Za-z_][A-Za-z0-9_.]*$/, str)
end
end