treaty 0.17.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.
Files changed (80) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +2 -2
  3. data/config/locales/en.yml +6 -2
  4. data/lib/treaty/engine.rb +1 -1
  5. data/lib/treaty/{attribute/entity → entity/attribute}/attribute.rb +4 -4
  6. data/lib/treaty/entity/attribute/base.rb +184 -0
  7. data/lib/treaty/entity/attribute/builder/base.rb +275 -0
  8. data/lib/treaty/entity/attribute/collection.rb +67 -0
  9. data/lib/treaty/entity/attribute/dsl.rb +92 -0
  10. data/lib/treaty/entity/attribute/helper_mapper.rb +74 -0
  11. data/lib/treaty/entity/attribute/option/base.rb +190 -0
  12. data/lib/treaty/entity/attribute/option/conditionals/base.rb +92 -0
  13. data/lib/treaty/entity/attribute/option/conditionals/if_conditional.rb +136 -0
  14. data/lib/treaty/entity/attribute/option/conditionals/unless_conditional.rb +153 -0
  15. data/lib/treaty/entity/attribute/option/modifiers/as_modifier.rb +93 -0
  16. data/lib/treaty/entity/attribute/option/modifiers/cast_modifier.rb +285 -0
  17. data/lib/treaty/entity/attribute/option/modifiers/computed_modifier.rb +128 -0
  18. data/lib/treaty/entity/attribute/option/modifiers/default_modifier.rb +105 -0
  19. data/lib/treaty/entity/attribute/option/modifiers/transform_modifier.rb +114 -0
  20. data/lib/treaty/entity/attribute/option/registry.rb +157 -0
  21. data/lib/treaty/entity/attribute/option/registry_initializer.rb +117 -0
  22. data/lib/treaty/entity/attribute/option/validators/format_validator.rb +222 -0
  23. data/lib/treaty/entity/attribute/option/validators/inclusion_validator.rb +94 -0
  24. data/lib/treaty/entity/attribute/option/validators/required_validator.rb +100 -0
  25. data/lib/treaty/entity/attribute/option/validators/type_validator.rb +219 -0
  26. data/lib/treaty/entity/attribute/option_normalizer.rb +168 -0
  27. data/lib/treaty/entity/attribute/option_orchestrator.rb +192 -0
  28. data/lib/treaty/entity/attribute/validation/attribute_validator.rb +147 -0
  29. data/lib/treaty/entity/attribute/validation/base.rb +76 -0
  30. data/lib/treaty/entity/attribute/validation/nested_array_validator.rb +207 -0
  31. data/lib/treaty/entity/attribute/validation/nested_object_validator.rb +105 -0
  32. data/lib/treaty/entity/attribute/validation/nested_transformer.rb +432 -0
  33. data/lib/treaty/entity/attribute/validation/orchestrator/base.rb +262 -0
  34. data/lib/treaty/entity/base.rb +90 -0
  35. data/lib/treaty/entity/builder.rb +44 -0
  36. data/lib/treaty/{info/entity → entity/info}/builder.rb +8 -8
  37. data/lib/treaty/{info/entity → entity/info}/dsl.rb +2 -2
  38. data/lib/treaty/{info/entity → entity/info}/result.rb +2 -2
  39. data/lib/treaty/entity.rb +7 -75
  40. data/lib/treaty/request/attribute/attribute.rb +1 -1
  41. data/lib/treaty/request/attribute/builder.rb +24 -1
  42. data/lib/treaty/request/entity.rb +1 -1
  43. data/lib/treaty/request/factory.rb +6 -6
  44. data/lib/treaty/request/validator.rb +1 -1
  45. data/lib/treaty/response/attribute/attribute.rb +1 -1
  46. data/lib/treaty/response/attribute/builder.rb +24 -1
  47. data/lib/treaty/response/entity.rb +1 -1
  48. data/lib/treaty/response/factory.rb +6 -6
  49. data/lib/treaty/response/validator.rb +1 -1
  50. data/lib/treaty/version.rb +1 -1
  51. metadata +35 -34
  52. data/lib/treaty/attribute/base.rb +0 -182
  53. data/lib/treaty/attribute/builder/base.rb +0 -143
  54. data/lib/treaty/attribute/collection.rb +0 -65
  55. data/lib/treaty/attribute/dsl.rb +0 -90
  56. data/lib/treaty/attribute/entity/builder.rb +0 -23
  57. data/lib/treaty/attribute/helper_mapper.rb +0 -72
  58. data/lib/treaty/attribute/option/base.rb +0 -188
  59. data/lib/treaty/attribute/option/conditionals/base.rb +0 -90
  60. data/lib/treaty/attribute/option/conditionals/if_conditional.rb +0 -134
  61. data/lib/treaty/attribute/option/conditionals/unless_conditional.rb +0 -151
  62. data/lib/treaty/attribute/option/modifiers/as_modifier.rb +0 -91
  63. data/lib/treaty/attribute/option/modifiers/cast_modifier.rb +0 -283
  64. data/lib/treaty/attribute/option/modifiers/computed_modifier.rb +0 -126
  65. data/lib/treaty/attribute/option/modifiers/default_modifier.rb +0 -103
  66. data/lib/treaty/attribute/option/modifiers/transform_modifier.rb +0 -112
  67. data/lib/treaty/attribute/option/registry.rb +0 -155
  68. data/lib/treaty/attribute/option/registry_initializer.rb +0 -115
  69. data/lib/treaty/attribute/option/validators/format_validator.rb +0 -220
  70. data/lib/treaty/attribute/option/validators/inclusion_validator.rb +0 -92
  71. data/lib/treaty/attribute/option/validators/required_validator.rb +0 -98
  72. data/lib/treaty/attribute/option/validators/type_validator.rb +0 -217
  73. data/lib/treaty/attribute/option_normalizer.rb +0 -166
  74. data/lib/treaty/attribute/option_orchestrator.rb +0 -190
  75. data/lib/treaty/attribute/validation/attribute_validator.rb +0 -145
  76. data/lib/treaty/attribute/validation/base.rb +0 -74
  77. data/lib/treaty/attribute/validation/nested_array_validator.rb +0 -205
  78. data/lib/treaty/attribute/validation/nested_object_validator.rb +0 -103
  79. data/lib/treaty/attribute/validation/nested_transformer.rb +0 -430
  80. data/lib/treaty/attribute/validation/orchestrator/base.rb +0 -260
@@ -0,0 +1,105 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Treaty
4
+ module Entity
5
+ module Attribute
6
+ module Option
7
+ module Modifiers
8
+ # Sets default values for attributes when value is nil.
9
+ #
10
+ # ## Usage Examples
11
+ #
12
+ # Simple mode with static value:
13
+ # integer :limit, default: 12
14
+ # string :status, default: "pending"
15
+ # boolean :active, default: false
16
+ #
17
+ # Simple mode with dynamic value (Proc):
18
+ # datetime :created_at, default: -> { Time.current }
19
+ # string :uuid, default: -> { SecureRandom.uuid }
20
+ #
21
+ # Advanced mode:
22
+ # integer :limit, default: { is: 12, message: nil }
23
+ #
24
+ # ## Use Cases
25
+ #
26
+ # 1. **Response defaults** (most common):
27
+ # ```ruby
28
+ # response 200 do
29
+ # object :meta do
30
+ # integer :limit, default: 12
31
+ # integer :page, default: 1
32
+ # end
33
+ # end
34
+ # # Service returns: { meta: { page: 1 } }
35
+ # # Output: { meta: { page: 1, limit: 12 } }
36
+ # ```
37
+ #
38
+ # 2. **Request defaults**:
39
+ # ```ruby
40
+ # request do
41
+ # string :format, default: "json"
42
+ # end
43
+ # # Input: {}
44
+ # # Service receives: { format: "json" }
45
+ # ```
46
+ #
47
+ # ## Important Notes
48
+ #
49
+ # - Default is applied ONLY when value is nil
50
+ # - Empty strings, empty arrays, false are NOT replaced
51
+ # - Proc defaults are called at transformation time
52
+ # - Procs receive no arguments
53
+ #
54
+ # ## Array and Object Types
55
+ #
56
+ # NOTE: DO NOT use `default: []` or `default: {}` for array/object types!
57
+ # Array and object types automatically represent empty collections.
58
+ #
59
+ # Incorrect:
60
+ # array :tags, default: [] # Wrong! Redundant
61
+ # object :meta, default: {} # Wrong! Redundant
62
+ #
63
+ # Correct:
64
+ # array :tags # Automatically handles empty array
65
+ # object :meta # Automatically handles empty object
66
+ #
67
+ # ## Advanced Mode
68
+ #
69
+ # Schema format: `{ is: value_or_proc, message: nil }`
70
+ class DefaultModifier < Treaty::Entity::Attribute::Option::Base
71
+ # Validates schema (no validation needed)
72
+ # Default value can be any type
73
+ #
74
+ # @return [void]
75
+ def validate_schema!
76
+ # Schema structure is already normalized by OptionNormalizer.
77
+ # Default value can be any type, so nothing specific to validate here.
78
+ end
79
+
80
+ # Applies default value if current value is nil
81
+ # Empty strings, empty arrays, and false are NOT replaced
82
+ #
83
+ # @param value [Object] The current value
84
+ # @param _root_data [Hash] Unused root data parameter
85
+ # @return [Object] Default value if original is nil, otherwise original value
86
+ def transform_value(value, _root_data = {})
87
+ # Only apply default if value is nil
88
+ # Empty strings, empty arrays, false are NOT replaced
89
+ return value unless value.nil?
90
+
91
+ default_value = option_value
92
+
93
+ # If default value is a Proc, call it to get the value
94
+ if default_value.is_a?(Proc)
95
+ default_value.call
96
+ else
97
+ default_value
98
+ end
99
+ end
100
+ end
101
+ end
102
+ end
103
+ end
104
+ end
105
+ end
@@ -0,0 +1,114 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Treaty
4
+ module Entity
5
+ module Attribute
6
+ module Option
7
+ module Modifiers
8
+ # Transforms attribute values using custom lambda functions.
9
+ #
10
+ # ## Usage Examples
11
+ #
12
+ # Simple mode:
13
+ # integer :amount, transform: ->(value:) { value * 100 }
14
+ # string :title, transform: ->(value:) { value.strip.upcase }
15
+ #
16
+ # Advanced mode with custom error message:
17
+ # integer :amount, transform: {
18
+ # is: ->(value:) { value * 100 },
19
+ # message: "Failed to transform amount"
20
+ # }
21
+ #
22
+ # ## Use Cases
23
+ #
24
+ # 1. **Request transformation**:
25
+ # ```ruby
26
+ # request do
27
+ # integer :amount_cents, transform: ->(value:) { value * 100 }
28
+ # end
29
+ # # Input: { amount_cents: 10 }
30
+ # # Service receives: { amount_cents: 1000 }
31
+ # ```
32
+ #
33
+ # 2. **Response transformation**:
34
+ # ```ruby
35
+ # response 200 do
36
+ # string :title, transform: ->(value:) { value.titleize }
37
+ # end
38
+ # # Service returns: { title: "hello world" }
39
+ # # Output: { title: "Hello World" }
40
+ # ```
41
+ #
42
+ # 3. **Complex transformations**:
43
+ # ```ruby
44
+ # string :email, transform: ->(value:) { value.downcase.strip }
45
+ # datetime :timestamp, transform: ->(value:) { value.iso8601 }
46
+ # ```
47
+ #
48
+ # ## Important Notes
49
+ #
50
+ # - Lambda must accept named argument `value:`
51
+ # - All exceptions raised in lambda are caught and re-raised as Validation errors
52
+ # - Transformation is applied during Phase 3 (after validation)
53
+ # - Can be combined with other options (required, default, as, etc.)
54
+ #
55
+ # ## Error Handling
56
+ #
57
+ # If the lambda raises any exception, it's caught and converted to a
58
+ # Treaty::Exceptions::Validation with appropriate error message.
59
+ #
60
+ # ## Advanced Mode
61
+ #
62
+ # Schema format: `{ is: lambda, message: nil }`
63
+ class TransformModifier < Treaty::Entity::Attribute::Option::Base
64
+ # Validates that transform value is a lambda
65
+ #
66
+ # @raise [Treaty::Exceptions::Validation] If transform is not a Proc/lambda
67
+ # @return [void]
68
+ def validate_schema!
69
+ transform_lambda = option_value
70
+
71
+ return if transform_lambda.respond_to?(:call)
72
+
73
+ raise Treaty::Exceptions::Validation,
74
+ I18n.t(
75
+ "treaty.attributes.modifiers.transform.invalid_type",
76
+ attribute: @attribute_name,
77
+ type: transform_lambda.class
78
+ )
79
+ end
80
+
81
+ # Applies transformation to the value using the provided lambda
82
+ # Catches all exceptions and re-raises as Validation errors
83
+ # Skips transformation for nil values (handled by RequiredValidator)
84
+ #
85
+ # @param value [Object] The current value
86
+ # @param _root_data [Hash] Unused root data parameter
87
+ # @return [Object] Transformed value
88
+ def transform_value(value, _root_data = {}) # rubocop:disable Metrics/MethodLength
89
+ return value if value.nil? # Transform doesn't modify nil, required validator handles it.
90
+
91
+ transform_lambda = option_value
92
+
93
+ # Call lambda with named argument
94
+ transform_lambda.call(value:)
95
+ rescue StandardError => e
96
+ attributes = {
97
+ attribute: @attribute_name,
98
+ error: e.message
99
+ }
100
+
101
+ # Catch all exceptions from lambda execution
102
+ error_message = resolve_custom_message(**attributes) || I18n.t(
103
+ "treaty.attributes.modifiers.transform.execution_error",
104
+ **attributes
105
+ )
106
+
107
+ raise Treaty::Exceptions::Validation, error_message
108
+ end
109
+ end
110
+ end
111
+ end
112
+ end
113
+ end
114
+ end
@@ -0,0 +1,157 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Treaty
4
+ module Entity
5
+ module Attribute
6
+ module Option
7
+ # Central registry for all option processors (validators, modifiers, and conditionals).
8
+ #
9
+ # ## Purpose
10
+ #
11
+ # Provides a centralized registry pattern for managing all option processors.
12
+ # Enables dynamic discovery and extensibility of the option system.
13
+ #
14
+ # ## Responsibilities
15
+ #
16
+ # 1. **Registration** - Stores option processor classes
17
+ # 2. **Retrieval** - Provides access to registered processors
18
+ # 3. **Categorization** - Organizes processors by category (validator/modifier/conditional)
19
+ # 4. **Validation** - Checks if options are registered
20
+ #
21
+ # ## Registered Options
22
+ #
23
+ # ### Validators (sorted by position)
24
+ # - `:type` → TypeValidator (position: 100)
25
+ # - `:required` → RequiredValidator (position: 200)
26
+ # - `:inclusion` → InclusionValidator (position: 300)
27
+ # - `:format` → FormatValidator (position: 400)
28
+ #
29
+ # ### Modifiers (sorted by position)
30
+ # - `:transform` → TransformModifier (position: 500)
31
+ # - `:cast` → CastModifier (position: 600)
32
+ # - `:computed` → ComputedModifier (position: 700)
33
+ # - `:default` → DefaultModifier (position: 800)
34
+ # - `:as` → AsModifier (position: 900)
35
+ #
36
+ # ### Conditionals (no position - handled separately)
37
+ # - `:if` → IfConditional
38
+ # - `:unless` → UnlessConditional
39
+ #
40
+ # ## Usage
41
+ #
42
+ # Registration (done in RegistryInitializer):
43
+ # Registry.register(:required, RequiredValidator, category: :validator, position: 200)
44
+ # Registry.register(:if, IfConditional, category: :conditional)
45
+ #
46
+ # Retrieval (done in OptionOrchestrator):
47
+ # processor_class = Registry.processor_for(:required)
48
+ # processor = processor_class.new(...)
49
+ #
50
+ # ## Extensibility
51
+ #
52
+ # To add a new option:
53
+ # 1. Create processor class inheriting from Option::Base
54
+ # 2. Register it: `Registry.register(:my_option, MyProcessor, category: :validator)`
55
+ # 3. Option becomes available in DSL immediately
56
+ #
57
+ # ## Architecture
58
+ #
59
+ # Works with:
60
+ # - RegistryInitializer - Populates registry with built-in options
61
+ # - OptionOrchestrator - Uses registry to build processors
62
+ # - Option::Base - Base class for all registered processors
63
+ class Registry
64
+ class << self
65
+ # Register an option processor
66
+ #
67
+ # @param option_name [Symbol] The name of the option (e.g., :required, :as, :default)
68
+ # @param processor_class [Class] The processor class
69
+ # @param category [Symbol] The category (:validator, :modifier, or :conditional)
70
+ # @param position [Integer, nil] Execution order position (nil for conditionals)
71
+ def register(option_name, processor_class, category:, position: nil)
72
+ registry[option_name] = {
73
+ processor_class:,
74
+ category:,
75
+ position:
76
+ }
77
+ end
78
+
79
+ # Get processor class for an option
80
+ #
81
+ # @param option_name [Symbol] The name of the option
82
+ # @return [Class, nil] The processor class or nil if not found
83
+ def processor_for(option_name)
84
+ registry.dig(option_name, :processor_class)
85
+ end
86
+
87
+ # Get category for an option
88
+ #
89
+ # @param option_name [Symbol] The name of the option
90
+ # @return [Symbol, nil] The category (:validator or :modifier) or nil if not found
91
+ def category_for(option_name)
92
+ registry.dig(option_name, :category)
93
+ end
94
+
95
+ # Get position for an option
96
+ #
97
+ # @param option_name [Symbol] The name of the option
98
+ # @return [Integer, nil] The execution order position or nil if not set
99
+ def position_for(option_name)
100
+ registry.dig(option_name, :position)
101
+ end
102
+
103
+ # Check if an option is registered
104
+ #
105
+ # @param option_name [Symbol] The name of the option
106
+ # @return [Boolean]
107
+ def registered?(option_name)
108
+ registry.key?(option_name)
109
+ end
110
+
111
+ # Get all registered option names
112
+ #
113
+ # @return [Array<Symbol>]
114
+ def all_options
115
+ registry.keys
116
+ end
117
+
118
+ # Get all validators
119
+ #
120
+ # @return [Hash] Hash of option_name => processor_class for validators
121
+ def validators
122
+ registry.select { |_, info| info.fetch(:category) == :validator }
123
+ .transform_values { |info| info.fetch(:processor_class) }
124
+ end
125
+
126
+ # Get all modifiers
127
+ #
128
+ # @return [Hash] Hash of option_name => processor_class for modifiers
129
+ def modifiers
130
+ registry.select { |_, info| info.fetch(:category) == :modifier }
131
+ .transform_values { |info| info.fetch(:processor_class) }
132
+ end
133
+
134
+ # Get all conditionals
135
+ #
136
+ # @return [Hash] Hash of option_name => processor_class for conditionals
137
+ def conditionals
138
+ registry.select { |_, info| info.fetch(:category) == :conditional }
139
+ .transform_values { |info| info.fetch(:processor_class) }
140
+ end
141
+
142
+ # Reset registry (mainly for testing)
143
+ def reset!
144
+ @registry = nil
145
+ end
146
+
147
+ private
148
+
149
+ def registry
150
+ @registry ||= {}
151
+ end
152
+ end
153
+ end
154
+ end
155
+ end
156
+ end
157
+ end
@@ -0,0 +1,117 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Treaty
4
+ module Entity
5
+ module Attribute
6
+ module Option
7
+ # Initializes and registers all built-in option processors with the Registry.
8
+ #
9
+ # ## Purpose
10
+ #
11
+ # Centralized registration point for all option processors (validators, modifiers, and conditionals).
12
+ # Automatically registers all built-in options when loaded.
13
+ #
14
+ # ## Responsibilities
15
+ #
16
+ # 1. **Validator Registration** - Registers all built-in validators
17
+ # 2. **Modifier Registration** - Registers all built-in modifiers
18
+ # 3. **Conditional Registration** - Registers all built-in conditionals
19
+ # 4. **Auto-Loading** - Executes automatically when file is loaded
20
+ #
21
+ # ## Built-in Validators (sorted by position)
22
+ #
23
+ # - `:type` → TypeValidator (position: 100) - Validates value types
24
+ # - `:required` → RequiredValidator (position: 200) - Validates required/optional attributes
25
+ # - `:inclusion` → InclusionValidator (position: 300) - Validates value is in allowed set
26
+ # - `:format` → FormatValidator (position: 400) - Validates string values match specific formats
27
+ #
28
+ # ## Built-in Modifiers (sorted by position)
29
+ #
30
+ # - `:transform` → TransformModifier (position: 500) - Transforms values using custom lambdas
31
+ # - `:cast` → CastModifier (position: 600) - Converts values between types automatically
32
+ # - `:computed` → ComputedModifier (position: 700) - Computes values from all raw data
33
+ # - `:default` → DefaultModifier (position: 800) - Provides default values
34
+ # - `:as` → AsModifier (position: 900) - Renames attributes
35
+ #
36
+ # ## Built-in Conditionals (no position - handled separately)
37
+ #
38
+ # - `:if` → IfConditional - Conditionally includes attributes based on runtime data
39
+ # - `:unless` → UnlessConditional - Conditionally excludes attributes based on runtime data
40
+ #
41
+ # ## Auto-Registration
42
+ #
43
+ # This file calls `register_all!` when loaded, ensuring all processors
44
+ # are available immediately.
45
+ #
46
+ # ## Adding New Options
47
+ #
48
+ # To add a new option processor:
49
+ #
50
+ # 1. Create the processor class (inherit from Option::Base)
51
+ # 2. Add registration call here:
52
+ # ```ruby
53
+ # def register_validators!
54
+ # Registry.register(:required, Validators::RequiredValidator, category: :validator)
55
+ # Registry.register(:my_option, Validators::MyValidator, category: :validator)
56
+ # end
57
+ # ```
58
+ #
59
+ # ## Architecture
60
+ #
61
+ # Works with:
62
+ # - Registry - Stores processor registrations
63
+ # - Option::Base - Base class for all processors
64
+ # - OptionOrchestrator - Uses registered processors
65
+ module RegistryInitializer
66
+ class << self
67
+ # Registers all built-in option processors
68
+ # Called automatically when this file is loaded
69
+ #
70
+ # @return [void]
71
+ def register_all!
72
+ register_validators!
73
+ register_modifiers!
74
+ register_conditionals!
75
+ end
76
+
77
+ private
78
+
79
+ # Registers all built-in validators
80
+ # Position determines execution order (lower = earlier)
81
+ #
82
+ # @return [void]
83
+ def register_validators!
84
+ Registry.register(:type, Validators::TypeValidator, category: :validator, position: 100)
85
+ Registry.register(:required, Validators::RequiredValidator, category: :validator, position: 200)
86
+ Registry.register(:inclusion, Validators::InclusionValidator, category: :validator, position: 300)
87
+ Registry.register(:format, Validators::FormatValidator, category: :validator, position: 400)
88
+ end
89
+
90
+ # Registers all built-in modifiers
91
+ # Position determines execution order (lower = earlier)
92
+ #
93
+ # @return [void]
94
+ def register_modifiers!
95
+ Registry.register(:transform, Modifiers::TransformModifier, category: :modifier, position: 500)
96
+ Registry.register(:cast, Modifiers::CastModifier, category: :modifier, position: 600)
97
+ Registry.register(:computed, Modifiers::ComputedModifier, category: :modifier, position: 700)
98
+ Registry.register(:default, Modifiers::DefaultModifier, category: :modifier, position: 800)
99
+ Registry.register(:as, Modifiers::AsModifier, category: :modifier, position: 900)
100
+ end
101
+
102
+ # Registers all built-in conditionals
103
+ #
104
+ # @return [void]
105
+ def register_conditionals!
106
+ Registry.register(:if, Conditionals::IfConditional, category: :conditional)
107
+ Registry.register(:unless, Conditionals::UnlessConditional, category: :conditional)
108
+ end
109
+ end
110
+ end
111
+ end
112
+ end
113
+ end
114
+ end
115
+
116
+ # Auto-register all options when this file is loaded
117
+ Treaty::Entity::Attribute::Option::RegistryInitializer.register_all!