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.
- checksums.yaml +4 -4
- data/README.md +1 -1
- data/lib/treaty/action/base.rb +11 -0
- data/lib/treaty/action/context/callable.rb +90 -0
- data/lib/treaty/action/context/dsl.rb +56 -0
- data/lib/treaty/action/context/workspace.rb +92 -0
- data/lib/treaty/action/executor/inventory.rb +136 -0
- data/lib/treaty/{info/rest → action/info}/builder.rb +2 -2
- data/lib/treaty/{info/rest → action/info}/dsl.rb +2 -2
- data/lib/treaty/{info/rest → action/info}/result.rb +2 -2
- data/lib/treaty/action/inventory/collection.rb +77 -0
- data/lib/treaty/action/inventory/factory.rb +108 -0
- data/lib/treaty/action/inventory/inventory.rb +146 -0
- data/lib/treaty/action/request/attribute/attribute.rb +76 -0
- data/lib/treaty/action/request/attribute/builder.rb +98 -0
- data/lib/treaty/action/request/entity.rb +78 -0
- data/lib/treaty/action/request/factory.rb +116 -0
- data/lib/treaty/action/request/validator.rb +120 -0
- data/lib/treaty/action/response/attribute/attribute.rb +79 -0
- data/lib/treaty/action/response/attribute/builder.rb +96 -0
- data/lib/treaty/action/response/entity.rb +79 -0
- data/lib/treaty/action/response/factory.rb +129 -0
- data/lib/treaty/action/response/validator.rb +111 -0
- data/lib/treaty/action/result.rb +81 -0
- data/lib/treaty/action/versions/collection.rb +47 -0
- data/lib/treaty/action/versions/dsl.rb +116 -0
- data/lib/treaty/action/versions/execution/request.rb +287 -0
- data/lib/treaty/action/versions/executor.rb +61 -0
- data/lib/treaty/action/versions/factory.rb +253 -0
- data/lib/treaty/action/versions/resolver.rb +150 -0
- data/lib/treaty/action/versions/semantic.rb +64 -0
- data/lib/treaty/action/versions/workspace.rb +106 -0
- data/lib/treaty/action.rb +31 -0
- data/lib/treaty/controller/dsl.rb +1 -1
- data/lib/treaty/entity/attribute/base.rb +1 -1
- data/lib/treaty/entity/attribute/builder/base.rb +1 -1
- data/lib/treaty/entity/attribute/dsl.rb +1 -1
- data/lib/treaty/entity/base.rb +1 -1
- data/lib/treaty/entity/builder.rb +62 -5
- data/lib/treaty/version.rb +1 -1
- metadata +32 -31
- data/lib/treaty/base.rb +0 -9
- data/lib/treaty/context/callable.rb +0 -26
- data/lib/treaty/context/dsl.rb +0 -12
- data/lib/treaty/context/workspace.rb +0 -32
- data/lib/treaty/executor/inventory.rb +0 -122
- data/lib/treaty/inventory/collection.rb +0 -71
- data/lib/treaty/inventory/factory.rb +0 -91
- data/lib/treaty/inventory/inventory.rb +0 -92
- data/lib/treaty/request/attribute/attribute.rb +0 -25
- data/lib/treaty/request/attribute/builder.rb +0 -46
- data/lib/treaty/request/entity.rb +0 -33
- data/lib/treaty/request/factory.rb +0 -81
- data/lib/treaty/request/validator.rb +0 -60
- data/lib/treaty/response/attribute/attribute.rb +0 -25
- data/lib/treaty/response/attribute/builder.rb +0 -46
- data/lib/treaty/response/entity.rb +0 -33
- data/lib/treaty/response/factory.rb +0 -87
- data/lib/treaty/response/validator.rb +0 -53
- data/lib/treaty/result.rb +0 -23
- data/lib/treaty/versions/collection.rb +0 -15
- data/lib/treaty/versions/dsl.rb +0 -42
- data/lib/treaty/versions/execution/request.rb +0 -177
- data/lib/treaty/versions/executor.rb +0 -14
- data/lib/treaty/versions/factory.rb +0 -112
- data/lib/treaty/versions/resolver.rb +0 -70
- data/lib/treaty/versions/semantic.rb +0 -22
- data/lib/treaty/versions/workspace.rb +0 -43
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Treaty
|
|
4
|
+
module Action
|
|
5
|
+
module Inventory
|
|
6
|
+
# Individual inventory item with lazy evaluation.
|
|
7
|
+
#
|
|
8
|
+
# ## Purpose
|
|
9
|
+
#
|
|
10
|
+
# Represents a single piece of data that can be passed from controller
|
|
11
|
+
# to service. Stores the name and source, and can be evaluated against
|
|
12
|
+
# a controller context when needed.
|
|
13
|
+
#
|
|
14
|
+
# ## Usage
|
|
15
|
+
#
|
|
16
|
+
# Created by:
|
|
17
|
+
# - Inventory::Factory (when `provide` is called)
|
|
18
|
+
#
|
|
19
|
+
# Consumed by:
|
|
20
|
+
# - Inventory::Collection (for bulk evaluation)
|
|
21
|
+
# - Executor::Inventory (for lazy single-item evaluation)
|
|
22
|
+
#
|
|
23
|
+
# ## Source Types
|
|
24
|
+
#
|
|
25
|
+
# | Type | Example | Evaluation |
|
|
26
|
+
# |------|---------|------------|
|
|
27
|
+
# | Symbol | `:current_user` | Calls `context.send(:current_user)` |
|
|
28
|
+
# | Proc | `-> { Time.current }` | Calls `context.instance_exec(&proc)` |
|
|
29
|
+
# | Other | `10`, `"string"` | Returns value as-is |
|
|
30
|
+
#
|
|
31
|
+
# ## Lazy Evaluation
|
|
32
|
+
#
|
|
33
|
+
# The source is NOT evaluated at creation time. Evaluation happens
|
|
34
|
+
# only when `evaluate(context)` is called, typically during treaty
|
|
35
|
+
# execution when the service needs the value.
|
|
36
|
+
#
|
|
37
|
+
# ## Example
|
|
38
|
+
#
|
|
39
|
+
# # Symbol source (method call)
|
|
40
|
+
# item = Inventory.new(name: :current_user, source: :current_user)
|
|
41
|
+
# item.evaluate(controller) # => calls controller.current_user
|
|
42
|
+
#
|
|
43
|
+
# # Proc source (block execution)
|
|
44
|
+
# item = Inventory.new(name: :meta, source: -> { { time: Time.current } })
|
|
45
|
+
# item.evaluate(controller) # => executes block in controller context
|
|
46
|
+
#
|
|
47
|
+
# # Direct value
|
|
48
|
+
# item = Inventory.new(name: :limit, source: 10)
|
|
49
|
+
# item.evaluate(controller) # => 10
|
|
50
|
+
class Inventory
|
|
51
|
+
# @return [Symbol] Inventory item name
|
|
52
|
+
attr_reader :name
|
|
53
|
+
|
|
54
|
+
# @return [Symbol, Proc, Object] Source for evaluation
|
|
55
|
+
attr_reader :source
|
|
56
|
+
|
|
57
|
+
# Creates a new inventory item
|
|
58
|
+
#
|
|
59
|
+
# @param name [Symbol] Item name (must be non-empty Symbol)
|
|
60
|
+
# @param source [Symbol, Proc, Object] Evaluation source
|
|
61
|
+
# @raise [Treaty::Exceptions::Inventory] If name is invalid
|
|
62
|
+
# @raise [Treaty::Exceptions::Inventory] If source is nil
|
|
63
|
+
def initialize(name:, source:)
|
|
64
|
+
validate_name!(name)
|
|
65
|
+
validate_source!(source)
|
|
66
|
+
|
|
67
|
+
@name = name
|
|
68
|
+
@source = source
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# Evaluates source against controller context
|
|
72
|
+
#
|
|
73
|
+
# Behavior depends on source type:
|
|
74
|
+
# - Symbol: calls method on context
|
|
75
|
+
# - Proc: executes in context scope
|
|
76
|
+
# - Other: returns value directly
|
|
77
|
+
#
|
|
78
|
+
# @param context [Object] Controller instance
|
|
79
|
+
# @return [Object] Evaluated value
|
|
80
|
+
# @raise [Treaty::Exceptions::Inventory] If evaluation fails
|
|
81
|
+
def evaluate(context) # rubocop:disable Metrics/MethodLength
|
|
82
|
+
case source
|
|
83
|
+
when Symbol
|
|
84
|
+
evaluate_symbol(context)
|
|
85
|
+
when Proc
|
|
86
|
+
evaluate_proc(context)
|
|
87
|
+
else
|
|
88
|
+
source
|
|
89
|
+
end
|
|
90
|
+
rescue StandardError => e
|
|
91
|
+
raise Treaty::Exceptions::Inventory,
|
|
92
|
+
I18n.t(
|
|
93
|
+
"treaty.inventory.evaluation_error",
|
|
94
|
+
name: @name,
|
|
95
|
+
error: e.message
|
|
96
|
+
)
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
private
|
|
100
|
+
|
|
101
|
+
# Evaluates Symbol source by calling method on context
|
|
102
|
+
#
|
|
103
|
+
# @param context [Object] Controller instance
|
|
104
|
+
# @return [Object] Method return value
|
|
105
|
+
def evaluate_symbol(context)
|
|
106
|
+
context.send(source)
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# Evaluates Proc source in context scope
|
|
110
|
+
#
|
|
111
|
+
# Uses instance_exec so proc has access to controller
|
|
112
|
+
# instance variables and private methods.
|
|
113
|
+
#
|
|
114
|
+
# @param context [Object] Controller instance
|
|
115
|
+
# @return [Object] Proc return value
|
|
116
|
+
def evaluate_proc(context)
|
|
117
|
+
context.instance_exec(&source)
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
# Validates that name is a non-empty Symbol
|
|
121
|
+
#
|
|
122
|
+
# @param name [Object] Name to validate
|
|
123
|
+
# @raise [Treaty::Exceptions::Inventory] If invalid
|
|
124
|
+
# @return [void]
|
|
125
|
+
def validate_name!(name)
|
|
126
|
+
return if name.is_a?(Symbol) && !name.to_s.empty?
|
|
127
|
+
|
|
128
|
+
raise Treaty::Exceptions::Inventory,
|
|
129
|
+
I18n.t("treaty.inventory.invalid_name", name: name.inspect)
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
# Validates that source is not nil
|
|
133
|
+
#
|
|
134
|
+
# @param source [Object] Source to validate
|
|
135
|
+
# @raise [Treaty::Exceptions::Inventory] If nil
|
|
136
|
+
# @return [void]
|
|
137
|
+
def validate_source!(source)
|
|
138
|
+
return unless source.nil?
|
|
139
|
+
|
|
140
|
+
raise Treaty::Exceptions::Inventory,
|
|
141
|
+
I18n.t("treaty.inventory.source_required")
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
end
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Treaty
|
|
4
|
+
module Action
|
|
5
|
+
module Request
|
|
6
|
+
module Attribute
|
|
7
|
+
# Request-specific attribute definition.
|
|
8
|
+
#
|
|
9
|
+
# ## Purpose
|
|
10
|
+
#
|
|
11
|
+
# Extends Entity::Attribute::Base with Request-specific behavior.
|
|
12
|
+
# Key difference: attributes are **required by default**.
|
|
13
|
+
#
|
|
14
|
+
# ## Default Behavior
|
|
15
|
+
#
|
|
16
|
+
# Unlike Response attributes (optional by default), Request attributes
|
|
17
|
+
# default to `required: true`. This enforces strict input validation.
|
|
18
|
+
#
|
|
19
|
+
# ## Usage
|
|
20
|
+
#
|
|
21
|
+
# Created internally by:
|
|
22
|
+
# - Request::Attribute::Builder (when defining nested attributes)
|
|
23
|
+
# - Request::Entity (when defining top-level attributes)
|
|
24
|
+
#
|
|
25
|
+
# ## Nesting
|
|
26
|
+
#
|
|
27
|
+
# Object and array types create nested builders:
|
|
28
|
+
#
|
|
29
|
+
# request do
|
|
30
|
+
# object :post do # Creates Attribute with nested builder
|
|
31
|
+
# string :title # Nested attribute (required by default)
|
|
32
|
+
# array :tags do # Nested array
|
|
33
|
+
# string :_self # Array element definition
|
|
34
|
+
# end
|
|
35
|
+
# end
|
|
36
|
+
# end
|
|
37
|
+
#
|
|
38
|
+
# ## Example
|
|
39
|
+
#
|
|
40
|
+
# # These are equivalent:
|
|
41
|
+
# string :title # required by default
|
|
42
|
+
# string :title, :required # explicit required
|
|
43
|
+
#
|
|
44
|
+
# # Must explicitly mark optional:
|
|
45
|
+
# string :bio, :optional
|
|
46
|
+
class Attribute < Treaty::Entity::Attribute::Base
|
|
47
|
+
private
|
|
48
|
+
|
|
49
|
+
# Sets default required behavior for request attributes
|
|
50
|
+
#
|
|
51
|
+
# Request attributes are required by default (is: true).
|
|
52
|
+
# This can be overridden with `:optional` helper or `required: false`.
|
|
53
|
+
#
|
|
54
|
+
# @return [void]
|
|
55
|
+
def apply_defaults!
|
|
56
|
+
@options[:required] ||= { is: true, message: nil }
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Creates nested builder for object/array type processing
|
|
60
|
+
#
|
|
61
|
+
# When a block is given to object or array attributes,
|
|
62
|
+
# creates a Builder to process the nested attribute definitions.
|
|
63
|
+
#
|
|
64
|
+
# @param block [Proc] Block containing nested attribute definitions
|
|
65
|
+
# @return [void]
|
|
66
|
+
def process_nested_attributes(&block)
|
|
67
|
+
return unless object_or_array?
|
|
68
|
+
|
|
69
|
+
builder = Builder.new(collection_of_attributes, @nesting_level + 1)
|
|
70
|
+
builder.instance_eval(&block)
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Treaty
|
|
4
|
+
module Action
|
|
5
|
+
module Request
|
|
6
|
+
module Attribute
|
|
7
|
+
# DSL builder for defining request attributes.
|
|
8
|
+
#
|
|
9
|
+
# ## Purpose
|
|
10
|
+
#
|
|
11
|
+
# Provides Request-specific implementation of the attribute builder.
|
|
12
|
+
# Creates Request::Attribute::Attribute instances instead of generic ones.
|
|
13
|
+
#
|
|
14
|
+
# ## Inheritance
|
|
15
|
+
#
|
|
16
|
+
# Extends Treaty::Entity::Attribute::Builder::Base which provides:
|
|
17
|
+
# - DSL interface (string, integer, object, array, etc.)
|
|
18
|
+
# - method_missing magic for type-based method calls
|
|
19
|
+
# - Helper support (:required, :optional)
|
|
20
|
+
# - Entity reuse via use_entity
|
|
21
|
+
#
|
|
22
|
+
# ## Usage
|
|
23
|
+
#
|
|
24
|
+
# Used internally by:
|
|
25
|
+
# - Request::Attribute::Attribute (when processing nested object/array blocks)
|
|
26
|
+
#
|
|
27
|
+
# ## DSL Example
|
|
28
|
+
#
|
|
29
|
+
# request do
|
|
30
|
+
# object :post do
|
|
31
|
+
# string :title, :required
|
|
32
|
+
# string :content
|
|
33
|
+
# array :tags do
|
|
34
|
+
# string :_self
|
|
35
|
+
# end
|
|
36
|
+
# end
|
|
37
|
+
# end
|
|
38
|
+
#
|
|
39
|
+
# ## Methods
|
|
40
|
+
#
|
|
41
|
+
# Implements abstract methods from base class:
|
|
42
|
+
# - `create_attribute` - Creates Request::Attribute::Attribute
|
|
43
|
+
# - `deep_copy_attribute` - Deep copies attribute for use_entity support
|
|
44
|
+
class Builder < Treaty::Entity::Attribute::Builder::Base
|
|
45
|
+
private
|
|
46
|
+
|
|
47
|
+
# Creates a new request attribute instance
|
|
48
|
+
#
|
|
49
|
+
# Called by base class when defining attributes via DSL.
|
|
50
|
+
#
|
|
51
|
+
# @param name [Symbol] Attribute name
|
|
52
|
+
# @param type [Symbol] Attribute type (:string, :integer, :object, etc.)
|
|
53
|
+
# @param helpers [Array<Symbol>] Helper symbols (:required, :optional)
|
|
54
|
+
# @param nesting_level [Integer] Current nesting depth
|
|
55
|
+
# @param options [Hash] Attribute options (default:, format:, etc.)
|
|
56
|
+
# @param block [Proc] Block for nested attributes (object/array)
|
|
57
|
+
# @return [Treaty::Action::Request::Attribute::Attribute] Created attribute instance
|
|
58
|
+
def create_attribute(name, type, *helpers, nesting_level:, **options, &block)
|
|
59
|
+
Attribute.new(
|
|
60
|
+
name,
|
|
61
|
+
type,
|
|
62
|
+
*helpers,
|
|
63
|
+
nesting_level:,
|
|
64
|
+
**options,
|
|
65
|
+
&block
|
|
66
|
+
)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# Deep copies an attribute with adjusted nesting level
|
|
70
|
+
#
|
|
71
|
+
# Used when copying attributes from Entity classes via use_entity.
|
|
72
|
+
# Recursively copies nested attributes for object/array types.
|
|
73
|
+
#
|
|
74
|
+
# @param source_attribute [Treaty::Entity::Attribute::Base] Source attribute to copy
|
|
75
|
+
# @param new_nesting_level [Integer] Nesting level for copied attribute
|
|
76
|
+
# @return [Treaty::Action::Request::Attribute::Attribute] Deep copied attribute
|
|
77
|
+
def deep_copy_attribute(source_attribute, new_nesting_level) # rubocop:disable Metrics/MethodLength
|
|
78
|
+
copied = Attribute.new(
|
|
79
|
+
source_attribute.name,
|
|
80
|
+
source_attribute.type,
|
|
81
|
+
nesting_level: new_nesting_level,
|
|
82
|
+
**deep_copy_options(source_attribute.options)
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
return copied unless source_attribute.nested?
|
|
86
|
+
|
|
87
|
+
source_attribute.collection_of_attributes.each do |nested_source|
|
|
88
|
+
nested_copied = deep_copy_attribute(nested_source, new_nesting_level + 1)
|
|
89
|
+
copied.collection_of_attributes << nested_copied
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
copied
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Treaty
|
|
4
|
+
module Action
|
|
5
|
+
module Request
|
|
6
|
+
# Internal entity class for request attribute definitions.
|
|
7
|
+
#
|
|
8
|
+
# ## Purpose
|
|
9
|
+
#
|
|
10
|
+
# Provides DSL interface for defining request attributes when using
|
|
11
|
+
# inline block syntax in treaty definitions. Serves as the anonymous
|
|
12
|
+
# class base when `request do ... end` is used.
|
|
13
|
+
#
|
|
14
|
+
# ## Usage
|
|
15
|
+
#
|
|
16
|
+
# Created internally by:
|
|
17
|
+
# - Request::Factory (when using inline DSL blocks)
|
|
18
|
+
#
|
|
19
|
+
# ## DSL Interface
|
|
20
|
+
#
|
|
21
|
+
# Includes Treaty::Entity::Attribute::DSL which provides:
|
|
22
|
+
# - Type methods: string, integer, boolean, date, time, datetime
|
|
23
|
+
# - Structure methods: object, array
|
|
24
|
+
# - Helper support: :required, :optional
|
|
25
|
+
#
|
|
26
|
+
# ## Difference from Treaty::Entity::Base
|
|
27
|
+
#
|
|
28
|
+
# While Treaty::Entity::Base creates standalone entity classes,
|
|
29
|
+
# Request::Entity creates Request-specific attributes with:
|
|
30
|
+
# - Required by default behavior
|
|
31
|
+
# - Request::Attribute::Attribute instances
|
|
32
|
+
#
|
|
33
|
+
# ## Example
|
|
34
|
+
#
|
|
35
|
+
# # When you write:
|
|
36
|
+
# version 1 do
|
|
37
|
+
# request do
|
|
38
|
+
# object :post do
|
|
39
|
+
# string :title, :required
|
|
40
|
+
# end
|
|
41
|
+
# end
|
|
42
|
+
# end
|
|
43
|
+
#
|
|
44
|
+
# # Factory creates: Class.new(Request::Entity)
|
|
45
|
+
# # and calls string, object etc. on it
|
|
46
|
+
class Entity
|
|
47
|
+
include Treaty::Entity::Attribute::DSL
|
|
48
|
+
|
|
49
|
+
class << self
|
|
50
|
+
private
|
|
51
|
+
|
|
52
|
+
# Creates request-specific attribute instances
|
|
53
|
+
#
|
|
54
|
+
# Called by DSL methods (string, integer, etc.) to create
|
|
55
|
+
# Request::Attribute::Attribute instead of generic attributes.
|
|
56
|
+
#
|
|
57
|
+
# @param name [Symbol] Attribute name
|
|
58
|
+
# @param type [Symbol] Attribute type
|
|
59
|
+
# @param helpers [Array<Symbol>] Helper symbols (:required, :optional)
|
|
60
|
+
# @param nesting_level [Integer] Current nesting depth
|
|
61
|
+
# @param options [Hash] Attribute options
|
|
62
|
+
# @param block [Proc] Block for nested attributes
|
|
63
|
+
# @return [Treaty::Action::Request::Attribute::Attribute] Created attribute
|
|
64
|
+
def create_attribute(name, type, *helpers, nesting_level:, **options, &block)
|
|
65
|
+
Attribute::Attribute.new(
|
|
66
|
+
name,
|
|
67
|
+
type,
|
|
68
|
+
*helpers,
|
|
69
|
+
nesting_level:,
|
|
70
|
+
**options,
|
|
71
|
+
&block
|
|
72
|
+
)
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Treaty
|
|
4
|
+
module Action
|
|
5
|
+
module Request
|
|
6
|
+
# Factory for creating request attribute collections.
|
|
7
|
+
#
|
|
8
|
+
# ## Purpose
|
|
9
|
+
#
|
|
10
|
+
# Captures request attribute definitions from treaty DSL and provides
|
|
11
|
+
# access to the resulting attribute collection. Supports both inline
|
|
12
|
+
# block syntax and Entity class references.
|
|
13
|
+
#
|
|
14
|
+
# ## Usage
|
|
15
|
+
#
|
|
16
|
+
# Created internally by:
|
|
17
|
+
# - Versions::Factory (when `request do ... end` is called)
|
|
18
|
+
#
|
|
19
|
+
# Consumed by:
|
|
20
|
+
# - Request::Validator (to validate incoming params)
|
|
21
|
+
# - Info::Builder (to build request schema information)
|
|
22
|
+
#
|
|
23
|
+
# ## Definition Modes
|
|
24
|
+
#
|
|
25
|
+
# ### Inline Block Mode
|
|
26
|
+
#
|
|
27
|
+
# request do
|
|
28
|
+
# object :post do
|
|
29
|
+
# string :title, :required
|
|
30
|
+
# end
|
|
31
|
+
# end
|
|
32
|
+
#
|
|
33
|
+
# ### Entity Class Mode
|
|
34
|
+
#
|
|
35
|
+
# request Posts::Create::RequestEntity
|
|
36
|
+
#
|
|
37
|
+
# ## Implementation
|
|
38
|
+
#
|
|
39
|
+
# Uses method_missing to forward DSL calls to a dynamically created
|
|
40
|
+
# Entity class. The Entity class collects all attribute definitions.
|
|
41
|
+
#
|
|
42
|
+
# ## Example
|
|
43
|
+
#
|
|
44
|
+
# factory = Request::Factory.new
|
|
45
|
+
# factory.object :post do
|
|
46
|
+
# factory.string :title, :required
|
|
47
|
+
# end
|
|
48
|
+
# factory.collection_of_attributes # => Collection with post attribute
|
|
49
|
+
class Factory
|
|
50
|
+
# Registers an Entity class for request schema
|
|
51
|
+
#
|
|
52
|
+
# Use this to reference a pre-defined Entity class instead of
|
|
53
|
+
# inline attribute definitions.
|
|
54
|
+
#
|
|
55
|
+
# @param entity_class [Class] Must be Treaty::Entity::Base subclass
|
|
56
|
+
# @raise [Treaty::Exceptions::Validation] If entity_class is invalid
|
|
57
|
+
# @return [void]
|
|
58
|
+
def use_entity(entity_class)
|
|
59
|
+
validate_entity_class!(entity_class)
|
|
60
|
+
@entity_class = entity_class
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Returns the collection of defined attributes
|
|
64
|
+
#
|
|
65
|
+
# @return [Treaty::Entity::Attribute::Collection] Attribute collection
|
|
66
|
+
def collection_of_attributes
|
|
67
|
+
return Treaty::Entity::Attribute::Collection.new if @entity_class.nil?
|
|
68
|
+
|
|
69
|
+
@entity_class.collection_of_attributes
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Forwards DSL method calls to internal Entity class
|
|
73
|
+
#
|
|
74
|
+
# Creates an anonymous Entity class on first call, then forwards
|
|
75
|
+
# all DSL methods (string, integer, object, etc.) to it.
|
|
76
|
+
#
|
|
77
|
+
# @param type [Symbol] Attribute type (method name)
|
|
78
|
+
# @param helpers [Array] Helper symbols and arguments
|
|
79
|
+
# @param options [Hash] Attribute options
|
|
80
|
+
# @param block [Proc] Block for nested attributes
|
|
81
|
+
# @return [void]
|
|
82
|
+
def method_missing(type, *helpers, **options, &block)
|
|
83
|
+
@entity_class ||= Class.new(Entity)
|
|
84
|
+
|
|
85
|
+
@entity_class.public_send(type, *helpers, **options, &block)
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# Checks if method should be handled by method_missing
|
|
89
|
+
#
|
|
90
|
+
# @param name [Symbol] Method name
|
|
91
|
+
# @return [Boolean]
|
|
92
|
+
def respond_to_missing?(name, *)
|
|
93
|
+
super
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
private
|
|
97
|
+
|
|
98
|
+
# Validates that entity_class is a Treaty::Entity::Base subclass
|
|
99
|
+
#
|
|
100
|
+
# @param entity_class [Class] Class to validate
|
|
101
|
+
# @raise [Treaty::Exceptions::Validation] If validation fails
|
|
102
|
+
# @return [void]
|
|
103
|
+
def validate_entity_class!(entity_class)
|
|
104
|
+
return if entity_class.is_a?(Class) && entity_class < Treaty::Entity::Base
|
|
105
|
+
|
|
106
|
+
raise Treaty::Exceptions::Validation,
|
|
107
|
+
I18n.t(
|
|
108
|
+
"treaty.request.factory.invalid_entity_class",
|
|
109
|
+
type: entity_class.class,
|
|
110
|
+
value: entity_class
|
|
111
|
+
)
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
end
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Treaty
|
|
4
|
+
module Action
|
|
5
|
+
module Request
|
|
6
|
+
# Validates incoming request parameters against schema.
|
|
7
|
+
#
|
|
8
|
+
# ## Purpose
|
|
9
|
+
#
|
|
10
|
+
# Validates incoming request params against the request schema defined
|
|
11
|
+
# in the treaty version. Runs all validators (required, type, inclusion,
|
|
12
|
+
# format) and transformations (default, cast, transform, as).
|
|
13
|
+
#
|
|
14
|
+
# ## Usage
|
|
15
|
+
#
|
|
16
|
+
# Called internally by:
|
|
17
|
+
# - Versions::Execution::Base (before delegating to service)
|
|
18
|
+
#
|
|
19
|
+
# ## Validation Flow
|
|
20
|
+
#
|
|
21
|
+
# 1. Convert ActionController::Parameters to hash if needed
|
|
22
|
+
# 2. Check if request schema is defined
|
|
23
|
+
# 3. Create dynamic Orchestrator with version's attributes
|
|
24
|
+
# 4. Run validation pipeline (validate + transform)
|
|
25
|
+
# 5. Return validated/transformed params or raise error
|
|
26
|
+
#
|
|
27
|
+
# ## Error Handling
|
|
28
|
+
#
|
|
29
|
+
# Raises Treaty::Exceptions::Validation with detailed messages
|
|
30
|
+
# including attribute path and specific validation failures.
|
|
31
|
+
#
|
|
32
|
+
# ## Example
|
|
33
|
+
#
|
|
34
|
+
# # Typically called via class method:
|
|
35
|
+
# validated = Request::Validator.validate!(
|
|
36
|
+
# params: controller.params,
|
|
37
|
+
# version_factory: version_factory
|
|
38
|
+
# )
|
|
39
|
+
#
|
|
40
|
+
# # validated contains transformed params ready for service
|
|
41
|
+
class Validator
|
|
42
|
+
class << self
|
|
43
|
+
# Validates request parameters
|
|
44
|
+
#
|
|
45
|
+
# @param params [Hash, ActionController::Parameters] Request params
|
|
46
|
+
# @param version_factory [Treaty::Action::Versions::Factory] Version with request schema
|
|
47
|
+
# @return [Hash] Validated and transformed parameters
|
|
48
|
+
# @raise [Treaty::Exceptions::Validation] If validation fails
|
|
49
|
+
def validate!(params:, version_factory:)
|
|
50
|
+
new(params:, version_factory:).validate!
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Creates new validator instance
|
|
55
|
+
#
|
|
56
|
+
# @param params [Hash, ActionController::Parameters] Request params
|
|
57
|
+
# @param version_factory [Treaty::Action::Versions::Factory] Version with request schema
|
|
58
|
+
def initialize(params:, version_factory:)
|
|
59
|
+
@params = params
|
|
60
|
+
@version_factory = version_factory
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Runs validation pipeline
|
|
64
|
+
#
|
|
65
|
+
# @return [Hash] Validated and transformed parameters
|
|
66
|
+
# @raise [Treaty::Exceptions::Validation] If validation fails
|
|
67
|
+
def validate!
|
|
68
|
+
validate_request_attributes!
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
private
|
|
72
|
+
|
|
73
|
+
# Converts params to plain hash
|
|
74
|
+
#
|
|
75
|
+
# Handles both ActionController::Parameters (with to_unsafe_h)
|
|
76
|
+
# and plain Hash objects.
|
|
77
|
+
#
|
|
78
|
+
# @return [Hash] Plain hash of request data
|
|
79
|
+
def request_data
|
|
80
|
+
@request_data ||= begin
|
|
81
|
+
@params.to_unsafe_h
|
|
82
|
+
rescue NoMethodError
|
|
83
|
+
@params
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# Validates and transforms request data
|
|
88
|
+
#
|
|
89
|
+
# Creates dynamic Orchestrator class that uses version's request
|
|
90
|
+
# attributes for validation. Returns raw data if no schema defined.
|
|
91
|
+
#
|
|
92
|
+
# @return [Hash] Validated and transformed data
|
|
93
|
+
# @raise [Treaty::Exceptions::Validation] If validation fails
|
|
94
|
+
def validate_request_attributes!
|
|
95
|
+
return request_data unless request_attributes_exist?
|
|
96
|
+
|
|
97
|
+
orchestrator_class = Class.new(Treaty::Entity::Attribute::Validation::Orchestrator::Base) do
|
|
98
|
+
define_method(:collection_of_attributes) do
|
|
99
|
+
@version_factory.request_factory.collection_of_attributes
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
orchestrator_class.validate!(
|
|
104
|
+
version_factory: @version_factory,
|
|
105
|
+
data: request_data
|
|
106
|
+
)
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# Checks if request schema is defined for this version
|
|
110
|
+
#
|
|
111
|
+
# @return [Boolean] True if request attributes exist
|
|
112
|
+
def request_attributes_exist?
|
|
113
|
+
return false if @version_factory.request_factory&.collection_of_attributes&.empty?
|
|
114
|
+
|
|
115
|
+
@version_factory.request_factory.collection_of_attributes.exists?
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
end
|