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