Module: Hecks::Runtime::ReactionInvocation

Defined in:
lib/hecks/runtime/reaction_invocation.rb

Overview

Turns facts selected by a policy or process manager into the same receiver/payload envelope an outside caller uses. Reaction declarations historically selected both through one with: map, so this is the one compatibility seam that separates receiver identities from facts after resolving the declaration and before re-entering the dispatcher.

Defined Under Namespace

Classes: Scope, Target

Class Method Summary collapse

Class Method Details

.build(registry:, verb:, projected:, explicit:, passthrough: [], source_receiver: nil) ⇒ Object

A reaction without an explicit projection retains wholesale legacy forwarding. Choosing with: opts into the strict envelope: only target command attributes enter with:, while identities become to:.



68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
# File 'lib/hecks/runtime/reaction_invocation.rb', line 68

def build(registry:, verb:, projected:, explicit:, passthrough: [], source_receiver: nil)
  args = projected.transform_keys(&:to_sym)
  unless explicit
    return args unless source_receiver

    # Compatibility calls still belong to Dispatcher when the target is
    # absent: it owns the canonical UnknownVerb wording. Resolution here
    # is only an optional opportunity to lift same-aggregate Event.id.
    begin
      target = resolve_target(registry, verb)
    rescue UnknownVerb
      return args
    end
    inherited_receiver = source_receiver_for(target, source_receiver)
    return inherited_receiver ? args.merge(to: inherited_receiver) : args
  end

  target = resolve_target(registry, verb)
  inherited_receiver = source_receiver_for(target, source_receiver)
  facts = command_facts(target.command, args)
  consumed = facts.keys

  if target.entities.empty? && target.command.creates?
    refuse_unconsumed!(target.command, args, consumed, passthrough)
    return { with: facts }
  end

  aggregate_identity, aggregate_keys = identity_for(
    target.aggregate,
    args,
    aliases:     aggregate_aliases(target),
    value_owner: target.aggregate
  )
  aggregate_identity ||= inherited_receiver
  require_identity!(verb, target.aggregate, aggregate_identity)
  consumed.concat(aggregate_keys)

  entity_identities = target.entities.map do |entity|
    identity, keys = identity_for(
      entity,
      args,
      aliases:     [Naming.reference_key(entity.hecks_name)],
      value_owner: target.aggregate
    )
    require_identity!(verb, entity, identity)
    consumed.concat(keys)
    identity
  end

  refuse_unconsumed!(target.command, args, consumed, passthrough)

  route = if entity_identities.empty?
            aggregate_identity
          else
            { aggregate: aggregate_identity, entities: entity_identities }
          end
  { to: route, with: facts }
end

.projection_declared?(declaration) ⇒ Boolean

The holding IR historically represented both an omitted projection and an explicitly empty with: {} as the same empty array. Builders now preserve declaration presence off-wire; reconstructed/legacy IR falls back to the old non-empty reading.

Returns:

  • (Boolean)


26
27
28
29
30
31
32
# File 'lib/hecks/runtime/reaction_invocation.rb', line 26

def projection_declared?(declaration)
  if declaration.instance_variable_defined?(:@projection_declared)
    declaration.instance_variable_get(:@projection_declared)
  else
    !declaration.with_spec.to_a.empty?
  end
end

.resolve_mapping(with_spec:, scopes:, bindings: {}, label: "reaction") ⇒ Object

A reaction's source names resolve lexically, not globally. Policies supply one event/row scope. Process managers supply current event then opening-event memory, while correlation is an explicit binding ahead of both. Missing names are refused here rather than materialized as nil and accidentally presented as target command facts.



39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
# File 'lib/hecks/runtime/reaction_invocation.rb', line 39

def resolve_mapping(with_spec:, scopes:, bindings: {}, label: "reaction")
  normalized_bindings = bindings.transform_keys(&:to_sym)
  normalized_scopes = scopes.map do |scope|
    scope = Scope.new(name: scope.first, facts: scope.last) unless scope.is_a?(Scope)
    Scope.new(name: scope.name.to_s, facts: scope.facts.transform_keys(&:to_sym))
  end

  with_spec.to_h do |key, source|
    value = if !source.is_a?(Symbol)
              source
            elsif normalized_bindings.key?(source)
              normalized_bindings.fetch(source)
            else
              visible = normalized_scopes.find { |scope| scope.facts.key?(source) }
              unless visible
                names = normalized_scopes.map(&:name).join(" then ")
                raise UnknownArgument,
                      "#{label}'s with: reads :#{source}, which is not visible in #{names}"
              end
              visible.facts.fetch(source)
            end

    [key.to_sym, Value.materialize(value)]
  end
end