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,132 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Yes
4
+ module Core
5
+ class Aggregate
6
+ module Dsl
7
+ module ClassResolvers
8
+ # Creates and registers authorizer classes for aggregates based on
9
+ # Yes::Core::Authorization::CommandCerbosAuthorizer.
10
+ #
11
+ # This class resolver generates authorizer classes associated with aggregates.
12
+ # Each authorizer class defines a RESOURCE constant containing the
13
+ # associated read model class and resource name used for authorization checks.
14
+ #
15
+ # @example Generated authorizer class structure
16
+ # # Generated for a 'User' aggregate
17
+ # class GeneratedAuthorizer < Yes::Core::Authorization::CommandCerbosAuthorizer
18
+ # RESOURCE = { read_model: Auth::Resources::User, name: 'user' }.freeze
19
+ # end
20
+ class Authorizer < Base
21
+ # Initializes a new authorizer class resolver
22
+ #
23
+ # @param options [Yes::Core::Aggregate::HasAuthorizer::AuthorizerOptions] Data object containing:
24
+ # - authorizer_base_class [Class] The authorizer class to use.
25
+ # - context [String] The name of the context (e.g., 'CompanyManagement')
26
+ # - aggregate [String] The name of the aggregate (e.g., 'Company')
27
+ # - read_model_class [Class, nil] Optional read model class. Defaults to
28
+ # `Auth::Resources::<AggregateName>`.
29
+ # - resource_name [String, nil] Optional resource name for Cerbos. Defaults to
30
+ # aggregate name underscored (e.g., 'company').
31
+ # - authorizer_block [Proc, nil] An optional block defining the custom logic for the `call` method
32
+ # if `authorizer_base_class` is not `Yes::Core::Authorization::CommandCerbosAuthorizer`.
33
+ def initialize(options)
34
+ @resource_name = options.resource_name
35
+ @read_model_class = options.read_model_class
36
+ @authorizer_class = options.authorizer_base_class
37
+ @custom_call_logic = options.authorizer_block
38
+ @draftable = options.draftable
39
+ @changes_read_model_class = options.changes_read_model_class
40
+ # Base class expects context_name and aggregate_name parameters
41
+ super(options.context, options.aggregate)
42
+ end
43
+
44
+ # Creates and registers the authorizer class in the Yes::Core configuration
45
+ #
46
+ # @return [Class] The found or generated authorizer class that was registered
47
+ def call
48
+ Yes::Core.configuration.register_aggregate_authorizer_class(
49
+ context_name,
50
+ aggregate_name,
51
+ find_or_generate_class
52
+ )
53
+ end
54
+
55
+ private
56
+
57
+ # @!attribute [r] resource_name
58
+ # @return [String] The name of the resource used for authorization.
59
+ # @!attribute [r] read_model_class
60
+ # @return [Class] The read model class associated with the resource.
61
+ # @!attribute [r] custom_call_logic
62
+ # @return [Proc, nil] The custom block provided for the call method logic.
63
+ # @!attribute [r] changes_read_model_class
64
+ # @return [Class, nil] The changes read model class for draftable aggregates.
65
+ attr_reader :resource_name, :read_model_class, :authorizer_class, :custom_call_logic, :changes_read_model_class
66
+
67
+ # @return [Symbol] Returns :authorizer as the class type
68
+ def class_type
69
+ :authorizer
70
+ end
71
+
72
+ # Checks if the aggregate is draftable
73
+ #
74
+ # @return [Boolean] true if the aggregate is draftable, false otherwise
75
+ def draftable?
76
+ @draftable == true
77
+ end
78
+
79
+ # Generates a new authorizer class dynamically.
80
+ # The generated class inherits from the specified authorizer_class.
81
+ # If the base class is Yes::Core::Authorization::CommandCerbosAuthorizer, it sets the RESOURCE constant.
82
+ # Otherwise, if a block was provided during initialization, it defines a `call` method
83
+ # executing that block's logic.
84
+ #
85
+ # @raise [ArgumentError] If the base class is not CommandCerbosAuthorizer and no block was provided.
86
+ # @return [Class] A new authorizer class.
87
+ def generate_class
88
+ klass = Class.new(authorizer_class)
89
+
90
+ if authorizer_class == Yes::Core::Authorization::CommandCerbosAuthorizer
91
+ resource_attributes = { read_model: read_model_class, name: resource_name }
92
+ resource_attributes[:draft_read_model] = changes_read_model_class if draftable?
93
+ klass.const_set(:RESOURCE, resource_attributes.freeze)
94
+ else
95
+ define_custom_call_logic(klass)
96
+ end
97
+
98
+ klass
99
+ end
100
+
101
+ # Defines custom call logic for an authorizer class when not using CommandCerbosAuthorizer
102
+ #
103
+ # @param klass [Class] The dynamically generated authorizer class
104
+ # @raise [ArgumentError] If no custom_call_logic block was provided during initialization
105
+ # @return [void]
106
+ def define_custom_call_logic(klass)
107
+ _custom_logic = custom_call_logic
108
+ unless _custom_logic
109
+ raise ArgumentError,
110
+ "A block must be provided to define the 'call' method logic when not using CommandCerbosAuthorizer."
111
+ end
112
+
113
+ # Define call as a class method (matching CommandCerbosAuthorizer pattern)
114
+ klass.define_singleton_method(:call) do |current_command, current_auth_data|
115
+ @_exec_command = current_command
116
+ @_exec_auth_data = current_auth_data
117
+ define_singleton_method(:command) { @_exec_command }
118
+ define_singleton_method(:auth_data) { @_exec_auth_data }
119
+ begin
120
+ instance_exec(&_custom_logic)
121
+ ensure
122
+ @_exec_command = nil
123
+ @_exec_auth_data = nil
124
+ end
125
+ end
126
+ end
127
+ end
128
+ end
129
+ end
130
+ end
131
+ end
132
+ end
@@ -0,0 +1,80 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Yes
4
+ module Core
5
+ class Aggregate
6
+ module Dsl
7
+ module ClassResolvers
8
+ # Base class for creating and registering aggregate-related classes
9
+ #
10
+ # This class provides the foundation for resolving and generating various
11
+ # aggregate-related classes such as commands, events, and handlers.
12
+ # It handles class registration and provides a template for class generation.
13
+ #
14
+ # @abstract Subclass and override {#class_type}, {#class_name}, and {#generate_class}
15
+ # to implement specific class resolver behavior
16
+ class Base
17
+ # Creates and registers the class in the Yes::Core configuration
18
+ #
19
+ # @return [Class] The found or generated class that was registered
20
+ def call
21
+ Yes::Core.configuration.register_aggregate_class(
22
+ context_name,
23
+ aggregate_name,
24
+ class_name,
25
+ class_type,
26
+ find_or_generate_class
27
+ )
28
+ end
29
+
30
+ private
31
+
32
+ # @return [ConstantResolver] The resolver for finding and setting constants
33
+ # @return [ClassNameConvention] The convention for generating class names
34
+ # @return [String] The name of the context
35
+ # @return [String] The name of the aggregate
36
+ attr_reader :constant_resolver, :class_name_convention, :context_name, :aggregate_name
37
+
38
+ # @param context_name [String] The name of the context
39
+ # @param aggregate_name [String] The name of the aggregate
40
+ def initialize(context_name, aggregate_name)
41
+ @context_name = context_name
42
+ @aggregate_name = aggregate_name
43
+
44
+ @class_name_convention = ClassNameConvention.new(
45
+ context: context_name,
46
+ aggregate: aggregate_name
47
+ )
48
+ @constant_resolver = ConstantResolver.new(class_name_convention)
49
+ end
50
+
51
+ # Finds an existing class or generates a new one based on conventions
52
+ #
53
+ # @return [Class] The found or generated class
54
+ def find_or_generate_class
55
+ constant_resolver.find_conventional_class(class_type, class_name) ||
56
+ constant_resolver.set_constant_for(class_type, class_name, generate_class)
57
+ end
58
+
59
+ # @abstract
60
+ # @return [Symbol] The type of class being resolved (e.g., :command, :event, :handler)
61
+ def class_type
62
+ raise NotImplementedError, "#{self.class} must implement #class_type"
63
+ end
64
+
65
+ # @return [String] The name of the class to be generated
66
+ def class_name
67
+ nil
68
+ end
69
+
70
+ # @abstract
71
+ # @return [Class] The generated class
72
+ def generate_class
73
+ raise NotImplementedError, "#{self.class} must implement #generate_class"
74
+ end
75
+ end
76
+ end
77
+ end
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Yes
4
+ module Core
5
+ class Aggregate
6
+ module Dsl
7
+ module ClassResolvers
8
+ module Command
9
+ # Base class for command authorizers
10
+ class Authorizer < Base
11
+ # Common methods for all authorizers
12
+ def class_type
13
+ :authorizer
14
+ end
15
+
16
+ def class_name
17
+ command_data.name
18
+ end
19
+
20
+ # This should be overridden by subclasses
21
+ def generate_class
22
+ raise NotImplementedError, "#{self.class} must implement #generate_class"
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Yes
4
+ module Core
5
+ class Aggregate
6
+ module Dsl
7
+ module ClassResolvers
8
+ module Command
9
+ # Factory class for creating appropriate command authorizer resolver
10
+ # based on the aggregate-level authorizer type
11
+ class AuthorizerFactory
12
+ # Creates and returns the appropriate authorizer resolver
13
+ # based on the aggregate's authorizer class type
14
+ #
15
+ # @param command_data [CommandData] Command data with aggregate information
16
+ # @return [SimpleAuthorizer, CerbosAuthorizer, nil] Appropriate authorizer or nil
17
+ def self.create(command_data)
18
+ # Only create command authorizers if there's an aggregate-level authorizer
19
+ aggregate_authorizer_class = command_data.aggregate_class.authorizer_class
20
+ return nil unless aggregate_authorizer_class
21
+
22
+ if aggregate_authorizer_class <= Yes::Core::Authorization::CommandCerbosAuthorizer
23
+ CerbosAuthorizer.new(command_data)
24
+ else
25
+ SimpleAuthorizer.new(command_data)
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Yes
4
+ module Core
5
+ class Aggregate
6
+ module Dsl
7
+ module ClassResolvers
8
+ module Command
9
+ # Base class for command-related class resolvers
10
+ #
11
+ # This class extends the base resolver functionality to handle
12
+ # command-specific class generation. It provides a foundation for
13
+ # resolvers that need to work with commands, such as command classes
14
+ # and their associated events.
15
+ #
16
+ # @abstract Subclass and implement the required methods from {ClassResolvers::Base}
17
+ class Base < ClassResolvers::Base
18
+ # Initializes a new command-based class resolver
19
+ #
20
+ # @param command_data [Yes::Core::Aggregate::Dsl::CommandData] The command data instance
21
+ # containing metadata about the command being processed
22
+ def initialize(command_data)
23
+ @command_data = command_data
24
+
25
+ super(command_data.context_name, command_data.aggregate_name)
26
+ end
27
+
28
+ private
29
+
30
+ # @return [Yes::Core::Aggregate::Dsl::CommandData] The command data instance
31
+ attr_reader :command_data
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,114 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Yes
4
+ module Core
5
+ class Aggregate
6
+ module Dsl
7
+ module ClassResolvers
8
+ module Command
9
+ # Command resolver for Cerbos-based authorizers
10
+ class CerbosAuthorizer < Authorizer
11
+ private
12
+
13
+ # Generates a Cerbos authorizer class with RESOURCE constant
14
+ # and optional DSL methods for resource_attributes and cerbos_payload
15
+ #
16
+ # @return [Class] The generated authorizer class
17
+ def generate_class
18
+ klass = Class.new(command_data.aggregate_class.authorizer_class)
19
+
20
+ # Apply Cerbos-specific overrides if we have a block
21
+ apply_cerbos_overrides(klass) if command_data.authorizer_block
22
+
23
+ klass
24
+ end
25
+
26
+ # Applies Cerbos-specific DSL overrides to allow customization
27
+ # of resource_attributes and cerbos_payload methods
28
+ #
29
+ # @param klass [Class] the class being generated
30
+ # rubocop:disable Metrics/MethodLength
31
+ def apply_cerbos_overrides(klass)
32
+ # Store the block for later use
33
+ user_block = command_data.authorizer_block
34
+
35
+ # Define instance variables and accessor methods for the DSL
36
+ klass.class_eval do
37
+ # Resource attributes storage
38
+ class << self
39
+ attr_accessor :_resource_attributes_block, :_cerbos_payload_block
40
+ end
41
+
42
+ # Define accessor methods for the DSL
43
+ define_method(:command) { @_command }
44
+ define_method(:resource) { @_resource }
45
+ define_method(:auth_data) { @_auth_data }
46
+
47
+ # Define resource_attributes method that will be called by external code
48
+ define_singleton_method(:resource_attributes) do |resource = nil, command = nil, &block|
49
+ if block
50
+ # Store the block for later use
51
+ self._resource_attributes_block = block
52
+ elsif _resource_attributes_block
53
+ # Execute the previously stored block
54
+ instance = new
55
+ instance.instance_variable_set(:@_command, command)
56
+ instance.instance_variable_set(:@_resource, resource)
57
+
58
+ begin
59
+ instance.instance_eval(&_resource_attributes_block)
60
+ ensure
61
+ instance.remove_instance_variable(:@_command) if instance.instance_variable_defined?(:@_command)
62
+ instance.remove_instance_variable(:@_resource) if instance.instance_variable_defined?(:@_resource)
63
+ end
64
+ elsif defined?(super)
65
+ # No block defined, call the original method if available
66
+ super(resource, command)
67
+ end
68
+ end
69
+
70
+ # Define cerbos_payload method that will be called by external code
71
+ define_singleton_method(:cerbos_payload) do |command = nil, resource = nil, auth = nil, &block|
72
+ if block
73
+ # Store the block for later use
74
+ self._cerbos_payload_block = block
75
+ elsif _cerbos_payload_block
76
+ # Execute the previously stored block
77
+ instance = new
78
+ instance.instance_variable_set(:@_command, command)
79
+ instance.instance_variable_set(:@_resource, resource)
80
+ instance.instance_variable_set(:@_auth_data, auth)
81
+
82
+ begin
83
+ instance.instance_eval(&_cerbos_payload_block)
84
+ ensure
85
+ instance.remove_instance_variable(:@_command) if instance.instance_variable_defined?(:@_command)
86
+ instance.remove_instance_variable(:@_resource) if instance.instance_variable_defined?(:@_resource)
87
+ instance.remove_instance_variable(:@_auth_data) if instance.instance_variable_defined?(:@_auth_data)
88
+ end
89
+ elsif defined?(super)
90
+ # No block defined, call the original method if available
91
+ super(command, resource, auth)
92
+ end
93
+ end
94
+ end
95
+
96
+ # We need the resource_attributes and cerbos_payload to be callable from class context
97
+ # in the user block, but also store blocks for later execution
98
+ klass.instance_eval do
99
+ # Initial setup
100
+ self._resource_attributes_block = nil
101
+ self._cerbos_payload_block = nil
102
+
103
+ # Execute the user block to define overrides
104
+ instance_eval(&user_block)
105
+ end
106
+ end
107
+ # rubocop:enable Metrics/MethodLength
108
+ end
109
+ end
110
+ end
111
+ end
112
+ end
113
+ end
114
+ end
@@ -0,0 +1,70 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Yes
4
+ module Core
5
+ class Aggregate
6
+ module Dsl
7
+ module ClassResolvers
8
+ module Command
9
+ # Resolves or generates a command class for a command
10
+ class Command < Base
11
+ private
12
+
13
+ # @return [Symbol] The type of class being resolved
14
+ def class_type
15
+ :command
16
+ end
17
+
18
+ # @return [String] The name of the class to be generated
19
+ def class_name
20
+ command_data.name
21
+ end
22
+
23
+ # Generates a command class
24
+ #
25
+ # @return [Class] The generated command class
26
+ def generate_class
27
+ context = context_name
28
+ aggregate = aggregate_name
29
+ payload_attributes = command_data.payload_attributes || {}
30
+
31
+ Class.new(Yes::Core::Command) do
32
+ # Define the aggregate_id attribute for the command
33
+ attribute :"#{aggregate.underscore}_id", Yes::Core::Types::UUID
34
+
35
+ # Define payload attributes if any
36
+ payload_attributes.each do |attr_name, attr_type|
37
+ # Handle simple type (not a hash)
38
+ unless attr_type.is_a?(Hash)
39
+ attribute attr_name, Yes::Core::TypeLookup.type_for(attr_type, context)
40
+ next
41
+ end
42
+
43
+ resolved_type = Yes::Core::TypeLookup.type_for(attr_type[:type], context)
44
+
45
+ case [attr_type[:optional] == true, attr_type[:nullable] == true]
46
+ when [false, false]
47
+ # required key, non-nullable value
48
+ attribute attr_name, resolved_type
49
+ when [false, true]
50
+ # required key, nullable value
51
+ attribute attr_name, resolved_type.optional
52
+ when [true, false]
53
+ # optional key, non-nullable value
54
+ attribute? attr_name, resolved_type
55
+ when [true, true]
56
+ # optional key, nullable value
57
+ attribute? attr_name, resolved_type.optional
58
+ end
59
+ end
60
+
61
+ alias_method :aggregate_id, :"#{aggregate.underscore}_id"
62
+ end
63
+ end
64
+ end
65
+ end
66
+ end
67
+ end
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,88 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Yes
4
+ module Core
5
+ class Aggregate
6
+ module Dsl
7
+ module ClassResolvers
8
+ module Command
9
+ # Resolves or generates an event class for a command
10
+ class Event < Base
11
+ private
12
+
13
+ # @return [Symbol] The type of class being resolved
14
+ def class_type
15
+ :event
16
+ end
17
+
18
+ # @return [String] The name of the class to be generated
19
+ def class_name
20
+ command_data.event_name
21
+ end
22
+
23
+ # Generates an event class
24
+ #
25
+ # @return [Class] The generated event class
26
+ def generate_class
27
+ aggregate = aggregate_name
28
+ context = context_name
29
+ payload_attributes = command_data.payload_attributes || {}
30
+ encrypted_attributes = command_data.encrypted_attributes || []
31
+
32
+ Class.new(Yes::Core::Event) do
33
+ define_method :schema do
34
+ Dry::Schema.Params do
35
+ # Helper to build attribute definition with support for optional key and nullable value
36
+ build_attribute = proc do |attr_name, attr_type_config|
37
+ # Handle simple type (not a hash)
38
+ unless attr_type_config.is_a?(Hash)
39
+ required(attr_name).value(Yes::Core::TypeLookup.type_for(attr_type_config, context, :event))
40
+ next
41
+ end
42
+
43
+ resolved_type = Yes::Core::TypeLookup.type_for(attr_type_config[:type], context, :event)
44
+
45
+ case [attr_type_config[:optional] == true, attr_type_config[:nullable] == true]
46
+ when [false, false]
47
+ # required key, non-nullable value
48
+ required(attr_name).value(resolved_type)
49
+ when [false, true]
50
+ # required key, nullable value
51
+ required(attr_name).maybe(resolved_type)
52
+ when [true, false]
53
+ # optional key, non-nullable value
54
+ optional(attr_name).value(resolved_type)
55
+ when [true, true]
56
+ # optional key, nullable value
57
+ optional(attr_name).maybe(resolved_type)
58
+ end
59
+ end
60
+
61
+ # Define the aggregate_id attribute for the event
62
+ required(:"#{aggregate.underscore}_id").value(Yes::Core::Types::UUID)
63
+
64
+ # Define payload attributes if any
65
+ payload_attributes.each do |attr_name, attr_type|
66
+ build_attribute.call(attr_name, attr_type)
67
+ end
68
+ end
69
+ end
70
+
71
+ # Add encryption_schema class method if there are encrypted attributes
72
+ if encrypted_attributes.any?
73
+ define_singleton_method :encryption_schema do
74
+ {
75
+ key: ->(data) { data[:"#{aggregate.underscore}_id"] },
76
+ attributes: encrypted_attributes
77
+ }
78
+ end
79
+ end
80
+ end
81
+ end
82
+ end
83
+ end
84
+ end
85
+ end
86
+ end
87
+ end
88
+ end
@@ -0,0 +1,84 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Yes
4
+ module Core
5
+ class Aggregate
6
+ module Dsl
7
+ module ClassResolvers
8
+ module Command
9
+ # Creates and registers guard evaluator classes for aggregate commands
10
+ #
11
+ # This class resolver generates plain guard evaluator classes that process
12
+ # commands in aggregates.
13
+ #
14
+ class GuardEvaluator < Base
15
+ private
16
+
17
+ # Returns the class type symbol for the guard evaluator
18
+ #
19
+ # @return [Symbol] Returns :guard_evaluator as the class type
20
+ # @api private
21
+ def class_type
22
+ :guard_evaluator
23
+ end
24
+
25
+ # Returns the name for the guard evaluator class
26
+ #
27
+ # @return [String] The name of the guard evaluator class derived from the attribute
28
+ # @api private
29
+ def class_name
30
+ command_data.name
31
+ end
32
+
33
+ # Creates a new guard evaluator class
34
+ #
35
+ # By default, includes a no_change guard that checks if the command would modify the aggregate's state.
36
+ # This guard is omitted if the command has a custom state_update block since we cannot rely on the default
37
+ # attribute update logic.
38
+ #
39
+ # @return [Class] A new guard evaluator class inheriting from Yes::Core::CommandHandling::GuardEvaluator
40
+ # @api private
41
+ def generate_class
42
+ command_name = command_data.name
43
+ context_name = command_data.context_name
44
+ aggregate_name = command_data.aggregate_name
45
+
46
+ # Add the no_change guard to the command data's guard names
47
+ # This will be added by the GuardEvaluator unless there's an update_state block
48
+ # or if it was already added by a shortcut expansion
49
+ command_data.add_guard(:no_change) unless command_data.guard_names.include?(:no_change) || command_data.update_state_block
50
+
51
+ Class.new(Yes::Core::CommandHandling::GuardEvaluator) do
52
+ guard :no_change do
53
+ state_updater_class = Yes::Core.configuration.aggregate_class(
54
+ context_name, aggregate_name, command_name, :state_updater
55
+ )
56
+
57
+ next true if state_updater_class.update_state_block
58
+
59
+ payload = raw_payload.except(:"#{aggregate_name.underscore}_id")
60
+
61
+ next true if payload.empty?
62
+
63
+ has_changes = false
64
+ I18n.with_locale(payload.delete(:locale) || I18n.locale) do
65
+ payload.each do |attribute, new_value|
66
+ current_value = public_send(attribute)
67
+ if current_value != new_value
68
+ has_changes = true
69
+ break
70
+ end
71
+ end
72
+ end
73
+
74
+ has_changes
75
+ end
76
+ end
77
+ end
78
+ end
79
+ end
80
+ end
81
+ end
82
+ end
83
+ end
84
+ end