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) ⇒ 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
# 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)
  @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
  # 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, &block) ⇒ Object



205
206
207
208
209
210
211
212
# File 'lib/hecks/bluebook/dsl/entity_builder.rb', line 205

def self.build(name, owner_value_objects: [], owner_named_givens: {},
               identity_name_prefix: nil, identity_value_object_installer: nil, &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)
  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



84
85
86
87
# File 'lib/hecks/bluebook/dsl/entity_builder.rb', line 84

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



186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
# File 'lib/hecks/bluebook/dsl/entity_builder.rb', line 186

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.



106
107
108
# File 'lib/hecks/bluebook/dsl/entity_builder.rb', line 106

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

#description(value) ⇒ Object



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

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



125
126
127
# File 'lib/hecks/bluebook/dsl/entity_builder.rb', line 125

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

#given_impl(description, &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.



153
154
155
156
157
158
159
160
161
162
163
164
165
166
# File 'lib/hecks/bluebook/dsl/entity_builder.rb', line 153

def given_impl(description, &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
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



68
69
70
71
72
73
74
75
76
# File 'lib/hecks/bluebook/dsl/entity_builder.rb', line 68

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



78
79
80
81
# File 'lib/hecks/bluebook/dsl/entity_builder.rb', line 78

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.



181
182
183
184
# File 'lib/hecks/bluebook/dsl/entity_builder.rb', line 181

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



129
130
131
# File 'lib/hecks/bluebook/dsl/entity_builder.rb', line 129

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

#query_impl(name, &block) ⇒ Object



110
111
112
# File 'lib/hecks/bluebook/dsl/entity_builder.rb', line 110

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.



53
54
55
56
57
# File 'lib/hecks/bluebook/dsl/entity_builder.rb', line 53

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