yes-core 1.2.0 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -21,6 +21,28 @@ module Yes
21
21
  # end
22
22
  # end
23
23
  module CommandTestDsl
24
+ # Returns the event-type aggregate prefix that the runtime publishes
25
+ # for a given aggregate and draft flag. Mirrors
26
+ # `CommandUtils#aggregate_name_with_draft_suffix` so DSL-generated
27
+ # `expected_event_type` values match the runtime-published event
28
+ # types — including the case where `draftable changes_read_model:`
29
+ # was set explicitly, which makes the camelized read-model name the
30
+ # event-type prefix instead of the generic `<Aggregate>Draft`.
31
+ #
32
+ # @param aggregate_class [Class] The aggregate class under test
33
+ # @param draft [Boolean] Whether the test exercises a draft command
34
+ # @return [String] The event-type aggregate prefix
35
+ def self.expected_event_prefix(aggregate_class, draft:)
36
+ return aggregate_class.aggregate unless draft
37
+
38
+ if aggregate_class.respond_to?(:_changes_read_model_explicit) &&
39
+ aggregate_class._changes_read_model_explicit
40
+ aggregate_class.changes_read_model_name.camelize
41
+ else
42
+ "#{aggregate_class.aggregate}Draft"
43
+ end
44
+ end
45
+
24
46
  # Defines a test block for a command
25
47
  #
26
48
  # @param command_name [String, Symbol] the name of the command to test
@@ -41,8 +63,8 @@ module Yes
41
63
  end
42
64
  let(:command_data) { {} }
43
65
  let(:expected_event_type) do
44
- "#{aggregate_class.context}::#{aggregate_class.aggregate}" \
45
- "#{'Draft' if draft}#{aggregate_class.commands[command].event_name.to_s.classify}"
66
+ prefix = CommandTestDsl.expected_event_prefix(aggregate_class, draft:)
67
+ "#{aggregate_class.context}::#{prefix}#{aggregate_class.commands[command].event_name.to_s.classify}"
46
68
  end
47
69
  let(:expected_event_data) { command_data_with_id }
48
70
  let(:expected_event_metadata) { nil }
@@ -99,6 +121,59 @@ module Yes
99
121
  def setup(&)
100
122
  before(&)
101
123
  end
124
+
125
+ # Defines a test block for a command group, mirroring {.command}.
126
+ #
127
+ # @param group_name [String, Symbol] the command_group name
128
+ # @param options [Array<Hash>] additional options (e.g., `draft: true`)
129
+ # @yield block for configuring test cases (success, invalid, no_change)
130
+ def command_group(group_name, *options, &block)
131
+ describe group_name.to_s, *options do
132
+ let(:draft) { options.first&.dig(:draft) }
133
+ let(:aggregate) { described_class.new(draft:) } unless method_defined?(:aggregate)
134
+
135
+ subject { aggregate.public_send(group, command_data) }
136
+
137
+ let(:group) { group_name.to_sym }
138
+ let(:aggregate_class) { aggregate.class }
139
+ let(:command_data) { {} }
140
+ let(:expected_event_types) do
141
+ prefix = CommandTestDsl.expected_event_prefix(aggregate_class, draft:)
142
+ aggregate_class.command_groups[group].sub_command_names.map do |sub_name|
143
+ sub_event_name = aggregate_class.commands[sub_name].event_name.to_s.classify
144
+ "#{aggregate_class.context}::#{prefix}#{sub_event_name}"
145
+ end
146
+ end
147
+ let(:success_attributes) { command_data.without(:locale) } unless method_defined?(:success_attributes)
148
+
149
+ class_eval(&block) if block_given?
150
+ end
151
+ end
152
+
153
+ # Defines a successful test for a command group.
154
+ def success_group(description = 'when successfully executing command group', options = {}, &block)
155
+ context description, options do
156
+ instance_eval(&block) if block_given?
157
+ it_behaves_like 'successful command group'
158
+ end
159
+ end
160
+
161
+ # Defines an invalid-transition test for a command group.
162
+ def invalid_group(description, options = {}, &block)
163
+ context "when #{description}", options do
164
+ instance_eval(&block) if block_given?
165
+ it_behaves_like 'invalid command group transition'
166
+ end
167
+ end
168
+
169
+ # Defines a no-change test for a command group.
170
+ def no_change_group(description = 'when command group causes no change', options = {}, &block)
171
+ context description.to_s, options do
172
+ instance_eval(&block) if block_given?
173
+ before { aggregate.public_send(group, command_data) }
174
+ it_behaves_like 'no change command group transition'
175
+ end
176
+ end
102
177
  end
103
178
  end
104
179
  end
@@ -75,3 +75,48 @@ RSpec.shared_examples 'no change transition' do
75
75
  )
76
76
  end
77
77
  end
78
+
79
+ RSpec.shared_examples 'successful command group' do
80
+ it 'returns a successful CommandGroupResponse' do
81
+ expect(subject).to be_a(Yes::Core::Commands::CommandGroupResponse)
82
+ expect(subject).to be_success
83
+ end
84
+
85
+ it 'publishes one event per sub-command in declaration order' do
86
+ expect(subject.events.map(&:type)).to eq(expected_event_types)
87
+ end
88
+
89
+ it 'reflects the cumulative state on the read model' do
90
+ if success_attributes.any?
91
+ expect { subject }.to change {
92
+ aggregate.read_model.reload.attributes.to_h.symbolize_keys.slice(*success_attributes.keys)
93
+ }.to(success_attributes)
94
+ end
95
+ end
96
+ end
97
+
98
+ RSpec.shared_examples 'invalid command group transition' do
99
+ it 'returns an InvalidTransition error and no events' do
100
+ aggregate_failures do
101
+ expect(subject).not_to be_success
102
+ expect(subject.error).to be_a(Yes::Core::CommandHandling::GuardEvaluator::InvalidTransition)
103
+ expect(subject.events).to be_empty
104
+ end
105
+ end
106
+
107
+ it 'does not change the aggregate state' do
108
+ success_attributes.each_key do |attribute|
109
+ expect { subject }.not_to(change { aggregate.public_send(attribute) })
110
+ end
111
+ end
112
+ end
113
+
114
+ RSpec.shared_examples 'no change command group transition' do
115
+ it 'returns a NoChangeTransition error and no events' do
116
+ aggregate_failures do
117
+ expect(subject).not_to be_success
118
+ expect(subject.error).to be_a(Yes::Core::CommandHandling::GuardEvaluator::NoChangeTransition)
119
+ expect(subject.events).to be_empty
120
+ end
121
+ end
122
+ end
@@ -51,6 +51,27 @@ module Yes
51
51
  fetch_class(name, :guard_evaluator)
52
52
  end
53
53
 
54
+ # Builds a command_group instance for a given group name and flat payload.
55
+ # The aggregate_id is injected automatically.
56
+ #
57
+ # @param group_name [Symbol] The command group name
58
+ # @param payload [Hash] The flat / partially-nested input payload
59
+ # @return [Yes::Core::Commands::CommandGroup] The instantiated group command
60
+ # @raise [RuntimeError] If the command_group class cannot be found
61
+ def build_group_command(group_name, payload)
62
+ group_class = fetch_class(group_name, :command_group)
63
+ group_class.new("#{aggregate.underscore}_id": aggregate_id, **payload)
64
+ end
65
+
66
+ # Fetches the guard evaluator class for a given command group name.
67
+ #
68
+ # @param group_name [Symbol] The command group name
69
+ # @return [Class] The guard evaluator class
70
+ # @raise [RuntimeError] If the guard evaluator class cannot be found
71
+ def fetch_guard_evaluator_class_for_group(group_name)
72
+ fetch_class(group_name, :command_group_guard_evaluator)
73
+ end
74
+
54
75
  # Fetches the state updater class for a given command name
55
76
  #
56
77
  # @param name [Symbol] The command name
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Yes
4
4
  module Core
5
- VERSION = '1.2.0'
5
+ VERSION = '1.3.0'
6
6
  end
7
7
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: yes-core
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.2.0
4
+ version: 1.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Nico Ritsche
@@ -195,11 +195,16 @@ files:
195
195
  - lib/yes/core/aggregate/dsl/class_resolvers/command/guard_evaluator.rb
196
196
  - lib/yes/core/aggregate/dsl/class_resolvers/command/simple_authorizer.rb
197
197
  - lib/yes/core/aggregate/dsl/class_resolvers/command/state_updater.rb
198
+ - lib/yes/core/aggregate/dsl/class_resolvers/command_group/base.rb
199
+ - lib/yes/core/aggregate/dsl/class_resolvers/command_group/command.rb
200
+ - lib/yes/core/aggregate/dsl/class_resolvers/command_group/guard_evaluator.rb
198
201
  - lib/yes/core/aggregate/dsl/class_resolvers/read_model.rb
199
202
  - lib/yes/core/aggregate/dsl/class_resolvers/read_model_filter.rb
200
203
  - lib/yes/core/aggregate/dsl/class_resolvers/read_model_serializer.rb
201
204
  - lib/yes/core/aggregate/dsl/command_data.rb
202
205
  - lib/yes/core/aggregate/dsl/command_definer.rb
206
+ - lib/yes/core/aggregate/dsl/command_group_data.rb
207
+ - lib/yes/core/aggregate/dsl/command_group_definer.rb
203
208
  - lib/yes/core/aggregate/dsl/command_shortcut_expander.rb
204
209
  - lib/yes/core/aggregate/dsl/constant_resolver.rb
205
210
  - lib/yes/core/aggregate/dsl/method_definers/attribute/accessor.rb
@@ -208,6 +213,9 @@ files:
208
213
  - lib/yes/core/aggregate/dsl/method_definers/command/base.rb
209
214
  - lib/yes/core/aggregate/dsl/method_definers/command/can_command.rb
210
215
  - lib/yes/core/aggregate/dsl/method_definers/command/command.rb
216
+ - lib/yes/core/aggregate/dsl/method_definers/command_group/base.rb
217
+ - lib/yes/core/aggregate/dsl/method_definers/command_group/can_command_group.rb
218
+ - lib/yes/core/aggregate/dsl/method_definers/command_group/command_group.rb
211
219
  - lib/yes/core/aggregate/has_authorizer.rb
212
220
  - lib/yes/core/aggregate/has_read_model.rb
213
221
  - lib/yes/core/aggregate/read_model_rebuilder.rb
@@ -223,6 +231,8 @@ files:
223
231
  - lib/yes/core/command.rb
224
232
  - lib/yes/core/command_handling/aggregate_tracker.rb
225
233
  - lib/yes/core/command_handling/command_executor.rb
234
+ - lib/yes/core/command_handling/command_group_executor.rb
235
+ - lib/yes/core/command_handling/command_group_handler.rb
226
236
  - lib/yes/core/command_handling/command_handler.rb
227
237
  - lib/yes/core/command_handling/event_publisher.rb
228
238
  - lib/yes/core/command_handling/guard_evaluator.rb
@@ -233,7 +243,10 @@ files:
233
243
  - lib/yes/core/command_handling/read_model_updater.rb
234
244
  - lib/yes/core/command_handling/state_updater.rb
235
245
  - lib/yes/core/commands/bus.rb
246
+ - lib/yes/core/commands/command_group.rb
247
+ - lib/yes/core/commands/command_group_response.rb
236
248
  - lib/yes/core/commands/group.rb
249
+ - lib/yes/core/commands/group_payload_normalizer.rb
237
250
  - lib/yes/core/commands/group_response.rb
238
251
  - lib/yes/core/commands/helper.rb
239
252
  - lib/yes/core/commands/notifier.rb