easy_talk 3.1.0 → 3.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 (68) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +15 -39
  3. data/.yardopts +13 -0
  4. data/CHANGELOG.md +164 -0
  5. data/README.md +442 -1529
  6. data/Rakefile +27 -0
  7. data/docs/.gitignore +1 -0
  8. data/docs/about.markdown +28 -8
  9. data/docs/getting-started.markdown +102 -0
  10. data/docs/index.markdown +51 -4
  11. data/docs/json_schema_compliance.md +169 -0
  12. data/docs/nested-models.markdown +216 -0
  13. data/docs/primitive-schema-rfc.md +894 -0
  14. data/docs/property-types.markdown +212 -0
  15. data/docs/schema-definition.markdown +180 -0
  16. data/lib/easy_talk/builders/base_builder.rb +6 -3
  17. data/lib/easy_talk/builders/boolean_builder.rb +2 -1
  18. data/lib/easy_talk/builders/collection_helpers.rb +4 -0
  19. data/lib/easy_talk/builders/composition_builder.rb +16 -13
  20. data/lib/easy_talk/builders/integer_builder.rb +2 -1
  21. data/lib/easy_talk/builders/null_builder.rb +4 -1
  22. data/lib/easy_talk/builders/number_builder.rb +4 -1
  23. data/lib/easy_talk/builders/object_builder.rb +109 -33
  24. data/lib/easy_talk/builders/registry.rb +182 -0
  25. data/lib/easy_talk/builders/string_builder.rb +3 -1
  26. data/lib/easy_talk/builders/temporal_builder.rb +7 -0
  27. data/lib/easy_talk/builders/tuple_builder.rb +89 -0
  28. data/lib/easy_talk/builders/typed_array_builder.rb +19 -6
  29. data/lib/easy_talk/builders/union_builder.rb +5 -1
  30. data/lib/easy_talk/configuration.rb +47 -2
  31. data/lib/easy_talk/error_formatter/base.rb +100 -0
  32. data/lib/easy_talk/error_formatter/error_code_mapper.rb +82 -0
  33. data/lib/easy_talk/error_formatter/flat.rb +38 -0
  34. data/lib/easy_talk/error_formatter/json_pointer.rb +38 -0
  35. data/lib/easy_talk/error_formatter/jsonapi.rb +64 -0
  36. data/lib/easy_talk/error_formatter/path_converter.rb +53 -0
  37. data/lib/easy_talk/error_formatter/rfc7807.rb +69 -0
  38. data/lib/easy_talk/error_formatter.rb +143 -0
  39. data/lib/easy_talk/errors.rb +3 -0
  40. data/lib/easy_talk/errors_helper.rb +66 -34
  41. data/lib/easy_talk/json_schema_equality.rb +46 -0
  42. data/lib/easy_talk/keywords.rb +0 -1
  43. data/lib/easy_talk/model.rb +148 -89
  44. data/lib/easy_talk/model_helper.rb +17 -0
  45. data/lib/easy_talk/naming_strategies.rb +24 -0
  46. data/lib/easy_talk/property.rb +23 -94
  47. data/lib/easy_talk/ref_helper.rb +33 -0
  48. data/lib/easy_talk/schema.rb +199 -0
  49. data/lib/easy_talk/schema_definition.rb +57 -5
  50. data/lib/easy_talk/schema_methods.rb +111 -0
  51. data/lib/easy_talk/sorbet_extension.rb +1 -0
  52. data/lib/easy_talk/tools/function_builder.rb +1 -1
  53. data/lib/easy_talk/type_introspection.rb +222 -0
  54. data/lib/easy_talk/types/base_composer.rb +2 -1
  55. data/lib/easy_talk/types/composer.rb +4 -0
  56. data/lib/easy_talk/types/tuple.rb +77 -0
  57. data/lib/easy_talk/validation_adapters/active_model_adapter.rb +617 -0
  58. data/lib/easy_talk/validation_adapters/active_model_schema_validation.rb +106 -0
  59. data/lib/easy_talk/validation_adapters/base.rb +156 -0
  60. data/lib/easy_talk/validation_adapters/none_adapter.rb +45 -0
  61. data/lib/easy_talk/validation_adapters/registry.rb +87 -0
  62. data/lib/easy_talk/validation_builder.rb +29 -309
  63. data/lib/easy_talk/version.rb +1 -1
  64. data/lib/easy_talk.rb +42 -0
  65. metadata +38 -7
  66. data/docs/404.html +0 -25
  67. data/docs/_posts/2024-05-07-welcome-to-jekyll.markdown +0 -29
  68. data/easy_talk.gemspec +0 -39
@@ -0,0 +1,212 @@
1
+ ---
2
+ layout: page
3
+ title: Property Types
4
+ permalink: /property-types/
5
+ ---
6
+
7
+ # Property Types
8
+
9
+ EasyTalk supports Ruby's built-in types plus Sorbet-style generic types for more complex schemas.
10
+
11
+ ## Basic Types
12
+
13
+ ### String
14
+
15
+ ```ruby
16
+ property :name, String
17
+ # => { "type": "string" }
18
+ ```
19
+
20
+ **Constraints:**
21
+
22
+ | Constraint | Description | Example |
23
+ |------------|-------------|---------|
24
+ | `min_length` | Minimum length | `min_length: 1` |
25
+ | `max_length` | Maximum length | `max_length: 100` |
26
+ | `pattern` | Regex pattern | `pattern: /^[a-z]+$/` |
27
+ | `format` | JSON Schema format | `format: "email"` |
28
+ | `enum` | Allowed values | `enum: %w[a b c]` |
29
+
30
+ **Common formats:** `email`, `uri`, `uuid`, `date`, `date-time`, `time`, `ipv4`, `ipv6`, `hostname`
31
+
32
+ ### Integer
33
+
34
+ ```ruby
35
+ property :age, Integer
36
+ # => { "type": "integer" }
37
+ ```
38
+
39
+ **Constraints:**
40
+
41
+ | Constraint | Description | Example |
42
+ |------------|-------------|---------|
43
+ | `minimum` | Minimum value (inclusive) | `minimum: 0` |
44
+ | `maximum` | Maximum value (inclusive) | `maximum: 100` |
45
+ | `exclusive_minimum` | Minimum value (exclusive) | `exclusive_minimum: 0` |
46
+ | `exclusive_maximum` | Maximum value (exclusive) | `exclusive_maximum: 100` |
47
+ | `multiple_of` | Must be multiple of | `multiple_of: 5` |
48
+ | `enum` | Allowed values | `enum: [1, 2, 3]` |
49
+
50
+ ### Float / Number
51
+
52
+ ```ruby
53
+ property :price, Float
54
+ # => { "type": "number" }
55
+ ```
56
+
57
+ Supports the same constraints as Integer.
58
+
59
+ ### Boolean
60
+
61
+ ```ruby
62
+ property :active, T::Boolean
63
+ # => { "type": "boolean" }
64
+ ```
65
+
66
+ Note: Use `T::Boolean` (not Ruby's `TrueClass`/`FalseClass`).
67
+
68
+ ## Date and Time Types
69
+
70
+ ### Date
71
+
72
+ ```ruby
73
+ property :birth_date, Date
74
+ # => { "type": "string", "format": "date" }
75
+ ```
76
+
77
+ ### DateTime
78
+
79
+ ```ruby
80
+ property :created_at, DateTime
81
+ # => { "type": "string", "format": "date-time" }
82
+ ```
83
+
84
+ ### Time
85
+
86
+ ```ruby
87
+ property :start_time, Time
88
+ # => { "type": "string", "format": "time" }
89
+ ```
90
+
91
+ ## Generic Types
92
+
93
+ EasyTalk uses Sorbet-style generics for complex types.
94
+
95
+ ### Arrays
96
+
97
+ ```ruby
98
+ property :tags, T::Array[String]
99
+ # => { "type": "array", "items": { "type": "string" } }
100
+ ```
101
+
102
+ **Array Constraints:**
103
+
104
+ | Constraint | Description | Example |
105
+ |------------|-------------|---------|
106
+ | `min_items` | Minimum array length | `min_items: 1` |
107
+ | `max_items` | Maximum array length | `max_items: 10` |
108
+ | `unique_items` | All items must be unique | `unique_items: true` |
109
+
110
+ ```ruby
111
+ property :scores, T::Array[Integer], min_items: 1, max_items: 5
112
+ ```
113
+
114
+ ### Nullable Types
115
+
116
+ Use `T.nilable` to allow null values:
117
+
118
+ ```ruby
119
+ property :nickname, T.nilable(String)
120
+ # => { "anyOf": [{ "type": "string" }, { "type": "null" }] }
121
+ ```
122
+
123
+ **Note:** `T.nilable` makes the property nullable but still required. To make it optional as well:
124
+
125
+ ```ruby
126
+ property :nickname, T.nilable(String), optional: true
127
+ ```
128
+
129
+ Or use the helper method:
130
+
131
+ ```ruby
132
+ nullable_optional_property :nickname, String
133
+ ```
134
+
135
+ ## Nested Models
136
+
137
+ Reference other EasyTalk models directly:
138
+
139
+ ```ruby
140
+ class Address
141
+ include EasyTalk::Model
142
+ define_schema do
143
+ property :street, String
144
+ property :city, String
145
+ end
146
+ end
147
+
148
+ class User
149
+ include EasyTalk::Model
150
+ define_schema do
151
+ property :name, String
152
+ property :address, Address # Nested model
153
+ end
154
+ end
155
+ ```
156
+
157
+ Arrays of models:
158
+
159
+ ```ruby
160
+ property :addresses, T::Array[Address]
161
+ ```
162
+
163
+ ## Composition Types
164
+
165
+ ### OneOf
166
+
167
+ Exactly one schema must match:
168
+
169
+ ```ruby
170
+ property :contact, T::OneOf[Email, Phone]
171
+ ```
172
+
173
+ ### AnyOf
174
+
175
+ At least one schema must match:
176
+
177
+ ```ruby
178
+ property :identifier, T::AnyOf[UserId, Email, Username]
179
+ ```
180
+
181
+ ### AllOf
182
+
183
+ All schemas must match (for combining schemas):
184
+
185
+ ```ruby
186
+ property :profile, T::AllOf[BasicInfo, ExtendedInfo]
187
+ ```
188
+
189
+ ## Null Type
190
+
191
+ For explicit null-only values:
192
+
193
+ ```ruby
194
+ property :deprecated_field, NilClass
195
+ # => { "type": "null" }
196
+ ```
197
+
198
+ ## Type Summary
199
+
200
+ | Ruby Type | JSON Schema Type |
201
+ |-----------|------------------|
202
+ | `String` | `"string"` |
203
+ | `Integer` | `"integer"` |
204
+ | `Float` | `"number"` |
205
+ | `T::Boolean` | `"boolean"` |
206
+ | `Date` | `"string"` + `"date"` format |
207
+ | `DateTime` | `"string"` + `"date-time"` format |
208
+ | `Time` | `"string"` + `"time"` format |
209
+ | `T::Array[T]` | `"array"` |
210
+ | `T.nilable(T)` | `anyOf` with null |
211
+ | `NilClass` | `"null"` |
212
+ | Model class | `"object"` (inline or `$ref`) |
@@ -0,0 +1,180 @@
1
+ ---
2
+ layout: page
3
+ title: Schema Definition
4
+ permalink: /schema-definition/
5
+ ---
6
+
7
+ # Schema Definition
8
+
9
+ The `define_schema` block is where you declare your model's structure. It provides a clean DSL for defining JSON Schema properties and metadata.
10
+
11
+ ## Basic Structure
12
+
13
+ ```ruby
14
+ class MyModel
15
+ include EasyTalk::Model
16
+
17
+ define_schema do
18
+ title "Model Title"
19
+ description "What this model represents"
20
+
21
+ property :field_name, Type, constraints...
22
+ end
23
+ end
24
+ ```
25
+
26
+ ## Schema Metadata
27
+
28
+ ### title
29
+
30
+ Sets the schema title (appears in JSON Schema output):
31
+
32
+ ```ruby
33
+ define_schema do
34
+ title "User Account"
35
+ end
36
+ ```
37
+
38
+ ### description
39
+
40
+ Adds a description to the schema:
41
+
42
+ ```ruby
43
+ define_schema do
44
+ description "Represents a user account in the system"
45
+ end
46
+ ```
47
+
48
+ ## Defining Properties
49
+
50
+ The `property` method defines a schema property:
51
+
52
+ ```ruby
53
+ property :name, Type, option: value, ...
54
+ ```
55
+
56
+ ### Required vs Optional
57
+
58
+ By default, all properties are **required**. Use `optional: true` to make a property optional:
59
+
60
+ ```ruby
61
+ define_schema do
62
+ property :name, String # Required
63
+ property :nickname, String, optional: true # Optional
64
+ end
65
+ ```
66
+
67
+ ### Property Titles and Descriptions
68
+
69
+ Add metadata to individual properties:
70
+
71
+ ```ruby
72
+ property :email, String,
73
+ title: "Email Address",
74
+ description: "The user's primary email"
75
+ ```
76
+
77
+ ### Property Renaming
78
+
79
+ Use `:as` to rename a property in the JSON Schema output:
80
+
81
+ ```ruby
82
+ property :created_at, String, as: :createdAt
83
+ ```
84
+
85
+ This creates a property named `createdAt` in the schema while keeping `created_at` as the Ruby attribute.
86
+
87
+ ## Type Constraints
88
+
89
+ Different types support different constraints. See [Property Types](property-types) for the full list.
90
+
91
+ ### String Constraints
92
+
93
+ ```ruby
94
+ property :username, String,
95
+ min_length: 3,
96
+ max_length: 20,
97
+ pattern: /^[a-z0-9_]+$/
98
+ ```
99
+
100
+ ### Numeric Constraints
101
+
102
+ ```ruby
103
+ property :age, Integer,
104
+ minimum: 0,
105
+ maximum: 150
106
+
107
+ property :price, Float,
108
+ exclusive_minimum: 0
109
+ ```
110
+
111
+ ### Enum Values
112
+
113
+ ```ruby
114
+ property :status, String, enum: %w[active inactive pending]
115
+ ```
116
+
117
+ ## Composition
118
+
119
+ ### compose
120
+
121
+ Use `compose` to include schemas from other models:
122
+
123
+ ```ruby
124
+ class FullProfile
125
+ include EasyTalk::Model
126
+
127
+ define_schema do
128
+ compose T::AllOf[BasicInfo, ContactInfo, Preferences]
129
+ end
130
+ end
131
+ ```
132
+
133
+ ### Composition Types
134
+
135
+ - `T::AllOf[A, B]` - Must match all schemas
136
+ - `T::AnyOf[A, B]` - Must match at least one schema
137
+ - `T::OneOf[A, B]` - Must match exactly one schema
138
+
139
+ ## Configuration Options
140
+
141
+ ### Per-Model Validation Control
142
+
143
+ Disable automatic validations for a specific model:
144
+
145
+ ```ruby
146
+ define_schema(validations: false) do
147
+ property :data, String
148
+ end
149
+ ```
150
+
151
+ ### Per-Property Validation Control
152
+
153
+ Disable validation for specific properties:
154
+
155
+ ```ruby
156
+ property :legacy_field, String, validate: false
157
+ ```
158
+
159
+ ## Example: Complete Model
160
+
161
+ ```ruby
162
+ class Product
163
+ include EasyTalk::Model
164
+
165
+ define_schema do
166
+ title "Product"
167
+ description "A product in the catalog"
168
+
169
+ property :id, String, format: "uuid"
170
+ property :name, String, min_length: 1, max_length: 100
171
+ property :description, String, optional: true
172
+ property :price, Float, minimum: 0
173
+ property :currency, String, enum: %w[USD EUR GBP], default: "USD"
174
+ property :category, String
175
+ property :tags, T::Array[String], optional: true
176
+ property :active, T::Boolean, default: true
177
+ property :created_at, DateTime, as: :createdAt
178
+ end
179
+ end
180
+ ```
@@ -12,7 +12,9 @@ module EasyTalk
12
12
  COMMON_OPTIONS = {
13
13
  title: { type: T.nilable(String), key: :title },
14
14
  description: { type: T.nilable(String), key: :description },
15
- optional: { type: T.nilable(T::Boolean), key: :optional }
15
+ optional: { type: T.nilable(T::Boolean), key: :optional },
16
+ as: { type: T.nilable(T.any(String, Symbol)), key: :as },
17
+ validate: { type: T.nilable(T::Boolean), key: :validate }
16
18
  }.freeze
17
19
 
18
20
  attr_reader :property_name, :schema, :options
@@ -21,7 +23,7 @@ module EasyTalk
21
23
  params(
22
24
  property_name: Symbol,
23
25
  schema: T::Hash[Symbol, T.untyped],
24
- options: T::Hash[Symbol, String],
26
+ options: T::Hash[Symbol, T.untyped],
25
27
  valid_options: T::Hash[Symbol, T.untyped]
26
28
  ).void
27
29
  end
@@ -42,7 +44,7 @@ module EasyTalk
42
44
  # Builds the schema object based on the provided options.
43
45
  sig { returns(T::Hash[Symbol, T.untyped]) }
44
46
  def build
45
- @valid_options.each_with_object(schema) do |(constraint_name, value), obj|
47
+ @valid_options.except(:ref).each_with_object(schema) do |(constraint_name, value), obj|
46
48
  next if @options[constraint_name].nil?
47
49
 
48
50
  # Use our centralized validation
@@ -57,6 +59,7 @@ module EasyTalk
57
59
  end
58
60
  end
59
61
 
62
+ sig { returns(T::Boolean) }
60
63
  def self.collection_type?
61
64
  false
62
65
  end
@@ -1,4 +1,5 @@
1
1
  # frozen_string_literal: true
2
+ # typed: true
2
3
 
3
4
  require_relative 'base_builder'
4
5
 
@@ -14,7 +15,7 @@ module EasyTalk
14
15
  default: { type: T::Boolean, key: :default }
15
16
  }.freeze
16
17
 
17
- sig { params(name: Symbol, constraints: Hash).void }
18
+ sig { params(name: Symbol, constraints: T::Hash[Symbol, T.untyped]).void }
18
19
  def initialize(name, constraints = {})
19
20
  super(name, { type: 'boolean' }, constraints, VALID_OPTIONS)
20
21
  end
@@ -1,9 +1,13 @@
1
1
  # frozen_string_literal: true
2
+ # typed: true
2
3
 
3
4
  module EasyTalk
4
5
  module Builders
5
6
  # Base builder class for array-type properties.
6
7
  module CollectionHelpers
8
+ extend T::Sig
9
+
10
+ sig { returns(T::Boolean) }
7
11
  def collection_type?
8
12
  true
9
13
  end
@@ -1,6 +1,8 @@
1
1
  # frozen_string_literal: true
2
+ # typed: true
2
3
 
3
4
  require_relative 'collection_helpers'
5
+ require_relative '../ref_helper'
4
6
 
5
7
  module EasyTalk
6
8
  module Builders
@@ -15,22 +17,24 @@ module EasyTalk
15
17
  'OneOfBuilder' => 'oneOf'
16
18
  }.freeze
17
19
 
18
- sig { params(name: Symbol, type: T.untyped, _constraints: Hash).void }
20
+ sig { params(name: Symbol, type: T.untyped, constraints: T::Hash[Symbol, T.untyped]).void }
19
21
  # Initializes a new instance of the CompositionBuilder class.
20
22
  #
21
23
  # @param name [Symbol] The name of the composition.
22
24
  # @param type [Class] The type of the composition.
23
- # @param _constraints [Hash] The constraints for the composition (not used in this method).
24
- def initialize(name, type, _constraints)
25
+ # @param constraints [Hash] The constraints for the composition.
26
+ def initialize(name, type, constraints)
25
27
  @composer_type = self.class.name.split('::').last
26
28
  @name = name
27
29
  @type = type
28
30
  @context = {}
31
+ @constraints = constraints
29
32
  end
30
33
 
31
34
  # Builds the composed JSON schema.
32
35
  #
33
- # @return [void]
36
+ # @return [Hash] The composed JSON schema.
37
+ sig { returns(T::Hash[Symbol, T.untyped]) }
34
38
  def build
35
39
  @context[@name.to_sym] = {
36
40
  type: 'object',
@@ -41,6 +45,7 @@ module EasyTalk
41
45
  # Returns the composer keyword based on the composer type.
42
46
  #
43
47
  # @return [String] The composer keyword.
48
+ sig { returns(T.nilable(String)) }
44
49
  def composer_keyword
45
50
  COMPOSER_TO_KEYWORD[@composer_type]
46
51
  end
@@ -48,19 +53,16 @@ module EasyTalk
48
53
  # Returns an array of schemas for the composed JSON schema.
49
54
  #
50
55
  # @return [Array<Hash>] The array of schemas.
56
+ sig { returns(T::Array[T.untyped]) }
51
57
  def schemas
52
58
  items.map do |type|
53
- if type.respond_to?(:schema)
59
+ if EasyTalk::RefHelper.should_use_ref?(type, @constraints)
60
+ EasyTalk::RefHelper.build_ref_schema(type, @constraints)
61
+ elsif type.respond_to?(:schema)
54
62
  type.schema
55
63
  else
56
- # Map Float type to 'number' in JSON Schema
57
- json_type = case type.to_s
58
- when 'Float', 'BigDecimal'
59
- 'number'
60
- else
61
- type.to_s.downcase
62
- end
63
- { type: json_type }
64
+ # Map Ruby type to JSON Schema type
65
+ { type: TypeIntrospection.json_schema_type(type) }
64
66
  end
65
67
  end
66
68
  end
@@ -68,6 +70,7 @@ module EasyTalk
68
70
  # Returns the items of the type.
69
71
  #
70
72
  # @return [T.untyped] The items of the type.
73
+ sig { returns(T.untyped) }
71
74
  def items
72
75
  @type.items
73
76
  end
@@ -1,4 +1,5 @@
1
1
  # frozen_string_literal: true
2
+ # typed: true
2
3
 
3
4
  require_relative 'base_builder'
4
5
 
@@ -20,7 +21,7 @@ module EasyTalk
20
21
  }.freeze
21
22
 
22
23
  # Initializes a new instance of the IntegerBuilder class.
23
- sig { params(name: Symbol, constraints: Hash).void }
24
+ sig { params(name: Symbol, constraints: T::Hash[Symbol, T.untyped]).void }
24
25
  def initialize(name, constraints = {})
25
26
  super(name, { type: 'integer' }, constraints, VALID_OPTIONS)
26
27
  end
@@ -1,4 +1,5 @@
1
1
  # frozen_string_literal: true
2
+ # typed: true
2
3
 
3
4
  require_relative 'base_builder'
4
5
 
@@ -6,8 +7,10 @@ module EasyTalk
6
7
  module Builders
7
8
  # builder class for Null properties.
8
9
  class NullBuilder < BaseBuilder
10
+ extend T::Sig
11
+
9
12
  # Initializes a new instance of the NullBuilder class.
10
- sig { params(name: Symbol, _constraints: Hash).void }
13
+ sig { params(name: Symbol, _constraints: T::Hash[Symbol, T.untyped]).void }
11
14
  def initialize(name, _constraints = {})
12
15
  super(name, { type: 'null' }, {}, {})
13
16
  end
@@ -1,4 +1,5 @@
1
1
  # frozen_string_literal: true
2
+ # typed: true
2
3
 
3
4
  require_relative 'base_builder'
4
5
 
@@ -6,6 +7,8 @@ module EasyTalk
6
7
  module Builders
7
8
  # Builder class for number properties.
8
9
  class NumberBuilder < BaseBuilder
10
+ extend T::Sig
11
+
9
12
  VALID_OPTIONS = {
10
13
  multiple_of: { type: T.any(Integer, Float), key: :multipleOf },
11
14
  minimum: { type: T.any(Integer, Float), key: :minimum },
@@ -18,7 +21,7 @@ module EasyTalk
18
21
  }.freeze
19
22
 
20
23
  # Initializes a new instance of the NumberBuilder class.
21
- sig { params(name: Symbol, constraints: Hash).void }
24
+ sig { params(name: Symbol, constraints: T::Hash[Symbol, T.untyped]).void }
22
25
  def initialize(name, constraints = {})
23
26
  super(name, { type: 'number' }, constraints, VALID_OPTIONS)
24
27
  end