easy_talk 3.1.0 → 3.2.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.
- checksums.yaml +4 -4
- data/.rubocop.yml +4 -0
- data/.yardopts +13 -0
- data/CHANGELOG.md +75 -0
- data/README.md +616 -35
- data/Rakefile +27 -0
- data/docs/.gitignore +1 -0
- data/docs/about.markdown +28 -8
- data/docs/getting-started.markdown +102 -0
- data/docs/index.markdown +51 -4
- data/docs/json_schema_compliance.md +55 -0
- data/docs/nested-models.markdown +216 -0
- data/docs/property-types.markdown +212 -0
- data/docs/schema-definition.markdown +180 -0
- data/lib/easy_talk/builders/base_builder.rb +4 -2
- data/lib/easy_talk/builders/composition_builder.rb +10 -12
- data/lib/easy_talk/builders/object_builder.rb +45 -30
- data/lib/easy_talk/builders/registry.rb +168 -0
- data/lib/easy_talk/builders/typed_array_builder.rb +15 -4
- data/lib/easy_talk/configuration.rb +31 -1
- data/lib/easy_talk/error_formatter/base.rb +100 -0
- data/lib/easy_talk/error_formatter/error_code_mapper.rb +82 -0
- data/lib/easy_talk/error_formatter/flat.rb +38 -0
- data/lib/easy_talk/error_formatter/json_pointer.rb +38 -0
- data/lib/easy_talk/error_formatter/jsonapi.rb +64 -0
- data/lib/easy_talk/error_formatter/path_converter.rb +53 -0
- data/lib/easy_talk/error_formatter/rfc7807.rb +69 -0
- data/lib/easy_talk/error_formatter.rb +143 -0
- data/lib/easy_talk/errors.rb +2 -0
- data/lib/easy_talk/errors_helper.rb +63 -34
- data/lib/easy_talk/model.rb +123 -90
- data/lib/easy_talk/model_helper.rb +13 -0
- data/lib/easy_talk/naming_strategies.rb +20 -0
- data/lib/easy_talk/property.rb +16 -94
- data/lib/easy_talk/ref_helper.rb +27 -0
- data/lib/easy_talk/schema.rb +198 -0
- data/lib/easy_talk/schema_definition.rb +7 -1
- data/lib/easy_talk/schema_methods.rb +80 -0
- data/lib/easy_talk/tools/function_builder.rb +1 -1
- data/lib/easy_talk/type_introspection.rb +178 -0
- data/lib/easy_talk/types/base_composer.rb +2 -1
- data/lib/easy_talk/types/composer.rb +4 -0
- data/lib/easy_talk/validation_adapters/active_model_adapter.rb +329 -0
- data/lib/easy_talk/validation_adapters/base.rb +144 -0
- data/lib/easy_talk/validation_adapters/none_adapter.rb +36 -0
- data/lib/easy_talk/validation_adapters/registry.rb +87 -0
- data/lib/easy_talk/validation_builder.rb +28 -309
- data/lib/easy_talk/version.rb +1 -1
- data/lib/easy_talk.rb +41 -0
- metadata +26 -4
- data/docs/404.html +0 -25
- data/docs/_posts/2024-05-07-welcome-to-jekyll.markdown +0 -29
- 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
|
|
@@ -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
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require_relative 'collection_helpers'
|
|
4
|
+
require_relative '../ref_helper'
|
|
4
5
|
|
|
5
6
|
module EasyTalk
|
|
6
7
|
module Builders
|
|
@@ -15,17 +16,18 @@ module EasyTalk
|
|
|
15
16
|
'OneOfBuilder' => 'oneOf'
|
|
16
17
|
}.freeze
|
|
17
18
|
|
|
18
|
-
sig { params(name: Symbol, type: T.untyped,
|
|
19
|
+
sig { params(name: Symbol, type: T.untyped, constraints: Hash).void }
|
|
19
20
|
# Initializes a new instance of the CompositionBuilder class.
|
|
20
21
|
#
|
|
21
22
|
# @param name [Symbol] The name of the composition.
|
|
22
23
|
# @param type [Class] The type of the composition.
|
|
23
|
-
# @param
|
|
24
|
-
def initialize(name, type,
|
|
24
|
+
# @param constraints [Hash] The constraints for the composition.
|
|
25
|
+
def initialize(name, type, constraints)
|
|
25
26
|
@composer_type = self.class.name.split('::').last
|
|
26
27
|
@name = name
|
|
27
28
|
@type = type
|
|
28
29
|
@context = {}
|
|
30
|
+
@constraints = constraints
|
|
29
31
|
end
|
|
30
32
|
|
|
31
33
|
# Builds the composed JSON schema.
|
|
@@ -50,17 +52,13 @@ module EasyTalk
|
|
|
50
52
|
# @return [Array<Hash>] The array of schemas.
|
|
51
53
|
def schemas
|
|
52
54
|
items.map do |type|
|
|
53
|
-
if
|
|
55
|
+
if EasyTalk::RefHelper.should_use_ref?(type, @constraints)
|
|
56
|
+
EasyTalk::RefHelper.build_ref_schema(type, @constraints)
|
|
57
|
+
elsif type.respond_to?(:schema)
|
|
54
58
|
type.schema
|
|
55
59
|
else
|
|
56
|
-
# Map
|
|
57
|
-
|
|
58
|
-
when 'Float', 'BigDecimal'
|
|
59
|
-
'number'
|
|
60
|
-
else
|
|
61
|
-
type.to_s.downcase
|
|
62
|
-
end
|
|
63
|
-
{ type: json_type }
|
|
60
|
+
# Map Ruby type to JSON Schema type
|
|
61
|
+
{ type: TypeIntrospection.json_schema_type(type) }
|
|
64
62
|
end
|
|
65
63
|
end
|
|
66
64
|
end
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require_relative 'base_builder'
|
|
4
|
+
require_relative '../model_helper'
|
|
4
5
|
|
|
5
6
|
module EasyTalk
|
|
6
7
|
module Builders
|
|
@@ -33,8 +34,8 @@ module EasyTalk
|
|
|
33
34
|
def initialize(schema_definition)
|
|
34
35
|
# Keep a reference to the original schema definition
|
|
35
36
|
@schema_definition = schema_definition
|
|
36
|
-
#
|
|
37
|
-
@original_schema = schema_definition.schema
|
|
37
|
+
# Deep duplicate the raw schema hash so we can mutate it safely
|
|
38
|
+
@original_schema = deep_dup(schema_definition.schema)
|
|
38
39
|
|
|
39
40
|
# We'll collect required property names in this Set
|
|
40
41
|
@required_properties = Set.new
|
|
@@ -56,6 +57,24 @@ module EasyTalk
|
|
|
56
57
|
|
|
57
58
|
private
|
|
58
59
|
|
|
60
|
+
##
|
|
61
|
+
# Deep duplicates a hash, including nested hashes.
|
|
62
|
+
# This prevents mutations from leaking back to the original schema.
|
|
63
|
+
#
|
|
64
|
+
def deep_dup(obj)
|
|
65
|
+
case obj
|
|
66
|
+
when Hash
|
|
67
|
+
obj.transform_values { |v| deep_dup(v) }
|
|
68
|
+
when Array
|
|
69
|
+
obj.map { |v| deep_dup(v) }
|
|
70
|
+
when Class, Module
|
|
71
|
+
# Don't duplicate Class or Module objects - they represent types
|
|
72
|
+
obj
|
|
73
|
+
else
|
|
74
|
+
obj.duplicable? ? obj.dup : obj
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
59
78
|
##
|
|
60
79
|
# Main aggregator: merges the top-level schema keys (like :properties, :subschemas)
|
|
61
80
|
# into a single hash that we'll feed to BaseBuilder.
|
|
@@ -99,16 +118,18 @@ module EasyTalk
|
|
|
99
118
|
# Cache with a key based on property name and its full configuration
|
|
100
119
|
@properties_cache ||= {}
|
|
101
120
|
|
|
102
|
-
properties_hash.each_with_object({}) do |(
|
|
103
|
-
|
|
121
|
+
properties_hash.each_with_object({}) do |(original_name, prop_options), result|
|
|
122
|
+
# Use :as constraint for property name without mutating original constraints
|
|
123
|
+
property_name = (prop_options[:constraints][:as] || original_name).to_sym
|
|
124
|
+
cache_key = [property_name, prop_options].hash
|
|
104
125
|
|
|
105
126
|
# Use cache if the exact property and configuration have been processed before
|
|
106
127
|
@properties_cache[cache_key] ||= begin
|
|
107
|
-
mark_required_unless_optional(
|
|
108
|
-
build_property(
|
|
128
|
+
mark_required_unless_optional(property_name, prop_options)
|
|
129
|
+
build_property(property_name, prop_options)
|
|
109
130
|
end
|
|
110
131
|
|
|
111
|
-
result[
|
|
132
|
+
result[property_name] = @properties_cache[cache_key]
|
|
112
133
|
end
|
|
113
134
|
end
|
|
114
135
|
|
|
@@ -145,8 +166,8 @@ module EasyTalk
|
|
|
145
166
|
|
|
146
167
|
# Memoize so we only build each property once
|
|
147
168
|
@property_cache[prop_name] ||= begin
|
|
148
|
-
# Remove
|
|
149
|
-
constraints = prop_options[:constraints].except(:optional)
|
|
169
|
+
# Remove internal constraints that shouldn't be passed to Property
|
|
170
|
+
constraints = prop_options[:constraints].except(:optional, :as)
|
|
150
171
|
prop_type = prop_options[:type]
|
|
151
172
|
|
|
152
173
|
# Track models that will use $ref for later $defs generation
|
|
@@ -165,10 +186,11 @@ module EasyTalk
|
|
|
165
186
|
# Check if this type should use $ref
|
|
166
187
|
if should_collect_ref?(prop_type, constraints)
|
|
167
188
|
@ref_models.add(prop_type)
|
|
189
|
+
elsif prop_type.is_a?(EasyTalk::Types::Composer)
|
|
190
|
+
collect_ref_models(prop_type.items, constraints)
|
|
168
191
|
# Handle typed arrays with EasyTalk model items
|
|
169
|
-
elsif
|
|
170
|
-
inner_type
|
|
171
|
-
@ref_models.add(inner_type) if should_collect_ref?(inner_type, constraints)
|
|
192
|
+
elsif typed_array?(prop_type)
|
|
193
|
+
extract_inner_types(prop_type).each { |inner_type| collect_ref_models(inner_type, constraints) }
|
|
172
194
|
# Handle nilable types
|
|
173
195
|
elsif nilable_with_model?(prop_type)
|
|
174
196
|
actual_type = T::Utils::Nilable.get_underlying_type(prop_type)
|
|
@@ -180,7 +202,7 @@ module EasyTalk
|
|
|
180
202
|
# Determines if a type should be collected for $ref based on config and constraints.
|
|
181
203
|
#
|
|
182
204
|
def should_collect_ref?(check_type, constraints)
|
|
183
|
-
return false unless easytalk_model?(check_type)
|
|
205
|
+
return false unless ModelHelper.easytalk_model?(check_type)
|
|
184
206
|
|
|
185
207
|
# Per-property constraint takes precedence
|
|
186
208
|
return constraints[:ref] if constraints.key?(:ref)
|
|
@@ -189,25 +211,18 @@ module EasyTalk
|
|
|
189
211
|
EasyTalk.configuration.use_refs
|
|
190
212
|
end
|
|
191
213
|
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
#
|
|
195
|
-
def easytalk_model?(check_type)
|
|
196
|
-
check_type.is_a?(Class) &&
|
|
197
|
-
check_type.respond_to?(:schema) &&
|
|
198
|
-
check_type.respond_to?(:ref_template) &&
|
|
199
|
-
defined?(EasyTalk::Model) &&
|
|
200
|
-
check_type.include?(EasyTalk::Model)
|
|
214
|
+
def typed_array?(prop_type)
|
|
215
|
+
prop_type.is_a?(T::Types::TypedArray)
|
|
201
216
|
end
|
|
202
217
|
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
#
|
|
206
|
-
def typed_array_with_model?(prop_type)
|
|
207
|
-
return false unless prop_type.is_a?(T::Types::TypedArray)
|
|
218
|
+
def extract_inner_types(prop_type)
|
|
219
|
+
return [] unless typed_array?(prop_type)
|
|
208
220
|
|
|
209
|
-
|
|
210
|
-
|
|
221
|
+
if prop_type.type.is_a?(EasyTalk::Types::Composer)
|
|
222
|
+
prop_type.type.items
|
|
223
|
+
else
|
|
224
|
+
[prop_type.type.raw_type]
|
|
225
|
+
end
|
|
211
226
|
end
|
|
212
227
|
|
|
213
228
|
##
|
|
@@ -219,7 +234,7 @@ module EasyTalk
|
|
|
219
234
|
return false unless prop_type.types.any? { |t| t.raw_type == NilClass }
|
|
220
235
|
|
|
221
236
|
actual_type = T::Utils::Nilable.get_underlying_type(prop_type)
|
|
222
|
-
easytalk_model?(actual_type)
|
|
237
|
+
ModelHelper.easytalk_model?(actual_type)
|
|
223
238
|
end
|
|
224
239
|
|
|
225
240
|
##
|