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,233 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Yes
4
+ module Core
5
+ class Aggregate
6
+ module Dsl
7
+ #
8
+ # Class which converts attributes passed to Yes::Core::Aggregate::command in shorthand
9
+ # form to list of attributes and commands to define
10
+ #
11
+ class CommandShortcutExpander
12
+ ExpandedCommandShortcut = Data.define(:attributes, :commands)
13
+ CommandSpecification = Data.define(:name, :block)
14
+ AttributeSpecification = Data.define(:name, :type, :options)
15
+ class InvalidShortcut < Yes::Core::Error; end
16
+
17
+ SPECIAL_CASE_NAMES = {
18
+ change: :change,
19
+ enable: :enable,
20
+ activate: :enable,
21
+ publish: :publish
22
+ }.freeze
23
+
24
+ class << self
25
+ #
26
+ # Detects basic usage of command invocation, so the aggregate knows it's not a shortcut
27
+ #
28
+ # @param [Array<Object>] *args
29
+ # @param [Hash<Object, Object>] **kwargs
30
+ # @param [Proc] &block
31
+ #
32
+ # @return [Boolean]
33
+ #
34
+ def base_case?(*args, **kwargs, &block)
35
+ args.size == 1 && kwargs.empty? && block.present?
36
+ end
37
+ end
38
+
39
+ attr_reader :args, :kwargs, :block
40
+
41
+ #
42
+ # Takes all parameters passed to command invocation
43
+ #
44
+ # @param [Array<Object>] *args
45
+ # @param [Hash<Object, Object>] **kwargs
46
+ # @param [Proc] &block
47
+ #
48
+ def initialize(*args, **kwargs, &block)
49
+ @args = args
50
+ @kwargs = kwargs
51
+ @block = block
52
+ end
53
+
54
+ #
55
+ # Expands shortcut to Data object with list of attributes and commands
56
+ #
57
+ # @raise [InvalidShortcut] if shortcut is not recognized
58
+ # @return [ExpandedCommandShortcut] list of attributes and commands to
59
+ # generate
60
+ #
61
+ def call
62
+ case args
63
+ in [[Symbol, Symbol], Symbol]
64
+ handle_toggle_commands
65
+ in [Symbol => name, *]
66
+ raise InvalidShortcut unless SPECIAL_CASE_NAMES.include?(name)
67
+
68
+ send(:"handle_#{SPECIAL_CASE_NAMES[name]}_command")
69
+ else
70
+ raise InvalidShortcut
71
+ end
72
+ end
73
+
74
+ private
75
+
76
+ def handle_toggle_commands
77
+ raise InvalidShortcut if block.present?
78
+
79
+ command_subject = args.second
80
+ attribute_name = kwargs[:attribute].presence || command_subject
81
+
82
+ set_flag_command, unset_flag_command = args.first
83
+
84
+ attributes =
85
+ if kwargs[:attribute] == false
86
+ []
87
+ else
88
+ [
89
+ AttributeSpecification.new(
90
+ name: attribute_name,
91
+ type: :boolean,
92
+ options: {}
93
+ )
94
+ ]
95
+ end
96
+
97
+ commands = [
98
+ CommandSpecification.new(
99
+ name: :"#{set_flag_command}_#{command_subject}",
100
+ block: proc do
101
+ guard(:no_change) { !send(attribute_name) }
102
+ update_state do
103
+ send(attribute_name) { true }
104
+ end
105
+ end
106
+ ),
107
+ CommandSpecification.new(
108
+ name: :"#{unset_flag_command}_#{command_subject}",
109
+ block: proc do
110
+ guard(:no_change) { send(attribute_name) }
111
+ update_state do
112
+ send(attribute_name) { false }
113
+ end
114
+ end
115
+ )
116
+ ]
117
+
118
+ ExpandedCommandShortcut.new(attributes:, commands:)
119
+ end
120
+
121
+ def handle_change_command
122
+ raise InvalidShortcut if args.size > 3
123
+
124
+ command_subject = args[1]
125
+ attribute_name = kwargs[:attribute].presence || command_subject
126
+ attribute_type = args[2].presence || :string
127
+ localized = kwargs[:localized] || false
128
+ should_encrypt = kwargs[:encrypt] || false
129
+ additional_block = block # needs to be captured in a local variable to ensure proper closure in this scope
130
+
131
+ payload_options = { attribute_name => attribute_type }
132
+ payload_options[:locale] = :locale if localized
133
+
134
+ attributes =
135
+ if kwargs[:attribute] == false
136
+ []
137
+ else
138
+ [
139
+ AttributeSpecification.new(
140
+ name: attribute_name,
141
+ type: attribute_type,
142
+ options: { localized: }
143
+ )
144
+ ]
145
+ end
146
+
147
+ commands = [
148
+ CommandSpecification.new(
149
+ name: :"change_#{command_subject}",
150
+ block: proc do
151
+ guard(:no_change) { value_changed?(send(attribute_name), payload.send(attribute_name)) }
152
+ payload(**payload_options)
153
+ encrypt(attribute_name) if should_encrypt
154
+ instance_eval(&additional_block) if additional_block.present?
155
+ end
156
+ )
157
+ ]
158
+
159
+ ExpandedCommandShortcut.new(attributes:, commands:)
160
+ end
161
+
162
+ def handle_enable_command
163
+ raise InvalidShortcut if args.size > 2
164
+
165
+ command_verb = args.first
166
+ command_subject = args.second
167
+ attribute_name = kwargs[:attribute].presence || command_subject
168
+ additional_block = block
169
+
170
+ attributes =
171
+ if kwargs[:attribute] == false
172
+ []
173
+ else
174
+ [
175
+ AttributeSpecification.new(
176
+ name: attribute_name,
177
+ type: :boolean,
178
+ options: {}
179
+ )
180
+ ]
181
+ end
182
+
183
+ commands = [
184
+ CommandSpecification.new(
185
+ name: :"#{command_verb}_#{command_subject}",
186
+ block: proc do
187
+ guard(:no_change) { !send(attribute_name) }
188
+ update_state do
189
+ send(attribute_name) { true }
190
+ end
191
+ instance_eval(&additional_block) if additional_block.present?
192
+ end
193
+ )
194
+ ]
195
+
196
+ ExpandedCommandShortcut.new(attributes:, commands:)
197
+ end
198
+
199
+ def handle_publish_command
200
+ raise InvalidShortcut if args.size > 1 || block.present?
201
+
202
+ attribute_name = kwargs[:attribute].presence || :published
203
+
204
+ attributes =
205
+ if kwargs[:attribute] == false
206
+ []
207
+ else
208
+ [
209
+ AttributeSpecification.new(
210
+ name: attribute_name,
211
+ type: :boolean,
212
+ options: {}
213
+ )
214
+ ]
215
+ end
216
+
217
+ commands = [
218
+ CommandSpecification.new(
219
+ name: :publish,
220
+ block: proc do
221
+ guard(:no_change) { !send(attribute_name) }
222
+ update_state { send(attribute_name) { true } }
223
+ end
224
+ )
225
+ ]
226
+
227
+ ExpandedCommandShortcut.new(attributes:, commands:)
228
+ end
229
+ end
230
+ end
231
+ end
232
+ end
233
+ end
@@ -0,0 +1,67 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Yes
4
+ module Core
5
+ class Aggregate
6
+ module Dsl
7
+ # Handles finding and setting constants based on conventional naming
8
+ #
9
+ # @api private
10
+ class ConstantResolver
11
+ # @param class_name_convention [ClassNameConvention] Convention for generating class names
12
+ def initialize(class_name_convention)
13
+ @class_name_convention = class_name_convention
14
+ end
15
+
16
+ # Attempts to find a class based on conventional naming
17
+ #
18
+ # @param type [Symbol] The type of class to find (:command, :event, or :handler)
19
+ # @param name [Symbol] The name of the class
20
+ # @return [Class, nil] The found class or nil if not found
21
+ def find_conventional_class(type, name)
22
+ class_name = class_name_convention.class_name_for(type, name)
23
+ class_name.constantize
24
+ rescue NameError
25
+ nil
26
+ end
27
+
28
+ # Sets the generated class as a constant in the appropriate module path
29
+ #
30
+ # @param type [Symbol] The type of class (:command, :event, or :handler, ...)
31
+ # @param name [Symbol] The name for the class
32
+ # @param klass [Class] The class to set as constant
33
+ # @return [Class] The set class
34
+ def set_constant_for(type, name, klass)
35
+ class_name = class_name_convention.class_name_for(type, name)
36
+ modules = class_name.split('::')
37
+ class_name = modules.pop
38
+
39
+ parent_module = create_module_hierarchy(modules)
40
+ parent_module.const_set(class_name, klass)
41
+ end
42
+
43
+ private
44
+
45
+ attr_reader :class_name_convention
46
+
47
+ def create_module_hierarchy(modules)
48
+ # Start with the root namespace if the path is absolute (starts with ::)
49
+ base = if modules.first && modules.first.empty?
50
+ Object.tap { modules.shift }
51
+ else
52
+ Object
53
+ end
54
+
55
+ modules.inject(base) do |mod, module_name|
56
+ if mod.const_defined?(module_name, false)
57
+ mod.const_get(module_name, false)
58
+ else
59
+ mod.const_set(module_name, Module.new)
60
+ end
61
+ end
62
+ end
63
+ end
64
+ end
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Yes
4
+ module Core
5
+ class Aggregate
6
+ module Dsl
7
+ module MethodDefiners
8
+ module Attribute
9
+ # Defines the accessor method for an attribute
10
+ class Accessor < Base
11
+ # Defines a reader method for the attribute that reads from the read model
12
+ # @return [void]
13
+ def call
14
+ name = @name
15
+
16
+ aggregate_class.define_method(name) do
17
+ return nil unless self.class.read_model_enabled?
18
+
19
+ read_model.public_send(name)
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Yes
4
+ module Core
5
+ class Aggregate
6
+ module Dsl
7
+ module MethodDefiners
8
+ module Attribute
9
+ # Defines the accessor methods for an aggregate attribute
10
+ class AggregateAccessor < Base
11
+ # Defines reader methods for the aggregate attribute that reads from the read model
12
+ # @return [void]
13
+ def call
14
+ name = @name
15
+ id_name = :"#{name}_id"
16
+
17
+ # Define the id accessor
18
+ aggregate_class.define_method(id_name) do
19
+ read_model.public_send(id_name)
20
+ end
21
+
22
+ # Define the aggregate accessor
23
+ aggregate_class.define_method(name) do
24
+ id = read_model.public_send(id_name)
25
+ return nil unless id
26
+
27
+ "#{self.class.context}::#{name.to_s.camelize}::Aggregate".constantize.new(id)
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Yes
4
+ module Core
5
+ class Aggregate
6
+ module Dsl
7
+ module MethodDefiners
8
+ module Attribute
9
+ # Base class for attribute method definers that provides common functionality
10
+ # for defining attribute-related methods on aggregate classes.
11
+ #
12
+ # @abstract Subclass and override {#call} to implement custom attribute method definition
13
+ class Base
14
+ # Initializes a new attribute method definer
15
+ #
16
+ # @param attribute_data [Object] Object containing attribute configuration
17
+ # @option attribute_data [Symbol] :name The attribute name
18
+ # @option attribute_data [Class] :aggregate_class The target aggregate class where methods will be defined
19
+ def initialize(attribute_data)
20
+ @name = attribute_data.name
21
+ @aggregate_class = attribute_data.aggregate_class
22
+ end
23
+
24
+ # Defines the attribute-related methods on the aggregate class
25
+ #
26
+ # @abstract
27
+ # @raise [NotImplementedError] when called on the base class
28
+ # @return [void]
29
+ def call
30
+ raise NotImplementedError, "#{self.class} must implement #call"
31
+ end
32
+
33
+ private
34
+
35
+ attr_reader :name, :aggregate_class
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Yes
4
+ module Core
5
+ class Aggregate
6
+ module Dsl
7
+ module MethodDefiners
8
+ module Command
9
+ # Base class for command method definers that provides common functionality
10
+ # for defining command-related methods on aggregate classes.
11
+ #
12
+ # @abstract Subclass and override {#call} to implement custom command method definition
13
+ class Base
14
+ # Initializes a new command method definer
15
+ #
16
+ # @param command_data [Object] Object containing command configuration
17
+ # @option command_data [Symbol] :name The command name
18
+ # @option command_data [Class] :aggregate_class The target aggregate class where methods will be defined
19
+ def initialize(command_data)
20
+ @name = command_data.name
21
+ @aggregate_class = command_data.aggregate_class
22
+ end
23
+
24
+ # Defines the command-related methods on the aggregate class
25
+ #
26
+ # @abstract
27
+ # @raise [NotImplementedError] when called on the base class
28
+ # @return [void]
29
+ def call
30
+ raise NotImplementedError, "#{self.class} must implement #call"
31
+ end
32
+
33
+ private
34
+
35
+ attr_reader :name, :aggregate_class
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Yes
4
+ module Core
5
+ class Aggregate
6
+ module Dsl
7
+ module MethodDefiners
8
+ module Command
9
+ # Defines a can_<command_name>? method on the aggregate class
10
+ class CanCommand < Base
11
+ # @return [void]
12
+ def call
13
+ can_change_method = :"can_#{name}?"
14
+ error_method = :"#{name}_error"
15
+
16
+ aggregate_class.attr_accessor error_method
17
+ command_name = @name
18
+
19
+ aggregate_class.define_method(can_change_method) do |payload = {}|
20
+ payload = command_utilities.prepare_default_payload(command_name, payload, self.class)
21
+ payload = command_utilities.prepare_command_payload(command_name, payload, self.class)
22
+ payload = command_utilities.prepare_assign_command_payload(command_name, payload)
23
+ cmd = command_utilities.build_command(command_name, payload)
24
+ guard_evaluator_class = command_utilities.fetch_guard_evaluator_class(command_name)
25
+
26
+ Yes::Core::CommandHandling::GuardRunner.new(self).call(
27
+ cmd, command_name, guard_evaluator_class, skip_guards: false
28
+ ).present?
29
+ rescue Yes::Core::CommandHandling::GuardEvaluator::InvalidTransition,
30
+ Yes::Core::CommandHandling::GuardEvaluator::NoChangeTransition,
31
+ Yes::Core::Command::Invalid
32
+ false
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Yes
4
+ module Core
5
+ class Aggregate
6
+ module Dsl
7
+ module MethodDefiners
8
+ module Command
9
+ # Defines a command method on the aggregate class
10
+ class Command < Base
11
+ # @return [void]
12
+ def call
13
+ command_name = @name
14
+
15
+ aggregate_class.define_method(command_name) do |payload = nil, **options|
16
+ payload = payload.clone if payload.is_a?(Hash)
17
+
18
+ # Extract and remove guards option from options, default to true
19
+ guards = options.delete(:guards)
20
+ guards = true if guards.nil?
21
+
22
+ # Extract and remove metadata option from options if present
23
+ metadata = options.delete(:metadata)
24
+
25
+ # Handle different calling patterns:
26
+ # 1. command(value) or command(value, guards: false) - shorthand form with single value
27
+ # 2. command({attr: value}) or command({attr: value}, guards: false) - hash form
28
+ # 3. command(attr: value) - ALL kwargs are treated as payload (no options)
29
+ # 4. command() - no arguments (for commands without payload)
30
+
31
+ # If no positional argument was provided but kwargs were given (after removing guards/metadata),
32
+ # treat all remaining kwargs as payload
33
+ if payload.nil? && !options.empty?
34
+ payload = options
35
+ {}
36
+ elsif payload.nil?
37
+ payload = {}
38
+ end
39
+
40
+ # Pass metadata to CommandHandler which will merge it into the event metadata
41
+ Yes::Core::CommandHandling::CommandHandler.new(self).call(command_name, payload, guards:, metadata:)
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,86 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Yes
4
+ module Core
5
+ class Aggregate
6
+ # Provides authorizer functionality for aggregates.
7
+ #
8
+ # This concern automatically sets up an authorizer class for the aggregate
9
+ # using `Yes::Core::Aggregate::Dsl::ClassResolvers::Authorizer`.
10
+ # The authorizer class is typically used for command authorization via Cerbos.
11
+ #
12
+ module HasAuthorizer
13
+ extend ActiveSupport::Concern
14
+
15
+ # Mutable struct for holding authorizer options
16
+ AuthorizerOptions = Struct.new(
17
+ :authorizer_base_class,
18
+ :context,
19
+ :aggregate,
20
+ :read_model_class,
21
+ :resource_name,
22
+ :authorizer_block,
23
+ :draftable,
24
+ :changes_read_model_class,
25
+ keyword_init: true
26
+ )
27
+
28
+ included do
29
+ class << self
30
+ # @!attribute [rw] authorizer_class
31
+ # @return [Class] The resolved authorizer class used for command authorization.
32
+ #
33
+ # @!attribute [rw] authorizer_options
34
+ # @return [AuthorizerOptions] Options used when building the authorizer.
35
+ attr_accessor :authorizer_class, :authorizer_options
36
+ end
37
+ end
38
+
39
+ class_methods do
40
+ # Finds or generates the authorizer class using `ClassResolvers::Authorizer`,
41
+ # passing the authorizer parameters.
42
+ # Registers the authorizer in the configuration and stores the resolved class internally.
43
+ # @return [Class] The resolved authorizer class.
44
+ def setup_authorizer_classes
45
+ return unless authorizer_options
46
+
47
+ authorizer_options.read_model_class ||= read_model_class
48
+ authorizer_options.resource_name ||= aggregate.underscore
49
+
50
+ # Set draftable and changes read model class if the aggregate is draftable
51
+ authorizer_options.draftable ||= draftable?
52
+ authorizer_options.changes_read_model_class ||= changes_read_model_class if draftable?
53
+
54
+ self.authorizer_class = Yes::Core::Aggregate::Dsl::ClassResolvers::Authorizer.new(authorizer_options).call
55
+
56
+ commands.each_value do |command_data|
57
+ Dsl::ClassResolvers::Command::AuthorizerFactory.create(command_data)&.call
58
+ end
59
+ end
60
+
61
+ # @param cerbos [Boolean] Whether to use Cerbos authorizer.
62
+ # @param read_model_class [Class] The read model class to use for the authorizer.
63
+ # @param resource_name [String] The resource name to use for the authorizer.
64
+ # @param block [Proc] The block to use for the authorizer.
65
+ # @return [Class] The authorizer class.
66
+ def authorize(cerbos: false, read_model_class: nil, resource_name: nil, &block)
67
+ authorizer_base_class = if cerbos
68
+ Yes::Core::Authorization::CommandCerbosAuthorizer
69
+ else
70
+ Yes::Core::Authorization::CommandAuthorizer
71
+ end
72
+
73
+ self.authorizer_options = AuthorizerOptions.new(
74
+ authorizer_base_class:,
75
+ context:,
76
+ aggregate:,
77
+ read_model_class:,
78
+ resource_name:,
79
+ authorizer_block: block
80
+ )
81
+ end
82
+ end
83
+ end
84
+ end
85
+ end
86
+ end