Class: Hecks::Bluebook::DSL::EntityBuilder
- Inherits:
-
Object
- Object
- Hecks::Bluebook::DSL::EntityBuilder
- Includes:
- AttributeCollector, IdentityDeclaration, RuleReference, WordGate
- Defined in:
- lib/hecks/bluebook/dsl/entity_builder.rb
Constant Summary collapse
- GRAMMAR_CONTEXT =
"Entity"
Constants included from WordGate
Constants included from RuleReference
RuleReference::BOOTSTRAP_FALLBACK
Class Method Summary collapse
Instance Method Summary collapse
-
#belongs_to_impl(type, as: nil, optional: false) ⇒ Object
rubocop:enable Naming/PredicatePrefix.
- #build ⇒ Object
-
#command_impl(name, from: nil, &block) ⇒ Object
from:— seeAggregateBuilder#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). - #description(value) ⇒ Object
-
#entity_impl(name, &block) ⇒ Object
S17, ADR 0026 — A PIECE NESTED INSIDE A PIECE.
-
#given_impl(description, declared_by: nil, &predicate) ⇒ Object
A PRECONDITION SHARED ACROSS THIS PIECE'S OWN COMMANDS, DECLARED ONCE — the same move
AggregateBuilder#givenalready makes, one level down. -
#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.
- #has_one_impl(type, as: nil, optional: false) ⇒ Object
-
#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
constructor
A new instance of EntityBuilder.
-
#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). - #lifecycle_impl(field, default:, &block) ⇒ Object
- #query_impl(name, &block) ⇒ Object
-
#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.
Methods included from RuleReference
build_rule, lookup, resolve_hash_chain, resolve_owner_keyed, resolve_sibling_scan, verify_resolves_via!
Methods included from IdentityDeclaration
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 |
#build ⇒ Object
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, **) unless .empty? raise Malformed, "#{@name}.has_many takes no #{.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 |