Module: Hecks::Bluebook::Assembly::Marks

Defined in:
lib/hecks/bluebook/assembly/marks.rb

Overview

The leaf shapes an assembly reads, and the encodings it undoes.

to_h spells things as text so the export stands on its own, and every one of those spellings has to come back apart here. This is the same family of work MetaValidator::Shapes does for the reconstruction — the difference is that Shapes rebuilds HASHES and this rebuilds OBJECTS, so it has to recover types rather than just strings.

ENCODING LOSSES ARE THE LARGEST FAMILY OF BUG IN THIS CODEBASE, and every member has the same shape: reading an object where to_h holds a spelling. So each method below names the spelling it inverts.

Constant Summary collapse

OPTIONS =

EVERY OTHER SPECIFICATION OPTION, from one table.

Each entry names the struct and which of its members carry a value that rode Literal's spelling rather than plain text. A ninth option is one row here and nothing else — the language already holds it, because it holds options as an open map rather than a field each.

{
  offset:         [QuerySpecification::Common::OffsetSpec,        %i[value]],
  cursor:         [QuerySpecification::Common::CursorSpec,        %i[value]],
  null_semantics: [QuerySpecification::Common::NullSemantics,     []],
  authorization:  [QuerySpecification::Common::AuthorizationSpec, %i[policy tenant]],
  inspection:     [QuerySpecification::Common::InspectionSpec,    []]
}.freeze
SYMBOLIC =

mode and policy are read as symbols because the DSL declares them that way — nulls :last, authorize :customer_access — and to_h spells them with to_s, so the colon is not there to strip.

%i[mode policy tenant].freeze

Class Method Summary collapse

Class Method Details

.appended(fields) ⇒ Object



136
137
138
# File 'lib/hecks/bluebook/assembly/marks.rb', line 136

def appended(fields)
  Array(fields).to_h { |field, source| [field.to_sym, read(source)] }
end

.attribute(field) ⇒ Object

Attribute#to_h spells a type with to_s, so a reference arrives as "Reference" and has to become an edge again. Everything else is a name and stays one.



21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
# File 'lib/hecks/bluebook/assembly/marks.rb', line 21

def attribute(field)
  type = field[:type].to_s
  target = type[/\AReference<(.+)>\z/, 1]

  Attribute.new(
    name:         field[:name],
    type:         target ? Reference.new(target) : type,
    list:         field[:list] ? true : false,
    default:      field[:default],
    # The LAST place optionality can be dropped, and the one that was
    # dropping it. Every bluebook in the registry is the round-trip
    # product — MetaValidator dispatches the declaration in and reads it
    # back — so a fact this constructor does not carry is a fact the
    # language cannot state about itself, however plainly the source
    # wrote it.
    optional:     field[:optional] ? true : false,
    pattern:      field[:pattern],
    # THE SAME LESSON, ONE FACT LATER. `admits` is not on `to_h` — the
    # wire does not carry it, on purpose — but it must still survive the
    # round trip, because the grammar registry keeps the ASSEMBLED graph
    # and downstream projections read the link off that. Dropped here, the
    # language could not say `admits` about itself no matter how plainly
    # the source wrote it, which is word for word what the note above
    # already learned about `optional`.
    admits:       field[:admits],
    relationship: field[:relationship]
  )
end

.bindings(with) ⇒ Object

A saga's argument bindings. Each value rides Literal's spelling, which marks a Symbol with a leading colon — lose it and an argument reads as a string of the same name.



100
# File 'lib/hecks/bluebook/assembly/marks.rb', line 100

def bindings(with) = Array(with).to_h { |key, value| [key.to_sym, read(value)] }

.classified(source) ⇒ Object

A SET reads one thing, and classified_source said which: an argument by name, or a literal by value.



142
143
144
145
146
147
148
149
150
# File 'lib/hecks/bluebook/assembly/marks.rb', line 142

def classified(source)
  return nil if source.nil?

  case source[:kind].to_s
  when "argument" then source[:name].to_sym
  when "state"    then StateRef.new(source[:name].to_sym)
  else source[:value]
  end
end

.given(rule) ⇒ Object



106
107
108
# File 'lib/hecks/bluebook/assembly/marks.rb', line 106

def given(rule)
  Given.new(description: rule[:description], canonical: rule[:canonical])
end

.group_by_field(row) ⇒ Object

A group_by field's own name — the builder's native shape is already {field: :symbol}, so this matches it rather than leaving field as the String Shapes#group_by_field reads back.



81
# File 'lib/hecks/bluebook/assembly/marks.rb', line 81

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

.head(row) ⇒ Object

A read model's gathered head. The keys must be symbols whichever way the declaration arrived, and as must be one too: it NAMES the reader the projection answers to, and ReadModel#to_h spells it to_s.



74
75
76
# File 'lib/hecks/bluebook/assembly/marks.rb', line 74

def head(row)
  row.to_h { |key, value| [key.to_sym, key.to_sym == :as ? value.to_sym : value] }
end

.identity_path(part) ⇒ Object

ONE PART OF AN IDENTITY. It goes in as a row so the language can hold an ordered list of them, and comes back out as the path it always was — a String, because identity_paths splits paths and never symbols.



58
# File 'lib/hecks/bluebook/assembly/marks.rb', line 58

def identity_path(part) = part[:value].to_s

.invariant(rule) ⇒ Object



102
103
104
# File 'lib/hecks/bluebook/assembly/marks.rb', line 102

def invariant(rule)
  Invariant.new(description: rule[:description], canonical: rule[:canonical])
end

.limit(declared) ⇒ Object



185
186
187
188
189
# File 'lib/hecks/bluebook/assembly/marks.rb', line 185

def limit(declared)
  return nil unless declared

  QuerySpecification::Common::LimitSpec.new(value: read(declared[:value]))
end

.member(pairs) ⇒ Object

A member's fields — an OPEN MAP, which is why Member is its own root in the language and why the pairs arrive as a list rather than a value object.

The values are UNMARKED, because ValueObject#to_h spells them with to_s and the language stores them as text: member code: "JPY", minor_units: 0 came back with a minor_units of "0", and a closed set that admits the string would refuse the number the caller passes.



67
68
69
# File 'lib/hecks/bluebook/assembly/marks.rb', line 67

def member(pairs)
  pairs.to_h { |key, value| [key.to_sym, unmark_scalar(value)] }
end

.mutation(change) ⇒ Object

Mutation#to_h branches on the operation, so this does too.

An APPEND binds several fields at once, each either an ARGUMENT (a Symbol, wearing its colon) or a LITERAL — the distinction that is the whole reason append: { direction: "out" } was once indistinguishable from an argument named out.



125
126
127
128
129
130
131
132
133
134
# File 'lib/hecks/bluebook/assembly/marks.rb', line 125

def mutation(change)
  target = change[:target].to_sym
  op     = change[:op].to_sym

  # `:delegate` (CommandBuilder#delegates_to's own comment) rides
  # the SAME multi-binding shape `:append` does.
  return Mutation.new(target: target, op: op, source: appended(change[:fields])) if [:append, :delegate].include?(op)

  Mutation.new(target: target, op: op, source: classified(change[:source]))
end

.option(name, declared) ⇒ Object



210
211
212
213
214
215
# File 'lib/hecks/bluebook/assembly/marks.rb', line 210

def option(name, declared)
  return nil if declared.nil?

  holder, marked = OPTIONS.fetch(name)
  holder.new(**Hash(declared).to_h { |key, value| [key, option_value(key, value, marked)] })
end

.option_value(key, value, marked) ⇒ Object



217
218
219
220
221
222
223
# File 'lib/hecks/bluebook/assembly/marks.rb', line 217

def option_value(key, value, marked)
  return nil               if value.nil?
  return read(value)       if marked.include?(key)
  return value.to_sym      if SYMBOLIC.include?(key)

  value
end

.order_by(declared) ⇒ Object



177
178
179
180
181
182
183
# File 'lib/hecks/bluebook/assembly/marks.rb', line 177

def order_by(declared)
  return nil unless declared

  QuerySpecification::Common::OrderBy.new(
    field: declared[:field], direction: declared[:direction].to_sym
  )
end

.projected_field(row) ⇒ Object

S12, ADR 0025 — projects :name, from: :"reference.remote_field". All three fields are identifiers, unlike Invariant/Given's own free text, so — like attribute's own name/type below — they come back as Symbols.



114
115
116
117
# File 'lib/hecks/bluebook/assembly/marks.rb', line 114

def projected_field(row)
  ProjectedField.new(name: row[:name].to_sym, reference: row[:reference].to_sym,
                     remote_field: row[:remote_field].to_sym)
end

.read(value) ⇒ Object

EVERY LITERAL FIELD ON THE WIRE, read back — one spelling, one reader.

A where-clause value, a saga's argument bindings, an append binding, a limit: all of them ride Literal's self-describing form, so all of them come back through here. There were two readers (read and unmark) that disagreed about quoted strings and numbers, and which one a call site got was a coin toss the comments had to keep apologising for.

AN OBJECT LITERAL is the one that bit. A saga leg binds narrative: { text: "transfer out" } — a value object's fields written inline — and to_s on a Hash used to be its inspect form, so it came back as text. Read as a string it reached the runtime as "{:text=>\"transfer out\"}", coercion refused it, the debit leg was never delivered, and the whole settlement wire stopped: banking emitted TransferRequested five times and TransferDebited never. A whole-history replay gate caught what every other gate missed, because a saga that silently does nothing looks exactly like a saga with nothing to do.



169
# File 'lib/hecks/bluebook/assembly/marks.rb', line 169

def read(value) = Literal.read(value)

.shape_field(field) ⇒ Object

The same shape. Two names because a head's field and a verb's argument are different things in the language even though the IR keeps one class for both — Aggregate.Attribute and Command.Argument are separate verbs.



53
# File 'lib/hecks/bluebook/assembly/marks.rb', line 53

def shape_field(field) = attribute(field)

.unmark_scalar(value) ⇒ Object

A scalar that was written as itself rather than inspected — a member's value, where the language holds text and the type has to be read back from the shape of it. Unlike read, a bare word stays a String here, because a closed set admits words far more often than symbols.



87
88
89
90
91
92
93
94
95
# File 'lib/hecks/bluebook/assembly/marks.rb', line 87

def unmark_scalar(value)
  text = value.to_s
  return true       if text == "true"
  return false      if text == "false"
  return text.to_i  if text.match?(/\A-?\d+\z/)
  return text.to_f  if text.match?(/\A-?\d+\.\d+\z/)

  text
end

.where_clause(clause) ⇒ Object



171
172
173
174
175
# File 'lib/hecks/bluebook/assembly/marks.rb', line 171

def where_clause(clause)
  QuerySpecification::Common::WhereClause.new(
    field: clause[:field], op: clause[:op].to_sym, value: read(clause[:value])
  )
end