treaty 0.7.0 → 0.8.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 (47) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +5 -0
  3. data/config/locales/en.yml +3 -0
  4. data/lib/treaty/attribute/base.rb +13 -5
  5. data/lib/treaty/attribute/dsl.rb +90 -0
  6. data/lib/treaty/attribute/entity/attribute.rb +25 -0
  7. data/lib/treaty/attribute/entity/builder.rb +23 -0
  8. data/lib/treaty/attribute/option/base.rb +17 -1
  9. data/lib/treaty/attribute/option/modifiers/as_modifier.rb +5 -3
  10. data/lib/treaty/attribute/option/validators/inclusion_validator.rb +20 -8
  11. data/lib/treaty/attribute/option/validators/required_validator.rb +8 -2
  12. data/lib/treaty/attribute/option/validators/type_validator.rb +51 -40
  13. data/lib/treaty/attribute/option_orchestrator.rb +7 -5
  14. data/lib/treaty/attribute/validation/nested_array_validator.rb +18 -12
  15. data/lib/treaty/attribute/validation/nested_transformer.rb +18 -12
  16. data/lib/treaty/base.rb +1 -1
  17. data/lib/treaty/controller/dsl.rb +4 -1
  18. data/lib/treaty/entity.rb +84 -0
  19. data/lib/treaty/info/entity/builder.rb +50 -0
  20. data/lib/treaty/info/entity/dsl.rb +28 -0
  21. data/lib/treaty/info/entity/result.rb +15 -0
  22. data/lib/treaty/info/rest/builder.rb +110 -0
  23. data/lib/treaty/info/rest/dsl.rb +28 -0
  24. data/lib/treaty/info/rest/result.rb +15 -0
  25. data/lib/treaty/request/attribute/attribute.rb +1 -0
  26. data/lib/treaty/request/attribute/builder.rb +1 -0
  27. data/lib/treaty/request/entity.rb +33 -0
  28. data/lib/treaty/request/factory.rb +61 -14
  29. data/lib/treaty/request/validator.rb +65 -0
  30. data/lib/treaty/response/attribute/attribute.rb +1 -0
  31. data/lib/treaty/response/attribute/builder.rb +1 -0
  32. data/lib/treaty/response/entity.rb +33 -0
  33. data/lib/treaty/response/factory.rb +61 -14
  34. data/lib/treaty/response/validator.rb +57 -0
  35. data/lib/treaty/version.rb +1 -1
  36. data/lib/treaty/versions/execution/request.rb +10 -5
  37. data/lib/treaty/versions/factory.rb +16 -5
  38. data/lib/treaty/versions/resolver.rb +8 -2
  39. data/lib/treaty/versions/workspace.rb +2 -2
  40. metadata +15 -8
  41. data/lib/treaty/info/builder.rb +0 -108
  42. data/lib/treaty/info/dsl.rb +0 -26
  43. data/lib/treaty/info/result.rb +0 -13
  44. data/lib/treaty/request/attribute/validation/orchestrator.rb +0 -19
  45. data/lib/treaty/request/attribute/validator.rb +0 -50
  46. data/lib/treaty/response/attribute/validation/orchestrator.rb +0 -19
  47. data/lib/treaty/response/attribute/validator.rb +0 -44
@@ -0,0 +1,84 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Treaty
4
+ # Base class for defining DTO (Data Transfer Object) entities in Treaty.
5
+ #
6
+ # ## Purpose
7
+ #
8
+ # Treaty::Entity provides a base class for creating reusable DTO classes
9
+ # that can be used in both request and response definitions. This allows
10
+ # for better code organization and reusability of common data structures.
11
+ #
12
+ # ## Usage
13
+ #
14
+ # Create a DTO class by inheriting from Treaty::Entity:
15
+ #
16
+ # ```ruby
17
+ # class PostEntity < Treaty::Entity
18
+ # string :id
19
+ # string :title
20
+ # string :content
21
+ # datetime :created_at
22
+ # end
23
+ # ```
24
+ #
25
+ # Then use it in your treaty definitions:
26
+ #
27
+ # ```ruby
28
+ # class CreateTreaty < ApplicationTreaty
29
+ # version 1 do
30
+ # request PostEntity
31
+ # response 201, PostEntity
32
+ # end
33
+ # end
34
+ # ```
35
+ #
36
+ # ## Attribute Defaults
37
+ #
38
+ # Unlike request/response blocks, Entity attributes are required by default:
39
+ # - All attributes have `required: true` unless explicitly marked as `:optional`
40
+ # - Use `:optional` helper to make attributes optional:
41
+ # ```ruby
42
+ # string :title # required by default
43
+ # string :summary, :optional # optional
44
+ # ```
45
+ #
46
+ # ## Features
47
+ #
48
+ # - **Type Safety** - Enforce strict type checking for all attributes
49
+ # - **Nested Structures** - Support for nested objects and arrays
50
+ # - **Validation** - Built-in validation for all attribute types
51
+ # - **Reusability** - Define once, use in multiple treaties
52
+ # - **Options** - Full support for attribute options (required, default, as, etc.)
53
+ #
54
+ # ## Supported Types
55
+ #
56
+ # - `string` - String values
57
+ # - `integer` - Integer values
58
+ # - `boolean` - Boolean values (true/false)
59
+ # - `datetime` - DateTime values
60
+ # - `array` - Array values (with nested type definition)
61
+ # - `object` - Object values (with nested attributes)
62
+ class Entity
63
+ include Info::Entity::DSL
64
+ include Attribute::DSL
65
+
66
+ class << self
67
+ private
68
+
69
+ # Creates an Attribute::Entity::Attribute for this Entity class
70
+ #
71
+ # @return [Attribute::Entity::Attribute] Created attribute instance
72
+ def create_attribute(name, type, *helpers, nesting_level:, **options, &block)
73
+ Attribute::Entity::Attribute.new(
74
+ name,
75
+ type,
76
+ *helpers,
77
+ nesting_level:,
78
+ **options,
79
+ &block
80
+ )
81
+ end
82
+ end
83
+ end
84
+ end
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Treaty
4
+ module Info
5
+ module Entity
6
+ class Builder
7
+ attr_reader :attributes
8
+
9
+ def self.build(...)
10
+ new.build(...)
11
+ end
12
+
13
+ def build(collection_of_attributes:)
14
+ build_all(
15
+ attributes: collection_of_attributes
16
+ )
17
+
18
+ self
19
+ end
20
+
21
+ private
22
+
23
+ def build_all(attributes:)
24
+ @attributes = build_versions_with(attributes)
25
+ end
26
+
27
+ ##########################################################################
28
+
29
+ def build_versions_with(collection, current_level = 0)
30
+ collection.to_h do |attribute|
31
+ [
32
+ attribute.name,
33
+ {
34
+ type: attribute.type,
35
+ options: attribute.options,
36
+ attributes: build_nested_attributes(attribute, current_level)
37
+ }
38
+ ]
39
+ end
40
+ end
41
+
42
+ def build_nested_attributes(attribute, current_level)
43
+ return {} unless attribute.nested?
44
+
45
+ build_versions_with(attribute.collection_of_attributes, current_level + 1)
46
+ end
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Treaty
4
+ module Info
5
+ module Entity
6
+ module DSL
7
+ def self.included(base)
8
+ base.extend(ClassMethods)
9
+ end
10
+
11
+ module ClassMethods
12
+ def info
13
+ builder = Builder.build(
14
+ collection_of_attributes:
15
+ )
16
+
17
+ Result.new(builder)
18
+ end
19
+
20
+ # API: Treaty Web
21
+ def treaty?
22
+ true
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Treaty
4
+ module Info
5
+ module Entity
6
+ class Result
7
+ attr_reader :attributes
8
+
9
+ def initialize(builder)
10
+ @attributes = builder.attributes
11
+ end
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,110 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Treaty
4
+ module Info
5
+ module Rest
6
+ class Builder
7
+ attr_reader :versions
8
+
9
+ def self.build(...)
10
+ new.build(...)
11
+ end
12
+
13
+ def build(collection_of_versions:)
14
+ build_all(
15
+ versions: collection_of_versions
16
+ )
17
+
18
+ self
19
+ end
20
+
21
+ private
22
+
23
+ def build_all(versions:)
24
+ build_versions_with(
25
+ collection: versions
26
+ )
27
+ end
28
+
29
+ ##########################################################################
30
+
31
+ def build_versions_with(collection:) # rubocop:disable Metrics/MethodLength
32
+ @versions = collection.map do |version|
33
+ gem_version = version.version.version
34
+ {
35
+ version: gem_version.version,
36
+ segments: gem_version.segments,
37
+ default: version.default_result,
38
+ summary: version.summary_text,
39
+ strategy: version.strategy_instance.code,
40
+ deprecated: version.deprecated_result,
41
+ executor: build_executor_with(version),
42
+ request: build_request_with(version),
43
+ response: build_response_with(version)
44
+ }
45
+ end
46
+ end
47
+
48
+ ##########################################################################
49
+
50
+ def build_executor_with(version)
51
+ {
52
+ executor: version.executor.executor,
53
+ method: version.executor.method
54
+ }
55
+ end
56
+
57
+ ##########################################################################
58
+
59
+ def build_request_with(version)
60
+ build_attributes_structure(version.request_factory)
61
+ end
62
+
63
+ def build_response_with(version)
64
+ response_factory = version.response_factory
65
+ {
66
+ status: response_factory.status
67
+ }.merge(build_attributes_structure(response_factory))
68
+ end
69
+
70
+ ##########################################################################
71
+
72
+ def build_attributes_structure(factory)
73
+ {
74
+ attributes: build_attributes_hash(factory.collection_of_attributes)
75
+ }
76
+ end
77
+
78
+ def build_attributes_hash(collection, current_level = 0)
79
+ # validate_nesting_level!(current_level)
80
+
81
+ collection.to_h do |attribute|
82
+ [
83
+ attribute.name,
84
+ {
85
+ type: attribute.type,
86
+ options: attribute.options,
87
+ attributes: build_nested_attributes(attribute, current_level)
88
+ }
89
+ ]
90
+ end
91
+ end
92
+
93
+ def build_nested_attributes(attribute, current_level)
94
+ return {} unless attribute.nested?
95
+
96
+ build_attributes_hash(attribute.collection_of_attributes, current_level + 1)
97
+ end
98
+
99
+ # def validate_nesting_level!(level)
100
+ # return unless level > Treaty::Engine.config.treaty.attribute_nesting_level
101
+ #
102
+ # raise Treaty::Exceptions::NestedAttributes,
103
+ # I18n.t("treaty.attributes.errors.nesting_level_exceeded",
104
+ # level:,
105
+ # max_level: Treaty::Engine.config.treaty.attribute_nesting_level)
106
+ # end
107
+ end
108
+ end
109
+ end
110
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Treaty
4
+ module Info
5
+ module Rest
6
+ module DSL
7
+ def self.included(base)
8
+ base.extend(ClassMethods)
9
+ end
10
+
11
+ module ClassMethods
12
+ def info
13
+ builder = Builder.build(
14
+ collection_of_versions:
15
+ )
16
+
17
+ Result.new(builder)
18
+ end
19
+
20
+ # API: Treaty Web
21
+ def treaty?
22
+ true
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Treaty
4
+ module Info
5
+ module Rest
6
+ class Result
7
+ attr_reader :versions
8
+
9
+ def initialize(builder)
10
+ @versions = builder.versions
11
+ end
12
+ end
13
+ end
14
+ end
15
+ end
@@ -3,6 +3,7 @@
3
3
  module Treaty
4
4
  module Request
5
5
  module Attribute
6
+ # Request-specific attribute that defaults to required: true
6
7
  class Attribute < Treaty::Attribute::Base
7
8
  private
8
9
 
@@ -3,6 +3,7 @@
3
3
  module Treaty
4
4
  module Request
5
5
  module Attribute
6
+ # Request-specific attribute builder
6
7
  class Builder < Treaty::Attribute::Builder::Base
7
8
  private
8
9
 
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Treaty
4
+ module Request
5
+ # Entity class for request definitions.
6
+ # Attributes are required by default.
7
+ #
8
+ # This class is used internally when defining request blocks.
9
+ # When you write a request block, Treaty creates an anonymous
10
+ # class based on Request::Entity.
11
+ class Entity
12
+ include Treaty::Attribute::DSL
13
+
14
+ class << self
15
+ private
16
+
17
+ # Creates a Request::Attribute::Attribute for this Request::Entity class
18
+ #
19
+ # @return [Request::Attribute::Attribute] Created attribute instance
20
+ def create_attribute(name, type, *helpers, nesting_level:, **options, &block)
21
+ Attribute::Attribute.new(
22
+ name,
23
+ type,
24
+ *helpers,
25
+ nesting_level:,
26
+ **options,
27
+ &block
28
+ )
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
@@ -2,33 +2,80 @@
2
2
 
3
3
  module Treaty
4
4
  module Request
5
+ # Factory for creating request definitions.
6
+ #
7
+ # Supports two modes:
8
+ # 1. Block mode: Creates an anonymous Request::Entity class with the block
9
+ # 2. Entity mode: Uses a provided Entity class directly
10
+ #
11
+ # ## Block Mode
12
+ #
13
+ # ```ruby
14
+ # request do
15
+ # object :post do
16
+ # string :title
17
+ # end
18
+ # end
19
+ # ```
20
+ #
21
+ # ## Entity Mode
22
+ #
23
+ # ```ruby
24
+ # request PostRequestEntity
25
+ # ```
5
26
  class Factory
6
- def attribute(name, type, *helpers, **options, &block)
7
- collection_of_attributes << Attribute::Attribute.new(
8
- name,
9
- type,
10
- *helpers,
11
- nesting_level: 0,
12
- **options,
13
- &block
14
- )
27
+ # Uses a provided Entity class
28
+ #
29
+ # @param entity_class [Class] Entity class to use
30
+ # @return [void]
31
+ # @raise [Treaty::Exceptions::Validation] if entity_class is not a valid Treaty::Entity subclass
32
+ def use_entity(entity_class)
33
+ validate_entity_class!(entity_class)
34
+ @entity_class = entity_class
15
35
  end
16
36
 
37
+ # Returns collection of attributes from the entity class
38
+ #
39
+ # @return [Collection] Collection of attributes
17
40
  def collection_of_attributes
18
- @collection_of_attributes ||= Treaty::Attribute::Collection.new
19
- end
41
+ return Treaty::Attribute::Collection.new if @entity_class.nil?
20
42
 
21
- ##########################################################################
43
+ @entity_class.collection_of_attributes
44
+ end
22
45
 
46
+ # Handles DSL methods for defining attributes
47
+ #
48
+ # This allows the factory to be used with method_missing
49
+ # for backwards compatibility with direct method calls.
50
+ # Creates an anonymous Request::Entity class on first use.
23
51
  def method_missing(type, *helpers, **options, &block)
24
- name = helpers.shift
52
+ # If no entity class yet, create one
53
+ @entity_class ||= Class.new(Entity)
25
54
 
26
- attribute(name, type, *helpers, **options, &block)
55
+ # Call the method on the entity class
56
+ @entity_class.public_send(type, *helpers, **options, &block)
27
57
  end
28
58
 
29
59
  def respond_to_missing?(name, *)
30
60
  super
31
61
  end
62
+
63
+ private
64
+
65
+ # Validates that the provided entity_class is a valid Treaty::Entity subclass
66
+ #
67
+ # @param entity_class [Class] Entity class to validate
68
+ # @raise [Treaty::Exceptions::Validation] if entity_class is not a valid Treaty::Entity subclass
69
+ def validate_entity_class!(entity_class)
70
+ return if entity_class.is_a?(Class) && entity_class < Treaty::Entity
71
+
72
+ raise Treaty::Exceptions::Validation,
73
+ I18n.t(
74
+ "treaty.request.factory.invalid_entity_class",
75
+ type: entity_class.class,
76
+ value: entity_class
77
+ )
78
+ end
32
79
  end
33
80
  end
34
81
  end
@@ -0,0 +1,65 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Treaty
4
+ module Request
5
+ # Validator for request data
6
+ class Validator
7
+ class << self
8
+ # Validates request parameters against the request definition
9
+ #
10
+ # @param params [Hash] Request parameters to validate
11
+ # @param version_factory [Versions::Factory] Version factory with request definition
12
+ # @return [Hash] Validated and transformed parameters
13
+ def validate!(params:, version_factory:)
14
+ new(params:, version_factory:).validate!
15
+ end
16
+ end
17
+
18
+ def initialize(params:, version_factory:)
19
+ @params = params
20
+ @version_factory = version_factory
21
+ end
22
+
23
+ def validate!
24
+ validate_request_attributes!
25
+ end
26
+
27
+ private
28
+
29
+ def request_data
30
+ @request_data ||= begin
31
+ @params.to_unsafe_h
32
+ rescue NoMethodError
33
+ @params
34
+ end
35
+ end
36
+
37
+ def validate_request_attributes! # rubocop:disable Metrics/MethodLength
38
+ return request_data unless adapter_strategy?
39
+ return request_data unless request_attributes_exist?
40
+
41
+ # For adapter strategy with attributes defined:
42
+ orchestrator_class = Class.new(Treaty::Attribute::Validation::Orchestrator::Base) do
43
+ define_method(:collection_of_attributes) do
44
+ @version_factory.request_factory.collection_of_attributes
45
+ end
46
+ end
47
+
48
+ orchestrator_class.validate!(
49
+ version_factory: @version_factory,
50
+ data: request_data
51
+ )
52
+ end
53
+
54
+ def adapter_strategy?
55
+ !@version_factory.strategy_instance.direct?
56
+ end
57
+
58
+ def request_attributes_exist?
59
+ return false if @version_factory.request_factory&.collection_of_attributes&.empty?
60
+
61
+ @version_factory.request_factory.collection_of_attributes.exists?
62
+ end
63
+ end
64
+ end
65
+ end
@@ -3,6 +3,7 @@
3
3
  module Treaty
4
4
  module Response
5
5
  module Attribute
6
+ # Response-specific attribute that defaults to required: false
6
7
  class Attribute < Treaty::Attribute::Base
7
8
  private
8
9
 
@@ -3,6 +3,7 @@
3
3
  module Treaty
4
4
  module Response
5
5
  module Attribute
6
+ # Response-specific attribute builder
6
7
  class Builder < Treaty::Attribute::Builder::Base
7
8
  private
8
9
 
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Treaty
4
+ module Response
5
+ # Entity class for response definitions.
6
+ # Attributes are optional by default.
7
+ #
8
+ # This class is used internally when defining response blocks.
9
+ # When you write a response block, Treaty creates an anonymous
10
+ # class based on Response::Entity.
11
+ class Entity
12
+ include Treaty::Attribute::DSL
13
+
14
+ class << self
15
+ private
16
+
17
+ # Creates a Response::Attribute::Attribute for this Response::Entity class
18
+ #
19
+ # @return [Response::Attribute::Attribute] Created attribute instance
20
+ def create_attribute(name, type, *helpers, nesting_level:, **options, &block)
21
+ Attribute::Attribute.new(
22
+ name,
23
+ type,
24
+ *helpers,
25
+ nesting_level:,
26
+ **options,
27
+ &block
28
+ )
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end