Module: Hecks::QuerySpecification::HopPath
- Defined in:
- lib/hecks/query_specification/hop_path.rb
Overview
ONE reading of a dotted query-field path that hops THROUGH a reference into another aggregate's own shape — FieldPath's sibling, not its member. FieldPath walks a SHAPE, which cannot loop, and answers nil, never raising, because there is nothing left to say beyond "not found." HopPath walks the REFERENCE GRAPH instead, which the language does not guarantee acyclic — aggregates only refuse a DIRECT bidirectional pair (BluebookBuilder#validate_no_bidirectional_references! explicitly declines to take a position on a longer ring) — so a walk here needs to say WHY it stopped, not just that it did.
Every method below takes an ATTRIBUTE ARRAY, never a "shape"
object — deliberately, because the two real callers hold their
attributes differently. AggregateBuilder (tier-1 seal, mid-
build) exposes attributes as a plain reader, but its own
attribute(name, type = String, ...) is the DSL's attribute-
DECLARING method — calling it as a finder would silently mint a
new String attribute instead of looking one up. A sealed
Aggregate (tier-2 seal, and every runtime target) has a
real attribute(name) finder, but taking the array either way
sidesteps the mismatch instead of asking every caller to know
which kind of object it holds.
Defined Under Namespace
Constant Summary collapse
- MAX_HOPS =
A hop chain long enough to matter is long enough to be a mistake — NOT a guard against a walk that cannot terminate. Nothing here loops forever regardless of how the reference graph is shaped: a hop chain is a literal dotted string, fixed at declaration time, and every step consumes exactly one of its own segments — the walk is bounded by what was TYPED, not by the graph. A self-referential aggregate hopping through itself more than once (
"parent.parent.name", a grandparent query) is real, common, and perfectly safe; this exists only to refuse a chain nobody meant to write this long. 8
Class Method Summary collapse
-
.hop_head?(field, attributes) ⇒ Boolean
Does this path's HEAD cross into another record via
/? THE OPERATOR IS THE ANSWER NOW, not a name collision to arbitrate —.walks fields inside this record,/crosses into another one, so a path with no/is never a hop, full stop, and the OLD "a real local attribute wins first" rule (needed only because.was overloaded for both meanings, andclient_idvsclientwas how the two were told apart) has nothing left to arbitrate. -
.hop_name(attribute) ⇒ Object
The segment name a Reference answers to in a hop path — its own declared attribute name, unchanged (ADR 0025, "References":
reference_tomints that bare name now, no_id, so there is no derivation left to apply). -
.next_hop(field, attributes) ⇒ Object
ONE STEP: does
field's head hop through one ofattributes' own references? Answers the resolvedHopplus the string still left to walk (itself possibly another/-hop, against the TARGET's own attributes, or a plain.-dotted field walk once the hops run out) — or nil, when the head names nothing or the path has no/at all. -
.plan(field, attributes) ⇒ Object
The WHOLE chain, resolved — every hop's target found, in order — for the one caller that needs it all at once: BluebookBuilder#validate_query_hops!, checking a hop chain before anything ever dispatches it.
Class Method Details
.hop_head?(field, attributes) ⇒ Boolean
Does this path's HEAD cross into another record via /? THE
OPERATOR IS THE ANSWER NOW, not a name collision to arbitrate —
. walks fields inside this record, / crosses into another
one, so a path with no / is never a hop, full stop, and the
OLD "a real local attribute wins first" rule (needed only
because . was overloaded for both meanings, and client_id
vs client was how the two were told apart) has nothing left
to arbitrate. Answerable from attributes alone — a Reference
knows its own target_name at declaration, before it can
resolve it — which is what lets the AGGREGATE seal recognise
a hop it cannot yet check.
69 70 71 72 73 74 |
# File 'lib/hecks/query_specification/hop_path.rb', line 69 def hop_head?(field, attributes) head, rest = field.to_s.split("/", 2) return false unless rest attributes.any? { |candidate| candidate.reference? && hop_name(candidate) == head } end |
.hop_name(attribute) ⇒ Object
The segment name a Reference answers to in a hop path — its own
declared attribute name, unchanged (ADR 0025, "References":
reference_to mints that bare name now, no _id, so there is
no derivation left to apply). proposal/client (a query hop)
and proposal.client (the Ruby accessor,
Facade::Handle#define_reference_accessors) name the same
concept the same way.
56 |
# File 'lib/hecks/query_specification/hop_path.rb', line 56 def hop_name(attribute) = attribute.name.to_s |
.next_hop(field, attributes) ⇒ Object
ONE STEP: does field's head hop through one of attributes'
own references? Answers the resolved Hop plus the string
still left to walk (itself possibly another /-hop, against
the TARGET's own attributes, or a plain .-dotted field walk
once the hops run out) — or nil, when the head names nothing or
the path has no / at all. This is the one primitive
Runtime::ReferenceHop needs: it recurses hop by hop through its
own apply, one ordinary same-aggregate query at a time, and
never needs the whole chain resolved up front the way a seal
does.
86 87 88 89 90 91 92 93 94 95 |
# File 'lib/hecks/query_specification/hop_path.rb', line 86 def next_hop(field, attributes) head, rest = field.to_s.split("/", 2) return nil unless rest attribute = attributes.find { |candidate| candidate.reference? && hop_name(candidate) == head } return nil unless attribute hop = Hop.new(attribute: attribute, target_name: attribute.type.target_name, target: attribute.type.resolve) [hop, rest] end |
.plan(field, attributes) ⇒ Object
The WHOLE chain, resolved — every hop's target found, in order — for the one caller that needs it all at once: BluebookBuilder#validate_query_hops!, checking a hop chain before anything ever dispatches it.
101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 |
# File 'lib/hecks/query_specification/hop_path.rb', line 101 def plan(field, attributes) hops = [] remaining = field.to_s current = attributes loop do step = next_hop(remaining, current) break unless step hop, rest = step return Plan.new(hops: hops, tail: nil, refusal: :too_deep) if hops.size >= MAX_HOPS # Pushed even unresolved — a caller reporting :unresolvable # needs THIS hop's own target_name (real, known at # declaration, regardless of whether resolve succeeded), not # whatever hop came before it. `.target` is nil on this one # entry; every caller checking `hops.last.target` already has # to handle that, the same way `next_hop`'s own caller does. hops << hop return Plan.new(hops: hops, tail: nil, refusal: :unresolvable) unless hop.target current = hop.target.attributes remaining = rest end Plan.new(hops: hops, tail: remaining, refusal: nil) end |