treaty 0.19.0 → 0.20.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 (68) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +1 -1
  3. data/lib/treaty/action/base.rb +11 -0
  4. data/lib/treaty/action/context/callable.rb +90 -0
  5. data/lib/treaty/action/context/dsl.rb +56 -0
  6. data/lib/treaty/action/context/workspace.rb +92 -0
  7. data/lib/treaty/action/executor/inventory.rb +136 -0
  8. data/lib/treaty/{info/rest → action/info}/builder.rb +2 -2
  9. data/lib/treaty/{info/rest → action/info}/dsl.rb +2 -2
  10. data/lib/treaty/{info/rest → action/info}/result.rb +2 -2
  11. data/lib/treaty/action/inventory/collection.rb +77 -0
  12. data/lib/treaty/action/inventory/factory.rb +108 -0
  13. data/lib/treaty/action/inventory/inventory.rb +146 -0
  14. data/lib/treaty/action/request/attribute/attribute.rb +76 -0
  15. data/lib/treaty/action/request/attribute/builder.rb +98 -0
  16. data/lib/treaty/action/request/entity.rb +78 -0
  17. data/lib/treaty/action/request/factory.rb +116 -0
  18. data/lib/treaty/action/request/validator.rb +120 -0
  19. data/lib/treaty/action/response/attribute/attribute.rb +79 -0
  20. data/lib/treaty/action/response/attribute/builder.rb +96 -0
  21. data/lib/treaty/action/response/entity.rb +79 -0
  22. data/lib/treaty/action/response/factory.rb +129 -0
  23. data/lib/treaty/action/response/validator.rb +111 -0
  24. data/lib/treaty/action/result.rb +81 -0
  25. data/lib/treaty/action/versions/collection.rb +47 -0
  26. data/lib/treaty/action/versions/dsl.rb +116 -0
  27. data/lib/treaty/action/versions/execution/request.rb +287 -0
  28. data/lib/treaty/action/versions/executor.rb +61 -0
  29. data/lib/treaty/action/versions/factory.rb +253 -0
  30. data/lib/treaty/action/versions/resolver.rb +150 -0
  31. data/lib/treaty/action/versions/semantic.rb +64 -0
  32. data/lib/treaty/action/versions/workspace.rb +106 -0
  33. data/lib/treaty/action.rb +31 -0
  34. data/lib/treaty/controller/dsl.rb +1 -1
  35. data/lib/treaty/entity/attribute/base.rb +1 -1
  36. data/lib/treaty/entity/attribute/builder/base.rb +1 -1
  37. data/lib/treaty/entity/attribute/dsl.rb +1 -1
  38. data/lib/treaty/entity/base.rb +1 -1
  39. data/lib/treaty/entity/builder.rb +62 -5
  40. data/lib/treaty/version.rb +1 -1
  41. metadata +32 -31
  42. data/lib/treaty/base.rb +0 -9
  43. data/lib/treaty/context/callable.rb +0 -26
  44. data/lib/treaty/context/dsl.rb +0 -12
  45. data/lib/treaty/context/workspace.rb +0 -32
  46. data/lib/treaty/executor/inventory.rb +0 -122
  47. data/lib/treaty/inventory/collection.rb +0 -71
  48. data/lib/treaty/inventory/factory.rb +0 -91
  49. data/lib/treaty/inventory/inventory.rb +0 -92
  50. data/lib/treaty/request/attribute/attribute.rb +0 -25
  51. data/lib/treaty/request/attribute/builder.rb +0 -46
  52. data/lib/treaty/request/entity.rb +0 -33
  53. data/lib/treaty/request/factory.rb +0 -81
  54. data/lib/treaty/request/validator.rb +0 -60
  55. data/lib/treaty/response/attribute/attribute.rb +0 -25
  56. data/lib/treaty/response/attribute/builder.rb +0 -46
  57. data/lib/treaty/response/entity.rb +0 -33
  58. data/lib/treaty/response/factory.rb +0 -87
  59. data/lib/treaty/response/validator.rb +0 -53
  60. data/lib/treaty/result.rb +0 -23
  61. data/lib/treaty/versions/collection.rb +0 -15
  62. data/lib/treaty/versions/dsl.rb +0 -42
  63. data/lib/treaty/versions/execution/request.rb +0 -177
  64. data/lib/treaty/versions/executor.rb +0 -14
  65. data/lib/treaty/versions/factory.rb +0 -112
  66. data/lib/treaty/versions/resolver.rb +0 -70
  67. data/lib/treaty/versions/semantic.rb +0 -22
  68. data/lib/treaty/versions/workspace.rb +0 -43
@@ -0,0 +1,116 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Treaty
4
+ module Action
5
+ module Versions
6
+ # DSL module for defining API versions in treaty classes.
7
+ #
8
+ # ## Purpose
9
+ #
10
+ # Provides the `version` class method for defining API versions.
11
+ # Each version can have its own request/response schema, executor,
12
+ # summary, and deprecation status.
13
+ #
14
+ # ## Usage
15
+ #
16
+ # Included in:
17
+ # - Treaty::Action::Base (as core DSL functionality)
18
+ #
19
+ # ## DSL Methods
20
+ #
21
+ # When included, provides:
22
+ # - `version` - Define a new API version with configuration block
23
+ # - `collection_of_versions` - Access all defined versions
24
+ #
25
+ # ## Version Definition
26
+ #
27
+ # class Posts::CreateTreaty < ApplicationTreaty
28
+ # version 1, default: true do
29
+ # summary "Initial version"
30
+ #
31
+ # request do
32
+ # object :post do
33
+ # string :title, :required
34
+ # end
35
+ # end
36
+ #
37
+ # response 201 do
38
+ # object :post do
39
+ # string :id
40
+ # end
41
+ # end
42
+ #
43
+ # delegate_to Posts::CreateService
44
+ # end
45
+ #
46
+ # version 2 do
47
+ # summary "Added tags"
48
+ # deprecated { ENV["V2_DEPRECATED"] == "true" }
49
+ # # ...
50
+ # end
51
+ # end
52
+ #
53
+ # ## Validation
54
+ #
55
+ # Validates that only one version is marked as default.
56
+ # Raises `VersionMultipleDefaults` if multiple defaults detected.
57
+ module DSL
58
+ # Hook called when module is included
59
+ #
60
+ # @param base [Class] The class including this module
61
+ def self.included(base)
62
+ base.extend(ClassMethods)
63
+ base.include(Workspace)
64
+ end
65
+
66
+ # Class methods added to including class
67
+ module ClassMethods
68
+ private
69
+
70
+ # Defines a new API version
71
+ #
72
+ # Creates a version factory, evaluates the configuration block,
73
+ # validates the configuration, and adds to collection.
74
+ #
75
+ # @param version [Integer, String, Array] Version identifier
76
+ # @param default [Boolean] Whether this is the default version
77
+ # @param block [Proc] Configuration block (request, response, delegate_to, etc.)
78
+ # @raise [Treaty::Exceptions::VersionMultipleDefaults] If multiple defaults
79
+ # @return [void]
80
+ def version(version, default: false, &block)
81
+ @version_factory = Factory.new(version:, default:)
82
+
83
+ @version_factory.instance_eval(&block)
84
+ @version_factory.validate_after_block!
85
+
86
+ validate_multiple_defaults! if @version_factory.default_result == true
87
+
88
+ collection_of_versions << @version_factory
89
+
90
+ @version_factory = nil
91
+ end
92
+
93
+ # Returns collection of all defined versions
94
+ #
95
+ # @return [Treaty::Action::Versions::Collection] Collection of version factories
96
+ def collection_of_versions
97
+ @collection_of_versions ||= Collection.new
98
+ end
99
+
100
+ # Validates that only one version is marked as default
101
+ #
102
+ # @raise [Treaty::Exceptions::VersionMultipleDefaults] If multiple defaults
103
+ # @return [void]
104
+ def validate_multiple_defaults!
105
+ existing_defaults = collection_of_versions.map(&:default_result).count(true)
106
+
107
+ return if existing_defaults.zero?
108
+
109
+ raise Treaty::Exceptions::VersionMultipleDefaults,
110
+ I18n.t("treaty.versioning.factory.multiple_defaults")
111
+ end
112
+ end
113
+ end
114
+ end
115
+ end
116
+ end
@@ -0,0 +1,287 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Treaty
4
+ module Action
5
+ module Versions
6
+ module Execution
7
+ # Executes the configured service/proc with validated parameters.
8
+ #
9
+ # ## Purpose
10
+ #
11
+ # Handles the actual execution of the delegated service, supporting
12
+ # multiple executor types: Class, String path, and Proc/Lambda.
13
+ # Provides special handling for Servactory services.
14
+ #
15
+ # ## Usage
16
+ #
17
+ # Called by:
18
+ # - Versions::Workspace (after request validation)
19
+ #
20
+ # ## Executor Types
21
+ #
22
+ # | Type | Example | Resolution |
23
+ # |------|---------|------------|
24
+ # | Class | `Posts::CreateService` | Direct call |
25
+ # | String | `"posts/create_service"` | Constantized then called |
26
+ # | Proc | `->(params:) { ... }` | Direct call |
27
+ #
28
+ # ## Servactory Integration
29
+ #
30
+ # Detects Servactory services via `servactory?` class method.
31
+ # Catches Servactory-specific exceptions and wraps them in
32
+ # `Treaty::Exceptions::Execution`.
33
+ #
34
+ # ## Execution Flow
35
+ #
36
+ # 1. Resolve executor (constantize string if needed)
37
+ # 2. Build call parameters (params + optional inventory)
38
+ # 3. Execute via appropriate method:
39
+ # - Proc: `executor.call(**params)`
40
+ # - Servactory: `executor.call!(**params)`
41
+ # - Regular: `executor.public_send(method, **params)`
42
+ # 4. Extract data from result (handles `.data` accessor)
43
+ #
44
+ # ## Example
45
+ #
46
+ # result = Execution::Request.execute!(
47
+ # version_factory: factory,
48
+ # validated_params: { post: { title: "Hello" } },
49
+ # inventory: inventory_collection,
50
+ # context: controller
51
+ # )
52
+ class Request # rubocop:disable Metrics/ClassLength
53
+ # Executes service with validated parameters (class method shortcut)
54
+ #
55
+ # @param version_factory [Treaty::Action::Versions::Factory] Version configuration
56
+ # @param validated_params [Hash] Validated request parameters
57
+ # @param inventory [Treaty::Action::Inventory::Collection, nil] Optional inventory
58
+ # @param context [Object, nil] Controller context for inventory
59
+ # @return [Hash] Service execution result
60
+ # @raise [Treaty::Exceptions::Execution] If execution fails
61
+ def self.execute!(...)
62
+ new(...).execute!
63
+ end
64
+
65
+ # Creates a new execution request instance
66
+ #
67
+ # @param version_factory [Treaty::Action::Versions::Factory] Version with executor configuration
68
+ # @param validated_params [Hash] Validated request parameters
69
+ # @param inventory [Treaty::Action::Inventory::Collection, nil] Optional inventory
70
+ # @param context [Object, nil] Controller context for inventory evaluation
71
+ def initialize(version_factory:, validated_params:, inventory: nil, context: nil)
72
+ @inventory = inventory
73
+ @context = context
74
+ @version_factory = version_factory
75
+ @validated_params = validated_params
76
+ end
77
+
78
+ # Executes the service and returns result
79
+ #
80
+ # @return [Hash] Execution result data
81
+ # @raise [Treaty::Exceptions::Execution] If executor missing or fails
82
+ def execute!
83
+ raise_executor_missing_error! if @version_factory.executor.nil?
84
+
85
+ extract_data_from_result
86
+ end
87
+
88
+ private
89
+
90
+ # Extracts data from execution result
91
+ #
92
+ # Handles different result types:
93
+ # - Proc results returned directly
94
+ # - Objects with `.data` accessor unwrapped
95
+ # - Other results returned directly
96
+ #
97
+ # @return [Object] Extracted result data
98
+ def extract_data_from_result
99
+ return execution_result if executor.is_a?(Proc)
100
+ return execution_result.data if execution_result.respond_to?(:data)
101
+
102
+ execution_result
103
+ end
104
+
105
+ ########################################################################
106
+
107
+ # Executes and caches the service result
108
+ #
109
+ # @return [Object] Raw execution result
110
+ def execution_result
111
+ @execution_result ||=
112
+ if executor.is_a?(Proc)
113
+ execute_proc
114
+ elsif servactory_service?
115
+ execute_servactory
116
+ else
117
+ execute_regular_class
118
+ end
119
+ end
120
+
121
+ ########################################################################
122
+
123
+ # Returns resolved executor (cached)
124
+ #
125
+ # @return [Class, Proc] Resolved executor
126
+ def executor
127
+ @executor ||= resolve_executor(@version_factory.executor.executor)
128
+ end
129
+
130
+ ########################################################################
131
+
132
+ # Resolves executor from various input types
133
+ #
134
+ # @param executor [Class, String, Symbol, Proc] Executor reference
135
+ # @return [Class, Proc] Resolved executor
136
+ # @raise [Treaty::Exceptions::Execution] If resolution fails
137
+ def resolve_executor(executor) # rubocop:disable Metrics/MethodLength
138
+ return executor if executor.is_a?(Proc) || executor.is_a?(Class)
139
+
140
+ if executor.is_a?(String) || executor.is_a?(Symbol)
141
+ string_executor = executor.to_s
142
+
143
+ if string_executor.empty?
144
+ raise Treaty::Exceptions::Execution,
145
+ I18n.t("treaty.execution.executor_empty")
146
+ end
147
+
148
+ constant_name = normalize_constant_name(executor)
149
+
150
+ begin
151
+ constant_name.constantize
152
+ rescue NameError
153
+ raise Treaty::Exceptions::Execution,
154
+ I18n.t("treaty.execution.executor_not_found", class_name: constant_name)
155
+ end
156
+ else
157
+ raise Treaty::Exceptions::Execution,
158
+ I18n.t("treaty.execution.executor_invalid_type", type: executor.class)
159
+ end
160
+ end
161
+
162
+ ########################################################################
163
+
164
+ # Normalizes string/symbol to constant name
165
+ #
166
+ # Handles path-style strings like "posts/create_service"
167
+ # converting to "Posts::CreateService".
168
+ #
169
+ # @param name [String, Symbol] Name to normalize
170
+ # @return [String] Constant name
171
+ def normalize_constant_name(name)
172
+ string = name.to_s
173
+
174
+ return string if string.include?("::")
175
+ return string.split("/").map(&:camelize).join("::") if string.include?("/")
176
+
177
+ string
178
+ end
179
+
180
+ ########################################################################
181
+ ########################################################################
182
+ ########################################################################
183
+
184
+ # Creates inventory executor for lazy evaluation
185
+ #
186
+ # @return [Treaty::Action::Executor::Inventory] Inventory executor
187
+ def evaluated_inventory
188
+ @evaluated_inventory ||= Treaty::Action::Executor::Inventory.new(@inventory, @context)
189
+ end
190
+
191
+ ########################################################################
192
+
193
+ # Executes Proc executor
194
+ #
195
+ # @return [Object] Proc result
196
+ # @raise [Treaty::Exceptions::Execution] If proc raises error
197
+ def execute_proc
198
+ executor.call(**build_call_params)
199
+ rescue StandardError => e
200
+ raise Treaty::Exceptions::Execution,
201
+ I18n.t("treaty.execution.proc_error", message: e.message)
202
+ end
203
+
204
+ # Executes Servactory service
205
+ #
206
+ # Uses `call!` method and catches Servactory-specific exceptions.
207
+ #
208
+ # @return [Object] Service result
209
+ # @raise [Treaty::Exceptions::Execution] If service raises error
210
+ def execute_servactory # rubocop:disable Metrics/MethodLength
211
+ executor.call!(**build_call_params)
212
+ rescue Servactory::Exceptions::Input => e
213
+ raise Treaty::Exceptions::Execution,
214
+ I18n.t("treaty.execution.servactory_input_error", message: e.message)
215
+ rescue Servactory::Exceptions::Internal => e
216
+ raise Treaty::Exceptions::Execution,
217
+ I18n.t("treaty.execution.servactory_internal_error", message: e.message)
218
+ rescue Servactory::Exceptions::Output => e
219
+ raise Treaty::Exceptions::Execution,
220
+ I18n.t("treaty.execution.servactory_output_error", message: e.message)
221
+ rescue Servactory::Exceptions::Failure => e
222
+ raise Treaty::Exceptions::Execution,
223
+ I18n.t("treaty.execution.servactory_failure_error", message: e.message)
224
+ end
225
+
226
+ # Executes regular class with configured method
227
+ #
228
+ # @return [Object] Method result
229
+ # @raise [Treaty::Exceptions::Execution] If method missing or raises error
230
+ def execute_regular_class # rubocop:disable Metrics/MethodLength
231
+ method_name = @version_factory.executor.method
232
+
233
+ unless executor.respond_to?(method_name)
234
+ raise Treaty::Exceptions::Execution,
235
+ I18n.t(
236
+ "treaty.execution.method_not_found",
237
+ method: method_name,
238
+ class_name: executor
239
+ )
240
+ end
241
+
242
+ executor.public_send(method_name, **build_call_params)
243
+ rescue StandardError => e
244
+ raise Treaty::Exceptions::Execution,
245
+ I18n.t("treaty.execution.regular_service_error", message: e.message)
246
+ end
247
+
248
+ ########################################################################
249
+ ########################################################################
250
+ ########################################################################
251
+
252
+ # Builds parameters hash for service call
253
+ #
254
+ # Includes validated params and optionally inventory if defined.
255
+ #
256
+ # @return [Hash] Call parameters
257
+ def build_call_params
258
+ if @inventory&.exists?
259
+ { params: @validated_params, inventory: evaluated_inventory }
260
+ else
261
+ { params: @validated_params }
262
+ end
263
+ end
264
+
265
+ # Raises error when executor not configured
266
+ #
267
+ # @raise [Treaty::Exceptions::Execution]
268
+ def raise_executor_missing_error!
269
+ raise Treaty::Exceptions::Execution,
270
+ I18n.t(
271
+ "treaty.execution.executor_missing",
272
+ version: @version_factory.version
273
+ )
274
+ end
275
+
276
+ # Checks if executor is a Servactory service
277
+ #
278
+ # @return [Boolean] True if executor responds to servactory?
279
+ def servactory_service?
280
+ executor.respond_to?(:servactory?) &&
281
+ executor.servactory?
282
+ end
283
+ end
284
+ end
285
+ end
286
+ end
287
+ end
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Treaty
4
+ module Action
5
+ module Versions
6
+ # Value object holding executor reference and method name.
7
+ #
8
+ # ## Purpose
9
+ #
10
+ # Stores the service class/proc and method name configured via
11
+ # `delegate_to` in version definitions. Separates executor
12
+ # configuration from execution logic.
13
+ #
14
+ # ## Usage
15
+ #
16
+ # Created by:
17
+ # - Versions::Factory (when `delegate_to` is called)
18
+ #
19
+ # Consumed by:
20
+ # - Versions::Execution::Request (to execute the service)
21
+ #
22
+ # ## Executor Types
23
+ #
24
+ # The `executor` attribute can hold:
25
+ # - Class reference: `Posts::CreateService`
26
+ # - String path: `"posts/create_service"`
27
+ # - Proc/Lambda: `->(params:) { ... }`
28
+ #
29
+ # ## Method Attribute
30
+ #
31
+ # The `method` attribute specifies which method to call:
32
+ # - Default: `:call`
33
+ # - Custom: specified via hash syntax `delegate_to Service => :perform`
34
+ #
35
+ # ## Example
36
+ #
37
+ # # In version definition:
38
+ # delegate_to Posts::CreateService # method defaults to :call
39
+ # delegate_to Posts::CreateService => :call! # explicit method
40
+ #
41
+ # # Creates:
42
+ # Executor.new(Posts::CreateService, :call)
43
+ class Executor
44
+ # @return [Class, String, Proc] Service class, path string, or proc
45
+ attr_reader :executor
46
+
47
+ # @return [Symbol] Method name to call on executor
48
+ attr_reader :method
49
+
50
+ # Creates a new executor value object
51
+ #
52
+ # @param executor [Class, String, Proc] Service reference
53
+ # @param method [Symbol] Method to invoke (default: :call)
54
+ def initialize(executor, method)
55
+ @executor = executor
56
+ @method = method
57
+ end
58
+ end
59
+ end
60
+ end
61
+ end