Module: Axn::Internal::SubfieldTree

Defined in:
lib/axn/internal/subfield_tree.rb

Overview

Groups an Axn's subfield configs into per-root trees keyed by WIRE KEY (the JSON property name a client sends), resolving each config's on: chain once. Emission, requiredness derivation, and the dropped-subfield query all read the same finished tree, so they cannot drift.

on: names a READER (reader_as — the as:/prefix: alias when present); schema properties are keyed by wire key (field). This builder is the single place that translation happens: the root on: segment is looked up among top-level readers first, then subfield readers (a subfield anchor attaches the config beneath that subfield's own resolved node). Remaining dotted on: segments become IMPLICIT nodes — intermediate keys with no declaration of their own.

Side-effect-free: inspects declared configs only; never runs user code.

Defined Under Namespace

Classes: Node, ResolutionResult, ResolvedPath

Class Method Summary collapse

Class Method Details

.attach_config!(config, anchor, anchor_hops, segments) ⇒ Object

Walk (creating implicit intermediates as needed) from anchor down segments, attach the config at the leaf, and return [leaf, hops].



93
94
95
96
97
98
99
100
101
102
103
104
105
# File 'lib/axn/internal/subfield_tree.rb', line 93

def attach_config!(config, anchor, anchor_hops, segments)
  hops = anchor_hops.dup
  node = anchor
  segments[0..-2].each do |seg|
    hops << [node, seg]
    node = (node.children[seg] ||= Node.new(configs: [], children: {}))
  end
  leaf_key = segments.last
  hops << [node, leaf_key]
  leaf = (node.children[leaf_key] ||= Node.new(configs: [], children: {}))
  leaf.configs << config
  [leaf, hops]
end

.build(field_configs, subfield_configs) ⇒ Object



50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
# File 'lib/axn/internal/subfield_tree.rb', line 50

def build(field_configs, subfield_configs)
  roots = field_configs.to_h { |c| [c.reader_as, Node.new(configs: [c], children: {})] }
  # config => ResolvedPath, identity-keyed: distinct declarations are distinct entries even if
  # they compare equal as Data values.
  index = {}.compare_by_identity
  field_configs.each { |c| index[c] = ResolvedPath.new(node: roots[c.reader_as], wire_path: [c.field], ancestors: [], parent_index: 0) }
  by_reader = {} # subfield reader_as => {node:, hops:} — anchor targets for a subfield-of-a-subfield
  deep_paths = [] # [config, hops] judged only once the tree is COMPLETE (an ancestor's type may be declared after the deep config)

  Array(subfield_configs).each do |config|
    root_key, *on_rest = config.on.to_s.split(".").map(&:to_sym)
    anchor_hops = []
    anchor = roots[root_key]
    if anchor.nil? && (entry = by_reader[root_key])
      anchor_hops = entry[:hops]
      anchor = entry[:node]
    end
    # Only a bare `on: :ambient_context` with no declared ambient field lands here — deliberately
    # excluded from the schema (EXCLUDED_FROM_INPUT_SCHEMA), so it is neither attached nor dropped.
    next if anchor.nil?

    segments = on_rest + config.field.to_s.split(".").map(&:to_sym)
    leaf, hops = attach_config!(config, anchor, anchor_hops, segments)

    # The chain always starts at a top-level root (an anchored subfield's hops were themselves
    # rooted there), so the first hop's node carries the root's wire key. The `on:` target sits
    # after the anchor chain plus any dotted-`on:` segments (the config's own field segments
    # descend BELOW it).
    index[config] = ResolvedPath.new(node: leaf, wire_path: [hops.first.first.config.field, *hops.map(&:last)],
                                     ancestors: hops, parent_index: anchor_hops.size + on_rest.size)

    # Every declared config bears a reader, so any subfield can anchor a later `on:`.
    by_reader[config.reader_as.to_sym] = { node: leaf, hops: }
    # Shallow (single hop off a top-level root) configs are always representable; only deeper
    # paths are candidates for dropping.
    deep_paths << [config, hops] if hops.size > 1
  end

  ResolutionResult.new(roots:, deep_paths:, index:)
end