treaty 0.18.0 → 0.20.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (129) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +1 -1
  3. data/config/locales/en.yml +3 -3
  4. data/lib/treaty/action/base.rb +11 -0
  5. data/lib/treaty/action/context/callable.rb +90 -0
  6. data/lib/treaty/action/context/dsl.rb +56 -0
  7. data/lib/treaty/action/context/workspace.rb +92 -0
  8. data/lib/treaty/action/executor/inventory.rb +136 -0
  9. data/lib/treaty/{info/rest → action/info}/builder.rb +2 -2
  10. data/lib/treaty/{info/rest → action/info}/dsl.rb +2 -2
  11. data/lib/treaty/{info/rest → action/info}/result.rb +2 -2
  12. data/lib/treaty/action/inventory/collection.rb +77 -0
  13. data/lib/treaty/action/inventory/factory.rb +108 -0
  14. data/lib/treaty/action/inventory/inventory.rb +146 -0
  15. data/lib/treaty/action/request/attribute/attribute.rb +76 -0
  16. data/lib/treaty/action/request/attribute/builder.rb +98 -0
  17. data/lib/treaty/action/request/entity.rb +78 -0
  18. data/lib/treaty/action/request/factory.rb +116 -0
  19. data/lib/treaty/action/request/validator.rb +120 -0
  20. data/lib/treaty/action/response/attribute/attribute.rb +79 -0
  21. data/lib/treaty/action/response/attribute/builder.rb +96 -0
  22. data/lib/treaty/action/response/entity.rb +79 -0
  23. data/lib/treaty/action/response/factory.rb +129 -0
  24. data/lib/treaty/action/response/validator.rb +111 -0
  25. data/lib/treaty/action/result.rb +81 -0
  26. data/lib/treaty/action/versions/collection.rb +47 -0
  27. data/lib/treaty/action/versions/dsl.rb +116 -0
  28. data/lib/treaty/action/versions/execution/request.rb +287 -0
  29. data/lib/treaty/action/versions/executor.rb +61 -0
  30. data/lib/treaty/action/versions/factory.rb +253 -0
  31. data/lib/treaty/action/versions/resolver.rb +150 -0
  32. data/lib/treaty/action/versions/semantic.rb +64 -0
  33. data/lib/treaty/action/versions/workspace.rb +106 -0
  34. data/lib/treaty/action.rb +31 -0
  35. data/lib/treaty/controller/dsl.rb +1 -1
  36. data/lib/treaty/engine.rb +1 -1
  37. data/lib/treaty/{attribute/entity → entity/attribute}/attribute.rb +4 -4
  38. data/lib/treaty/entity/attribute/base.rb +184 -0
  39. data/lib/treaty/entity/attribute/builder/base.rb +275 -0
  40. data/lib/treaty/entity/attribute/collection.rb +67 -0
  41. data/lib/treaty/entity/attribute/dsl.rb +92 -0
  42. data/lib/treaty/entity/attribute/helper_mapper.rb +74 -0
  43. data/lib/treaty/entity/attribute/option/base.rb +190 -0
  44. data/lib/treaty/entity/attribute/option/conditionals/base.rb +92 -0
  45. data/lib/treaty/entity/attribute/option/conditionals/if_conditional.rb +136 -0
  46. data/lib/treaty/entity/attribute/option/conditionals/unless_conditional.rb +153 -0
  47. data/lib/treaty/entity/attribute/option/modifiers/as_modifier.rb +93 -0
  48. data/lib/treaty/entity/attribute/option/modifiers/cast_modifier.rb +285 -0
  49. data/lib/treaty/entity/attribute/option/modifiers/computed_modifier.rb +128 -0
  50. data/lib/treaty/entity/attribute/option/modifiers/default_modifier.rb +105 -0
  51. data/lib/treaty/entity/attribute/option/modifiers/transform_modifier.rb +114 -0
  52. data/lib/treaty/entity/attribute/option/registry.rb +157 -0
  53. data/lib/treaty/entity/attribute/option/registry_initializer.rb +117 -0
  54. data/lib/treaty/entity/attribute/option/validators/format_validator.rb +222 -0
  55. data/lib/treaty/entity/attribute/option/validators/inclusion_validator.rb +94 -0
  56. data/lib/treaty/entity/attribute/option/validators/required_validator.rb +100 -0
  57. data/lib/treaty/entity/attribute/option/validators/type_validator.rb +219 -0
  58. data/lib/treaty/entity/attribute/option_normalizer.rb +168 -0
  59. data/lib/treaty/entity/attribute/option_orchestrator.rb +192 -0
  60. data/lib/treaty/entity/attribute/validation/attribute_validator.rb +147 -0
  61. data/lib/treaty/entity/attribute/validation/base.rb +76 -0
  62. data/lib/treaty/entity/attribute/validation/nested_array_validator.rb +207 -0
  63. data/lib/treaty/entity/attribute/validation/nested_object_validator.rb +105 -0
  64. data/lib/treaty/entity/attribute/validation/nested_transformer.rb +432 -0
  65. data/lib/treaty/entity/attribute/validation/orchestrator/base.rb +262 -0
  66. data/lib/treaty/entity/base.rb +90 -0
  67. data/lib/treaty/entity/builder.rb +101 -0
  68. data/lib/treaty/{info/entity → entity/info}/builder.rb +8 -8
  69. data/lib/treaty/{info/entity → entity/info}/dsl.rb +2 -2
  70. data/lib/treaty/{info/entity → entity/info}/result.rb +2 -2
  71. data/lib/treaty/entity.rb +7 -79
  72. data/lib/treaty/version.rb +1 -1
  73. metadata +66 -64
  74. data/lib/treaty/attribute/base.rb +0 -182
  75. data/lib/treaty/attribute/builder/base.rb +0 -273
  76. data/lib/treaty/attribute/collection.rb +0 -65
  77. data/lib/treaty/attribute/dsl.rb +0 -90
  78. data/lib/treaty/attribute/entity/builder.rb +0 -46
  79. data/lib/treaty/attribute/helper_mapper.rb +0 -72
  80. data/lib/treaty/attribute/option/base.rb +0 -188
  81. data/lib/treaty/attribute/option/conditionals/base.rb +0 -90
  82. data/lib/treaty/attribute/option/conditionals/if_conditional.rb +0 -134
  83. data/lib/treaty/attribute/option/conditionals/unless_conditional.rb +0 -151
  84. data/lib/treaty/attribute/option/modifiers/as_modifier.rb +0 -91
  85. data/lib/treaty/attribute/option/modifiers/cast_modifier.rb +0 -283
  86. data/lib/treaty/attribute/option/modifiers/computed_modifier.rb +0 -126
  87. data/lib/treaty/attribute/option/modifiers/default_modifier.rb +0 -103
  88. data/lib/treaty/attribute/option/modifiers/transform_modifier.rb +0 -112
  89. data/lib/treaty/attribute/option/registry.rb +0 -155
  90. data/lib/treaty/attribute/option/registry_initializer.rb +0 -115
  91. data/lib/treaty/attribute/option/validators/format_validator.rb +0 -220
  92. data/lib/treaty/attribute/option/validators/inclusion_validator.rb +0 -92
  93. data/lib/treaty/attribute/option/validators/required_validator.rb +0 -98
  94. data/lib/treaty/attribute/option/validators/type_validator.rb +0 -217
  95. data/lib/treaty/attribute/option_normalizer.rb +0 -166
  96. data/lib/treaty/attribute/option_orchestrator.rb +0 -190
  97. data/lib/treaty/attribute/validation/attribute_validator.rb +0 -145
  98. data/lib/treaty/attribute/validation/base.rb +0 -74
  99. data/lib/treaty/attribute/validation/nested_array_validator.rb +0 -205
  100. data/lib/treaty/attribute/validation/nested_object_validator.rb +0 -103
  101. data/lib/treaty/attribute/validation/nested_transformer.rb +0 -430
  102. data/lib/treaty/attribute/validation/orchestrator/base.rb +0 -260
  103. data/lib/treaty/base.rb +0 -9
  104. data/lib/treaty/context/callable.rb +0 -26
  105. data/lib/treaty/context/dsl.rb +0 -12
  106. data/lib/treaty/context/workspace.rb +0 -32
  107. data/lib/treaty/executor/inventory.rb +0 -122
  108. data/lib/treaty/inventory/collection.rb +0 -71
  109. data/lib/treaty/inventory/factory.rb +0 -91
  110. data/lib/treaty/inventory/inventory.rb +0 -92
  111. data/lib/treaty/request/attribute/attribute.rb +0 -25
  112. data/lib/treaty/request/attribute/builder.rb +0 -46
  113. data/lib/treaty/request/entity.rb +0 -33
  114. data/lib/treaty/request/factory.rb +0 -81
  115. data/lib/treaty/request/validator.rb +0 -60
  116. data/lib/treaty/response/attribute/attribute.rb +0 -25
  117. data/lib/treaty/response/attribute/builder.rb +0 -46
  118. data/lib/treaty/response/entity.rb +0 -33
  119. data/lib/treaty/response/factory.rb +0 -87
  120. data/lib/treaty/response/validator.rb +0 -53
  121. data/lib/treaty/result.rb +0 -23
  122. data/lib/treaty/versions/collection.rb +0 -15
  123. data/lib/treaty/versions/dsl.rb +0 -42
  124. data/lib/treaty/versions/execution/request.rb +0 -177
  125. data/lib/treaty/versions/executor.rb +0 -14
  126. data/lib/treaty/versions/factory.rb +0 -112
  127. data/lib/treaty/versions/resolver.rb +0 -70
  128. data/lib/treaty/versions/semantic.rb +0 -22
  129. data/lib/treaty/versions/workspace.rb +0 -43
@@ -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!
@@ -0,0 +1,222 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Treaty
4
+ module Entity
5
+ module Attribute
6
+ module Option
7
+ module Validators
8
+ # Validates that string attribute value matches a specific format.
9
+ #
10
+ # ## Supported Formats
11
+ #
12
+ # - `:uuid` - Universally unique identifier
13
+ # - `:email` - Email address (RFC 2822 compliant)
14
+ # - `:password` - Password (8-16 chars, must contain digit, lowercase, and uppercase)
15
+ # - `:duration` - ActiveSupport::Duration compatible string (e.g., "1 day", "2 hours")
16
+ # - `:date` - ISO 8601 date string (e.g., "2025-01-15")
17
+ # - `:datetime` - ISO 8601 datetime string (e.g., "2025-01-15T10:30:00Z")
18
+ # - `:time` - Time string (e.g., "10:30:00", "10:30 AM")
19
+ # - `:boolean` - Boolean string ("true", "false", "0", "1")
20
+ #
21
+ # ## Usage Examples
22
+ #
23
+ # Simple mode:
24
+ # string :email, format: :email
25
+ # string :started_on, format: :date
26
+ #
27
+ # Advanced mode:
28
+ # string :email, format: { is: :email }
29
+ # string :started_on, format: { is: :date, message: "Invalid date format" }
30
+ # string :started_on, format: { is: :date, message: ->(attribute:, value:, **) { "#{attribute} has invalid date: #{value}" } } # rubocop:disable Layout/LineLength
31
+ #
32
+ # ## Validation Rules
33
+ #
34
+ # - Only works with `:string` type attributes
35
+ # - Raises Treaty::Exceptions::Validation if used with non-string types
36
+ # - Skips validation for nil values (handled by RequiredValidator)
37
+ # - Each format has a pattern and/or validator for flexible validation
38
+ #
39
+ # ## Extensibility
40
+ #
41
+ # To add new formats, extend DEFAULT_FORMATS hash with format definition:
42
+ # DEFAULT_FORMATS[:custom_format] = {
43
+ # pattern: /regex/,
44
+ # validator: ->(value) { custom_validation_logic }
45
+ # }
46
+ class FormatValidator < Treaty::Entity::Attribute::Option::Base # rubocop:disable Metrics/ClassLength
47
+ # UUID format regex (8-4-4-4-12 hexadecimal pattern)
48
+ UUID_PATTERN = /\A[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\z/i
49
+
50
+ # Password format regex (8-16 chars, at least one digit, lowercase, and uppercase)
51
+ PASSWORD_PATTERN = /\A(?=.*[0-9])(?=.*[a-z])(?=.*[A-Z]).{8,16}\z/
52
+
53
+ # Boolean string format regex (accepts "true", "false", "0", "1" case-insensitive)
54
+ BOOLEAN_PATTERN = /\A(true|false|0|1)\z/i
55
+
56
+ # Default format definitions with patterns and validators
57
+ # Each format can have:
58
+ # - pattern: Regex for pattern matching
59
+ # - validator: Lambda for custom validation logic
60
+ DEFAULT_FORMATS = {
61
+ uuid: {
62
+ pattern: UUID_PATTERN,
63
+ validator: nil
64
+ },
65
+ email: {
66
+ pattern: URI::MailTo::EMAIL_REGEXP,
67
+ validator: nil
68
+ },
69
+ password: {
70
+ pattern: PASSWORD_PATTERN,
71
+ validator: nil
72
+ },
73
+ duration: {
74
+ pattern: nil,
75
+ validator: lambda do |value|
76
+ ActiveSupport::Duration.parse(value)
77
+ true
78
+ rescue StandardError
79
+ false
80
+ end
81
+ },
82
+ date: {
83
+ pattern: nil,
84
+ validator: lambda do |value|
85
+ Date.parse(value)
86
+ true
87
+ rescue ArgumentError, TypeError
88
+ false
89
+ end
90
+ },
91
+ datetime: {
92
+ pattern: nil,
93
+ validator: lambda do |value|
94
+ DateTime.parse(value)
95
+ true
96
+ rescue ArgumentError, TypeError
97
+ false
98
+ end
99
+ },
100
+ time: {
101
+ pattern: nil,
102
+ validator: lambda do |value|
103
+ Time.parse(value)
104
+ true
105
+ rescue ArgumentError, TypeError
106
+ false
107
+ end
108
+ },
109
+ boolean: {
110
+ pattern: BOOLEAN_PATTERN,
111
+ validator: nil
112
+ }
113
+ }.freeze
114
+
115
+ # Validates that format is only used with string type attributes
116
+ # and that the format name is valid
117
+ #
118
+ # @raise [Treaty::Exceptions::Validation] If format is used with non-string type
119
+ # @raise [Treaty::Exceptions::Validation] If format name is unknown
120
+ # @return [void]
121
+ def validate_schema! # rubocop:disable Metrics/MethodLength
122
+ # Format option only works with string types
123
+ unless @attribute_type == :string
124
+ raise Treaty::Exceptions::Validation,
125
+ I18n.t(
126
+ "treaty.attributes.validators.format.type_mismatch",
127
+ attribute: @attribute_name,
128
+ type: @attribute_type
129
+ )
130
+ end
131
+
132
+ format_name = option_value
133
+
134
+ # Validate that format name exists
135
+ return if formats.key?(format_name)
136
+
137
+ raise Treaty::Exceptions::Validation,
138
+ I18n.t(
139
+ "treaty.attributes.validators.format.unknown_format",
140
+ attribute: @attribute_name,
141
+ format_name:,
142
+ allowed: formats.keys.join(", ")
143
+ )
144
+ end
145
+
146
+ # Validates that the value matches the specified format
147
+ # Skips validation for nil values (handled by RequiredValidator)
148
+ #
149
+ # @param value [String] The value to validate
150
+ # @raise [Treaty::Exceptions::Validation] If value doesn't match format
151
+ # @return [void]
152
+ def validate_value!(value) # rubocop:disable Metrics/MethodLength, Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
153
+ return if value.nil? # Format validation doesn't check for nil, required does.
154
+
155
+ format_name = option_value
156
+ format_definition = formats[format_name]
157
+
158
+ # Allow blank values (empty strings should be caught by required validator)
159
+ return if value.to_s.strip.empty?
160
+
161
+ # Apply pattern matching if defined
162
+ if format_definition.fetch(:pattern)
163
+ return if value.match?(format_definition.fetch(:pattern))
164
+
165
+ # Pattern failed, and no validator - raise error
166
+ unless format_definition.fetch(:validator)
167
+ attributes = {
168
+ attribute: @attribute_name,
169
+ value:,
170
+ format_name:
171
+ }
172
+
173
+ message = resolve_custom_message(**attributes) || default_message(**attributes)
174
+
175
+ raise Treaty::Exceptions::Validation, message
176
+ end
177
+ end
178
+
179
+ # Apply validator if defined
180
+ return unless format_definition.fetch(:validator)
181
+ return if format_definition.fetch(:validator).call(value)
182
+
183
+ attributes = {
184
+ attribute: @attribute_name,
185
+ value:,
186
+ format_name:
187
+ }
188
+
189
+ message = resolve_custom_message(**attributes) || default_message(**attributes)
190
+
191
+ raise Treaty::Exceptions::Validation, message
192
+ end
193
+
194
+ private
195
+
196
+ # Returns the available formats (allows for extension)
197
+ #
198
+ # @return [Hash] Hash of available formats with their definitions
199
+ def formats
200
+ DEFAULT_FORMATS
201
+ end
202
+
203
+ # Generates default error message for format validation failure using I18n
204
+ #
205
+ # @param attribute [Symbol] The attribute name
206
+ # @param value [Object] The actual value that failed validation
207
+ # @param format_name [Symbol] The format name
208
+ # @return [String] Default error message
209
+ def default_message(attribute:, value:, format_name:)
210
+ I18n.t(
211
+ "treaty.attributes.validators.format.mismatch",
212
+ attribute:,
213
+ value:,
214
+ format_name:
215
+ )
216
+ end
217
+ end
218
+ end
219
+ end
220
+ end
221
+ end
222
+ end
@@ -0,0 +1,94 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Treaty
4
+ module Entity
5
+ module Attribute
6
+ module Option
7
+ module Validators
8
+ # Validates that attribute value is included in allowed set.
9
+ #
10
+ # ## Usage Examples
11
+ #
12
+ # Simple mode:
13
+ # string :provider, in: %w[twitter linkedin github]
14
+ #
15
+ # Advanced mode:
16
+ # string :provider, inclusion: { in: %w[twitter linkedin github], message: "Invalid provider" }
17
+ #
18
+ # ## Advanced Mode
19
+ #
20
+ # Uses `:in` as the value key (instead of default `:is`).
21
+ # Schema format: `{ in: [...], message: nil }`
22
+ class InclusionValidator < Treaty::Entity::Attribute::Option::Base
23
+ # Validates that allowed values are provided as non-empty array
24
+ #
25
+ # @raise [Treaty::Exceptions::Validation] If allowed values are not valid
26
+ # @return [void]
27
+ def validate_schema!
28
+ allowed_values = option_value
29
+
30
+ return if allowed_values.is_a?(Array) && !allowed_values.empty?
31
+
32
+ raise Treaty::Exceptions::Validation,
33
+ I18n.t(
34
+ "treaty.attributes.validators.inclusion.invalid_schema",
35
+ attribute: @attribute_name
36
+ )
37
+ end
38
+
39
+ # Validates that value is included in allowed set
40
+ # Skips validation for nil values (handled by RequiredValidator)
41
+ #
42
+ # @param value [Object] The value to validate
43
+ # @raise [Treaty::Exceptions::Validation] If value is not in allowed set
44
+ # @return [void]
45
+ def validate_value!(value)
46
+ return if value.nil? # Inclusion validation doesn't check for nil, required does.
47
+
48
+ allowed_values = option_value
49
+
50
+ return if allowed_values.include?(value)
51
+
52
+ attributes = {
53
+ attribute: @attribute_name,
54
+ value:,
55
+ allowed_values:
56
+ }
57
+
58
+ message = resolve_custom_message(**attributes) || default_message(**attributes)
59
+
60
+ raise Treaty::Exceptions::Validation, message
61
+ end
62
+
63
+ protected
64
+
65
+ # Returns the value key for inclusion validator
66
+ # Uses :in instead of default :is
67
+ #
68
+ # @return [Symbol] The value key (:in)
69
+ def value_key
70
+ :in
71
+ end
72
+
73
+ private
74
+
75
+ # Generates default error message with allowed values using I18n
76
+ #
77
+ # @param attribute [Symbol] The attribute name
78
+ # @param value [Object] The actual value that failed validation
79
+ # @param allowed_values [Array] Array of allowed values
80
+ # @return [String] Default error message
81
+ def default_message(attribute:, value:, allowed_values:)
82
+ I18n.t(
83
+ "treaty.attributes.validators.inclusion.not_included",
84
+ attribute:,
85
+ allowed: allowed_values.join(", "),
86
+ value:
87
+ )
88
+ end
89
+ end
90
+ end
91
+ end
92
+ end
93
+ end
94
+ end
@@ -0,0 +1,100 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Treaty
4
+ module Entity
5
+ module Attribute
6
+ module Option
7
+ module Validators
8
+ # Validates that attribute value is present (not nil and not empty).
9
+ #
10
+ # ## Usage Examples
11
+ #
12
+ # Helper mode:
13
+ # string :title, :required # Maps to required: true
14
+ # string :bio, :optional # Maps to required: false
15
+ #
16
+ # Simple mode:
17
+ # string :title, required: true
18
+ # string :bio, required: false
19
+ #
20
+ # Advanced mode:
21
+ # string :title, required: { is: true, message: "Title is mandatory" }
22
+ #
23
+ # ## Default Behavior
24
+ #
25
+ # - Request attributes: required by default (required: true)
26
+ # - Response attributes: optional by default (required: false)
27
+ #
28
+ # ## Validation Rules
29
+ #
30
+ # A value is considered present if:
31
+ # - It is not nil
32
+ # - It is not empty (for String, Array, Hash)
33
+ #
34
+ # ## Advanced Mode
35
+ #
36
+ # Schema format: `{ is: true/false, message: nil }`
37
+ class RequiredValidator < Treaty::Entity::Attribute::Option::Base
38
+ # Validates schema (no validation needed, already normalized)
39
+ #
40
+ # @return [void]
41
+ def validate_schema!
42
+ # Schema structure is already normalized by OptionNormalizer.
43
+ # Nothing to validate here.
44
+ end
45
+
46
+ # Validates that required attribute has a present value
47
+ #
48
+ # @param value [Object] The value to validate
49
+ # @raise [Treaty::Exceptions::Validation] If required but value is missing/empty
50
+ # @return [void]
51
+ def validate_value!(value)
52
+ return unless required?
53
+ return if present?(value)
54
+
55
+ message = resolve_custom_message(
56
+ attribute: @attribute_name,
57
+ value:
58
+ ) || default_message
59
+
60
+ raise Treaty::Exceptions::Validation, message
61
+ end
62
+
63
+ private
64
+
65
+ # Checks if attribute is required
66
+ #
67
+ # @return [Boolean] True if attribute is required
68
+ def required?
69
+ return false if @option_schema.nil?
70
+
71
+ # Use option_value helper which correctly extracts value based on mode
72
+ option_value == true
73
+ end
74
+
75
+ # Checks if value is present (not nil and not empty)
76
+ #
77
+ # @param value [Object] The value to check
78
+ # @return [Boolean] True if value is present
79
+ def present?(value)
80
+ return false if value.nil?
81
+ return false if value.respond_to?(:empty?) && value.empty?
82
+
83
+ true
84
+ end
85
+
86
+ # Generates default error message using I18n
87
+ #
88
+ # @return [String] Default error message
89
+ def default_message
90
+ I18n.t(
91
+ "treaty.attributes.validators.required.blank",
92
+ attribute: @attribute_name
93
+ )
94
+ end
95
+ end
96
+ end
97
+ end
98
+ end
99
+ end
100
+ end