Class: Hecks::Bluebook::DSL::ValueObjectBuilder

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

Constant Summary collapse

GRAMMAR_CONTEXT =
"ValueObject"

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 AttributeCollector

#attribute_impl, #attributes, #closed_sets, #list_of_impl

Constructor Details

#initialize(name, owner_value_objects: []) ⇒ ValueObjectBuilder

Returns a new instance of ValueObjectBuilder.



12
13
14
15
16
17
# File 'lib/hecks/bluebook/dsl/value_object_builder.rb', line 12

def initialize(name, owner_value_objects: [])
  @name       = name
  @invariants = []
  @members    = []
  @owner_value_objects = owner_value_objects
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: [], &block) ⇒ Object



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

def self.build(name, owner_value_objects: [], &block)
  builder = new(name, owner_value_objects: owner_value_objects)
  builder.instance_eval(&block) if block
  builder.build
end

Instance Method Details

#buildObject



130
131
132
133
134
135
136
137
138
139
140
141
142
143
# File 'lib/hecks/bluebook/dsl/value_object_builder.rb', line 130

def build
  if @inline_closed_set_field && attributes.size > 1
    raise Malformed,
          "#{@name}'s one_of: on :#{@inline_closed_set_field} only works when it is the " \
          "value object's only attribute — #{attributes.size} declared here; write bare " \
          "member lines instead for a multi-field set"
  end

  ValueObject.declare(
    name: @name, attributes: attributes,
    invariants: @invariants, members: @members,
    closed_set: @closed_set || !@members.empty?
  )
end

#invariant_impl(description, &predicate) ⇒ Object

NO BLOCK is a REFERENCE, not a fresh declaration — the same move S10 already made for CommandBuilder#given (ADR 0025, "a precondition shared across commands is declared once... a command references it by name"), one level over: a rule shared across SIBLING value objects on the same aggregate, declared once, on the first one to need it. Real, live redundancy this closes: Account's own Money/PositiveMoney both declared invariant("a currency is a three-letter code") { currency.to_s.size == 3 }, byte for byte. Declare it before the value objects that reference it — resolution happens at the referencing value object's own build time, against whatever sibling value objects the aggregate has already built, the same ordering rule given carries. RENAMED FROM invariant — item #13's full metaprogrammed dispatch (slice 4b). Bootstrap-reachable, in GenericDispatch::BOOTSTRAP_CALLS_FALLBACK.



104
105
106
107
108
109
110
111
# File 'lib/hecks/bluebook/dsl/value_object_builder.rb', line 104

def invariant_impl(description, &predicate)
  return reference_named_invariant(description) unless predicate

  # moved to the language: given "a rule says what it means", on Shape.Assert

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

#member_impl(**fields) ⇒ Object

RENAMED FROM member — item #13's full metaprogrammed dispatch (slice 4c). Bootstrap-reachable, in GenericDispatch::BOOTSTRAP_CALLS_FALLBACK.

Raises:



82
83
84
85
86
# File 'lib/hecks/bluebook/dsl/value_object_builder.rb', line 82

def member_impl(**fields)
  raise Malformed, "#{@name} declared an empty member" if fields.empty?

  @members << fields
end

#one_of_impl(*values, &block) ⇒ Object

THE WRAPPER BLOCK IS GONE (ADR 0025, "Attributes" — "closed sets lose the wrapper block"). member lines are bare now, written directly in the value_object body with no one_of do ... end around them — build's own closed_set: @closed_set || !@members.empty? already treats a non-empty @members as closed, so nothing else has to change for that to work. A single-field set has an even shorter spelling: attribute's own one_of: keyword (AttributeCollector#install_inline_closed_set, overridden below).

This also removes a real, documented landmine: the block form collided with AttributeCollector#one_of's own type-position method (one_of("a", "b"), different arity) the moment both were mixed into the same builder — a value_object could never actually use the inline form on one of its own attributes. One one_of method now, not two.

LEGACY UNDER SHADOW-PARSING (S0a's own bridge) — frozen era text still writes the block form (2 locations in examples/banking/data/eras/banking/1.bluebook, duplicated once more in that era's own archive copy), so EraGuard.shadow_parse still needs to read it. block_given? is what tells the two calling shapes apart: the type-position form (one_of("a", "b")) never passes a block, only the wrapper does. RENAMED FROM one_of — item #13's full metaprogrammed dispatch (slice 5). SAME NAME as AttributeCollector#one_of_impl's own — required for the super(*values) call below to keep resolving; see that method's own comment. Reached directly through its own "ValueObject"-context Keyword row (the block-wrapper form has its own row, distinct from "Type"'s), not through the Type-position fallback this word's OTHER context uses.



50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
# File 'lib/hecks/bluebook/dsl/value_object_builder.rb', line 50

def one_of_impl(*values, &block)
  unless block
    # NO VALUES, NO BLOCK is the SCALAR spelling — nonsensical, not
    # merely inert: it names a closed set with nothing in it. The
    # original block form caught this by a side effect (`@closed_set
    # = true` ran unconditionally, before the `if block`), and this
    # keeps the same refusal rather than letting the call fall
    # through and silently do nothing.
    if values.empty?
      raise Malformed,
            "#{@name}'s one_of names no values — one_of(\"a\", \"b\") takes at least one, or " \
            "give the attribute its own one_of: [...] for a named closed set"
    end

    # AttributeCollector's own `one_of(*values)` — Ruby's normal
    # `super` reaches the included module's method from here.
    return super(*values)
  end

  unless MetaValidator.shadow_parsing?
    raise Malformed,
          "#{@name}'s one_of do ... end wrapper is gone — give the single attribute its " \
          "own one_of: [...], or write bare member lines with no wrapper for a multi-field set"
  end

  @closed_set = true
  instance_eval(&block)
end