treaty 0.0.1 → 0.1.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 (76) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +19 -18
  3. data/Rakefile +4 -2
  4. data/lib/treaty/attribute/base.rb +172 -0
  5. data/lib/treaty/attribute/builder/base.rb +142 -0
  6. data/lib/treaty/attribute/collection.rb +65 -0
  7. data/lib/treaty/attribute/helper_mapper.rb +72 -0
  8. data/lib/treaty/attribute/option/base.rb +159 -0
  9. data/lib/treaty/attribute/option/modifiers/as_modifier.rb +87 -0
  10. data/lib/treaty/attribute/option/modifiers/default_modifier.rb +103 -0
  11. data/lib/treaty/attribute/option/registry.rb +128 -0
  12. data/lib/treaty/attribute/option/registry_initializer.rb +90 -0
  13. data/lib/treaty/attribute/option/validators/inclusion_validator.rb +80 -0
  14. data/lib/treaty/attribute/option/validators/required_validator.rb +94 -0
  15. data/lib/treaty/attribute/option/validators/type_validator.rb +153 -0
  16. data/lib/treaty/attribute/option_normalizer.rb +150 -0
  17. data/lib/treaty/attribute/option_orchestrator.rb +186 -0
  18. data/lib/treaty/attribute/validation/attribute_validator.rb +144 -0
  19. data/lib/treaty/attribute/validation/base.rb +93 -0
  20. data/lib/treaty/attribute/validation/nested_array_validator.rb +194 -0
  21. data/lib/treaty/attribute/validation/nested_object_validator.rb +103 -0
  22. data/lib/treaty/attribute/validation/nested_transformer.rb +240 -0
  23. data/lib/treaty/attribute/validation/orchestrator/base.rb +196 -0
  24. data/lib/treaty/base.rb +9 -0
  25. data/lib/treaty/configuration.rb +17 -0
  26. data/lib/treaty/context/callable.rb +24 -0
  27. data/lib/treaty/context/dsl.rb +12 -0
  28. data/lib/treaty/context/workspace.rb +28 -0
  29. data/lib/treaty/controller/dsl.rb +38 -0
  30. data/lib/treaty/engine.rb +37 -0
  31. data/lib/treaty/exceptions/base.rb +8 -0
  32. data/lib/treaty/exceptions/class_name.rb +11 -0
  33. data/lib/treaty/exceptions/deprecated.rb +8 -0
  34. data/lib/treaty/exceptions/execution.rb +8 -0
  35. data/lib/treaty/exceptions/method_name.rb +8 -0
  36. data/lib/treaty/exceptions/nested_attributes.rb +8 -0
  37. data/lib/treaty/exceptions/strategy.rb +8 -0
  38. data/lib/treaty/exceptions/unexpected.rb +8 -0
  39. data/lib/treaty/exceptions/validation.rb +8 -0
  40. data/lib/treaty/info/builder.rb +122 -0
  41. data/lib/treaty/info/dsl.rb +26 -0
  42. data/lib/treaty/info/result.rb +13 -0
  43. data/lib/treaty/request/attribute/attribute.rb +24 -0
  44. data/lib/treaty/request/attribute/builder.rb +22 -0
  45. data/lib/treaty/request/attribute/validation/orchestrator.rb +27 -0
  46. data/lib/treaty/request/attribute/validator.rb +50 -0
  47. data/lib/treaty/request/factory.rb +32 -0
  48. data/lib/treaty/request/scope/collection.rb +21 -0
  49. data/lib/treaty/request/scope/factory.rb +42 -0
  50. data/lib/treaty/response/attribute/attribute.rb +24 -0
  51. data/lib/treaty/response/attribute/builder.rb +22 -0
  52. data/lib/treaty/response/attribute/validation/orchestrator.rb +27 -0
  53. data/lib/treaty/response/attribute/validator.rb +44 -0
  54. data/lib/treaty/response/factory.rb +38 -0
  55. data/lib/treaty/response/scope/collection.rb +21 -0
  56. data/lib/treaty/response/scope/factory.rb +42 -0
  57. data/lib/treaty/result.rb +22 -0
  58. data/lib/treaty/strategy.rb +31 -0
  59. data/lib/treaty/support/loader.rb +24 -0
  60. data/lib/treaty/version.rb +8 -1
  61. data/lib/treaty/versions/collection.rb +15 -0
  62. data/lib/treaty/versions/dsl.rb +30 -0
  63. data/lib/treaty/versions/execution/request.rb +151 -0
  64. data/lib/treaty/versions/executor.rb +14 -0
  65. data/lib/treaty/versions/factory.rb +93 -0
  66. data/lib/treaty/versions/resolver.rb +72 -0
  67. data/lib/treaty/versions/semantic.rb +22 -0
  68. data/lib/treaty/versions/workspace.rb +40 -0
  69. data/lib/treaty.rb +3 -3
  70. metadata +184 -27
  71. data/.standard.yml +0 -3
  72. data/CHANGELOG.md +0 -5
  73. data/CODE_OF_CONDUCT.md +0 -84
  74. data/LICENSE.txt +0 -21
  75. data/sig/treaty.rbs +0 -4
  76. data/treaty.gemspec +0 -35
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 72d96fdd6fd04921e3317e4cb6da530b650b59b7f3ea49df6f9a0eed48074c3c
4
- data.tar.gz: 45838415475fbfabe6570fc29cea16d283edea99aa52f6c09ac1e37cc030c627
3
+ metadata.gz: d99db2724f9dd4fd84a7f315d1acf070dcf70a7f3669dab9980334dc2c179076
4
+ data.tar.gz: d02611a96da26130b8504818fabd9adbc10fa72b8984fc8330e7b63aec79c7ea
5
5
  SHA512:
6
- metadata.gz: b26072eccaa4620c6e5797eb7fc549ba9b4765bb1f2cc56f6bc669cf2c84cc63533e6fb2a1588b89d629245964dc67d20d7031467551c6cfcce74a3149c0f098
7
- data.tar.gz: 90f4a34188088c711d318b1c2a9696e18219a34c9def18b4230f02303b0ed54cda24818d1dbb49568dd6c8ff482c4e65b744a93c656fddeb8b7db97d70cc1a6e
6
+ metadata.gz: 492777e146c0042d9c78df8e9783a17d4aa705c629705425d7cd0db0ce2eaf5d8a2cf58b817c95fe1bc9025739044eb89959e91b04eb7e8dd205c6da9ab68251
7
+ data.tar.gz: 6133706b11654af81a88b32489d88d78f86a66df79a80a072c29fde23e2ea6130e0bee4ffe401def62f52a80ce293954228dd746bc57cf01d38e17833179f350
data/README.md CHANGED
@@ -1,31 +1,32 @@
1
1
  # Treaty
2
2
 
3
- A gem to use pact-verifier via FFI in order to support the latest pact features.
3
+ > [!WARNING]
4
+ > This project is currently under development.
4
5
 
5
- Should be a replacement for `pact-ruby`, but most likely is very much incompatible
6
- due to large specification differences.
6
+ ## Quick Start
7
7
 
8
- :warning: This is just me hacking around with `ffi` and the pact verifier. Do not use this project.
9
- ## Installation
8
+ ### Installation
10
9
 
11
- Install the gem and add to the application's Gemfile by executing:
10
+ ```ruby
11
+ gem "treaty"
12
+ ```
12
13
 
13
- $ bundle add treaty
14
+ ## Documentation
14
15
 
15
- If bundler is not being used to manage dependencies, install the gem by executing:
16
+ Complete documentation is available in the [docs](./docs) directory:
16
17
 
17
- $ gem install treaty
18
-
19
- ## Usage
20
-
21
- ## Development
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
22
23
 
23
24
  ## Contributing
24
25
 
25
- ## License
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.
26
29
 
27
- The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
28
-
29
- ## Code of Conduct
30
+ ## License
30
31
 
31
- Everyone interacting in the Treaty project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/[USERNAME]/treaty/blob/main/CODE_OF_CONDUCT.md).
32
+ Treaty is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
data/Rakefile CHANGED
@@ -5,6 +5,8 @@ require "rspec/core/rake_task"
5
5
 
6
6
  RSpec::Core::RakeTask.new(:spec)
7
7
 
8
- require "standard/rake"
8
+ require "rubocop/rake_task"
9
9
 
10
- task default: %i[spec standard]
10
+ RuboCop::RakeTask.new
11
+
12
+ task default: %i[spec rubocop]
@@ -0,0 +1,172 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Treaty
4
+ module Attribute
5
+ # Base class for all attribute definitions in Treaty DSL.
6
+ #
7
+ # ## Purpose
8
+ #
9
+ # Represents a single attribute defined in request/response scopes.
10
+ # Handles:
11
+ # - Attribute metadata (name, type, nesting level)
12
+ # - Helper mode to simple mode conversion
13
+ # - Simple mode to advanced mode normalization
14
+ # - Nested attributes (for object and array types)
15
+ #
16
+ # ## Usage
17
+ #
18
+ # Attributes are created through DSL methods:
19
+ # string :title, :required
20
+ # integer :age, default: 18
21
+ # object :author do
22
+ # string :name
23
+ # end
24
+ #
25
+ # ## Processing Flow
26
+ #
27
+ # 1. Extract helpers from arguments (`:required`, `:optional`)
28
+ # 2. Convert helpers to simple mode options
29
+ # 3. Merge with explicit options
30
+ # 4. Normalize all options to advanced mode
31
+ # 5. Apply defaults (required: true for request, false for response)
32
+ # 6. Process nested attributes if block given
33
+ #
34
+ # ## Nested Attributes
35
+ #
36
+ # Object and array types can have nested attributes:
37
+ # - `object` - nested attributes as direct children
38
+ # - `array` - nested attributes define array element structure
39
+ #
40
+ # Special scope `:_self` is used for simple arrays:
41
+ # array :tags do
42
+ # string :_self # Array of strings
43
+ # end
44
+ class Base
45
+ attr_reader :name,
46
+ :type,
47
+ :options,
48
+ :nesting_level
49
+
50
+ # Creates a new attribute instance
51
+ #
52
+ # @param name [Symbol] The attribute name
53
+ # @param type [Symbol] The attribute type (:string, :integer, :object, :array, etc.)
54
+ # @param helpers [Array<Symbol>] Helper symbols (:required, :optional)
55
+ # @param nesting_level [Integer] Current nesting depth (default: 0)
56
+ # @param options [Hash] Attribute options (required, default, as, etc.)
57
+ # @param block [Proc] Block for defining nested attributes (for object/array types)
58
+ def initialize(name, type, *helpers, nesting_level: 0, **options, &block)
59
+ @name = name
60
+ @type = type
61
+ @nesting_level = nesting_level
62
+
63
+ validate_nesting_level!
64
+
65
+ # Separate helpers from non-helper symbols.
66
+ @helpers = extract_helpers(helpers)
67
+
68
+ # Merge helper options with explicit options.
69
+ merged_options = merge_options(@helpers, options)
70
+
71
+ # Normalize all options to advanced mode.
72
+ @options = OptionNormalizer.normalize(merged_options)
73
+
74
+ apply_defaults!
75
+
76
+ # Process nested attributes for object and array types.
77
+ process_nested_attributes(&block) if block_given?
78
+ end
79
+
80
+ # Returns collection of nested attributes for this attribute
81
+ #
82
+ # @return [Collection] Collection of nested attributes
83
+ def collection_of_attributes
84
+ @collection_of_attributes ||= Collection.new
85
+ end
86
+
87
+ # Checks if this attribute has nested attributes
88
+ #
89
+ # @return [Boolean] True if attribute is object/array with nested attributes
90
+ def nested?
91
+ object_or_array? && collection_of_attributes.exists?
92
+ end
93
+
94
+ # Checks if this attribute is an object or array type
95
+ #
96
+ # @return [Boolean] True if type is :object or :array
97
+ def object_or_array?
98
+ object? || array?
99
+ end
100
+
101
+ # Checks if this attribute is an object type
102
+ #
103
+ # @return [Boolean] True if type is :object
104
+ def object?
105
+ @type == :object
106
+ end
107
+
108
+ # Checks if this attribute is an array type
109
+ #
110
+ # @return [Boolean] True if type is :array
111
+ def array?
112
+ @type == :array
113
+ end
114
+
115
+ private
116
+
117
+ # Validates that nesting level doesn't exceed maximum allowed depth
118
+ #
119
+ # @raise [Treaty::Exceptions::NestedAttributes] If nesting exceeds limit
120
+ # @return [void]
121
+ def validate_nesting_level!
122
+ return unless @nesting_level > Treaty::Engine.config.treaty.attribute_nesting_level
123
+
124
+ # TODO: It is necessary to implement a translation system (I18n).
125
+ raise Treaty::Exceptions::NestedAttributes,
126
+ "Nesting level #{@nesting_level} exceeds maximum allowed level of " \
127
+ "#{Treaty::Engine.config.treaty.attribute_nesting_level}"
128
+ end
129
+
130
+ # Extracts helper symbols from arguments
131
+ #
132
+ # @param helpers [Array] Mixed array that may contain helper symbols
133
+ # @return [Array<Symbol>] Filtered array of valid helper symbols
134
+ def extract_helpers(helpers)
135
+ helpers.select do |helper|
136
+ helper.is_a?(Symbol) && HelperMapper.helper?(helper)
137
+ end
138
+ end
139
+
140
+ # Merges helper-derived options with explicit options
141
+ #
142
+ # @param helpers [Array<Symbol>] Helper symbols to convert
143
+ # @param explicit_options [Hash] Explicitly provided options
144
+ # @return [Hash] Merged options hash
145
+ def merge_options(helpers, explicit_options)
146
+ helper_options = HelperMapper.map(helpers)
147
+ helper_options.merge(explicit_options)
148
+ end
149
+
150
+ # Applies default values for options based on context (request/response)
151
+ # Must be implemented in subclasses
152
+ #
153
+ # @raise [NotImplementedError] If subclass doesn't implement
154
+ # @return [void]
155
+ def apply_defaults!
156
+ # Must be implemented in subclasses
157
+ raise NotImplementedError, "#{self.class} must implement #apply_defaults!"
158
+ end
159
+
160
+ # Processes nested attributes block for object/array types
161
+ # Must be implemented in subclasses
162
+ #
163
+ # @param block [Proc] Block containing nested attribute definitions
164
+ # @raise [NotImplementedError] If subclass doesn't implement
165
+ # @return [void]
166
+ def process_nested_attributes(&block)
167
+ # Must be implemented in subclasses
168
+ raise NotImplementedError, "#{self.class} must implement #process_nested_attributes"
169
+ end
170
+ end
171
+ end
172
+ end
@@ -0,0 +1,142 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Treaty
4
+ module Attribute
5
+ module Builder
6
+ # Base DSL builder for defining attributes in request/response scopes.
7
+ #
8
+ # ## Purpose
9
+ #
10
+ # Provides the DSL interface for defining attributes within scopes.
11
+ # Handles method_missing magic to support type-based method calls.
12
+ #
13
+ # ## Responsibilities
14
+ #
15
+ # 1. **DSL Interface** - Provides clean syntax for attribute definitions
16
+ # 2. **Method Dispatch** - Routes type methods (string, integer, etc.) to attribute creation
17
+ # 3. **Helper Support** - Handles helper symbols in various positions
18
+ # 4. **Nesting Tracking** - Tracks nesting level for nested attributes
19
+ #
20
+ # ## DSL Usage
21
+ #
22
+ # The builder enables this clean DSL syntax:
23
+ #
24
+ # ```ruby
25
+ # request do
26
+ # scope :user do
27
+ # string :name, :required
28
+ # integer :age, default: 18
29
+ # object :profile do
30
+ # string :bio
31
+ # end
32
+ # end
33
+ # end
34
+ # ```
35
+ #
36
+ # ## Method Dispatch
37
+ #
38
+ # ### Type-based Methods
39
+ # When you call `string :name`, it routes through `method_missing`:
40
+ # 1. `string` becomes the type
41
+ # 2. `:name` becomes the attribute name
42
+ # 3. Calls `attribute(:name, :string, ...)`
43
+ #
44
+ # ### Helper Position Handling
45
+ # Handles helpers in different positions:
46
+ #
47
+ # ```ruby
48
+ # string :required, :name # Helper first, then name
49
+ # string :name, :required # Name first, then helper
50
+ # ```
51
+ #
52
+ # Both resolve to the same attribute definition.
53
+ #
54
+ # ## Nesting
55
+ #
56
+ # Tracks nesting level for:
57
+ # - Validation (enforcing maximum nesting depth)
58
+ # - Error messages (showing context)
59
+ #
60
+ # Maximum nesting level is configured in Treaty::Engine.config.
61
+ #
62
+ # ## Subclass Requirements
63
+ #
64
+ # Subclasses must implement:
65
+ # - `create_attribute` - Creates the appropriate attribute type (Request/Response)
66
+ #
67
+ # ## Architecture
68
+ #
69
+ # Used by:
70
+ # - Request::Builder - For request attribute definitions
71
+ # - Response::Builder - For response attribute definitions
72
+ class Base
73
+ attr_reader :nesting_level,
74
+ :collection_of_attributes
75
+
76
+ # Creates a new builder instance
77
+ #
78
+ # @param collection_of_attributes [Collection] Collection to add attributes to
79
+ # @param nesting_level [Integer] Current nesting depth
80
+ def initialize(collection_of_attributes, nesting_level)
81
+ @collection_of_attributes = collection_of_attributes
82
+ @nesting_level = nesting_level
83
+ end
84
+
85
+ # Defines an attribute with explicit type
86
+ #
87
+ # @param name [Symbol] The attribute name
88
+ # @param type [Symbol] The attribute type
89
+ # @param helpers [Array<Symbol>] Helper symbols (:required, :optional)
90
+ # @param options [Hash] Attribute options
91
+ # @param block [Proc] Block for nested attributes
92
+ # @return [void]
93
+ def attribute(name, type, *helpers, **options, &block)
94
+ @collection_of_attributes << create_attribute(
95
+ name,
96
+ type,
97
+ *helpers,
98
+ nesting_level: @nesting_level,
99
+ **options,
100
+ &block
101
+ )
102
+ end
103
+
104
+ # Handles DSL methods like `string :name` where method name is the type
105
+ #
106
+ # @param type [Symbol] The attribute type (method name)
107
+ # @param name [Symbol] The attribute name (first argument)
108
+ # @param helpers [Array<Symbol>] Helper symbols
109
+ # @param options [Hash] Attribute options
110
+ # @param block [Proc] Block for nested attributes
111
+ # @return [void]
112
+ def method_missing(type, name, *helpers, **options, &block)
113
+ if name.is_a?(Symbol) && HelperMapper.helper?(name)
114
+ helpers.unshift(name)
115
+ name = helpers.shift
116
+ end
117
+
118
+ attribute(name, type, *helpers, **options, &block)
119
+ end
120
+
121
+ # Checks if method should be handled by method_missing
122
+ #
123
+ # @param name [Symbol] Method name
124
+ # @return [Boolean]
125
+ def respond_to_missing?(name, *)
126
+ super
127
+ end
128
+
129
+ private
130
+
131
+ # Creates an attribute instance (must be implemented in subclasses)
132
+ #
133
+ # @raise [NotImplementedError] If subclass doesn't implement
134
+ # @return [Attribute::Base] Created attribute instance
135
+ def create_attribute(*)
136
+ # Must be implemented in subclasses
137
+ raise NotImplementedError, "#{self.class} must implement #create_attribute"
138
+ end
139
+ end
140
+ end
141
+ end
142
+ end
@@ -0,0 +1,65 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "forwardable"
4
+
5
+ module Treaty
6
+ module Attribute
7
+ # Collection wrapper for sets of attributes.
8
+ #
9
+ # ## Purpose
10
+ #
11
+ # Provides a unified interface for working with collections of attributes.
12
+ # Uses Ruby Set internally for uniqueness but exposes Array-like interface.
13
+ #
14
+ # ## Usage
15
+ #
16
+ # Used internally by:
17
+ # - Scope factories (to store attributes in a scope)
18
+ # - Attribute::Base (to store nested attributes)
19
+ #
20
+ # ## Methods
21
+ #
22
+ # Delegates common collection methods to internal Set:
23
+ # - `<<` - Add attribute
24
+ # - `each`, `map`, `select`, `reject` - Iteration
25
+ # - `find`, `first` - Access
26
+ # - `size`, `empty?` - Size checks
27
+ # - `to_h` - Convert to hash
28
+ #
29
+ # Custom methods:
30
+ # - `exists?` - Returns true if collection is not empty
31
+ #
32
+ # ## Example
33
+ #
34
+ # collection = Collection.new
35
+ # collection << Attribute::Base.new(:name, :string)
36
+ # collection << Attribute::Base.new(:age, :integer)
37
+ # collection.size # => 2
38
+ # collection.exists? # => true
39
+ class Collection
40
+ extend Forwardable
41
+
42
+ def_delegators :@collection,
43
+ :<<,
44
+ :to_h, :map,
45
+ :each_with_object, :each,
46
+ :select, :reject, :size,
47
+ :find, :first,
48
+ :empty?
49
+
50
+ # Creates a new collection instance
51
+ #
52
+ # @param collection [Set] Initial collection (default: empty Set)
53
+ def initialize(collection = Set.new)
54
+ @collection = collection
55
+ end
56
+
57
+ # Checks if collection has any elements
58
+ #
59
+ # @return [Boolean] True if collection is not empty
60
+ def exists?
61
+ !empty?
62
+ end
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,72 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Treaty
4
+ module Attribute
5
+ # Maps DSL helper symbols to their simple mode option equivalents.
6
+ #
7
+ # ## Purpose
8
+ #
9
+ # Helpers provide the most concise syntax for common options.
10
+ # They are syntactic sugar that gets converted to simple mode options.
11
+ #
12
+ # ## Available Helpers
13
+ #
14
+ # - `:required` → `required: true`
15
+ # - `:optional` → `required: false`
16
+ #
17
+ # ## Usage Examples
18
+ #
19
+ # Helper mode (most concise):
20
+ # string :title, :required
21
+ # string :bio, :optional
22
+ #
23
+ # Equivalent to simple mode:
24
+ # string :title, required: true
25
+ # string :bio, required: false
26
+ #
27
+ # ## Processing Flow
28
+ #
29
+ # 1. Helper mode: `string :title, :required`
30
+ # 2. HelperMapper: `:required` → `required: true`
31
+ # 3. OptionNormalizer: `required: true` → `{ is: true, message: nil }`
32
+ # 4. Final: Advanced mode used internally
33
+ #
34
+ # ## Adding New Helpers
35
+ #
36
+ # To add a new helper:
37
+ # ```ruby
38
+ # HELPER_MAPPINGS = {
39
+ # required: { required: true },
40
+ # optional: { required: false },
41
+ # my_helper: { my_option: :smth } # New helper example
42
+ # }.freeze
43
+ # ```
44
+ class HelperMapper
45
+ HELPER_MAPPINGS = {
46
+ required: { required: true },
47
+ optional: { required: false }
48
+ }.freeze
49
+
50
+ class << self
51
+ # Maps helper symbols to their simple mode equivalents
52
+ #
53
+ # @param helpers [Array<Symbol>] Array of helper symbols
54
+ # @return [Hash] Simple mode options hash
55
+ def map(helpers)
56
+ helpers.each_with_object({}) do |helper, result|
57
+ mapping = HELPER_MAPPINGS.fetch(helper)
58
+ result.merge!(mapping) if mapping.present?
59
+ end
60
+ end
61
+
62
+ # Checks if a symbol is a registered helper
63
+ #
64
+ # @param symbol [Symbol] Symbol to check
65
+ # @return [Boolean] True if symbol is a helper
66
+ def helper?(symbol)
67
+ HELPER_MAPPINGS.key?(symbol)
68
+ end
69
+ end
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,159 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Treaty
4
+ module Attribute
5
+ module Option
6
+ # Base class for all option processors (validators and modifiers).
7
+ #
8
+ # ## Option Modes
9
+ #
10
+ # Treaty supports two modes for defining options:
11
+ #
12
+ # 1. **Simple Mode** - Concise syntax for common cases:
13
+ # - `required: true`
14
+ # - `as: :value`
15
+ # - `default: 12`
16
+ # - `in: %w[twitter linkedin]`
17
+ #
18
+ # 2. **Advanced Mode** - Extended syntax with custom messages:
19
+ # - `required: { is: true, message: "Custom error" }`
20
+ # - `as: { is: :value, message: nil }`
21
+ # - `inclusion: { in: %w[...], message: "Must be one of..." }`
22
+ #
23
+ # ## Helpers
24
+ #
25
+ # Helpers are shortcuts in DSL that map to simple mode options:
26
+ # - `:required` → `required: true`
27
+ # - `:optional` → `required: false`
28
+ #
29
+ # ## Advanced Mode Keys
30
+ #
31
+ # Each option in advanced mode has a value key:
32
+ # - Default key: `:is` (used by most options)
33
+ # - Special key: `:in` (used by inclusion validator)
34
+ #
35
+ # The value key is defined by overriding `value_key` method in subclasses.
36
+ #
37
+ # ## Processing Phases
38
+ #
39
+ # Each option processor can participate in three phases:
40
+ # - Phase 1: Schema validation (validate DSL definition correctness)
41
+ # - Phase 2: Value validation (validate runtime data values)
42
+ # - Phase 3: Value transformation (transform values: defaults, renaming, etc.)
43
+ class Base
44
+ # Creates a new option processor instance
45
+ #
46
+ # @param attribute_name [Symbol] The name of the attribute
47
+ # @param attribute_type [Symbol] The type of the attribute
48
+ # @param option_schema [Object] The option schema (simple or advanced mode)
49
+ def initialize(attribute_name:, attribute_type:, option_schema:)
50
+ @attribute_name = attribute_name
51
+ @attribute_type = attribute_type
52
+ @option_schema = option_schema
53
+ end
54
+
55
+ # Phase 1: Validates schema (DSL definition)
56
+ # Override in subclasses if validation is needed
57
+ #
58
+ # @raise [Treaty::Exceptions::Validation] If schema is invalid
59
+ # @return [void]
60
+ def validate_schema!
61
+ # No-op by default
62
+ end
63
+
64
+ # Phase 2: Validates value (runtime data)
65
+ # Override in subclasses if validation is needed
66
+ #
67
+ # @param value [Object] The value to validate
68
+ # @raise [Treaty::Exceptions::Validation] If value is invalid
69
+ # @return [void]
70
+ def validate_value!(value)
71
+ # No-op by default
72
+ end
73
+
74
+ # Phase 3: Transforms value
75
+ # Returns transformed value or original if no transformation needed
76
+ # Override in subclasses if transformation is needed
77
+ #
78
+ # @param value [Object] The value to transform
79
+ # @return [Object] Transformed value
80
+ def transform_value(value)
81
+ value
82
+ end
83
+
84
+ # Indicates if this option processor transforms attribute names
85
+ # Override in subclasses if needed (e.g., AsModifier)
86
+ #
87
+ # @return [Boolean] True if this processor transforms names
88
+ def transforms_name?
89
+ false
90
+ end
91
+
92
+ # Returns the target name for the attribute if this processor transforms names
93
+ # Override in subclasses if needed (e.g., AsModifier)
94
+ #
95
+ # @return [Symbol] The target attribute name
96
+ def target_name
97
+ @attribute_name
98
+ end
99
+
100
+ protected
101
+
102
+ # Returns the value key for this option in advanced mode
103
+ # Default is :is, but can be overridden (e.g., :in for inclusion)
104
+ #
105
+ # @return [Symbol] The key used to store the value in advanced mode
106
+ def value_key
107
+ :is
108
+ end
109
+
110
+ # Checks if option is enabled
111
+ # Handles both simple mode (boolean) and advanced mode (hash with value key)
112
+ #
113
+ # @return [Boolean] Whether the option is enabled
114
+ def option_enabled?
115
+ return false if @option_schema.nil?
116
+ return @option_schema if @option_schema.is_a?(TrueClass) || @option_schema.is_a?(FalseClass)
117
+
118
+ @option_schema.fetch(value_key, false)
119
+ end
120
+
121
+ # Extracts the actual value from normalized schema
122
+ # Works with both simple mode and advanced mode
123
+ #
124
+ # In simple mode: returns the value directly
125
+ # In advanced mode: extracts value using the appropriate key (is/in)
126
+ #
127
+ # @return [Object] The actual value from the option schema
128
+ def option_value
129
+ return @option_schema unless @option_schema.is_a?(Hash)
130
+
131
+ @option_schema.fetch(value_key, nil)
132
+ end
133
+
134
+ # Gets custom error message from advanced mode schema
135
+ #
136
+ # @return [String, nil] Custom error message or nil for default message
137
+ def custom_message
138
+ return nil unless @option_schema.is_a?(Hash)
139
+
140
+ @option_schema.fetch(:message, nil)
141
+ end
142
+
143
+ # Checks if schema is in advanced mode
144
+ #
145
+ # @return [Boolean] True if schema is in advanced mode (hash with value key)
146
+ def advanced_mode?
147
+ @option_schema.is_a?(Hash) && @option_schema.key?(value_key)
148
+ end
149
+
150
+ # Checks if schema is in simple mode
151
+ #
152
+ # @return [Boolean] True if schema is in simple mode (not a hash or no value key)
153
+ def simple_mode?
154
+ !advanced_mode?
155
+ end
156
+ end
157
+ end
158
+ end
159
+ end