treaty 0.1.0 → 0.3.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 (36) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +104 -16
  3. data/config/locales/en.yml +97 -0
  4. data/lib/treaty/attribute/base.rb +10 -8
  5. data/lib/treaty/attribute/builder/base.rb +3 -2
  6. data/lib/treaty/attribute/option/base.rb +1 -0
  7. data/lib/treaty/attribute/option/modifiers/as_modifier.rb +3 -2
  8. data/lib/treaty/attribute/option/validators/inclusion_validator.rb +6 -6
  9. data/lib/treaty/attribute/option/validators/required_validator.rb +2 -4
  10. data/lib/treaty/attribute/option/validators/type_validator.rb +39 -15
  11. data/lib/treaty/attribute/option_normalizer.rb +2 -1
  12. data/lib/treaty/attribute/option_orchestrator.rb +4 -3
  13. data/lib/treaty/attribute/validation/base.rb +2 -3
  14. data/lib/treaty/attribute/validation/nested_array_validator.rb +12 -7
  15. data/lib/treaty/attribute/validation/nested_transformer.rb +13 -7
  16. data/lib/treaty/attribute/validation/orchestrator/base.rb +2 -4
  17. data/lib/treaty/controller/dsl.rb +2 -2
  18. data/lib/treaty/exceptions/base.rb +39 -0
  19. data/lib/treaty/exceptions/class_name.rb +39 -0
  20. data/lib/treaty/exceptions/deprecated.rb +46 -0
  21. data/lib/treaty/exceptions/execution.rb +58 -0
  22. data/lib/treaty/exceptions/method_name.rb +47 -0
  23. data/lib/treaty/exceptions/nested_attributes.rb +57 -0
  24. data/lib/treaty/exceptions/not_implemented.rb +32 -0
  25. data/lib/treaty/exceptions/strategy.rb +55 -0
  26. data/lib/treaty/exceptions/unexpected.rb +62 -0
  27. data/lib/treaty/exceptions/validation.rb +89 -0
  28. data/lib/treaty/info/builder.rb +3 -3
  29. data/lib/treaty/request/attribute/attribute.rb +1 -1
  30. data/lib/treaty/response/attribute/attribute.rb +1 -1
  31. data/lib/treaty/strategy.rb +2 -2
  32. data/lib/treaty/version.rb +1 -1
  33. data/lib/treaty/versions/execution/request.rb +24 -28
  34. data/lib/treaty/versions/factory.rb +3 -4
  35. data/lib/treaty/versions/resolver.rb +3 -6
  36. metadata +21 -5
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d99db2724f9dd4fd84a7f315d1acf070dcf70a7f3669dab9980334dc2c179076
4
- data.tar.gz: d02611a96da26130b8504818fabd9adbc10fa72b8984fc8330e7b63aec79c7ea
3
+ metadata.gz: 34516da12bec1bd3df351363dacd13fc3e7e9c31806fef4a6aecf37be88754a8
4
+ data.tar.gz: 71bb387ce08aa6a3d42de04bef6e2c7e281089184e0815ad2679a9b87df30213
5
5
  SHA512:
6
- metadata.gz: 492777e146c0042d9c78df8e9783a17d4aa705c629705425d7cd0db0ce2eaf5d8a2cf58b817c95fe1bc9025739044eb89959e91b04eb7e8dd205c6da9ab68251
7
- data.tar.gz: 6133706b11654af81a88b32489d88d78f86a66df79a80a072c29fde23e2ea6130e0bee4ffe401def62f52a80ce293954228dd746bc57cf01d38e17833179f350
6
+ metadata.gz: '0398e7fa2f958bb24331f239cedd5db66880e2e08f402b82659b63a99941176e42ff64c867bdcd5c2aae054e4b35410f132d44f37724046c14b5c6967e9c6e6a'
7
+ data.tar.gz: 25433a8f90b4e6ee4528e3fa933be6ad7a6443ad593b12c99b9cc2fb3a110723b92690a2bbb2843e7de14fc27f5e7b806008fd361c31bbd4c74bd2a906e18b7f
data/README.md CHANGED
@@ -1,32 +1,120 @@
1
- # Treaty
1
+ <div align="center">
2
+ <h1>Treaty</h1>
3
+ <p>A Ruby library for defining and managing REST API contracts with versioning support.</p>
4
+ </div>
2
5
 
3
- > [!WARNING]
4
- > This project is currently under development.
6
+ <div align="center">
5
7
 
6
- ## Quick Start
8
+ [![Gem Version](https://img.shields.io/gem/v/treaty.svg)](https://rubygems.org/gems/treaty)
9
+ [![Release Date](https://img.shields.io/github/release-date/servactory/treaty)](https://github.com/servactory/servactory/releases)
10
+ [![Gem Downloads](https://img.shields.io/gem/dt/treaty.svg)](https://rubygems.org/gems/treaty)
11
+ ![Ruby Version](https://img.shields.io/badge/Ruby-3.2%2B-red)
12
+
13
+ </div>
14
+
15
+ ## 📚 Documentation
16
+
17
+ Explore comprehensive guides and documentation at [docs](./docs):
18
+
19
+ - [Getting Started](./docs/getting-started.md) - installation and configuration
20
+ - [Core Concepts](./docs/core-concepts.md) - understand fundamental concepts
21
+ - [API Reference](./docs/api-reference.md) - complete API documentation
22
+ - [Examples](./docs/examples.md) - practical real-world examples
23
+ - [Internationalization](./docs/internationalization.md) - I18n and multilingual support
24
+ - [Full Documentation Index](./docs/README.md) - all documentation topics
25
+
26
+ ## 💡 Why Treaty?
27
+
28
+ Treaty provides a complete solution for building versioned APIs in Ruby on Rails:
29
+
30
+ - **Type Safety** - Enforce strict type checking for request and response data
31
+ - **API Versioning** - Manage multiple concurrent API versions effortlessly
32
+ - **Built-in Validation** - Validate incoming requests and outgoing responses automatically
33
+ - **Data Transformation** - Transform data seamlessly between different API versions
34
+ - **Deprecation Management** - Mark versions as deprecated with flexible conditions
35
+ - **Internationalization** - Full I18n support for multilingual error messages
36
+ - **Well-documented** - Comprehensive guides and examples for every feature
37
+
38
+ ## 🚀 Quick Start
7
39
 
8
40
  ### Installation
9
41
 
42
+ Add Treaty to your Gemfile:
43
+
10
44
  ```ruby
11
45
  gem "treaty"
12
46
  ```
13
47
 
14
- ## Documentation
48
+ Run:
15
49
 
16
- Complete documentation is available in the [docs](./docs) directory:
50
+ ```bash
51
+ bundle install
52
+ ```
17
53
 
18
- - [Getting Started](./docs/getting-started.md) - installation and first steps
19
- - [Core Concepts](./docs/core-concepts.md) - fundamental concepts
20
- - [API Reference](./docs/api-reference.md) - complete API documentation
21
- - [Examples](./docs/examples.md) - practical examples
22
- - [Full Documentation Index](./docs/README.md) - all documentation topics
54
+ ### Define Treaty
55
+
56
+ Create your first API contract in `app/treaties/posts/create_treaty.rb`:
57
+
58
+ ```ruby
59
+ module Posts
60
+ class CreateTreaty < ApplicationTreaty
61
+ version 1, default: true do
62
+ strategy Treaty::Strategy::ADAPTER
63
+
64
+ request do
65
+ scope :post do
66
+ string :title, :required
67
+ string :content, :required
68
+ string :summary, :optional
69
+ end
70
+ end
71
+
72
+ response 201 do
73
+ scope :post do
74
+ string :id
75
+ string :title
76
+ string :content
77
+ string :summary
78
+ datetime :created_at
79
+ end
80
+ end
81
+
82
+ delegate_to Posts::CreateService
83
+ end
84
+ end
85
+ end
86
+ ```
87
+
88
+ ### Use in Controller
89
+
90
+ Define the treaty in your controller `app/controllers/posts_controller.rb`:
91
+
92
+ ```ruby
93
+ class PostsController < ApplicationController
94
+ # Treaty automatically:
95
+ # 1. Validates incoming parameters according to request definition
96
+ # 2. Calls Posts::CreateService with validated data
97
+ # 3. Validates service response according to response definition
98
+ # 4. Returns transformed data to client
99
+ treaty :create
100
+ end
101
+ ```
102
+
103
+ ## 🤝 Contributing
104
+
105
+ We welcome contributions! You can help by:
106
+
107
+ - Reporting bugs and suggesting features
108
+ - Writing code and improving documentation
109
+ - Reviewing pull requests
110
+ - Sharing your experience with Treaty
111
+
112
+ Please read our [Contributing Guide](./CONTRIBUTING.md) before submitting a pull request.
23
113
 
24
- ## Contributing
114
+ ## 🙏 Acknowledgments
25
115
 
26
- This project is intended to be a safe, welcoming space for collaboration.
27
- Contributors are expected to adhere to the [Contributor Covenant](http://contributor-covenant.org) code of conduct.
28
- We recommend reading the [contributing guide](./CONTRIBUTING.md) as well.
116
+ Thank you to all [contributors](https://github.com/servactory/treaty/graphs/contributors) who have helped make Treaty better!
29
117
 
30
- ## License
118
+ ## 📄 License
31
119
 
32
120
  Treaty is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
@@ -0,0 +1,97 @@
1
+ en:
2
+ treaty:
3
+ # ============================================================================
4
+ # Attributes: Definition, validation, and processing
5
+ # ============================================================================
6
+ attributes:
7
+ # Attribute value validators
8
+ validators:
9
+ required:
10
+ blank: "Attribute '%{attribute}' is required but was not provided or is empty"
11
+
12
+ type:
13
+ unknown_type: "Unknown type '%{type}' for attribute '%{attribute}'. Allowed types: %{allowed}"
14
+ mismatch:
15
+ integer: "Attribute '%{attribute}' must be an Integer, got %{actual}"
16
+ string: "Attribute '%{attribute}' must be a String, got %{actual}"
17
+ boolean: "Attribute '%{attribute}' must be a Boolean (true or false), got %{actual}"
18
+ object: "Attribute '%{attribute}' must be a Hash (object), got %{actual}"
19
+ array: "Attribute '%{attribute}' must be an Array, got %{actual}"
20
+ datetime: "Attribute '%{attribute}' must be a DateTime/Time/Date, got %{actual}"
21
+
22
+ inclusion:
23
+ invalid_schema: "Option 'inclusion' for attribute '%{attribute}' must have a non-empty array of allowed values"
24
+ not_included: "Attribute '%{attribute}' must be one of: %{allowed}. Got: '%{value}'"
25
+
26
+ # Nested structures validation
27
+ nested:
28
+ # Orchestrator errors
29
+ orchestrator:
30
+ collection_not_implemented: "Subclass must implement the collection_of_scopes method"
31
+ scope_data_not_implemented: "Subclass must implement the scope_data_for method"
32
+
33
+ # Array validation errors
34
+ array:
35
+ element_validation_error: "Error in array '%{attribute}' at index %{index}: Element must match one of the defined types. Errors: %{errors}"
36
+ element_type_error: "Error in array '%{attribute}' at index %{index}: Expected Hash but got %{actual}"
37
+ attribute_error: "Error in array '%{attribute}' at index %{index}: %{message}"
38
+
39
+ # Attribute options
40
+ options:
41
+ unknown: "Unknown options for attribute '%{attribute}': %{unknown}. Known options: %{known}"
42
+
43
+ # Attribute modifiers
44
+ modifiers:
45
+ as:
46
+ invalid_type: "Option 'as' for attribute '%{attribute}' must be a Symbol. Got: %{type}"
47
+
48
+ # Attribute builder DSL
49
+ builder:
50
+ not_implemented: "%{class} must implement #create_attribute"
51
+
52
+ # Attribute-level errors
53
+ errors:
54
+ nesting_level_exceeded: "Nesting level %{level} exceeds maximum allowed level of %{max_level}"
55
+ apply_defaults_not_implemented: "%{class} must implement #apply_defaults!"
56
+ process_nested_not_implemented: "%{class} must implement #process_nested_attributes"
57
+
58
+ # ============================================================================
59
+ # Versioning: API version management and resolution
60
+ # ============================================================================
61
+ versioning:
62
+ # Version resolver
63
+ resolver:
64
+ current_version_required: "Current version is required for validation"
65
+ version_not_found: "Version %{version} not found in treaty definition"
66
+ version_deprecated: "Version %{version} is deprecated and cannot be used"
67
+
68
+ # Version factory
69
+ factory:
70
+ invalid_default_option: "Default option for version must be true, false, or a Proc, got: %{type}"
71
+ unknown_method: "Unknown method: %{method}"
72
+
73
+ # Strategy validation
74
+ strategy:
75
+ unknown: "Unknown strategy: %{strategy}"
76
+
77
+ # ============================================================================
78
+ # Execution: Service and executor invocation
79
+ # ============================================================================
80
+ execution:
81
+ executor_missing: "Executor is not defined for version %{version}"
82
+ executor_empty: "Executor cannot be an empty string"
83
+ executor_not_found: "Executor class `%{class_name}` not found"
84
+ executor_invalid_type: "Invalid executor type: %{type}. Expected Proc, Class, String, or Symbol"
85
+ method_not_found: "Method '%{method}' not found in class '%{class_name}'"
86
+ proc_error: "%{message}"
87
+ servactory_input_error: "%{message}"
88
+ servactory_internal_error: "%{message}"
89
+ servactory_output_error: "%{message}"
90
+ servactory_failure_error: "%{message}"
91
+ regular_service_error: "%{message}"
92
+
93
+ # ============================================================================
94
+ # Controller DSL: Rails controller integration
95
+ # ============================================================================
96
+ controller:
97
+ treaty_class_not_found: "%{class_name}"
@@ -121,10 +121,10 @@ module Treaty
121
121
  def validate_nesting_level!
122
122
  return unless @nesting_level > Treaty::Engine.config.treaty.attribute_nesting_level
123
123
 
124
- # TODO: It is necessary to implement a translation system (I18n).
125
124
  raise Treaty::Exceptions::NestedAttributes,
126
- "Nesting level #{@nesting_level} exceeds maximum allowed level of " \
127
- "#{Treaty::Engine.config.treaty.attribute_nesting_level}"
125
+ I18n.t("treaty.attributes.errors.nesting_level_exceeded",
126
+ level: @nesting_level,
127
+ max_level: Treaty::Engine.config.treaty.attribute_nesting_level)
128
128
  end
129
129
 
130
130
  # Extracts helper symbols from arguments
@@ -150,22 +150,24 @@ module Treaty
150
150
  # Applies default values for options based on context (request/response)
151
151
  # Must be implemented in subclasses
152
152
  #
153
- # @raise [NotImplementedError] If subclass doesn't implement
153
+ # @raise [Treaty::Exceptions::NotImplemented] If subclass doesn't implement
154
154
  # @return [void]
155
155
  def apply_defaults!
156
156
  # Must be implemented in subclasses
157
- raise NotImplementedError, "#{self.class} must implement #apply_defaults!"
157
+ raise Treaty::Exceptions::NotImplemented,
158
+ I18n.t("treaty.attributes.errors.apply_defaults_not_implemented", class: self.class)
158
159
  end
159
160
 
160
161
  # Processes nested attributes block for object/array types
161
162
  # Must be implemented in subclasses
162
163
  #
163
164
  # @param block [Proc] Block containing nested attribute definitions
164
- # @raise [NotImplementedError] If subclass doesn't implement
165
+ # @raise [Treaty::Exceptions::NotImplemented] If subclass doesn't implement
165
166
  # @return [void]
166
- def process_nested_attributes(&block)
167
+ def process_nested_attributes
167
168
  # Must be implemented in subclasses
168
- raise NotImplementedError, "#{self.class} must implement #process_nested_attributes"
169
+ raise Treaty::Exceptions::NotImplemented,
170
+ I18n.t("treaty.attributes.errors.process_nested_not_implemented", class: self.class)
169
171
  end
170
172
  end
171
173
  end
@@ -130,11 +130,12 @@ module Treaty
130
130
 
131
131
  # Creates an attribute instance (must be implemented in subclasses)
132
132
  #
133
- # @raise [NotImplementedError] If subclass doesn't implement
133
+ # @raise [Treaty::Exceptions::NotImplemented] If subclass doesn't implement
134
134
  # @return [Attribute::Base] Created attribute instance
135
135
  def create_attribute(*)
136
136
  # Must be implemented in subclasses
137
- raise NotImplementedError, "#{self.class} must implement #create_attribute"
137
+ raise Treaty::Exceptions::NotImplemented,
138
+ I18n.t("treaty.attributes.builder.not_implemented", class: self.class)
138
139
  end
139
140
  end
140
141
  end
@@ -132,6 +132,7 @@ module Treaty
132
132
  end
133
133
 
134
134
  # Gets custom error message from advanced mode schema
135
+ # Returns nil if no custom message, which triggers I18n default message
135
136
  #
136
137
  # @return [String, nil] Custom error message or nil for default message
137
138
  def custom_message
@@ -53,9 +53,10 @@ module Treaty
53
53
 
54
54
  return if target.is_a?(Symbol)
55
55
 
56
- # TODO: It is necessary to implement a translation system (I18n).
57
56
  raise Treaty::Exceptions::Validation,
58
- "Option 'as' for attribute '#{@attribute_name}' must be a Symbol. Got: #{target.class}"
57
+ I18n.t("treaty.attributes.modifiers.as.invalid_type",
58
+ attribute: @attribute_name,
59
+ type: target.class)
59
60
  end
60
61
 
61
62
  # Indicates that AsModifier transforms attribute names
@@ -28,9 +28,8 @@ module Treaty
28
28
 
29
29
  return if allowed_values.is_a?(Array) && !allowed_values.empty?
30
30
 
31
- # TODO: It is necessary to implement a translation system (I18n).
32
31
  raise Treaty::Exceptions::Validation,
33
- "Option 'inclusion' for attribute '#{@attribute_name}' must have a non-empty array of allowed values"
32
+ I18n.t("treaty.attributes.validators.inclusion.invalid_schema", attribute: @attribute_name)
34
33
  end
35
34
 
36
35
  # Validates that value is included in allowed set
@@ -48,7 +47,6 @@ module Treaty
48
47
 
49
48
  message = custom_message || default_message(allowed_values, value)
50
49
 
51
- # TODO: It is necessary to implement a translation system (I18n).
52
50
  raise Treaty::Exceptions::Validation, message
53
51
  end
54
52
 
@@ -64,14 +62,16 @@ module Treaty
64
62
 
65
63
  private
66
64
 
67
- # Generates default error message with allowed values
65
+ # Generates default error message with allowed values using I18n
68
66
  #
69
67
  # @param allowed_values [Array] Array of allowed values
70
68
  # @param value [Object] The actual value that failed validation
71
69
  # @return [String] Default error message
72
70
  def default_message(allowed_values, value)
73
- # TODO: It is necessary to implement a translation system (I18n).
74
- "Attribute '#{@attribute_name}' must be one of: #{allowed_values.join(', ')}. Got: '#{value}'"
71
+ I18n.t("treaty.attributes.validators.inclusion.not_included",
72
+ attribute: @attribute_name,
73
+ allowed: allowed_values.join(", "),
74
+ value:)
75
75
  end
76
76
  end
77
77
  end
@@ -53,7 +53,6 @@ module Treaty
53
53
 
54
54
  message = custom_message || default_message
55
55
 
56
- # TODO: It is necessary to implement a translation system (I18n).
57
56
  raise Treaty::Exceptions::Validation, message
58
57
  end
59
58
 
@@ -80,12 +79,11 @@ module Treaty
80
79
  true
81
80
  end
82
81
 
83
- # Generates default error message
82
+ # Generates default error message using I18n
84
83
  #
85
84
  # @return [String] Default error message
86
85
  def default_message
87
- # TODO: It is necessary to implement a translation system (I18n).
88
- "Attribute '#{@attribute_name}' is required but was not provided or is empty"
86
+ I18n.t("treaty.attributes.validators.required.blank", attribute: @attribute_name)
89
87
  end
90
88
  end
91
89
  end
@@ -10,6 +10,7 @@ module Treaty
10
10
  #
11
11
  # - `:integer` - Ruby Integer
12
12
  # - `:string` - Ruby String
13
+ # - `:boolean` - Ruby TrueClass or FalseClass
13
14
  # - `:object` - Ruby Hash (for nested objects)
14
15
  # - `:array` - Ruby Array (for collections)
15
16
  # - `:datetime` - Ruby DateTime, Time, or Date
@@ -19,6 +20,7 @@ module Treaty
19
20
  # Simple types:
20
21
  # integer :age
21
22
  # string :name
23
+ # boolean :published
22
24
  # datetime :created_at
23
25
  #
24
26
  # Nested structures:
@@ -41,7 +43,7 @@ module Treaty
41
43
  # TypeValidator doesn't use option_schema - it validates based on attribute_type.
42
44
  # This validator is always active for all attributes.
43
45
  class TypeValidator < Treaty::Attribute::Option::Base
44
- ALLOWED_TYPES = %i[integer string object array datetime].freeze
46
+ ALLOWED_TYPES = %i[integer string boolean object array datetime].freeze
45
47
 
46
48
  # Validates that the attribute type is one of the allowed types
47
49
  #
@@ -50,10 +52,11 @@ module Treaty
50
52
  def validate_schema!
51
53
  return if ALLOWED_TYPES.include?(@attribute_type)
52
54
 
53
- # TODO: It is necessary to implement a translation system (I18n).
54
55
  raise Treaty::Exceptions::Validation,
55
- "Unknown type '#{@attribute_type}' for attribute '#{@attribute_name}'. " \
56
- "Allowed types: #{ALLOWED_TYPES.join(', ')}"
56
+ I18n.t("treaty.attributes.validators.type.unknown_type",
57
+ type: @attribute_type,
58
+ attribute: @attribute_name,
59
+ allowed: ALLOWED_TYPES.join(", "))
57
60
  end
58
61
 
59
62
  # Validates that the value matches the declared type
@@ -62,7 +65,7 @@ module Treaty
62
65
  # @param value [Object] The value to validate
63
66
  # @raise [Treaty::Exceptions::Validation] If value type doesn't match
64
67
  # @return [void]
65
- def validate_value!(value) # rubocop:disable Metrics/MethodLength
68
+ def validate_value!(value) # rubocop:disable Metrics/MethodLength, Metrics/CyclomaticComplexity
66
69
  return if value.nil? # Type validation doesn't check for nil, required does.
67
70
 
68
71
  case @attribute_type
@@ -70,6 +73,8 @@ module Treaty
70
73
  validate_integer!(value)
71
74
  when :string
72
75
  validate_string!(value)
76
+ when :boolean
77
+ validate_boolean!(value)
73
78
  when :object
74
79
  validate_object!(value)
75
80
  when :array
@@ -89,9 +94,10 @@ module Treaty
89
94
  def validate_integer!(value)
90
95
  return if value.is_a?(Integer)
91
96
 
92
- # TODO: It is necessary to implement a translation system (I18n).
93
97
  raise Treaty::Exceptions::Validation,
94
- "Attribute '#{@attribute_name}' must be an Integer, got #{value.class}"
98
+ I18n.t("treaty.attributes.validators.type.mismatch.integer",
99
+ attribute: @attribute_name,
100
+ actual: value.class)
95
101
  end
96
102
 
97
103
  # Validates that value is a String
@@ -102,9 +108,24 @@ module Treaty
102
108
  def validate_string!(value)
103
109
  return if value.is_a?(String)
104
110
 
105
- # TODO: It is necessary to implement a translation system (I18n).
106
111
  raise Treaty::Exceptions::Validation,
107
- "Attribute '#{@attribute_name}' must be a String, got #{value.class}"
112
+ I18n.t("treaty.attributes.validators.type.mismatch.string",
113
+ attribute: @attribute_name,
114
+ actual: value.class)
115
+ end
116
+
117
+ # Validates that value is a Boolean (TrueClass or FalseClass)
118
+ #
119
+ # @param value [Object] The value to validate
120
+ # @raise [Treaty::Exceptions::Validation] If value is not a Boolean
121
+ # @return [void]
122
+ def validate_boolean!(value)
123
+ return if value.is_a?(TrueClass) || value.is_a?(FalseClass)
124
+
125
+ raise Treaty::Exceptions::Validation,
126
+ I18n.t("treaty.attributes.validators.type.mismatch.boolean",
127
+ attribute: @attribute_name,
128
+ actual: value.class)
108
129
  end
109
130
 
110
131
  # Validates that value is a Hash (object type)
@@ -115,9 +136,10 @@ module Treaty
115
136
  def validate_object!(value)
116
137
  return if value.is_a?(Hash)
117
138
 
118
- # TODO: It is necessary to implement a translation system (I18n).
119
139
  raise Treaty::Exceptions::Validation,
120
- "Attribute '#{@attribute_name}' must be a Hash (object), got #{value.class}"
140
+ I18n.t("treaty.attributes.validators.type.mismatch.object",
141
+ attribute: @attribute_name,
142
+ actual: value.class)
121
143
  end
122
144
 
123
145
  # Validates that value is an Array
@@ -128,9 +150,10 @@ module Treaty
128
150
  def validate_array!(value)
129
151
  return if value.is_a?(Array)
130
152
 
131
- # TODO: It is necessary to implement a translation system (I18n).
132
153
  raise Treaty::Exceptions::Validation,
133
- "Attribute '#{@attribute_name}' must be an Array, got #{value.class}"
154
+ I18n.t("treaty.attributes.validators.type.mismatch.array",
155
+ attribute: @attribute_name,
156
+ actual: value.class)
134
157
  end
135
158
 
136
159
  # Validates that value is a DateTime, Time, or Date
@@ -142,9 +165,10 @@ module Treaty
142
165
  # TODO: It is better to divide it into different methods for each class.
143
166
  return if value.is_a?(DateTime) || value.is_a?(Time) || value.is_a?(Date)
144
167
 
145
- # TODO: It is necessary to implement a translation system (I18n).
146
168
  raise Treaty::Exceptions::Validation,
147
- "Attribute '#{@attribute_name}' must be a DateTime/Time/Date, got #{value.class}"
169
+ I18n.t("treaty.attributes.validators.type.mismatch.datetime",
170
+ attribute: @attribute_name,
171
+ actual: value.class)
148
172
  end
149
173
  end
150
174
  end
@@ -128,10 +128,11 @@ module Treaty
128
128
  def normalize_value(value, value_key)
129
129
  if advanced_mode?(value, value_key)
130
130
  # Already in advanced mode, ensure it has both keys.
131
+ # message: nil means use I18n default message from validators
131
132
  { value_key => value.fetch(value_key), message: value.fetch(:message, nil) }
132
133
  else
133
134
  # Simple mode, convert to advanced.
134
- # TODO: It is necessary to implement a translation system (I18n).
135
+ # message: nil means use I18n default message from validators
135
136
  { value_key => value, message: nil }
136
137
  end
137
138
  end
@@ -176,10 +176,11 @@ module Treaty
176
176
 
177
177
  return if unknown_options.empty?
178
178
 
179
- # TODO: It is necessary to implement a translation system (I18n).
180
179
  raise Treaty::Exceptions::Validation,
181
- "Unknown options for attribute '#{@attribute.name}': #{unknown_options.join(', ')}. " \
182
- "Known options: #{Option::Registry.all_options.join(', ')}"
180
+ I18n.t("treaty.attributes.options.unknown",
181
+ attribute: @attribute.name,
182
+ unknown: unknown_options.join(", "),
183
+ known: Option::Registry.all_options.join(", "))
183
184
  end
184
185
  end
185
186
  end
@@ -71,12 +71,11 @@ module Treaty
71
71
  # Performs validation and transformation
72
72
  # Must be implemented in subclasses
73
73
  #
74
- # @raise [NotImplementedError] If subclass doesn't implement
74
+ # @raise [Treaty::Exceptions::NotImplemented] If subclass doesn't implement
75
75
  # @return [Hash] Validated and transformed data
76
76
  def validate!
77
- # TODO: It is necessary to implement a translation system (I18n).
78
77
  raise Treaty::Exceptions::Validation,
79
- "Subclass must implement the validate! method"
78
+ I18n.t("treaty.attributes.validators.nested.orchestrator.collection_not_implemented")
80
79
  end
81
80
 
82
81
  private
@@ -114,10 +114,11 @@ module Treaty
114
114
 
115
115
  return if validated
116
116
 
117
- # TODO: It is necessary to implement a translation system (I18n).
118
117
  raise Treaty::Exceptions::Validation,
119
- "Error in array '#{@attribute.name}' at index #{index}: " \
120
- "Item must match one of the defined types. Errors: #{errors.join('; ')}"
118
+ I18n.t("treaty.attributes.validators.nested.array.element_validation_error",
119
+ attribute: @attribute.name,
120
+ index:,
121
+ errors: errors.join("; "))
121
122
  end
122
123
 
123
124
  # Validates array item for complex arrays (with regular attributes)
@@ -130,18 +131,22 @@ module Treaty
130
131
  # @return [void]
131
132
  def validate_regular_array_item!(array_item, index) # rubocop:disable Metrics/MethodLength
132
133
  unless array_item.is_a?(Hash)
133
- # TODO: It is necessary to implement a translation system (I18n).
134
134
  raise Treaty::Exceptions::Validation,
135
- "Error in array '#{@attribute.name}' at index #{index}: Expected Hash but got #{array_item.class}"
135
+ I18n.t("treaty.attributes.validators.nested.array.element_type_error",
136
+ attribute: @attribute.name,
137
+ index:,
138
+ actual: array_item.class)
136
139
  end
137
140
 
138
141
  regular_validators.each do |nested_attribute, validator|
139
142
  nested_value = array_item.fetch(nested_attribute.name, nil)
140
143
  validator.validate_value!(nested_value)
141
144
  rescue Treaty::Exceptions::Validation => e
142
- # TODO: It is necessary to implement a translation system (I18n).
143
145
  raise Treaty::Exceptions::Validation,
144
- "Error in array '#{@attribute.name}' at index #{index}: #{e.message}"
146
+ I18n.t("treaty.attributes.validators.nested.array.attribute_error",
147
+ attribute: @attribute.name,
148
+ index:,
149
+ message: e.message)
145
150
  end
146
151
  end
147
152
 
@@ -171,9 +171,10 @@ module Treaty
171
171
  validator.validate_value!(item)
172
172
  rescue Treaty::Exceptions::Validation => e
173
173
  raise Treaty::Exceptions::Validation,
174
- "Error in array '#{attribute.name}' at index #{index}: " \
175
- "Element must match one of the defined types. " \
176
- "Errors: #{e.message}"
174
+ I18n.t("treaty.attributes.validators.nested.array.element_validation_error",
175
+ attribute: attribute.name,
176
+ index:,
177
+ errors: e.message)
177
178
  end
178
179
  end
179
180
 
@@ -183,11 +184,13 @@ module Treaty
183
184
  # @param index [Integer] Element index for error messages
184
185
  # @raise [Treaty::Exceptions::Validation] If item is not a Hash
185
186
  # @return [Hash] Transformed hash
186
- def transform_array_item(item, index)
187
+ def transform_array_item(item, index) # rubocop:disable Metrics/MethodLength
187
188
  unless item.is_a?(Hash)
188
189
  raise Treaty::Exceptions::Validation,
189
- "Error in array '#{attribute.name}' at index #{index}: " \
190
- "Expected Hash but got #{item.class}"
190
+ I18n.t("treaty.attributes.validators.nested.array.element_type_error",
191
+ attribute: attribute.name,
192
+ index:,
193
+ actual: item.class)
191
194
  end
192
195
 
193
196
  transformed = {}
@@ -227,7 +230,10 @@ module Treaty
227
230
  end
228
231
  rescue Treaty::Exceptions::Validation => e
229
232
  raise Treaty::Exceptions::Validation,
230
- "Error in array '#{attribute.name}' at index #{index}: #{e.message}"
233
+ I18n.t("treaty.attributes.validators.nested.array.attribute_error",
234
+ attribute: attribute.name,
235
+ index:,
236
+ message: e.message)
231
237
  end
232
238
 
233
239
  target_name = validator.target_name