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

Instance Method Details

#appended(bindings) ⇒ Object



251
252
253
# File 'lib/hecks/bluebook/meta_validator/shapes.rb', line 251

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



255
256
257
258
259
260
261
262
# File 'lib/hecks/bluebook/meta_validator/shapes.rb', line 255

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.



116
# File 'lib/hecks/bluebook/meta_validator/shapes.rb', line 116

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.



137
# File 'lib/hecks/bluebook/meta_validator/shapes.rb', line 137

def from(row) = decode_literal(text(row[:from]))

#gathered(parts) ⇒ Object



151
152
153
154
155
156
157
158
# File 'lib/hecks/bluebook/meta_validator/shapes.rb', line 151

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



206
# File 'lib/hecks/bluebook/meta_validator/shapes.rb', line 206

def group_by_field(row) = { field: text(row[:field]) }

#head(row) ⇒ Object



196
197
198
199
200
201
202
203
204
# File 'lib/hecks/bluebook/meta_validator/shapes.rb', line 196

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



181
182
183
184
185
186
# File 'lib/hecks/bluebook/meta_validator/shapes.rb', line 181

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).



241
242
243
244
245
246
247
248
249
# File 'lib/hecks/bluebook/meta_validator/shapes.rb', line 241

def mutation(target, op, bindings)
  base = { target: target.to_sym, op: op.to_sym, sign: Hecks::Bluebook::Mutation.sign_for(op) }
  # `:delegate`/`:corrects` (CommandBuilder#delegates_to's and
  # #corrects_impl's own comments) ride the SAME multi-binding
  # shape `:append` does.
  return base.merge(fields: appended(bindings)) if ["append", "delegate", "corrects"].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.



226
227
228
229
230
# File 'lib/hecks/bluebook/meta_validator/shapes.rb', line 226

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.



145
146
147
148
149
# File 'lib/hecks/bluebook/meta_validator/shapes.rb', line 145

def options_of(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.



174
175
176
177
178
179
# File 'lib/hecks/bluebook/meta_validator/shapes.rb', line 174

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.



105
106
107
108
# File 'lib/hecks/bluebook/meta_validator/shapes.rb', line 105

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.



123
124
125
# File 'lib/hecks/bluebook/meta_validator/shapes.rb', line 123

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:.



131
# File 'lib/hecks/bluebook/meta_validator/shapes.rb', line 131

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).



218
# File 'lib/hecks/bluebook/meta_validator/shapes.rb', line 218

def read_model_count(row) = (true if text(row[:count]).to_s == "true")

#rule(row) ⇒ Object



118
# File 'lib/hecks/bluebook/meta_validator/shapes.rb', line 118

def rule(row) = { description: text(row[:description]), canonical: text(row[:canonical]) }

#shape_field(field, aggregate_id = nil) ⇒ Object

An argument, a parameter, a piece's attribute — the three places an attribute is written that carry no owner id to strip... except a piece's own attribute now does : an entity is its own root (repeating the aggregate's whole shape one level down), so ITS attributes name a value object exactly the way an aggregate's own do (Judge#cell's own comment), and reconstruction has to undo the same owned-vs-reference split attribute, below, already undoes for an aggregate — not the cruder "any qualified name is a Reference" guess this used before, which mistook a piece's own value-object type for a cross-aggregate head every time.

aggregate_id, passed only for a piece's own attribute (an argument or a parameter carries no value-object type at all — see entity.bluebook's/command.bluebook's own comments on why — so the generic reader path calls this with one argument and gets the OLD reading unchanged), is the piece's OWNING aggregate — the same one its own attribute types were resolved against on the way in (Judge#owning_aggregate_ref).

A REFERENCE among the other two came in as the head's ID, because that is what Command/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.



79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
# File 'lib/hecks/bluebook/meta_validator/shapes.rb', line 79

def shape_field(field, aggregate_id = nil)
  return attribute(field, aggregate_id) if aggregate_id

  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



188
189
190
191
192
193
194
# File 'lib/hecks/bluebook/meta_validator/shapes.rb', line 188

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.



169
170
171
# File 'lib/hecks/bluebook/meta_validator/shapes.rb', line 169

def where_clause(row)
  { field: text(row[:field]), op: text(row[:op]), value: text(row[:value]) }
end