treaty 0.18.0 → 0.19.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/config/locales/en.yml +3 -3
- data/lib/treaty/engine.rb +1 -1
- data/lib/treaty/{attribute/entity → entity/attribute}/attribute.rb +4 -4
- data/lib/treaty/entity/attribute/base.rb +184 -0
- data/lib/treaty/entity/attribute/builder/base.rb +275 -0
- data/lib/treaty/entity/attribute/collection.rb +67 -0
- data/lib/treaty/entity/attribute/dsl.rb +92 -0
- data/lib/treaty/entity/attribute/helper_mapper.rb +74 -0
- data/lib/treaty/entity/attribute/option/base.rb +190 -0
- data/lib/treaty/entity/attribute/option/conditionals/base.rb +92 -0
- data/lib/treaty/entity/attribute/option/conditionals/if_conditional.rb +136 -0
- data/lib/treaty/entity/attribute/option/conditionals/unless_conditional.rb +153 -0
- data/lib/treaty/entity/attribute/option/modifiers/as_modifier.rb +93 -0
- data/lib/treaty/entity/attribute/option/modifiers/cast_modifier.rb +285 -0
- data/lib/treaty/entity/attribute/option/modifiers/computed_modifier.rb +128 -0
- data/lib/treaty/entity/attribute/option/modifiers/default_modifier.rb +105 -0
- data/lib/treaty/entity/attribute/option/modifiers/transform_modifier.rb +114 -0
- data/lib/treaty/entity/attribute/option/registry.rb +157 -0
- data/lib/treaty/entity/attribute/option/registry_initializer.rb +117 -0
- data/lib/treaty/entity/attribute/option/validators/format_validator.rb +222 -0
- data/lib/treaty/entity/attribute/option/validators/inclusion_validator.rb +94 -0
- data/lib/treaty/entity/attribute/option/validators/required_validator.rb +100 -0
- data/lib/treaty/entity/attribute/option/validators/type_validator.rb +219 -0
- data/lib/treaty/entity/attribute/option_normalizer.rb +168 -0
- data/lib/treaty/entity/attribute/option_orchestrator.rb +192 -0
- data/lib/treaty/entity/attribute/validation/attribute_validator.rb +147 -0
- data/lib/treaty/entity/attribute/validation/base.rb +76 -0
- data/lib/treaty/entity/attribute/validation/nested_array_validator.rb +207 -0
- data/lib/treaty/entity/attribute/validation/nested_object_validator.rb +105 -0
- data/lib/treaty/entity/attribute/validation/nested_transformer.rb +432 -0
- data/lib/treaty/entity/attribute/validation/orchestrator/base.rb +262 -0
- data/lib/treaty/entity/base.rb +90 -0
- data/lib/treaty/entity/builder.rb +44 -0
- data/lib/treaty/{info/entity → entity/info}/builder.rb +8 -8
- data/lib/treaty/{info/entity → entity/info}/dsl.rb +2 -2
- data/lib/treaty/{info/entity → entity/info}/result.rb +2 -2
- data/lib/treaty/entity.rb +7 -79
- data/lib/treaty/request/attribute/attribute.rb +1 -1
- data/lib/treaty/request/attribute/builder.rb +2 -2
- data/lib/treaty/request/entity.rb +1 -1
- data/lib/treaty/request/factory.rb +5 -5
- data/lib/treaty/request/validator.rb +1 -1
- data/lib/treaty/response/attribute/attribute.rb +1 -1
- data/lib/treaty/response/attribute/builder.rb +2 -2
- data/lib/treaty/response/entity.rb +1 -1
- data/lib/treaty/response/factory.rb +5 -5
- data/lib/treaty/response/validator.rb +1 -1
- data/lib/treaty/version.rb +1 -1
- metadata +35 -34
- data/lib/treaty/attribute/base.rb +0 -182
- data/lib/treaty/attribute/builder/base.rb +0 -273
- data/lib/treaty/attribute/collection.rb +0 -65
- data/lib/treaty/attribute/dsl.rb +0 -90
- data/lib/treaty/attribute/entity/builder.rb +0 -46
- data/lib/treaty/attribute/helper_mapper.rb +0 -72
- data/lib/treaty/attribute/option/base.rb +0 -188
- data/lib/treaty/attribute/option/conditionals/base.rb +0 -90
- data/lib/treaty/attribute/option/conditionals/if_conditional.rb +0 -134
- data/lib/treaty/attribute/option/conditionals/unless_conditional.rb +0 -151
- data/lib/treaty/attribute/option/modifiers/as_modifier.rb +0 -91
- data/lib/treaty/attribute/option/modifiers/cast_modifier.rb +0 -283
- data/lib/treaty/attribute/option/modifiers/computed_modifier.rb +0 -126
- data/lib/treaty/attribute/option/modifiers/default_modifier.rb +0 -103
- data/lib/treaty/attribute/option/modifiers/transform_modifier.rb +0 -112
- data/lib/treaty/attribute/option/registry.rb +0 -155
- data/lib/treaty/attribute/option/registry_initializer.rb +0 -115
- data/lib/treaty/attribute/option/validators/format_validator.rb +0 -220
- data/lib/treaty/attribute/option/validators/inclusion_validator.rb +0 -92
- data/lib/treaty/attribute/option/validators/required_validator.rb +0 -98
- data/lib/treaty/attribute/option/validators/type_validator.rb +0 -217
- data/lib/treaty/attribute/option_normalizer.rb +0 -166
- data/lib/treaty/attribute/option_orchestrator.rb +0 -190
- data/lib/treaty/attribute/validation/attribute_validator.rb +0 -145
- data/lib/treaty/attribute/validation/base.rb +0 -74
- data/lib/treaty/attribute/validation/nested_array_validator.rb +0 -205
- data/lib/treaty/attribute/validation/nested_object_validator.rb +0 -103
- data/lib/treaty/attribute/validation/nested_transformer.rb +0 -430
- data/lib/treaty/attribute/validation/orchestrator/base.rb +0 -260
|
@@ -1,72 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Treaty
|
|
4
|
-
module Attribute
|
|
5
|
-
# Maps DSL helper symbols to their simple mode option equivalents.
|
|
6
|
-
#
|
|
7
|
-
# ## Purpose
|
|
8
|
-
#
|
|
9
|
-
# Helpers provide the most concise syntax for common options.
|
|
10
|
-
# They are syntactic sugar that gets converted to simple mode options.
|
|
11
|
-
#
|
|
12
|
-
# ## Available Helpers
|
|
13
|
-
#
|
|
14
|
-
# - `:required` → `required: true`
|
|
15
|
-
# - `:optional` → `required: false`
|
|
16
|
-
#
|
|
17
|
-
# ## Usage Examples
|
|
18
|
-
#
|
|
19
|
-
# Helper mode (most concise):
|
|
20
|
-
# string :title, :required
|
|
21
|
-
# string :bio, :optional
|
|
22
|
-
#
|
|
23
|
-
# Equivalent to simple mode:
|
|
24
|
-
# string :title, required: true
|
|
25
|
-
# string :bio, required: false
|
|
26
|
-
#
|
|
27
|
-
# ## Processing Flow
|
|
28
|
-
#
|
|
29
|
-
# 1. Helper mode: `string :title, :required`
|
|
30
|
-
# 2. HelperMapper: `:required` → `required: true`
|
|
31
|
-
# 3. OptionNormalizer: `required: true` → `{ is: true, message: nil }`
|
|
32
|
-
# 4. Final: Advanced mode used internally
|
|
33
|
-
#
|
|
34
|
-
# ## Adding New Helpers
|
|
35
|
-
#
|
|
36
|
-
# To add a new helper:
|
|
37
|
-
# ```ruby
|
|
38
|
-
# HELPER_MAPPINGS = {
|
|
39
|
-
# required: { required: true },
|
|
40
|
-
# optional: { required: false },
|
|
41
|
-
# my_helper: { my_option: :smth } # New helper example
|
|
42
|
-
# }.freeze
|
|
43
|
-
# ```
|
|
44
|
-
class HelperMapper
|
|
45
|
-
HELPER_MAPPINGS = {
|
|
46
|
-
required: { required: true },
|
|
47
|
-
optional: { required: false }
|
|
48
|
-
}.freeze
|
|
49
|
-
|
|
50
|
-
class << self
|
|
51
|
-
# Maps helper symbols to their simple mode equivalents
|
|
52
|
-
#
|
|
53
|
-
# @param helpers [Array<Symbol>] Array of helper symbols
|
|
54
|
-
# @return [Hash] Simple mode options hash
|
|
55
|
-
def map(helpers)
|
|
56
|
-
helpers.each_with_object({}) do |helper, result|
|
|
57
|
-
mapping = HELPER_MAPPINGS.fetch(helper)
|
|
58
|
-
result.merge!(mapping) if mapping.present?
|
|
59
|
-
end
|
|
60
|
-
end
|
|
61
|
-
|
|
62
|
-
# Checks if a symbol is a registered helper
|
|
63
|
-
#
|
|
64
|
-
# @param symbol [Symbol] Symbol to check
|
|
65
|
-
# @return [Boolean] True if symbol is a helper
|
|
66
|
-
def helper?(symbol)
|
|
67
|
-
HELPER_MAPPINGS.key?(symbol)
|
|
68
|
-
end
|
|
69
|
-
end
|
|
70
|
-
end
|
|
71
|
-
end
|
|
72
|
-
end
|
|
@@ -1,188 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Treaty
|
|
4
|
-
module Attribute
|
|
5
|
-
module Option
|
|
6
|
-
# Base class for all option processors (validators and modifiers).
|
|
7
|
-
#
|
|
8
|
-
# ## Option Modes
|
|
9
|
-
#
|
|
10
|
-
# Treaty supports two modes for defining options:
|
|
11
|
-
#
|
|
12
|
-
# 1. **Simple Mode** - Concise syntax for common cases:
|
|
13
|
-
# - `required: true`
|
|
14
|
-
# - `as: :value`
|
|
15
|
-
# - `default: 12`
|
|
16
|
-
# - `in: %w[twitter linkedin]`
|
|
17
|
-
#
|
|
18
|
-
# 2. **Advanced Mode** - Extended syntax with custom messages:
|
|
19
|
-
# - `required: { is: true, message: "Custom error" }`
|
|
20
|
-
# - `as: { is: :value, message: nil }`
|
|
21
|
-
# - `inclusion: { in: %w[...], message: "Must be one of..." }`
|
|
22
|
-
#
|
|
23
|
-
# ## Helpers
|
|
24
|
-
#
|
|
25
|
-
# Helpers are shortcuts in DSL that map to simple mode options:
|
|
26
|
-
# - `:required` → `required: true`
|
|
27
|
-
# - `:optional` → `required: false`
|
|
28
|
-
#
|
|
29
|
-
# ## Advanced Mode Keys
|
|
30
|
-
#
|
|
31
|
-
# Each option in advanced mode has a value key:
|
|
32
|
-
# - Default key: `:is` (used by most options)
|
|
33
|
-
# - Special key: `:in` (used by inclusion validator)
|
|
34
|
-
#
|
|
35
|
-
# The value key is defined by overriding `value_key` method in subclasses.
|
|
36
|
-
#
|
|
37
|
-
# ## Processing Phases
|
|
38
|
-
#
|
|
39
|
-
# Each option processor can participate in three phases:
|
|
40
|
-
# - Phase 1: Schema validation (validate DSL definition correctness)
|
|
41
|
-
# - Phase 2: Value validation (validate runtime data values)
|
|
42
|
-
# - Phase 3: Value transformation (transform values: defaults, renaming, etc.)
|
|
43
|
-
class Base
|
|
44
|
-
# Creates a new option processor instance
|
|
45
|
-
#
|
|
46
|
-
# @param attribute_name [Symbol] The name of the attribute
|
|
47
|
-
# @param attribute_type [Symbol] The type of the attribute
|
|
48
|
-
# @param option_schema [Object] The option schema (simple or advanced mode)
|
|
49
|
-
def initialize(attribute_name:, attribute_type:, option_schema:)
|
|
50
|
-
@attribute_name = attribute_name
|
|
51
|
-
@attribute_type = attribute_type
|
|
52
|
-
@option_schema = option_schema
|
|
53
|
-
end
|
|
54
|
-
|
|
55
|
-
# Phase 1: Validates schema (DSL definition)
|
|
56
|
-
# Override in subclasses if validation is needed
|
|
57
|
-
#
|
|
58
|
-
# @raise [Treaty::Exceptions::Validation] If schema is invalid
|
|
59
|
-
# @return [void]
|
|
60
|
-
def validate_schema!
|
|
61
|
-
# No-op by default
|
|
62
|
-
end
|
|
63
|
-
|
|
64
|
-
# Phase 2: Validates value (runtime data)
|
|
65
|
-
# Override in subclasses if validation is needed
|
|
66
|
-
#
|
|
67
|
-
# @param value [Object] The value to validate
|
|
68
|
-
# @raise [Treaty::Exceptions::Validation] If value is invalid
|
|
69
|
-
# @return [void]
|
|
70
|
-
def validate_value!(value)
|
|
71
|
-
# No-op by default
|
|
72
|
-
end
|
|
73
|
-
|
|
74
|
-
# Phase 3: Transforms value
|
|
75
|
-
# Returns transformed value or original if no transformation needed
|
|
76
|
-
# Override in subclasses if transformation is needed
|
|
77
|
-
#
|
|
78
|
-
# @param value [Object] The value to transform
|
|
79
|
-
# @param _root_data [Hash] Full raw data from root level (used by computed modifier)
|
|
80
|
-
# @return [Object] Transformed value
|
|
81
|
-
def transform_value(value, _root_data = {})
|
|
82
|
-
value
|
|
83
|
-
end
|
|
84
|
-
|
|
85
|
-
# Indicates if this option processor transforms attribute names
|
|
86
|
-
# Override in subclasses if needed (e.g., AsModifier)
|
|
87
|
-
#
|
|
88
|
-
# @return [Boolean] True if this processor transforms names
|
|
89
|
-
def transforms_name?
|
|
90
|
-
false
|
|
91
|
-
end
|
|
92
|
-
|
|
93
|
-
# Returns the target name for the attribute if this processor transforms names
|
|
94
|
-
# Override in subclasses if needed (e.g., AsModifier)
|
|
95
|
-
#
|
|
96
|
-
# @return [Symbol] The target attribute name
|
|
97
|
-
def target_name
|
|
98
|
-
@attribute_name
|
|
99
|
-
end
|
|
100
|
-
|
|
101
|
-
protected
|
|
102
|
-
|
|
103
|
-
# Returns the value key for this option in advanced mode
|
|
104
|
-
# Default is :is, but can be overridden (e.g., :in for inclusion)
|
|
105
|
-
#
|
|
106
|
-
# @return [Symbol] The key used to store the value in advanced mode
|
|
107
|
-
def value_key
|
|
108
|
-
:is
|
|
109
|
-
end
|
|
110
|
-
|
|
111
|
-
# Checks if option is enabled
|
|
112
|
-
# Handles both simple mode (boolean) and advanced mode (hash with value key)
|
|
113
|
-
#
|
|
114
|
-
# @return [Boolean] Whether the option is enabled
|
|
115
|
-
def option_enabled?
|
|
116
|
-
return false if @option_schema.nil?
|
|
117
|
-
return @option_schema if @option_schema.is_a?(TrueClass) || @option_schema.is_a?(FalseClass)
|
|
118
|
-
|
|
119
|
-
@option_schema.fetch(value_key, false)
|
|
120
|
-
end
|
|
121
|
-
|
|
122
|
-
# Extracts the actual value from normalized schema
|
|
123
|
-
# Works with both simple mode and advanced mode
|
|
124
|
-
#
|
|
125
|
-
# In simple mode: returns the value directly
|
|
126
|
-
# In advanced mode: extracts value using the appropriate key (is/in)
|
|
127
|
-
#
|
|
128
|
-
# @return [Object] The actual value from the option schema
|
|
129
|
-
def option_value
|
|
130
|
-
return @option_schema unless @option_schema.is_a?(Hash)
|
|
131
|
-
|
|
132
|
-
@option_schema.fetch(value_key, nil)
|
|
133
|
-
end
|
|
134
|
-
|
|
135
|
-
# Gets custom error message from advanced mode schema
|
|
136
|
-
# Returns nil if no custom message, which triggers I18n default message
|
|
137
|
-
#
|
|
138
|
-
# @return [String, Proc, nil] Custom error message, lambda, or nil for default message
|
|
139
|
-
def custom_message
|
|
140
|
-
return nil unless @option_schema.is_a?(Hash)
|
|
141
|
-
|
|
142
|
-
@option_schema.fetch(:message, nil)
|
|
143
|
-
end
|
|
144
|
-
|
|
145
|
-
# Resolves custom message with lambda support
|
|
146
|
-
# If message is a lambda, calls it with provided named arguments
|
|
147
|
-
# Catches all exceptions from lambda execution and re-raises as Validation errors
|
|
148
|
-
#
|
|
149
|
-
# @param attributes [Hash] Named arguments to pass to lambda
|
|
150
|
-
# @return [String, nil] Resolved message string or nil
|
|
151
|
-
# @raise [Treaty::Exceptions::Validation] If custom message lambda raises an exception
|
|
152
|
-
def resolve_custom_message(**attributes) # rubocop:disable Metrics/MethodLength
|
|
153
|
-
message = custom_message
|
|
154
|
-
return nil if message.nil?
|
|
155
|
-
|
|
156
|
-
if message.respond_to?(:call)
|
|
157
|
-
message.call(**attributes)
|
|
158
|
-
else
|
|
159
|
-
message
|
|
160
|
-
end
|
|
161
|
-
rescue StandardError => e
|
|
162
|
-
# Catch all exceptions from custom message lambda execution
|
|
163
|
-
error_message = I18n.t(
|
|
164
|
-
"treaty.attributes.options.message_evaluation_error",
|
|
165
|
-
attribute: @attribute_name,
|
|
166
|
-
error: e.message
|
|
167
|
-
)
|
|
168
|
-
|
|
169
|
-
raise Treaty::Exceptions::Validation, error_message
|
|
170
|
-
end
|
|
171
|
-
|
|
172
|
-
# Checks if schema is in advanced mode
|
|
173
|
-
#
|
|
174
|
-
# @return [Boolean] True if schema is in advanced mode (hash with value key)
|
|
175
|
-
def advanced_mode?
|
|
176
|
-
@option_schema.is_a?(Hash) && @option_schema.key?(value_key)
|
|
177
|
-
end
|
|
178
|
-
|
|
179
|
-
# Checks if schema is in simple mode
|
|
180
|
-
#
|
|
181
|
-
# @return [Boolean] True if schema is in simple mode (not a hash or no value key)
|
|
182
|
-
def simple_mode?
|
|
183
|
-
!advanced_mode?
|
|
184
|
-
end
|
|
185
|
-
end
|
|
186
|
-
end
|
|
187
|
-
end
|
|
188
|
-
end
|
|
@@ -1,90 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Treaty
|
|
4
|
-
module Attribute
|
|
5
|
-
module Option
|
|
6
|
-
module Conditionals
|
|
7
|
-
# Base class for conditional option processors.
|
|
8
|
-
#
|
|
9
|
-
# ## Purpose
|
|
10
|
-
#
|
|
11
|
-
# Conditionals control whether an attribute should be processed at all.
|
|
12
|
-
# Unlike validators (which check data) and modifiers (which transform data),
|
|
13
|
-
# conditionals determine attribute visibility based on runtime conditions.
|
|
14
|
-
#
|
|
15
|
-
# ## Key Difference from Validators/Modifiers
|
|
16
|
-
#
|
|
17
|
-
# - **Validators**: Check if data is valid
|
|
18
|
-
# - **Modifiers**: Transform data values
|
|
19
|
-
# - **Conditionals**: Decide if attribute exists in output
|
|
20
|
-
#
|
|
21
|
-
# ## Processing
|
|
22
|
-
#
|
|
23
|
-
# Conditionals are evaluated BEFORE validators and modifiers:
|
|
24
|
-
# 1. If condition evaluates to `false` → attribute is skipped entirely
|
|
25
|
-
# 2. If condition evaluates to `true` → attribute is processed normally
|
|
26
|
-
#
|
|
27
|
-
# ## Mode Support
|
|
28
|
-
#
|
|
29
|
-
# Conditionals do NOT support simple/advanced modes.
|
|
30
|
-
# They only accept lambda/proc directly:
|
|
31
|
-
#
|
|
32
|
-
# ```ruby
|
|
33
|
-
# # Correct
|
|
34
|
-
# integer :rating, if: ->(**attributes) { attributes.dig(:post, :published_at).present? }
|
|
35
|
-
# array :tags, if: ->(post:) { post[:published_at].present? }
|
|
36
|
-
#
|
|
37
|
-
# # Incorrect - no simple/advanced mode
|
|
38
|
-
# integer :rating, if: true # Not supported
|
|
39
|
-
# integer :rating, if: { is: ..., message: ... } # Not supported
|
|
40
|
-
# ```
|
|
41
|
-
#
|
|
42
|
-
# ## Implementation
|
|
43
|
-
#
|
|
44
|
-
# Subclasses must implement:
|
|
45
|
-
# - `validate_schema!` - Validate the conditional schema at definition time
|
|
46
|
-
# - `evaluate_condition(data)` - Evaluate condition with runtime data
|
|
47
|
-
class Base < Treaty::Attribute::Option::Base
|
|
48
|
-
# Phase 1: Validates conditional schema
|
|
49
|
-
# Must be overridden in subclasses
|
|
50
|
-
#
|
|
51
|
-
# @raise [Treaty::Exceptions::Validation] If schema is invalid
|
|
52
|
-
# @return [void]
|
|
53
|
-
def validate_schema!
|
|
54
|
-
raise Treaty::Exceptions::NotImplemented,
|
|
55
|
-
"#{self.class} must implement #validate_schema!"
|
|
56
|
-
end
|
|
57
|
-
|
|
58
|
-
# Evaluates the conditional with runtime data
|
|
59
|
-
# Must be overridden in subclasses
|
|
60
|
-
#
|
|
61
|
-
# @param _data [Hash] Raw data to evaluate condition against
|
|
62
|
-
# @raise [Treaty::Exceptions::Validation] If evaluation fails
|
|
63
|
-
# @return [Boolean] True if attribute should be processed, false otherwise
|
|
64
|
-
def evaluate_condition(_data)
|
|
65
|
-
raise Treaty::Exceptions::NotImplemented,
|
|
66
|
-
"#{self.class} must implement #evaluate_condition"
|
|
67
|
-
end
|
|
68
|
-
|
|
69
|
-
# Conditionals do not validate values
|
|
70
|
-
# This is a no-op for conditionals
|
|
71
|
-
#
|
|
72
|
-
# @param _value [Object] The value (unused)
|
|
73
|
-
# @return [void]
|
|
74
|
-
def validate_value!(_value)
|
|
75
|
-
# No-op: conditionals don't validate values
|
|
76
|
-
end
|
|
77
|
-
|
|
78
|
-
# Conditionals do not transform values
|
|
79
|
-
# This is a no-op for conditionals
|
|
80
|
-
#
|
|
81
|
-
# @param value [Object] The value to pass through
|
|
82
|
-
# @return [Object] The unchanged value
|
|
83
|
-
def transform_value(value)
|
|
84
|
-
value
|
|
85
|
-
end
|
|
86
|
-
end
|
|
87
|
-
end
|
|
88
|
-
end
|
|
89
|
-
end
|
|
90
|
-
end
|
|
@@ -1,134 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Treaty
|
|
4
|
-
module Attribute
|
|
5
|
-
module Option
|
|
6
|
-
module Conditionals
|
|
7
|
-
# Conditionally includes attributes based on runtime data evaluation.
|
|
8
|
-
#
|
|
9
|
-
# ## Usage Examples
|
|
10
|
-
#
|
|
11
|
-
# Basic usage with keyword arguments splat:
|
|
12
|
-
# array :tags, if: ->(**attributes) { attributes.dig(:post, :published_at).present? }
|
|
13
|
-
# integer :rating, if: ->(**attributes) { attributes.dig(:post, :published_at).present? }
|
|
14
|
-
#
|
|
15
|
-
# Named argument pattern:
|
|
16
|
-
# array :tags, if: ->(post:) { post[:published_at].present? }
|
|
17
|
-
# integer :views, if: ->(post:) { post[:published_at].present? }
|
|
18
|
-
#
|
|
19
|
-
# Complex conditions:
|
|
20
|
-
# string :admin_note, if: (lambda do |**attributes|
|
|
21
|
-
# attributes.dig(:user, :role) == "admin" && attributes.dig(:post, :flagged)
|
|
22
|
-
# end)
|
|
23
|
-
#
|
|
24
|
-
# ## Use Cases
|
|
25
|
-
#
|
|
26
|
-
# 1. **Show fields only when published**:
|
|
27
|
-
# ```ruby
|
|
28
|
-
# response 200 do
|
|
29
|
-
# object :post do
|
|
30
|
-
# string :id
|
|
31
|
-
# string :title
|
|
32
|
-
# datetime :published_at, :optional
|
|
33
|
-
# integer :rating, if: ->(**attributes) { attributes.dig(:post, :published_at).present? }
|
|
34
|
-
# end
|
|
35
|
-
# end
|
|
36
|
-
# # If published_at is nil → rating is excluded from response
|
|
37
|
-
# # If published_at exists → rating is included
|
|
38
|
-
# ```
|
|
39
|
-
#
|
|
40
|
-
# 2. **Role-based field visibility**:
|
|
41
|
-
# ```ruby
|
|
42
|
-
# response 200 do
|
|
43
|
-
# object :user do
|
|
44
|
-
# string :name
|
|
45
|
-
# string :email, if: ->(user:) { user[:role] == "admin" }
|
|
46
|
-
# end
|
|
47
|
-
# end
|
|
48
|
-
# ```
|
|
49
|
-
#
|
|
50
|
-
# 3. **Nested attribute conditionals**:
|
|
51
|
-
# ```ruby
|
|
52
|
-
# object :post do
|
|
53
|
-
# string :title
|
|
54
|
-
# array :tags, if: ->(post:) { post[:published_at].present? } do
|
|
55
|
-
# string :_self
|
|
56
|
-
# end
|
|
57
|
-
# end
|
|
58
|
-
# ```
|
|
59
|
-
#
|
|
60
|
-
# ## Important Notes
|
|
61
|
-
#
|
|
62
|
-
# - Lambda receives raw data as named arguments
|
|
63
|
-
# - Lambda MUST return truthy/falsy value
|
|
64
|
-
# - If condition is false → attribute is completely omitted
|
|
65
|
-
# - If condition is true → attribute is validated and transformed normally
|
|
66
|
-
# - All exceptions in lambda are caught and wrapped in Treaty::Exceptions::Validation
|
|
67
|
-
# - Does NOT support simple mode (if: true) or advanced mode (if: { is: ..., message: ... })
|
|
68
|
-
#
|
|
69
|
-
# ## Error Handling
|
|
70
|
-
#
|
|
71
|
-
# If the lambda raises any exception, it's caught and converted to a
|
|
72
|
-
# Treaty::Exceptions::Validation with detailed error message including:
|
|
73
|
-
# - Attribute name
|
|
74
|
-
# - Original exception message
|
|
75
|
-
#
|
|
76
|
-
# ## Data Access Pattern
|
|
77
|
-
#
|
|
78
|
-
# The lambda receives the same data structure that the orchestrator processes.
|
|
79
|
-
# For nested attributes, you can access parent data using dig:
|
|
80
|
-
#
|
|
81
|
-
# ```ruby
|
|
82
|
-
# # For response with { post: { title: "...", published_at: "..." } }
|
|
83
|
-
# integer :rating, if: ->(**attributes) { attributes.dig(:post, :published_at).present? }
|
|
84
|
-
#
|
|
85
|
-
# # Alternative: named argument pattern
|
|
86
|
-
# integer :rating, if: ->(post:) { post[:published_at].present? }
|
|
87
|
-
# ```
|
|
88
|
-
class IfConditional < Treaty::Attribute::Option::Conditionals::Base
|
|
89
|
-
# Validates that if option is a callable (Proc/Lambda)
|
|
90
|
-
#
|
|
91
|
-
# @raise [Treaty::Exceptions::Validation] If if is not a Proc/lambda
|
|
92
|
-
# @return [void]
|
|
93
|
-
def validate_schema!
|
|
94
|
-
conditional_lambda = @option_schema
|
|
95
|
-
|
|
96
|
-
return if conditional_lambda.respond_to?(:call)
|
|
97
|
-
|
|
98
|
-
raise Treaty::Exceptions::Validation,
|
|
99
|
-
I18n.t(
|
|
100
|
-
"treaty.attributes.conditionals.if.invalid_type",
|
|
101
|
-
attribute: @attribute_name,
|
|
102
|
-
type: conditional_lambda.class
|
|
103
|
-
)
|
|
104
|
-
end
|
|
105
|
-
|
|
106
|
-
# Evaluates the conditional lambda with runtime data
|
|
107
|
-
# Returns boolean indicating if attribute should be processed
|
|
108
|
-
#
|
|
109
|
-
# @param data [Hash] Raw data from request/response/entity
|
|
110
|
-
# @raise [Treaty::Exceptions::Validation] If lambda execution fails
|
|
111
|
-
# @return [Boolean] True if attribute should be processed, false to skip it
|
|
112
|
-
def evaluate_condition(data)
|
|
113
|
-
conditional_lambda = @option_schema
|
|
114
|
-
|
|
115
|
-
# Call lambda with raw data as named arguments
|
|
116
|
-
# The lambda can use **attributes or specific named args like post:
|
|
117
|
-
result = conditional_lambda.call(**data)
|
|
118
|
-
|
|
119
|
-
# Convert result to boolean
|
|
120
|
-
!!result
|
|
121
|
-
rescue StandardError => e
|
|
122
|
-
# Catch all exceptions from lambda execution
|
|
123
|
-
raise Treaty::Exceptions::Validation,
|
|
124
|
-
I18n.t(
|
|
125
|
-
"treaty.attributes.conditionals.if.evaluation_error",
|
|
126
|
-
attribute: @attribute_name,
|
|
127
|
-
error: e.message
|
|
128
|
-
)
|
|
129
|
-
end
|
|
130
|
-
end
|
|
131
|
-
end
|
|
132
|
-
end
|
|
133
|
-
end
|
|
134
|
-
end
|
|
@@ -1,151 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Treaty
|
|
4
|
-
module Attribute
|
|
5
|
-
module Option
|
|
6
|
-
module Conditionals
|
|
7
|
-
# Conditionally excludes attributes based on runtime data evaluation.
|
|
8
|
-
#
|
|
9
|
-
# ## Usage Examples
|
|
10
|
-
#
|
|
11
|
-
# Basic usage with keyword arguments splat:
|
|
12
|
-
# array :tags, unless: ->(**attributes) { attributes.dig(:post, :published_at).present? }
|
|
13
|
-
# integer :draft_views, unless: ->(**attributes) { attributes.dig(:post, :published_at).present? }
|
|
14
|
-
#
|
|
15
|
-
# Named argument pattern:
|
|
16
|
-
# array :draft_notes, unless: ->(post:) { post[:published_at].present? }
|
|
17
|
-
# integer :edit_count, unless: ->(post:) { post[:published_at].present? }
|
|
18
|
-
#
|
|
19
|
-
# Complex conditions:
|
|
20
|
-
# string :internal_note, unless: (lambda do |**attributes|
|
|
21
|
-
# attributes.dig(:user, :role) == "admin" && attributes.dig(:post, :flagged)
|
|
22
|
-
# end)
|
|
23
|
-
#
|
|
24
|
-
# ## Use Cases
|
|
25
|
-
#
|
|
26
|
-
# 1. **Hide fields when published**:
|
|
27
|
-
# ```ruby
|
|
28
|
-
# response 200 do
|
|
29
|
-
# object :post do
|
|
30
|
-
# string :id
|
|
31
|
-
# string :title
|
|
32
|
-
# datetime :published_at, :optional
|
|
33
|
-
# integer :draft_views, unless: ->(**attributes) { attributes.dig(:post, :published_at).present? }
|
|
34
|
-
# end
|
|
35
|
-
# end
|
|
36
|
-
# # If published_at is nil → draft_views is included in response
|
|
37
|
-
# # If published_at exists → draft_views is excluded
|
|
38
|
-
# ```
|
|
39
|
-
#
|
|
40
|
-
# 2. **Role-based field exclusion**:
|
|
41
|
-
# ```ruby
|
|
42
|
-
# response 200 do
|
|
43
|
-
# object :user do
|
|
44
|
-
# string :name
|
|
45
|
-
# string :internal_id, unless: ->(user:) { user[:role] == "public" }
|
|
46
|
-
# end
|
|
47
|
-
# end
|
|
48
|
-
# ```
|
|
49
|
-
#
|
|
50
|
-
# 3. **Nested attribute conditionals**:
|
|
51
|
-
# ```ruby
|
|
52
|
-
# object :post do
|
|
53
|
-
# string :title
|
|
54
|
-
# array :draft_notes, unless: ->(post:) { post[:published_at].present? } do
|
|
55
|
-
# string :_self
|
|
56
|
-
# end
|
|
57
|
-
# end
|
|
58
|
-
# ```
|
|
59
|
-
#
|
|
60
|
-
# ## Important Notes
|
|
61
|
-
#
|
|
62
|
-
# - Lambda receives raw data as named arguments
|
|
63
|
-
# - Lambda MUST return truthy/falsy value
|
|
64
|
-
# - If condition is true → attribute is completely omitted (OPPOSITE of `if`)
|
|
65
|
-
# - If condition is false → attribute is validated and transformed normally
|
|
66
|
-
# - All exceptions in lambda are caught and wrapped in Treaty::Exceptions::Validation
|
|
67
|
-
# - Does NOT support simple mode (unless: true) or advanced mode (unless: { is: ..., message: ... })
|
|
68
|
-
#
|
|
69
|
-
# ## Difference from `if` Option
|
|
70
|
-
#
|
|
71
|
-
# `unless` is the logical opposite of `if`:
|
|
72
|
-
# - `if` includes attribute when condition is TRUE
|
|
73
|
-
# - `unless` includes attribute when condition is FALSE
|
|
74
|
-
#
|
|
75
|
-
# ```ruby
|
|
76
|
-
# # These are equivalent:
|
|
77
|
-
# integer :rating, if: ->(**attributes) { attributes.dig(:post, :published_at).present? }
|
|
78
|
-
# integer :rating, unless: ->(**attributes) { attributes.dig(:post, :published_at).blank? }
|
|
79
|
-
#
|
|
80
|
-
# # These are also equivalent:
|
|
81
|
-
# integer :draft_views, unless: ->(**attributes) { attributes.dig(:post, :published_at).present? }
|
|
82
|
-
# integer :draft_views, if: ->(**attributes) { attributes.dig(:post, :published_at).blank? }
|
|
83
|
-
# ```
|
|
84
|
-
#
|
|
85
|
-
# ## Error Handling
|
|
86
|
-
#
|
|
87
|
-
# If the lambda raises any exception, it's caught and converted to a
|
|
88
|
-
# Treaty::Exceptions::Validation with detailed error message including:
|
|
89
|
-
# - Attribute name
|
|
90
|
-
# - Original exception message
|
|
91
|
-
#
|
|
92
|
-
# ## Data Access Pattern
|
|
93
|
-
#
|
|
94
|
-
# The lambda receives the same data structure that the orchestrator processes.
|
|
95
|
-
# For nested attributes, you can access parent data using dig:
|
|
96
|
-
#
|
|
97
|
-
# ```ruby
|
|
98
|
-
# # For response with { post: { title: "...", published_at: "..." } }
|
|
99
|
-
# integer :draft_views, unless: ->(**attributes) { attributes.dig(:post, :published_at).present? }
|
|
100
|
-
#
|
|
101
|
-
# # Alternative: named argument pattern
|
|
102
|
-
# integer :draft_views, unless: ->(post:) { post[:published_at].present? }
|
|
103
|
-
# ```
|
|
104
|
-
class UnlessConditional < Treaty::Attribute::Option::Conditionals::Base
|
|
105
|
-
# Validates that unless option is a callable (Proc/Lambda)
|
|
106
|
-
#
|
|
107
|
-
# @raise [Treaty::Exceptions::Validation] If unless is not a Proc/lambda
|
|
108
|
-
# @return [void]
|
|
109
|
-
def validate_schema!
|
|
110
|
-
conditional_lambda = @option_schema
|
|
111
|
-
|
|
112
|
-
return if conditional_lambda.respond_to?(:call)
|
|
113
|
-
|
|
114
|
-
raise Treaty::Exceptions::Validation,
|
|
115
|
-
I18n.t(
|
|
116
|
-
"treaty.attributes.conditionals.unless.invalid_type",
|
|
117
|
-
attribute: @attribute_name,
|
|
118
|
-
type: conditional_lambda.class
|
|
119
|
-
)
|
|
120
|
-
end
|
|
121
|
-
|
|
122
|
-
# Evaluates the conditional lambda with runtime data
|
|
123
|
-
# Returns boolean indicating if attribute should be processed
|
|
124
|
-
#
|
|
125
|
-
# @param data [Hash] Raw data from request/response/entity
|
|
126
|
-
# @raise [Treaty::Exceptions::Validation] If lambda execution fails
|
|
127
|
-
# @return [Boolean] True if attribute should be processed (when condition is FALSE), false to skip it
|
|
128
|
-
def evaluate_condition(data)
|
|
129
|
-
conditional_lambda = @option_schema
|
|
130
|
-
|
|
131
|
-
# Call lambda with raw data as named arguments
|
|
132
|
-
# The lambda can use **attributes or specific named args like post:
|
|
133
|
-
result = conditional_lambda.call(**data)
|
|
134
|
-
|
|
135
|
-
# Convert result to boolean and NEGATE it (opposite of if)
|
|
136
|
-
# unless includes attribute when condition is FALSE
|
|
137
|
-
!result
|
|
138
|
-
rescue StandardError => e
|
|
139
|
-
# Catch all exceptions from lambda execution
|
|
140
|
-
raise Treaty::Exceptions::Validation,
|
|
141
|
-
I18n.t(
|
|
142
|
-
"treaty.attributes.conditionals.unless.evaluation_error",
|
|
143
|
-
attribute: @attribute_name,
|
|
144
|
-
error: e.message
|
|
145
|
-
)
|
|
146
|
-
end
|
|
147
|
-
end
|
|
148
|
-
end
|
|
149
|
-
end
|
|
150
|
-
end
|
|
151
|
-
end
|