Module: Hecks::Bluebook::MetaValidator::Shapes
- Included in:
- Reconstruction
- Defined in:
- lib/hecks/bluebook/meta_validator/shapes.rb
Overview
The leaf shapes of a reconstructed bluebook, and the encodings that go with them.
Reconstruction walks the tree; this rebuilds the small hashes at its tips — an attribute, a rule, a where-clause, a transition — and undoes the two things the walk encodes on the way in: an attribute's type offered as the ID of whatever it names, and an append flattened to one row per binding.
Separate from the traversal because they are different concerns, and because the two together were 211 code lines against a 200 limit.
Instance Method Summary collapse
- #appended(bindings) ⇒ Object
-
#attribute(field, aggregate_id) ⇒ Object
The type came in as the id of what it names, so it goes back out as the name — or, for another aggregate's head, as the encoding the IR spells.
- #classified(binding) ⇒ Object
-
#decode_literal(text) ⇒ Object
A literal, read back from the self-describing form Readings#encode_literal wrote — the same reader Assembly::Marks uses, because it is the same spelling.
-
#from(row) ⇒ Object
command "Debit", from: "open"— the SAME literal encodingprovenance/default:already ride (S10, ADR 0025), one state or an array of them, or nil for a command with no lifecycle guard. - #gathered(parts) ⇒ Object
- #group_by_field(row) ⇒ Object
- #head(row) ⇒ Object
-
#identity_path(part) ⇒ Object
ONE PART OF AN IDENTITY, read back as the path it went in as.
- #limit(row) ⇒ Object
-
#mutation(target, op, bindings) ⇒ Object
sign:— read the SAME way the declared side computes it (Bluebook::Mutation.sign_for, item #5 of the whole-project table-unification survey), not stored on any row here — the meta-domain's own Change entity carries nosignfield of its own (it is a pure function ofop, nothing to persist), so reconstruction recomputes it the same way a freshly-built Mutation's ownto_hlambda does, rather than leaving the key silently absent (spec/round_trip_spec's whole point: a field the language does not hold is a named gap, not a byte-for-byte one). -
#mutations(row) ⇒ Object
THE APPEND FLATTENING, IN REVERSE.
-
#options_of(row) ⇒ Object
THE OPTION ROWS, GATHERED BACK into the shapes
extra_options_to_hspells. -
#order_by(row) ⇒ Object
One object in the IR, two fields in the language.
-
#owned_type(type, aggregate_id) ⇒ Object
A TYPE THIS AGGREGATE OWNS, offered as its id and read back as its name.
-
#presence(text) ⇒ Object
An ABSENT pattern is nil, not "".
-
#projected_field(row) ⇒ Object
S12, ADR 0025 —
projects :name, from: :"reference.remote_field", read back the same three plain identifiersruleabove reads description/canonical as. -
#provenance(row) ⇒ Object
provenance from: {...}rides the same literal encodingdefault:does — an object literal, self-describing via Hecks::Literal — one level up: a whole keyword's argument rather than an attribute'sdefault:. -
#read_model_count(row) ⇒ Object
count's own boolean, read back the SAME wayhead's ownmanyis (text(row[:many]).to_s == "true") — except aReadModel.Countcommand is dispatched AT ALL only when@countwas truthy (MetaValidator::Judge#settersskips a setter whose every source is absent), so an undeclared read model's owncountfield never gets written and comes backnilhere, never"false"— matchingReadModel#to_h's owntrue/nilpair (neverfalse) exactly, rather than the unconditionaltrue/falsehead's ownmanyneeds (every head DOES get aGatherdispatch, declared or derived). - #rule(row) ⇒ Object
-
#shape_field(field) ⇒ Object
An argument, a parameter, a piece's attribute — the three places an attribute is written that carry no owner id to strip.
- #transition(row) ⇒ Object
-
#where_clause(row) ⇒ Object
The IR keeps a where's field as a STRING, not a symbol — it is read back out, never called.
Instance Method Details
#appended(bindings) ⇒ Object
231 232 233 |
# File 'lib/hecks/bluebook/meta_validator/shapes.rb', line 231 def appended(bindings) bindings.to_h { |binding| [text(binding[:field]).to_sym, text(binding[:source])] } end |
#attribute(field, aggregate_id) ⇒ Object
The type came in as the id of what it names, so it goes back out as the name — or, for another aggregate's head, as the encoding the IR spells.
17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 |
# File 'lib/hecks/bluebook/meta_validator/shapes.rb', line 17 def attribute(field, aggregate_id) type = text(field[:type]).to_s { name: text(field[:name])&.to_sym, type: owned_type(type, aggregate_id) || reference_type(type), list: text(field[:list]).to_s == "true", default: decode_literal(text(field[:default])), # Read back the same way `list` is — both are booleans about the # attribute, held as text, and dropping either would rebuild a # bluebook that no longer says what it said. optional: text(field[:optional]).to_s == "true", pattern: presence(text(field[:pattern])), # THE ROUND TRIP IS THE ONLY WAY IN. The grammar registry keeps the # ASSEMBLED graph — the language as its own judge read it back — so a # fact dropped here is a fact no downstream projection ever sees, no # matter how plainly the .bluebook file declares it. admits: presence(text(field[:admits])), relationship: presence(text(field[:relationship])) } end |
#classified(binding) ⇒ Object
235 236 237 238 239 240 241 242 |
# File 'lib/hecks/bluebook/meta_validator/shapes.rb', line 235 def classified(binding) kind = text(binding[:kind]) value = text(binding[:source]) return { kind: kind, name: value } if %w[argument state].include?(kind) { kind: "literal", value: decode_literal(value) } end |
#decode_literal(text) ⇒ Object
A literal, read back from the self-describing form Readings#encode_literal
wrote — the same reader Assembly::Marks uses, because it is the same
spelling. The forms are exactly the five the Primitive vocabulary admits —
String, Integer, Float, TrueClass, FalseClass — plus a symbol, and an
object literal, which is what to: { value: "good" } is: a value object's
fields written inline.
97 |
# File 'lib/hecks/bluebook/meta_validator/shapes.rb', line 97 def decode_literal(text) = Literal.read(text) |
#from(row) ⇒ Object
command "Debit", from: "open" — the SAME literal encoding
provenance/default: already ride (S10, ADR 0025), one
state or an array of them, or nil for a command with no
lifecycle guard.
118 |
# File 'lib/hecks/bluebook/meta_validator/shapes.rb', line 118 def from(row) = decode_literal(text(row[:from])) |
#gathered(parts) ⇒ Object
132 133 134 135 136 137 138 139 |
# File 'lib/hecks/bluebook/meta_validator/shapes.rb', line 132 def gathered(parts) repeated, single = parts.partition { |part| !text(part[:at]).to_s.empty? } return single.to_h { |part| [text(part[:key]).to_sym, text(part[:value])] } if repeated.empty? repeated.group_by { |part| text(part[:at]) } .values .map { |group| group.to_h { |part| [text(part[:key]).to_sym, text(part[:value])] } } end |
#group_by_field(row) ⇒ Object
187 |
# File 'lib/hecks/bluebook/meta_validator/shapes.rb', line 187 def group_by_field(row) = { field: text(row[:field]) } |
#head(row) ⇒ Object
177 178 179 180 181 182 183 184 185 |
# File 'lib/hecks/bluebook/meta_validator/shapes.rb', line 177 def head(row) { aggregate: text(row[:aggregate]), # A String, like an entity's identified_by. The IR is not uniform about # this and only a round trip says so. as: text(row[:as]), many: text(row[:many]).to_s == "true" } end |
#identity_path(part) ⇒ Object
ONE PART OF AN IDENTITY, read back as the path it went in as. The inverse
of Marks#identity_path, and named the same so the two directions read
as one table.
52 |
# File 'lib/hecks/bluebook/meta_validator/shapes.rb', line 52 def identity_path(part) = text(part[:value]).to_s |
#limit(row) ⇒ Object
162 163 164 165 166 167 |
# File 'lib/hecks/bluebook/meta_validator/shapes.rb', line 162 def limit(row) ceiling = text(row[:limit]) return nil if ceiling.to_s.empty? { value: ceiling } end |
#mutation(target, op, bindings) ⇒ Object
sign: — read the SAME way the declared side computes it
(Bluebook::Mutation.sign_for, item #5 of the whole-project
table-unification survey), not stored on any row here — the
meta-domain's own Change entity carries no sign field of its
own (it is a pure function of op, nothing to persist), so
reconstruction recomputes it the same way a freshly-built
Mutation's own to_h lambda does, rather than leaving the key
silently absent (spec/round_trip_spec's whole point: a field the
language does not hold is a named gap, not a byte-for-byte one).
222 223 224 225 226 227 228 229 |
# File 'lib/hecks/bluebook/meta_validator/shapes.rb', line 222 def mutation(target, op, bindings) base = { target: target.to_sym, op: op.to_sym, sign: Hecks::Bluebook::Mutation.sign_for(op) } # `:delegate` (CommandBuilder#delegates_to's own comment) rides # the SAME multi-binding shape `:append` does. return base.merge(fields: appended(bindings)) if ["append", "delegate"].include?(op) base.merge(source: classified(bindings.first)) end |
#mutations(row) ⇒ Object
THE APPEND FLATTENING, IN REVERSE.
An append binds several fields at once and the language's Change holds one field/kind/source triple, so the walk offers an append once PER BINDING. Rebuilding groups those rows back into the single mutation the IR keeps — the only place here that undoes something rather than simply reading it.
207 208 209 210 211 |
# File 'lib/hecks/bluebook/meta_validator/shapes.rb', line 207 def mutations(row) Array(row[:mutations]) .group_by { |change| [text(change[:target]), text(change[:op])] } .map { |(target, op), bindings| mutation(target, op, bindings) } end |
#options_of(row) ⇒ Object
THE OPTION ROWS, GATHERED BACK into the shapes extra_options_to_h spells.
One row per part, so a compound option is several rows and a repeated one is
several groups told apart by at. Grouping by option name and then by at
rebuilds both without either knowing which options exist — the whole point
of holding them as an open map.
126 127 128 129 130 |
# File 'lib/hecks/bluebook/meta_validator/shapes.rb', line 126 def (row) Array(row[:options]) .group_by { |part| text(part[:option]) } .to_h { |option, parts| [option.to_sym, gathered(parts)] } end |
#order_by(row) ⇒ Object
One object in the IR, two fields in the language.
155 156 157 158 159 160 |
# File 'lib/hecks/bluebook/meta_validator/shapes.rb', line 155 def order_by(row) field = text(row[:order_field]) return nil if field.to_s.empty? { field: field, direction: text(row[:order_way]) } end |
#owned_type(type, aggregate_id) ⇒ Object
A TYPE THIS AGGREGATE OWNS, offered as its id and read back as its name. Prefixed with the owner and the identity join, because that IS the value object's identity — the aggregate it belongs to, then its name.
42 43 44 45 46 47 |
# File 'lib/hecks/bluebook/meta_validator/shapes.rb', line 42 def owned_type(type, aggregate_id) prefix = Naming.identity([aggregate_id, ""]) return nil unless type.start_with?(prefix) type.delete_prefix(prefix) end |
#presence(text) ⇒ Object
An ABSENT pattern is nil, not "". The language holds every field as text, so a field nobody set comes back as the empty string — and "" is a real regex (it matches everything), so keeping it would turn "no pattern" into "a pattern that always passes" and quietly cost the IR its round trip.
86 87 88 89 |
# File 'lib/hecks/bluebook/meta_validator/shapes.rb', line 86 def presence(text) value = text.to_s value.empty? ? nil : value end |
#projected_field(row) ⇒ Object
S12, ADR 0025 — projects :name, from: :"reference.remote_field",
read back the same three plain identifiers rule above reads
description/canonical as.
104 105 106 |
# File 'lib/hecks/bluebook/meta_validator/shapes.rb', line 104 def projected_field(row) { name: text(row[:name]), reference: text(row[:reference]), remote_field: text(row[:remote_field]) } end |
#provenance(row) ⇒ Object
provenance from: {...} rides the same literal encoding default:
does — an object literal, self-describing via Hecks::Literal — one
level up: a whole keyword's argument rather than an attribute's
default:.
112 |
# File 'lib/hecks/bluebook/meta_validator/shapes.rb', line 112 def provenance(row) = decode_literal(text(row[:provenance])) |
#read_model_count(row) ⇒ Object
count's own boolean, read back the SAME way head's own
many is (text(row[:many]).to_s == "true") — except a
ReadModel.Count command is dispatched AT ALL only when
@count was truthy (MetaValidator::Judge#setters skips a
setter whose every source is absent), so an undeclared read
model's own count field never gets written and comes back
nil here, never "false" — matching ReadModel#to_h's own
true/nil pair (never false) exactly, rather than the
unconditional true/false head's own many needs (every
head DOES get a Gather dispatch, declared or derived).
199 |
# File 'lib/hecks/bluebook/meta_validator/shapes.rb', line 199 def read_model_count(row) = (true if text(row[:count]).to_s == "true") |
#rule(row) ⇒ Object
99 |
# File 'lib/hecks/bluebook/meta_validator/shapes.rb', line 99 def rule(row) = { description: text(row[:description]), canonical: text(row[:canonical]) } |
#shape_field(field) ⇒ Object
An argument, a parameter, a piece's attribute — the three places an attribute is written that carry no owner id to strip.
A REFERENCE among them came in as the head's ID, because that is what
Command/Entity/Query.Reference offer, so it goes back out as the
encoding the IR spells. A qualified name is the tell : an ordinary
type names something declared beside it (Money, AccountNumber) and
never carries a chapter, while a head's id always does.
62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 |
# File 'lib/hecks/bluebook/meta_validator/shapes.rb', line 62 def shape_field(field) type = text(field[:type]).to_s { name: text(field[:name])&.to_sym, # A QUALIFIED name is the tell : an ordinary type names something # declared beside it (`Money`, `AccountNumber`) and carries no join at # all, where a head's id always does (chapter + name, joined the way # every derived id is). type: type.include?(Naming::IDENTITY_JOIN) ? reference_type(type) : text(field[:type]), list: text(field[:list]).to_s == "true", default: decode_literal(text(field[:default])), optional: text(field[:optional]).to_s == "true", pattern: presence(text(field[:pattern])), admits: presence(text(field[:admits])), relationship: presence(text(field[:relationship])) } end |
#transition(row) ⇒ Object
169 170 171 172 173 174 175 |
# File 'lib/hecks/bluebook/meta_validator/shapes.rb', line 169 def transition(row) { command: text(row[:command]), from_state: text(row[:from_state]), to_state: text(row[:to_state]) } end |
#where_clause(row) ⇒ Object
The IR keeps a where's field as a STRING, not a symbol — it is read back
out, never called. The value stays RAW TEXT here on purpose : this
feeds the declaration hash Assembly::Marks#where_clause decodes
from (via read), and decoding twice is worse than once — a
kwarg reference (":ceiling") decoded here into the Symbol :ceiling
would have its colon stripped by read's own .to_s and come
back out as the plain string "ceiling", indistinguishable from a
literal of the same name. One decode, at the one place that builds
the object every comparator actually reads.
150 151 152 |
# File 'lib/hecks/bluebook/meta_validator/shapes.rb', line 150 def where_clause(row) { field: text(row[:field]), op: text(row[:op]), value: text(row[:value]) } end |