Module: Hecks::Runtime::Value::Admission

Included in:
Hecks::Runtime::Value
Defined in:
lib/hecks/runtime/value/admission.rb

Overview

Closed sets — one_of members and admits: declarations — and the refusals that make them rules rather than decoration. Extended into Value beside Coercion; self is the Value class.

Instance Method Summary collapse

Instance Method Details

#admit_declared_set(owner, attribute, value) ⇒ Object

THE SAME REFUSAL, FOR A SET NAMED SOMEWHERE ELSE.

admit_member above refuses a non-member when the value object BEING BUILT is itself the closed set — which is the only shape one_of can make, because it SYNTHESISES the set from the values written inline. An admits: attribute is the other direction: the value is an ordinary String or a plain text holder, and the set it must belong to was declared once, elsewhere, and is named rather than restated.

Without this the word was a DECLARATION and nothing more — read by projections, read by nobody at the door. A rule that cannot be the one to refuse is decoration, and this language has paid for that mistake before.

Raises:



38
39
40
41
42
43
44
45
46
47
48
49
# File 'lib/hecks/runtime/value/admission.rb', line 38

def admit_declared_set(owner, attribute, value)
  return value if attribute.nil? || attribute.admits.nil? || value.nil?

  admitted = admitted_members(owner, attribute)
  offered  = admitted_scalar(value)
  return value if admitted.include?(offered.to_s)

  raise InvariantViolation,
        RefusalWording.render("InvariantViolation", "admits_declared_set",
                              name: attribute.name, admits: attribute.admits,
                              admitted: admitted.map(&:inspect).join(", "), offered: offered.inspect)
end

#admit_member(value_object, fields) ⇒ Object

Raises:



11
12
13
14
15
16
17
18
19
20
21
22
23
# File 'lib/hecks/runtime/value/admission.rb', line 11

def admit_member(value_object, fields)
  return if value_object.members.empty?

  discriminant = value_object.attributes.first.name
  offered      = fields[discriminant]
  admitted     = value_object.members.map { |member| member[discriminant].to_s }
  return if admitted.include?(offered.to_s)

  raise InvariantViolation,
        RefusalWording.render("InvariantViolation", "closed_set_member",
                              type: value_object.hecks_name,
                              admitted: admitted.map(&:inspect).join(", "), offered: offered.inspect)
end

#admitted_members(owner, attribute) ⇒ Object

Vocabulary::MutationOp — the aggregate that HOLDS the set, then the set. Qualified because a closed set is a value object INSIDE an aggregate, which is exactly why admits could not be spelled as a reference: reference_to reaches aggregate heads and nothing below one.

RESOLVED LATE, like Reference#resolve and for the same reason — the set may be declared further down the file than the attribute that names it. And REFUSED when it resolves to nothing, also like Reference: a link checked against nothing is worse than no link, because it reads like a rule.



61
62
63
64
65
66
67
68
69
70
71
72
73
74
# File 'lib/hecks/runtime/value/admission.rb', line 61

def admitted_members(owner, attribute)
  aggregate_name, set_name = attribute.admits.to_s.split("::", 2)
  chapter = chapter_of(owner)
  set     = set_name && chapter&.aggregate(aggregate_name)&.value_object(set_name)

  unless set
    raise InvariantViolation,
          RefusalWording.render("InvariantViolation", "undeclared_set",
                                name: attribute.name, admits: attribute.admits)
  end

  discriminant = set.attributes.first.name
  set.members.map { |member| member.to_h[discriminant].to_s }
end

#admitted_scalar(value) ⇒ Object

An admitted value is a SCALAR however it arrived — as a bare string on a plain field, or wrapped in the one-field holder its type names.



88
89
90
91
92
93
# File 'lib/hecks/runtime/value/admission.rb', line 88

def admitted_scalar(value)
  return value unless value.is_a?(self)

  fields = value.to_h
  fields.size == 1 ? fields.values.first : value
end

#chapter_of(construct) ⇒ Object

UP TO THE CHAPTER, from wherever the attribute was declared. A value object's owner is its aggregate and an aggregate's owner is its chapter, so the walk stops at the first construct that can answer for an aggregate BY NAME — which is the chapter, and only the chapter.



80
81
82
83
84
# File 'lib/hecks/runtime/value/admission.rb', line 80

def chapter_of(construct)
  node = construct
  node = node.hecks_owner while node && !node.respond_to?(:aggregate) && node.respond_to?(:hecks_owner)
  node.respond_to?(:aggregate) ? node : nil
end