Class: Hecks::Bluebook::DSL::EntityBuilder

Inherits:
Object
  • Object
show all
Includes:
AttributeCollector, IdentityDeclaration, RuleReference, WordGate
Defined in:
lib/hecks/bluebook/dsl/entity_builder.rb

Constant Summary collapse

GRAMMAR_CONTEXT =
"Entity"

Constants included from WordGate

WordGate::NOT_ADMITTED

Constants included from RuleReference

RuleReference::BOOTSTRAP_FALLBACK

Class Method Summary collapse

Instance Method Summary collapse

Methods included from RuleReference

build_rule, lookup, resolve_hash_chain, resolve_owner_keyed, resolve_sibling_scan, verify_resolves_via!

Methods included from IdentityDeclaration

#identified_by_impl

Methods included from AttributeCollector

#attribute_impl, #attributes, #closed_sets, #list_of_impl, #one_of_impl

Constructor Details

#initialize(name, owner_value_objects: [], owner_named_givens: {}, identity_name_prefix: nil, identity_value_object_installer: nil, aggregate_name: nil, chapter_entity_named_givens: {}, chapter_entity_pending_givens: []) ⇒ EntityBuilder

Returns a new instance of EntityBuilder.



13
14
15
16
17
18
19
20
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
49
50
51
52
53
54
# File 'lib/hecks/bluebook/dsl/entity_builder.rb', line 13

def initialize(name, owner_value_objects: [], owner_named_givens: {},
               identity_name_prefix: nil, identity_value_object_installer: nil,
               aggregate_name: nil, chapter_entity_named_givens: {}, chapter_entity_pending_givens: [])
  @name         = name
  @commands     = []
  @queries      = []
  @entities     = []
  @named_givens = {}
  @invariants   = []
  @owner_value_objects = owner_value_objects
  @identity_name_prefix = identity_name_prefix || Naming.demodulise(name)
  @identity_value_object_installer = identity_value_object_installer
  # THE AGGREGATE-WIDE cross-entity given pool — ONE hash, the
  # SAME object, threaded unchanged through every piece nested
  # under one aggregate however deep (the identical shape
  # `@owner_value_objects` already threads — see this class'
  # own `entity` comment). `given`'s own block form writes
  # through to it; a SIBLING piece's bare command-level
  # reference reads from it via `CommandBuilder#
  # reference_named_given`.
  @owner_named_givens = owner_named_givens
  # ONE LEVEL WIDER STILL — the CHAPTER-WIDE, ENTITY-SCOPED pool
  # (the piece analogue of `AggregateBuilder#@chapter_named_givens`,
  # one level down). `@aggregate_name` names THIS piece's own
  # root, so the write-through below can key itself
  # "AggregateName.EntityName" — the same dotted addressing
  # convention `declared_by:` already uses chapter-wide, one
  # level up. See `#given_impl`'s own comment for what this
  # closes and `docs/implemented/resolution-rules/
  # chapter-entity-given.md` for the full algorithm.
  @aggregate_name = aggregate_name || Naming.demodulise(name)
  @chapter_entity_named_givens   = chapter_entity_named_givens
  @chapter_entity_pending_givens = chapter_entity_pending_givens
  # DEFERRED CONSTRUCTION — see `AggregateBuilder#drain_pending!`'s
  # own comment; the identical mechanism, one level down, so a
  # nested piece's own commands (Dispatch inside Handler) see
  # every SIBLING entity/command/query this piece goes on to
  # declare, not just whatever came before it textually.
  @pending_entities = []
  @pending_commands = []
  @pending_queries  = []
end

Dynamic Method Handling

This class handles dynamic methods through the method_missing method in the class Hecks::Bluebook::DSL::WordGate

Class Method Details

.build(name, owner_value_objects: [], owner_named_givens: {}, identity_name_prefix: nil, identity_value_object_installer: nil, aggregate_name: nil, chapter_entity_named_givens: {}, chapter_entity_pending_givens: [], &block) ⇒ Object



262
263
264
265
266
267
268
269
270
271
272
273
# File 'lib/hecks/bluebook/dsl/entity_builder.rb', line 262

def self.build(name, owner_value_objects: [], owner_named_givens: {},
               identity_name_prefix: nil, identity_value_object_installer: nil,
               aggregate_name: nil, chapter_entity_named_givens: {}, chapter_entity_pending_givens: [], &block)
  builder = new(name, owner_value_objects: owner_value_objects, owner_named_givens: owner_named_givens,
                      identity_name_prefix: identity_name_prefix,
                      identity_value_object_installer: identity_value_object_installer,
                      aggregate_name: aggregate_name,
                      chapter_entity_named_givens: chapter_entity_named_givens,
                      chapter_entity_pending_givens: chapter_entity_pending_givens)
  builder.instance_eval(&block) if block
  builder.build
end

Instance Method Details

#belongs_to_impl(type, as: nil, optional: false) ⇒ Object

rubocop:enable Naming/PredicatePrefix



97
98
99
100
# File 'lib/hecks/bluebook/dsl/entity_builder.rb', line 97

def belongs_to_impl(type, as: nil, optional: false)
  target = Naming.demodulise(type)
  relationship_attribute(target, :belongs_to, as || Naming.snake(target).to_sym, optional: optional)
end

#buildObject



243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
# File 'lib/hecks/bluebook/dsl/entity_builder.rb', line 243

def build
  drain_pending!
  resolve_pending_identity!
  seal_lifecycle_guards
  install_closed_sets!
  Entity.declare(
    name:          @name,
    description:   @description,
    identified_by: @identity_paths,
    attributes:    attributes,
    commands:      @commands,
    queries:       @queries,
    entities:      @entities,
    preconditions: @named_givens.values,
    invariants:    @invariants,
    lifecycle:     @lifecycle
  )
end

#command_impl(name, from: nil, &block) ⇒ Object

from: — see AggregateBuilder#command's own comment; the SAME guard, checked against this PIECE's own lifecycle field (S10, ADR 0025 — a piece's own state machine is checkable the same way a head's is). RENAMED FROM command/query/entity/lifecycle (all below) — item #13's full metaprogrammed dispatch (slice 4c), same reasoning as AggregateBuilder's own siblings: bootstrap- reachable, in BOOTSTRAP_CALLS_FALLBACK.



119
120
121
# File 'lib/hecks/bluebook/dsl/entity_builder.rb', line 119

def command_impl(name, from: nil, &block)
  @pending_commands << [name, from, block]
end

#description(value) ⇒ Object



56
# File 'lib/hecks/bluebook/dsl/entity_builder.rb', line 56

def description(value) = @description = value

#entity_impl(name, &block) ⇒ Object

S17, ADR 0026 — A PIECE NESTED INSIDE A PIECE. "A Dispatch [has] no life outside its Handler" (the ADR's own words) — the same reason Member nests inside ValueObject, one level further in. owner_value_objects passes straight through unchanged, not re-derived from this entity's own attributes — a piece mints no value objects of its own at any depth, so a NESTED piece's bare identified_by :field still resolves against the SAME root aggregate's value objects an outer piece's already does (AggregateBuilder#entity's own comment names this pool ; there is exactly one of them, however deep the nesting goes).



138
139
140
# File 'lib/hecks/bluebook/dsl/entity_builder.rb', line 138

def entity_impl(name, &block)
  @pending_entities << [name, block]
end

#given_impl(description, declared_by: nil, &predicate) ⇒ Object

A PRECONDITION SHARED ACROSS THIS PIECE'S OWN COMMANDS, DECLARED ONCE — the same move AggregateBuilder#given already makes, one level down. Real, live redundancy this closes: banking's own LedgerEntry.Amend/LedgerEntry.Reverse each repeated given("customer is active") { parent.customer.status == "active" } and given("account is open") { parent.status == "open" }, byte for byte, because a piece had no way to declare either once and reference it back — only the AGGREGATE could. DECLARE BEFORE THE COMMANDS THAT REFERENCE IT, the same ordering AggregateBuilder#given's own comment names — though since ADR 0028, command only queues a descriptor and actually builds at #drain_pending! time, well after this whole block (including every given in it) has already run, so textual order within the block no longer actually matters here; named for the reader anyway, since given's own resolution logic (CommandBuilder#reference_named_given) still reads whatever @named_givens holds AT THE COMMAND'S OWN BUILD TIME, not by magic. RENAMED FROM given — item #13's full metaprogrammed dispatch (slice 4b), same reasoning as reference_to_impl above.

BARE — NO BLOCK — REFERENCES ANOTHER PIECE'S OWN DECLARATION, ANYWHERE IN THE CHAPTER, not just a sibling under this same aggregate — one level wider than round 4's own cross-entity sharing, mirroring AggregateBuilder#given_impl's own chapter-wide shape exactly one level down. Real, live corpus this closes: Account::LedgerEntry and SafeDepositBox::Visit — two pieces under two DIFFERENT aggregates — independently typed given("customer is active") { parent.customer.status == "active" } byte for byte; neither the aggregate-level chapter pool (a DIFFERENT canonical — bare customer.status, the wrong scope for a piece's own command) nor the existing same-aggregate cross-entity pool (@owner_named_givens, scoped to ONE aggregate's own entity tree) could reach across the aggregate boundary. Resolved against @chapter_entity_named_ givens, keyed "AggregateName.EntityName" — see #reference_named_chapter_entity_given's own comment for the algorithm and docs/implemented/resolution-rules/ chapter-entity-given.md for the full write-up.

declared_by: is a PLAIN STRING ("Account.LedgerEntry"), not a constant — unlike AggregateBuilder#given_impl's own declared_by:, which names a real aggregate constant. A piece has no first-class, independently-addressable reference anywhere in this language (only its owning aggregate does); inventing one to make this ONE argument spelling symmetrical with the aggregate-level word is a real, separate, unscoped feature this fix does not need — ships textual now, the same way admits: shipped textual before its own constant-bridge existed, revisited only if a genuine, separate need for constant-addressed pieces shows up later.



197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
# File 'lib/hecks/bluebook/dsl/entity_builder.rb', line 197

def given_impl(description, declared_by: nil, &predicate)
  return reference_named_chapter_entity_given(description, declared_by: declared_by) unless predicate

  named = build_rule(Given, description, predicate, owner_name: @name, word: "given",
                      extraction_failure: "its source could not be read, so no other runtime could ever evaluate it")
  @named_givens[description] = named
  # WRITE-THROUGH, first-declared-wins (`||=`) — a SECOND piece
  # under the same aggregate independently declaring the exact
  # same description stays purely local to itself (no silent
  # overwrite of whatever the first piece already shared;
  # real, live case a fuzzer or a future codemod could easily
  # surface: two pieces phrasing an UNRELATED rule identically
  # by coincidence, same as an aggregate-level given already
  # tolerates today).
  @owner_named_givens[description] ||= named
  # WRITE-THROUGH, PER OWNER — the chapter-wide analogue of the
  # line above, keyed by [description, this piece's own dotted
  # "Aggregate.Entity" name] rather than description alone, the
  # identical reasoning `AggregateBuilder#given_impl`'s own
  # chapter write-through gives: two DIFFERENT pieces (anywhere
  # in the chapter) independently declaring the SAME description
  # are two DISTINCT candidates a later bare reference chooses
  # between (via `declared_by:` once there is more than one),
  # never silently merged into one slot.
  @chapter_entity_named_givens[description] ||= {}
  @chapter_entity_named_givens[description]["#{@aggregate_name}.#{@name}"] ||= named
end

#has_many_impl(type, as: nil, **options) ⇒ Object

has_many_impl/has_one_impl are DSL declaration keywords a domain author writes as bare has_many/has_one (matching belongs_to_impl alongside them) — not real predicates, so renaming to many?/one? per Naming/PredicatePrefix would break every bluebook that declares one. New on Entity (Wave 6, identity-and-relationships arc) — pieces never had relationship words before; named *_impl to match AggregateBuilder's own siblings, item #13's full metaprogrammed dispatch convention. rubocop:disable Naming/PredicatePrefix



81
82
83
84
85
86
87
88
89
# File 'lib/hecks/bluebook/dsl/entity_builder.rb', line 81

def has_many_impl(type, as: nil, **options)
  unless options.empty?
    raise Malformed, "#{@name}.has_many takes no #{options.keys.first}: — an empty list already means none"
  end

  plural = Naming.demodulise(type)
  relationship_attribute(Naming.singularize(plural), :has_many,
                         as || Naming.snake(plural).to_sym, list: true)
end

#has_one_impl(type, as: nil, optional: false) ⇒ Object



91
92
93
94
# File 'lib/hecks/bluebook/dsl/entity_builder.rb', line 91

def has_one_impl(type, as: nil, optional: false)
  target = Naming.demodulise(type)
  relationship_attribute(target, :has_one, as || Naming.snake(target).to_sym, optional: optional)
end

#invariant_impl(description, &predicate) ⇒ Object

A PIECE'S OWN SHAPE RULE (S10, ADR 0025's own "Rules" shape, one level down from ValueObjectBuilder#invariant, whose extraction/error pattern this mirrors) — checked against EVERY INSTANCE of this piece the aggregate holds, not once against the aggregate's own flat state (Admissibility#enforce_invariants's own recursive walk). No reference-by-name form (unlike given) — no known corpus need for a piece's own invariant to be shared with a SIBLING piece yet; if that need shows up, it is given's own cross-entity write-through pattern to extend, not a reason to invent a second one here speculatively. RENAMED FROM invariant — item #13's full metaprogrammed dispatch (slice 4b), same reasoning as given_impl above.



238
239
240
241
# File 'lib/hecks/bluebook/dsl/entity_builder.rb', line 238

def invariant_impl(description, &predicate)
  @invariants << build_rule(Invariant, description, predicate, owner_name: @name, word: "invariant",
                             extraction_failure: "it would be a rule the IR cannot carry")
end

#lifecycle_impl(field, default:, &block) ⇒ Object



142
143
144
# File 'lib/hecks/bluebook/dsl/entity_builder.rb', line 142

def lifecycle_impl(field, default:, &block)
  @lifecycle = LifecycleBuilder.build(field, default: default, &block)
end

#query_impl(name, &block) ⇒ Object



123
124
125
# File 'lib/hecks/bluebook/dsl/entity_builder.rb', line 123

def query_impl(name, &block)
  @pending_queries << [name, block]
end

#reference_to_impl(type, as: nil, optional: false) ⇒ Object

THE SAME FIELD AggregateBuilder's OWN reference_to BUILDS — a piece can hold a reference to another root exactly the way its own head can (Card.assignee_id, a Team's own id), just never to another PIECE, since there's no cross-piece addressing anywhere in this language to resolve one against. RENAMED FROM reference_to — item #13's full metaprogrammed dispatch (slice 4b). Bootstrap-reachable, in GenericDispatch::BOOTSTRAP_CALLS_FALLBACK.



66
67
68
69
70
# File 'lib/hecks/bluebook/dsl/entity_builder.rb', line 66

def reference_to_impl(type, as: nil, optional: false)
  target = Naming.demodulise(type)
  relationship_attribute(target, :reference_to,
                         as || default_reference_name(target), optional: optional)
end