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
-
.attach_config!(config, anchor, anchor_hops, segments) ⇒ Object
Walk (creating implicit intermediates as needed) from
anchordownsegments, attach the config at the leaf, and return [leaf, hops]. - .build(field_configs, subfield_configs) ⇒ Object
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 |