yes-core 1.0.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.
Files changed (128) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +5 -0
  3. data/LICENSE.txt +21 -0
  4. data/README.md +69 -0
  5. data/lib/yes/core/active_job_serializers/command_group_serializer.rb +29 -0
  6. data/lib/yes/core/active_job_serializers/dry_struct_serializer.rb +57 -0
  7. data/lib/yes/core/aggregate/draftable.rb +205 -0
  8. data/lib/yes/core/aggregate/dsl/attribute_data.rb +37 -0
  9. data/lib/yes/core/aggregate/dsl/attribute_definer.rb +54 -0
  10. data/lib/yes/core/aggregate/dsl/attribute_definers/aggregate.rb +36 -0
  11. data/lib/yes/core/aggregate/dsl/attribute_definers/standard.rb +36 -0
  12. data/lib/yes/core/aggregate/dsl/class_name_convention.rb +80 -0
  13. data/lib/yes/core/aggregate/dsl/class_resolvers/authorizer.rb +132 -0
  14. data/lib/yes/core/aggregate/dsl/class_resolvers/base.rb +80 -0
  15. data/lib/yes/core/aggregate/dsl/class_resolvers/command/authorizer.rb +30 -0
  16. data/lib/yes/core/aggregate/dsl/class_resolvers/command/authorizer_factory.rb +34 -0
  17. data/lib/yes/core/aggregate/dsl/class_resolvers/command/base.rb +38 -0
  18. data/lib/yes/core/aggregate/dsl/class_resolvers/command/cerbos_authorizer.rb +114 -0
  19. data/lib/yes/core/aggregate/dsl/class_resolvers/command/command.rb +70 -0
  20. data/lib/yes/core/aggregate/dsl/class_resolvers/command/event.rb +88 -0
  21. data/lib/yes/core/aggregate/dsl/class_resolvers/command/guard_evaluator.rb +84 -0
  22. data/lib/yes/core/aggregate/dsl/class_resolvers/command/simple_authorizer.rb +50 -0
  23. data/lib/yes/core/aggregate/dsl/class_resolvers/command/state_updater.rb +46 -0
  24. data/lib/yes/core/aggregate/dsl/class_resolvers/read_model.rb +75 -0
  25. data/lib/yes/core/aggregate/dsl/class_resolvers/read_model_filter.rb +88 -0
  26. data/lib/yes/core/aggregate/dsl/class_resolvers/read_model_serializer.rb +76 -0
  27. data/lib/yes/core/aggregate/dsl/command_data.rb +54 -0
  28. data/lib/yes/core/aggregate/dsl/command_definer.rb +263 -0
  29. data/lib/yes/core/aggregate/dsl/command_shortcut_expander.rb +233 -0
  30. data/lib/yes/core/aggregate/dsl/constant_resolver.rb +67 -0
  31. data/lib/yes/core/aggregate/dsl/method_definers/attribute/accessor.rb +28 -0
  32. data/lib/yes/core/aggregate/dsl/method_definers/attribute/aggregate_accessor.rb +36 -0
  33. data/lib/yes/core/aggregate/dsl/method_definers/attribute/base.rb +42 -0
  34. data/lib/yes/core/aggregate/dsl/method_definers/command/base.rb +42 -0
  35. data/lib/yes/core/aggregate/dsl/method_definers/command/can_command.rb +41 -0
  36. data/lib/yes/core/aggregate/dsl/method_definers/command/command.rb +50 -0
  37. data/lib/yes/core/aggregate/has_authorizer.rb +86 -0
  38. data/lib/yes/core/aggregate/has_read_model.rb +169 -0
  39. data/lib/yes/core/aggregate/read_model_rebuilder.rb +40 -0
  40. data/lib/yes/core/aggregate/shared_read_model_rebuilder.rb +158 -0
  41. data/lib/yes/core/aggregate.rb +404 -0
  42. data/lib/yes/core/authentication_error.rb +8 -0
  43. data/lib/yes/core/authorization/cerbos_client_provider.rb +27 -0
  44. data/lib/yes/core/authorization/command_authorizer.rb +40 -0
  45. data/lib/yes/core/authorization/command_cerbos_authorizer.rb +182 -0
  46. data/lib/yes/core/authorization/read_model_authorizer.rb +22 -0
  47. data/lib/yes/core/authorization/read_models_authorizer.rb +49 -0
  48. data/lib/yes/core/authorization/read_request_authorizer.rb +32 -0
  49. data/lib/yes/core/authorization/read_request_cerbos_authorizer.rb +112 -0
  50. data/lib/yes/core/command.rb +35 -0
  51. data/lib/yes/core/command_handling/aggregate_tracker.rb +33 -0
  52. data/lib/yes/core/command_handling/command_executor.rb +171 -0
  53. data/lib/yes/core/command_handling/command_handler.rb +124 -0
  54. data/lib/yes/core/command_handling/event_publisher.rb +189 -0
  55. data/lib/yes/core/command_handling/guard_evaluator.rb +165 -0
  56. data/lib/yes/core/command_handling/guard_runner.rb +76 -0
  57. data/lib/yes/core/command_handling/payload_proxy.rb +159 -0
  58. data/lib/yes/core/command_handling/read_model_recovery_service.rb +264 -0
  59. data/lib/yes/core/command_handling/read_model_revision_guard.rb +198 -0
  60. data/lib/yes/core/command_handling/read_model_updater.rb +103 -0
  61. data/lib/yes/core/command_handling/state_updater.rb +113 -0
  62. data/lib/yes/core/commands/bus.rb +46 -0
  63. data/lib/yes/core/commands/group.rb +135 -0
  64. data/lib/yes/core/commands/group_response.rb +13 -0
  65. data/lib/yes/core/commands/helper.rb +126 -0
  66. data/lib/yes/core/commands/notifier.rb +65 -0
  67. data/lib/yes/core/commands/processor.rb +137 -0
  68. data/lib/yes/core/commands/response.rb +63 -0
  69. data/lib/yes/core/commands/stateless/group_handler.rb +186 -0
  70. data/lib/yes/core/commands/stateless/group_response.rb +15 -0
  71. data/lib/yes/core/commands/stateless/handler.rb +292 -0
  72. data/lib/yes/core/commands/stateless/handler_helpers.rb +321 -0
  73. data/lib/yes/core/commands/stateless/response.rb +14 -0
  74. data/lib/yes/core/commands/stateless/subject.rb +41 -0
  75. data/lib/yes/core/commands/validator.rb +28 -0
  76. data/lib/yes/core/configuration.rb +432 -0
  77. data/lib/yes/core/data_decryptor.rb +59 -0
  78. data/lib/yes/core/data_encryptor.rb +60 -0
  79. data/lib/yes/core/encryption_metadata.rb +33 -0
  80. data/lib/yes/core/error.rb +14 -0
  81. data/lib/yes/core/error_messages.rb +37 -0
  82. data/lib/yes/core/event.rb +222 -0
  83. data/lib/yes/core/event_class_resolver.rb +40 -0
  84. data/lib/yes/core/generators/read_models/add_pending_update_tracking_generator.rb +43 -0
  85. data/lib/yes/core/generators/read_models/templates/add_pending_update_tracking.rb.erb +122 -0
  86. data/lib/yes/core/generators/read_models/templates/migration.rb.erb +9 -0
  87. data/lib/yes/core/generators/read_models/update_generator.rb +147 -0
  88. data/lib/yes/core/jobs/read_model_recovery_job.rb +219 -0
  89. data/lib/yes/core/middlewares/encryptor.rb +48 -0
  90. data/lib/yes/core/middlewares/timestamp.rb +29 -0
  91. data/lib/yes/core/middlewares/with_indifferent_access.rb +22 -0
  92. data/lib/yes/core/models/application_record.rb +9 -0
  93. data/lib/yes/core/open_telemetry/otl_span.rb +110 -0
  94. data/lib/yes/core/open_telemetry/trackable.rb +101 -0
  95. data/lib/yes/core/payload_store/base.rb +33 -0
  96. data/lib/yes/core/payload_store/errors.rb +13 -0
  97. data/lib/yes/core/payload_store/lookup.rb +44 -0
  98. data/lib/yes/core/process_managers/access_token_client.rb +107 -0
  99. data/lib/yes/core/process_managers/base.rb +40 -0
  100. data/lib/yes/core/process_managers/command_runner.rb +109 -0
  101. data/lib/yes/core/process_managers/service_client.rb +57 -0
  102. data/lib/yes/core/process_managers/state.rb +118 -0
  103. data/lib/yes/core/railtie.rb +58 -0
  104. data/lib/yes/core/read_model/builder.rb +267 -0
  105. data/lib/yes/core/read_model/event_handler.rb +64 -0
  106. data/lib/yes/core/read_model/filter.rb +118 -0
  107. data/lib/yes/core/read_model/filter_query_builder.rb +104 -0
  108. data/lib/yes/core/serializer.rb +21 -0
  109. data/lib/yes/core/subscriptions.rb +94 -0
  110. data/lib/yes/core/test_support/event_helpers.rb +27 -0
  111. data/lib/yes/core/test_support/jwt_helpers.rb +30 -0
  112. data/lib/yes/core/test_support/subscriptions_helper.rb +88 -0
  113. data/lib/yes/core/test_support/test_helper.rb +27 -0
  114. data/lib/yes/core/test_support.rb +5 -0
  115. data/lib/yes/core/transaction_details.rb +90 -0
  116. data/lib/yes/core/type_lookup.rb +88 -0
  117. data/lib/yes/core/types.rb +110 -0
  118. data/lib/yes/core/utils/aggregate_shortcuts.rb +164 -0
  119. data/lib/yes/core/utils/caller_utils.rb +37 -0
  120. data/lib/yes/core/utils/command_utils.rb +226 -0
  121. data/lib/yes/core/utils/error_notifier.rb +101 -0
  122. data/lib/yes/core/utils/event_name_resolver.rb +67 -0
  123. data/lib/yes/core/utils/exponential_retrier.rb +180 -0
  124. data/lib/yes/core/utils/hash_utils.rb +63 -0
  125. data/lib/yes/core/version.rb +7 -0
  126. data/lib/yes/core.rb +85 -0
  127. data/lib/yes.rb +0 -0
  128. metadata +324 -0
@@ -0,0 +1,321 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Yes
4
+ module Core
5
+ module Commands
6
+ module Stateless
7
+ # Provides helper methods for checking event stream state in stateless command handlers.
8
+ # Includes methods for checking if subjects have been added, removed, published, etc.
9
+ module HandlerHelpers
10
+ # @param subject [Yes::Core::Commands::Stateless::Subject]
11
+ # @param options [Hash]
12
+ # @return [Boolean]
13
+ def removed?(subject, options = {})
14
+ event_in_stream?(subject, 'Removed', options)
15
+ end
16
+
17
+ # @param subject [Yes::Core::Commands::Stateless::Subject]
18
+ # @param options [Hash]
19
+ # @return [Boolean]
20
+ def added?(subject, options = {})
21
+ event_in_stream?(subject, 'Added', options)
22
+ end
23
+
24
+ # @param subject [Yes::Core::Commands::Stateless::Subject]
25
+ # @param payload [Hash]
26
+ # @param event_name [String]
27
+ # @param options [Hash]
28
+ # @return [Boolean]
29
+ def item_added?(subject, payload, event_name, options = {})
30
+ added_event = last_event(subject, event_name, options.merge(payload: { equal: payload }))
31
+ return false unless added_event
32
+
33
+ removed_event = last_event(subject, event_name.sub('Added', 'Removed'),
34
+ options.merge(payload: { equal: payload }))
35
+ return true unless removed_event
36
+
37
+ added_event.stream_revision > removed_event.stream_revision
38
+ end
39
+
40
+ # @param subject [Yes::Core::Commands::Stateless::Subject]
41
+ # @param payload [Hash]
42
+ # @param event_name [String]
43
+ # @param options [Hash]
44
+ # @return [Boolean]
45
+ def item_removed?(subject, payload, event_name, options = {})
46
+ removed_event = last_event(subject, event_name, options.merge(payload: { equal: payload }))
47
+ return false unless removed_event
48
+
49
+ added_event = last_event(subject, event_name.sub('Removed', 'Added'),
50
+ options.merge(payload: { equal: payload }))
51
+ return true unless added_event
52
+
53
+ removed_event.stream_revision > added_event.stream_revision
54
+ end
55
+
56
+ # @param subject [Yes::Core::Commands::Stateless::Subject]
57
+ # @param payload [Hash]
58
+ # @param event_name [String]
59
+ # @param options [Hash]
60
+ # @return [Boolean]
61
+ def item_assigned?(subject, payload, event_name, options = {})
62
+ assigned_event = last_event(subject, event_name, options.merge(payload: { equal: payload }))
63
+ return false unless assigned_event
64
+
65
+ removed_event = last_event(subject, event_name.sub('Assigned', 'Unassigned'),
66
+ options.merge(payload: { equal: payload }))
67
+ return true unless removed_event
68
+
69
+ assigned_event.stream_revision > removed_event.stream_revision
70
+ end
71
+
72
+ # @param subject [Yes::Core::Commands::Stateless::Subject]
73
+ # @param payload [Hash]
74
+ # @param event_name [String]
75
+ # @param options [Hash]
76
+ # @return [Boolean]
77
+ def item_unassigned?(subject, payload, event_name, options = {})
78
+ unassigned_event = last_event(subject, event_name, options.merge(payload: { equal: payload }))
79
+ return false unless unassigned_event
80
+
81
+ added_event = last_event(subject, event_name.sub('Unassigned', 'Assigned'),
82
+ options.merge(payload: { equal: payload }))
83
+ return true unless added_event
84
+
85
+ unassigned_event.stream_revision > added_event.stream_revision
86
+ end
87
+
88
+ # @param subject [Yes::Core::Commands::Stateless::Subject]
89
+ # @param options [Hash]
90
+ # @return [Boolean]
91
+ def unpublished?(subject, options = {})
92
+ published_event = last_event(subject, 'Published', options)
93
+ return true unless published_event
94
+
95
+ unpublished_event = last_event(subject, 'Unpublished', options)
96
+ return false unless unpublished_event
97
+
98
+ unpublished_event.stream_revision > published_event.stream_revision
99
+ end
100
+
101
+ # @see #unpublished?
102
+ def published?(...)
103
+ !unpublished?(...)
104
+ end
105
+
106
+ # @param subject [Yes::Core::Commands::Stateless::Subject]
107
+ # @param event_name [String]
108
+ # @param options [Hash]
109
+ # @return [Boolean]
110
+ def activated?(subject, event_name, options = {})
111
+ activated_event = last_event(subject, event_name, options)
112
+ return false unless activated_event
113
+
114
+ deactivated_event = last_event(subject, event_name.sub('Activated', 'Deactivated'), options)
115
+ return true unless deactivated_event
116
+
117
+ activated_event.stream_revision > deactivated_event.stream_revision
118
+ end
119
+
120
+ # @param subject [Yes::Core::Commands::Stateless::Subject]
121
+ # @param event_name [String]
122
+ # @param options [Hash]
123
+ # @return [Boolean]
124
+ def deactivated?(subject, event_name, options = {})
125
+ deactivated_event = last_event(subject, event_name, options)
126
+ return false unless deactivated_event
127
+
128
+ activated_event = last_event(subject, event_name.sub('Deactivated', 'Activated'), options)
129
+ return true unless activated_event
130
+
131
+ deactivated_event.stream_revision > activated_event.stream_revision
132
+ end
133
+
134
+ # @param subject [Yes::Core::Commands::Stateless::Subject]
135
+ # @param event_name [String]
136
+ # @param options [Hash]
137
+ # @return [Boolean]
138
+ def enabled?(subject, event_name, options = {})
139
+ enabled_event = last_event(subject, event_name, options)
140
+ return false unless enabled_event
141
+
142
+ disabled_event = last_event(subject, event_name.sub('Enabled', 'Disabled'), options)
143
+ return true unless disabled_event
144
+
145
+ enabled_event.stream_revision > disabled_event.stream_revision
146
+ end
147
+
148
+ # @param subject [Yes::Core::Commands::Stateless::Subject]
149
+ # @param event_name [String]
150
+ # @param options [Hash]
151
+ # @return [Boolean]
152
+ def disabled?(subject, event_name, options = {})
153
+ disabled_event = last_event(subject, event_name, options)
154
+ return false unless disabled_event
155
+
156
+ enabled_event = last_event(subject, event_name.sub('Disabled', 'Enabled'), options)
157
+ return true unless enabled_event
158
+
159
+ disabled_event.stream_revision > enabled_event.stream_revision
160
+ end
161
+
162
+ # @param subject [Yes::Core::Commands::Stateless::Subject]
163
+ # @param payload [Hash]
164
+ # @param event_name [String]
165
+ # @param options [Hash]
166
+ # @return [Boolean]
167
+ def event_exists_with_payload?(subject, payload, event_name, options = {})
168
+ event_in_stream?(subject, event_name, options.merge(payload:))
169
+ end
170
+
171
+ # @param subject [Yes::Core::Commands::Stateless::Subject]
172
+ # @param event_name [String]
173
+ # @param options [Hash]
174
+ # @return [Boolean]
175
+ def event_in_stream?(subject, event_name, options = {})
176
+ last_event(subject, event_name, options).present?
177
+ end
178
+ alias changed? event_in_stream?
179
+
180
+ # @param subject [Yes::Core::Commands::Stateless::Subject]
181
+ # @param payload [Hash]
182
+ # @param event_name [String]
183
+ # @return [Boolean]
184
+ def no_change?(subject, payload, event_name)
185
+ options = { resolve_payload: true }
186
+ options[:locale] = payload[:locale] if payload[:locale]
187
+ event = last_event(
188
+ subject,
189
+ event_name,
190
+ options,
191
+ skip_decryption: false
192
+ )
193
+
194
+ return false unless event
195
+
196
+ payload = payload.as_json
197
+ event.data.all? { |k, v| payload[k] == v }
198
+ end
199
+
200
+ # @param subject [Yes::Core::Commands::Stateless::Subject]
201
+ # @param payload_field_key [String]
202
+ # @param event_name [String]
203
+ # @param options [Hash]
204
+ # @option options [Hash] :payload
205
+ # @option options [String] :locale
206
+ # @option options [Boolean] :resolve_payload
207
+ # @return [Boolean]
208
+ def partial_payload_field_changed?(subject, payload_field_key, event_name, options = {})
209
+ stream = subject.stream
210
+ enumerable_events = load_events(stream, options: { direction: 'Forwards' })
211
+
212
+ partial_data = {}
213
+ event_type = "#{subject.context}::#{subject.subject}#{event_name}"
214
+
215
+ enumerable_events.each do |result|
216
+ result.each do |event|
217
+ next unless event.type == event_type
218
+ next if options[:locale] && skip?(options[:locale], event)
219
+
220
+ event = resolve_payloads(event) if options[:payload] || options[:resolve_payload]
221
+
222
+ event.data[payload_field_key.to_s].each do |key, value|
223
+ partial_data[key] = value
224
+ end
225
+ end
226
+ end
227
+
228
+ partial_data.stringify_keys.merge(options[:payload].stringify_keys) != partial_data.stringify_keys
229
+ rescue PgEventstore::StreamNotFoundError
230
+ true
231
+ end
232
+
233
+ # @param locale [String]
234
+ # @param event [PgEventstore::Event]
235
+ # @return [Boolean]
236
+ def skip?(locale, event)
237
+ event_locale = event.data.transform_keys(&:to_s)['locale'].to_s
238
+ return false if event_locale.empty?
239
+
240
+ event_locale != locale.to_s
241
+ end
242
+
243
+ private
244
+
245
+ # Returns the last event of the given event_name for the given subject and context
246
+ # @param subject [Yes::Core::Commands::Stateless::Subject]
247
+ # @param event_name [String]
248
+ # @param options [Hash]
249
+ # @option options [Hash] :payload the payload to match, divided into :equal and :not_equal
250
+ # example: { equal: { medium_id: '123' }, not_equal: { position: 3 } }
251
+ # @param skip_decryption [Boolean]
252
+ # @return [PgEventstore::Event, nil]
253
+ def last_event(subject, event_name, options = {}, skip_decryption: true)
254
+ event_type = "#{subject.context}::#{subject.subject}#{event_name}"
255
+ stream = subject.stream
256
+
257
+ events_cache[stream] ||= {}
258
+ return events_cache[stream][event_type] if !options[:payload] && events_cache[stream][event_type]
259
+
260
+ events = load_events(stream, options:, skip_decryption:)
261
+
262
+ events.each do |result|
263
+ result.each do |event|
264
+ next unless event.type == event_type
265
+ next if options[:locale] && skip?(options[:locale], event)
266
+
267
+ event = resolve_payloads(event) if options[:payload] || options[:resolve_payload]
268
+
269
+ events_cache[stream][event.type] ||= event
270
+
271
+ next if options[:payload] && !payload_matches?(event.data, options[:payload])
272
+
273
+ return event
274
+ end
275
+ end
276
+
277
+ nil
278
+ rescue PgEventstore::StreamNotFoundError
279
+ nil
280
+ end
281
+
282
+ # @param stream [PgEventstore::Stream]
283
+ # @param options [Hash]
284
+ # @option options [String] :direction 'Backwards' or 'Forwards'
285
+ # @option options [Symbol] :from_revision :start or :end
286
+ # @param skip_decryption [Boolean]
287
+ # @return [Enumerator]
288
+ def load_events(stream, options: {}, skip_decryption: true)
289
+ options = { direction: 'Backwards' }.merge(options)
290
+ middlewares = Middlewares.without(:encryptor) if skip_decryption
291
+ PgEventstore.client.read_paginated(stream, options:, middlewares:)
292
+ end
293
+
294
+ # @param event [PgEventstore::Event]
295
+ # @return [PgEventstore::Event]
296
+ def resolve_payloads(event)
297
+ Yes::Core::PayloadStore::Lookup.new.call(event).each do |key, value|
298
+ event.data[key.to_s] = value
299
+ end
300
+
301
+ event
302
+ end
303
+
304
+ # @param event_data [Hash]
305
+ # @param payload [Hash]
306
+ # @return [Boolean]
307
+ def payload_matches?(event_data, payload)
308
+ (payload[:equal] || {}).all? { |k, v| event_data[k.to_s] == v } &&
309
+ (payload[:not_equal] || {}).all? { |k, v| event_data[k.to_s] != v }
310
+ end
311
+
312
+ # Define the implementation in your class.
313
+ # @return [Hash]
314
+ def events_cache
315
+ raise NotImplementedError
316
+ end
317
+ end
318
+ end
319
+ end
320
+ end
321
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Yes
4
+ module Core
5
+ module Commands
6
+ module Stateless
7
+ # Response object for stateless commands
8
+ class Response < Yes::Core::Commands::Response
9
+ attribute? :error, Types.Instance(Stateless::Handler::TransitionError).optional
10
+ end
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Yes
4
+ module Core
5
+ module Commands
6
+ module Stateless
7
+ # Subject object is responsible for holding subject data
8
+ #
9
+ # @attr subject [String]
10
+ # @attr aggregate_id [String]
11
+ # @attr context [String]
12
+ # @attr stream_prefix [String] value is optional
13
+ #
14
+ # @example
15
+ # Yes::Core::Commands::Stateless::Subject.new(
16
+ # context: 'ApprenticeshipPresentation',
17
+ # subject: 'Apprenticeship',
18
+ # aggregate_id: '123'
19
+ # )
20
+ Subject = Data.define(:subject, :aggregate_id, :context, :stream_prefix) do
21
+ OPTIONAL_FIELDS = %i[stream_prefix].index_with(nil).freeze
22
+
23
+ def initialize(**attrs)
24
+ super(**OPTIONAL_FIELDS.merge(attrs))
25
+ end
26
+
27
+ # @return [PgEventstore::Stream]
28
+ def stream
29
+ parts = computed_stream_prefix.split('::')
30
+ PgEventstore::Stream.new(context: parts[0], stream_name: parts[1..].join('::'), stream_id: aggregate_id)
31
+ end
32
+
33
+ # @return [String]
34
+ def computed_stream_prefix
35
+ stream_prefix || "#{context}::#{subject}"
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Yes
4
+ module Core
5
+ module Commands
6
+ # @abstract Subclass and override {.call} to implement a custom command validator.
7
+ #
8
+ # @example
9
+ # class MyCommandValidator < Yes::Core::Commands::Validator
10
+ # def self.call(command)
11
+ # raise CommandInvalid, 'Name is required' if command.payload[:name].blank?
12
+ # end
13
+ # end
14
+ class Validator
15
+ CommandInvalid = Class.new(Yes::Core::Error)
16
+
17
+ # Validates the given command. Must be implemented by subclasses.
18
+ #
19
+ # @param command [Yes::Core::Command] command to validate
20
+ # @raise [CommandInvalid] if command is invalid
21
+ # @raise [NotImplementedError] if not overridden
22
+ def self.call(command)
23
+ raise NotImplementedError
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end