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.
- checksums.yaml +4 -4
- data/.rubocop.yml +15 -39
- data/.yardopts +13 -0
- data/CHANGELOG.md +164 -0
- data/README.md +442 -1529
- 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 +169 -0
- data/docs/nested-models.markdown +216 -0
- data/docs/primitive-schema-rfc.md +894 -0
- data/docs/property-types.markdown +212 -0
- data/docs/schema-definition.markdown +180 -0
- data/lib/easy_talk/builders/base_builder.rb +6 -3
- data/lib/easy_talk/builders/boolean_builder.rb +2 -1
- data/lib/easy_talk/builders/collection_helpers.rb +4 -0
- data/lib/easy_talk/builders/composition_builder.rb +16 -13
- data/lib/easy_talk/builders/integer_builder.rb +2 -1
- data/lib/easy_talk/builders/null_builder.rb +4 -1
- data/lib/easy_talk/builders/number_builder.rb +4 -1
- data/lib/easy_talk/builders/object_builder.rb +109 -33
- data/lib/easy_talk/builders/registry.rb +182 -0
- data/lib/easy_talk/builders/string_builder.rb +3 -1
- data/lib/easy_talk/builders/temporal_builder.rb +7 -0
- data/lib/easy_talk/builders/tuple_builder.rb +89 -0
- data/lib/easy_talk/builders/typed_array_builder.rb +19 -6
- data/lib/easy_talk/builders/union_builder.rb +5 -1
- data/lib/easy_talk/configuration.rb +47 -2
- 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 +3 -0
- data/lib/easy_talk/errors_helper.rb +66 -34
- data/lib/easy_talk/json_schema_equality.rb +46 -0
- data/lib/easy_talk/keywords.rb +0 -1
- data/lib/easy_talk/model.rb +148 -89
- data/lib/easy_talk/model_helper.rb +17 -0
- data/lib/easy_talk/naming_strategies.rb +24 -0
- data/lib/easy_talk/property.rb +23 -94
- data/lib/easy_talk/ref_helper.rb +33 -0
- data/lib/easy_talk/schema.rb +199 -0
- data/lib/easy_talk/schema_definition.rb +57 -5
- data/lib/easy_talk/schema_methods.rb +111 -0
- data/lib/easy_talk/sorbet_extension.rb +1 -0
- data/lib/easy_talk/tools/function_builder.rb +1 -1
- data/lib/easy_talk/type_introspection.rb +222 -0
- data/lib/easy_talk/types/base_composer.rb +2 -1
- data/lib/easy_talk/types/composer.rb +4 -0
- data/lib/easy_talk/types/tuple.rb +77 -0
- data/lib/easy_talk/validation_adapters/active_model_adapter.rb +617 -0
- data/lib/easy_talk/validation_adapters/active_model_schema_validation.rb +106 -0
- data/lib/easy_talk/validation_adapters/base.rb +156 -0
- data/lib/easy_talk/validation_adapters/none_adapter.rb +45 -0
- data/lib/easy_talk/validation_adapters/registry.rb +87 -0
- data/lib/easy_talk/validation_builder.rb +29 -309
- data/lib/easy_talk/version.rb +1 -1
- data/lib/easy_talk.rb +42 -0
- metadata +38 -7
- data/docs/404.html +0 -25
- data/docs/_posts/2024-05-07-welcome-to-jekyll.markdown +0 -29
- data/easy_talk.gemspec +0 -39
data/README.md
CHANGED
|
@@ -2,1832 +2,745 @@
|
|
|
2
2
|
|
|
3
3
|
[](https://badge.fury.io/rb/easy_talk)
|
|
4
4
|
[](https://github.com/sergiobayona/easy_talk/actions/workflows/dev-build.yml)
|
|
5
|
+
[](https://codecov.io/gh/sergiobayona/easy_talk)
|
|
6
|
+
[](https://opensource.org/licenses/MIT)
|
|
7
|
+
[](https://www.ruby-lang.org)
|
|
8
|
+
[](https://rubygems.org/gems/easy_talk)
|
|
9
|
+
[](https://rubydoc.info/gems/easy_talk)
|
|
10
|
+
[](https://github.com/sergiobayona/easy_talk)
|
|
5
11
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
### What is EasyTalk?
|
|
9
|
-
EasyTalk is a Ruby library that simplifies defining and generating JSON Schema. It provides an intuitive interface for Ruby developers to define structured data models that can be used for validation and documentation.
|
|
10
|
-
|
|
11
|
-
### Key Features
|
|
12
|
-
* **Intuitive Schema Definition**: Use Ruby classes and methods to define JSON Schema documents easily.
|
|
13
|
-
* **Automatic ActiveModel Validations**: Schema constraints automatically generate corresponding ActiveModel validations (configurable).
|
|
14
|
-
* **Works for plain Ruby classes and ActiveModel classes**: Integrate with existing code or build from scratch.
|
|
15
|
-
* **LLM Function Support**: Ideal for integrating with Large Language Models (LLMs) such as OpenAI's GPT series. EasyTalk enables you to effortlessly create JSON Schema documents describing the inputs and outputs of LLM function calls.
|
|
16
|
-
* **Schema Composition**: Define EasyTalk models and reference them in other EasyTalk models to create complex schemas.
|
|
17
|
-
* **Enhanced Model Integration**: Automatic instantiation of nested EasyTalk models from hash attributes.
|
|
18
|
-
* **Flexible Configuration**: Global and per-model configuration options for fine-tuned control.
|
|
19
|
-
* **JSON Schema Version Support**: Configure the `$schema` keyword to declare which JSON Schema draft version your schemas conform to (Draft-04 through Draft 2020-12).
|
|
20
|
-
* **Schema Identification**: Configure the `$id` keyword to provide a unique identifier URI for your schemas.
|
|
21
|
-
* **Schema References**: Use `$ref` and `$defs` for reusable schema definitions, reducing duplication when models are used in multiple places.
|
|
22
|
-
|
|
23
|
-
### Use Cases
|
|
24
|
-
- API request/response validation
|
|
25
|
-
- LLM function definitions
|
|
26
|
-
- Object structure documentation
|
|
27
|
-
- Data validation and transformation
|
|
28
|
-
- Configuration schema definitions
|
|
29
|
-
|
|
30
|
-
### Inspiration
|
|
31
|
-
Inspired by Python's Pydantic library, EasyTalk brings similar functionality to the Ruby ecosystem, providing a Ruby-friendly approach to JSON Schema operations.
|
|
12
|
+
Ruby library for defining **structured data contracts** that generate **JSON Schema** *and* (optionally) **runtime validations** from the same definition.
|
|
32
13
|
|
|
33
|
-
|
|
14
|
+
Think “Pydantic-style ergonomics” for Ruby, with first-class JSON Schema output.
|
|
34
15
|
|
|
35
|
-
|
|
36
|
-
- Ruby 3.2 or higher
|
|
16
|
+
---
|
|
37
17
|
|
|
38
|
-
|
|
18
|
+
## Why EasyTalk?
|
|
39
19
|
|
|
40
|
-
|
|
20
|
+
You can hand-write JSON Schema, then hand-write validations, then hand-write error responses… and eventually you’ll ship a bug where those three disagree.
|
|
41
21
|
|
|
42
|
-
|
|
43
|
-
- **Migration**: Use class references instead of inline Hash definitions
|
|
22
|
+
EasyTalk makes the schema definition the single source of truth, so you can:
|
|
44
23
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
property :street, String
|
|
50
|
-
property :city, String
|
|
51
|
-
end
|
|
52
|
-
end
|
|
24
|
+
- **Define once, use everywhere**
|
|
25
|
+
One Ruby DSL gives you:
|
|
26
|
+
- `json_schema` for docs, OpenAPI, LLM tools, and external validators
|
|
27
|
+
- `valid?` / `errors` (when using `EasyTalk::Model`) for runtime validation
|
|
53
28
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
end
|
|
29
|
+
- **Stop arguing with JSON Schema’s verbosity**
|
|
30
|
+
Express constraints in Ruby where you already live:
|
|
31
|
+
```ruby
|
|
32
|
+
property :email, String, format: "email"
|
|
33
|
+
property :age, Integer, minimum: 18
|
|
34
|
+
property :tags, T::Array[String], min_items: 1
|
|
35
|
+
```
|
|
62
36
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
37
|
+
- **Use a richer type system than "string/integer/object"**
|
|
38
|
+
EasyTalk supports Sorbet-style types and composition:
|
|
39
|
+
- `T.nilable(Type)` for nullable fields
|
|
40
|
+
- `T::Array[Type]` for typed arrays
|
|
41
|
+
- `T::Tuple[Type1, Type2, ...]` for fixed-position typed arrays
|
|
42
|
+
- `T::Boolean`
|
|
43
|
+
- `T::AnyOf`, `T::OneOf`, `T::AllOf` for schema composition
|
|
70
44
|
|
|
71
|
-
|
|
72
|
-
|
|
45
|
+
- **Get validations for free (when you want them)**
|
|
46
|
+
With `auto_validations` enabled (default), schema constraints generate ActiveModel validations—**including nested models**, even inside arrays.
|
|
73
47
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
48
|
+
- **Make API errors consistent**
|
|
49
|
+
Format validation errors as:
|
|
50
|
+
- flat lists
|
|
51
|
+
- JSON Pointer
|
|
52
|
+
- **RFC 7807** problem details
|
|
53
|
+
- **JSON:API** error objects
|
|
77
54
|
|
|
78
|
-
|
|
55
|
+
- **LLM tool/function schemas without a second schema layer**
|
|
56
|
+
Use the same contract to generate JSON Schema for function/tool calling.
|
|
79
57
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
58
|
+
EasyTalk is for teams who want their data contracts to be **correct, reusable, and boring** (the good kind of boring).
|
|
59
|
+
|
|
60
|
+
---
|
|
61
|
+
|
|
62
|
+
## Installation
|
|
63
|
+
|
|
64
|
+
### Requirements
|
|
65
|
+
- Ruby **3.2+**
|
|
83
66
|
|
|
84
|
-
|
|
85
|
-
After installation, you can verify it's working by creating a simple model:
|
|
67
|
+
Add to your Gemfile:
|
|
86
68
|
|
|
87
69
|
```ruby
|
|
88
|
-
|
|
70
|
+
gem "easy_talk"
|
|
71
|
+
```
|
|
89
72
|
|
|
90
|
-
|
|
91
|
-
include EasyTalk::Model
|
|
92
|
-
|
|
93
|
-
define_schema do
|
|
94
|
-
property :name, String
|
|
95
|
-
end
|
|
96
|
-
end
|
|
73
|
+
Then:
|
|
97
74
|
|
|
98
|
-
|
|
75
|
+
```bash
|
|
76
|
+
bundle install
|
|
99
77
|
```
|
|
100
78
|
|
|
101
|
-
|
|
79
|
+
---
|
|
102
80
|
|
|
103
|
-
|
|
104
|
-
Here's a basic example to get you started with EasyTalk:
|
|
81
|
+
## Quick start
|
|
105
82
|
|
|
106
83
|
```ruby
|
|
84
|
+
require "easy_talk"
|
|
85
|
+
|
|
107
86
|
class User
|
|
108
87
|
include EasyTalk::Model
|
|
109
88
|
|
|
110
89
|
define_schema do
|
|
111
90
|
title "User"
|
|
112
91
|
description "A user of the system"
|
|
113
|
-
|
|
92
|
+
|
|
93
|
+
property :id, String
|
|
94
|
+
property :name, String, min_length: 2
|
|
114
95
|
property :email, String, format: "email"
|
|
115
96
|
property :age, Integer, minimum: 18
|
|
116
97
|
end
|
|
117
98
|
end
|
|
99
|
+
|
|
100
|
+
User.json_schema # => Ruby Hash (JSON Schema)
|
|
101
|
+
user = User.new(name: "A") # invalid: min_length is 2
|
|
102
|
+
user.valid? # => false
|
|
103
|
+
user.errors # => ActiveModel::Errors
|
|
118
104
|
```
|
|
119
105
|
|
|
120
|
-
|
|
121
|
-
Calling `User.json_schema` will generate:
|
|
106
|
+
**Generated JSON Schema:**
|
|
122
107
|
|
|
123
|
-
```
|
|
108
|
+
```json
|
|
124
109
|
{
|
|
125
|
-
"type"
|
|
126
|
-
"title"
|
|
127
|
-
"description"
|
|
128
|
-
"properties"
|
|
129
|
-
"
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
}
|
|
133
|
-
"email" => {
|
|
134
|
-
"type" => "string",
|
|
135
|
-
"format" => "email"
|
|
136
|
-
},
|
|
137
|
-
"age" => {
|
|
138
|
-
"type" => "integer",
|
|
139
|
-
"minimum" => 18
|
|
140
|
-
}
|
|
110
|
+
"type": "object",
|
|
111
|
+
"title": "User",
|
|
112
|
+
"description": "A user of the system",
|
|
113
|
+
"properties": {
|
|
114
|
+
"id": { "type": "string" },
|
|
115
|
+
"name": { "type": "string", "minLength": 2 },
|
|
116
|
+
"email": { "type": "string", "format": "email" },
|
|
117
|
+
"age": { "type": "integer", "minimum": 18 }
|
|
141
118
|
},
|
|
142
|
-
"required"
|
|
119
|
+
"required": ["id", "name", "email", "age"]
|
|
143
120
|
}
|
|
144
121
|
```
|
|
145
122
|
|
|
146
|
-
|
|
147
|
-
Creating and validating an instance of your model:
|
|
148
|
-
|
|
149
|
-
```ruby
|
|
150
|
-
user = User.new(name: "John Doe", email: "john@example.com", age: 25)
|
|
151
|
-
user.valid? # => true (automatically validates based on schema constraints)
|
|
123
|
+
---
|
|
152
124
|
|
|
153
|
-
|
|
154
|
-
user.valid? # => false (violates minimum: 18 constraint)
|
|
155
|
-
user.errors[:age] # => ["must be greater than or equal to 18"]
|
|
156
|
-
```
|
|
125
|
+
## Property constraints
|
|
157
126
|
|
|
158
|
-
|
|
127
|
+
| Constraint | Applies to | Example |
|
|
128
|
+
|------------|-----------|---------|
|
|
129
|
+
| `min_length` / `max_length` | String | `property :name, String, min_length: 2, max_length: 50` |
|
|
130
|
+
| `minimum` / `maximum` | Integer, Float | `property :age, Integer, minimum: 18, maximum: 120` |
|
|
131
|
+
| `format` | String | `property :email, String, format: "email"` |
|
|
132
|
+
| `pattern` | String | `property :zip, String, pattern: '^\d{5}$'` |
|
|
133
|
+
| `enum` | Any | `property :status, String, enum: ["active", "inactive"]` |
|
|
134
|
+
| `min_items` / `max_items` | Array, Tuple | `property :tags, T::Array[String], min_items: 1` |
|
|
135
|
+
| `unique_items` | Array, Tuple | `property :ids, T::Array[Integer], unique_items: true` |
|
|
136
|
+
| `additional_items` | Tuple | `property :coords, T::Tuple[Float, Float], additional_items: false` |
|
|
137
|
+
| `optional` | Any | `property :nickname, String, optional: true` |
|
|
138
|
+
| `default` | Any | `property :role, String, default: "user"` |
|
|
139
|
+
| `description` | Any | `property :name, String, description: "Full name"` |
|
|
140
|
+
| `title` | Any | `property :name, String, title: "User Name"` |
|
|
159
141
|
|
|
160
|
-
|
|
161
|
-
|
|
142
|
+
**Object-level constraints** (applied in `define_schema` block):
|
|
143
|
+
- `min_properties` / `max_properties` - Minimum/maximum number of properties
|
|
144
|
+
- `pattern_properties` - Schema for properties matching regex patterns
|
|
145
|
+
- `dependent_required` - Conditional property requirements
|
|
162
146
|
|
|
163
|
-
|
|
164
|
-
class MyModel
|
|
165
|
-
include EasyTalk::Model
|
|
147
|
+
When `auto_validations` is enabled (default), these constraints automatically generate corresponding ActiveModel validations.
|
|
166
148
|
|
|
167
|
-
|
|
168
|
-
title "My Model"
|
|
169
|
-
description "Description of my model"
|
|
170
|
-
property :some_property, String
|
|
171
|
-
property :another_property, Integer
|
|
172
|
-
end
|
|
173
|
-
end
|
|
174
|
-
```
|
|
149
|
+
---
|
|
175
150
|
|
|
176
|
-
|
|
151
|
+
## Core concepts
|
|
177
152
|
|
|
178
|
-
|
|
179
|
-
EasyTalk supports standard Ruby types directly:
|
|
153
|
+
### Required vs optional vs nullable (don't get tricked)
|
|
180
154
|
|
|
181
|
-
|
|
182
|
-
-
|
|
183
|
-
- `
|
|
184
|
-
- `Date`: Date values
|
|
185
|
-
- `DateTime`: Date and time values
|
|
155
|
+
JSON Schema distinguishes:
|
|
156
|
+
- **Optional**: property may be omitted (not in `required`)
|
|
157
|
+
- **Nullable**: property may be `null` (type includes `"null"`)
|
|
186
158
|
|
|
187
|
-
|
|
188
|
-
For complex types, EasyTalk uses Sorbet-style type notation:
|
|
159
|
+
EasyTalk mirrors that precisely:
|
|
189
160
|
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
161
|
+
```ruby
|
|
162
|
+
class Profile
|
|
163
|
+
include EasyTalk::Model
|
|
193
164
|
|
|
194
|
-
|
|
195
|
-
|
|
165
|
+
define_schema do
|
|
166
|
+
# required, not nullable
|
|
167
|
+
property :name, String
|
|
196
168
|
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
- `T::AllOf[Type1, Type2, ...]`: Value must match all of the specified schemas
|
|
169
|
+
# required, nullable (must exist, may be null)
|
|
170
|
+
property :age, T.nilable(Integer)
|
|
200
171
|
|
|
201
|
-
|
|
202
|
-
|
|
172
|
+
# optional, not nullable (may be omitted, but cannot be null if present)
|
|
173
|
+
property :nickname, String, optional: true
|
|
203
174
|
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
- `min_items`/`max_items`: Minimum/maximum number of items for arrays
|
|
212
|
-
- `unique_items`: Whether array items must be unique
|
|
175
|
+
# optional + nullable (may be omitted OR null)
|
|
176
|
+
property :bio, T.nilable(String), optional: true
|
|
177
|
+
# or, equivalently:
|
|
178
|
+
nullable_optional_property :website, String
|
|
179
|
+
end
|
|
180
|
+
end
|
|
181
|
+
```
|
|
213
182
|
|
|
214
|
-
|
|
215
|
-
|
|
183
|
+
By default, `T.nilable(Type)` makes a field **nullable but still required**.
|
|
184
|
+
If you want “nilable implies optional” behavior globally:
|
|
216
185
|
|
|
217
186
|
```ruby
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
property :middle_name, String, optional: true
|
|
187
|
+
EasyTalk.configure do |config|
|
|
188
|
+
config.nilable_is_optional = true
|
|
221
189
|
end
|
|
222
190
|
```
|
|
223
191
|
|
|
224
|
-
|
|
192
|
+
---
|
|
225
193
|
|
|
226
|
-
###
|
|
227
|
-
|
|
194
|
+
### Nested models (and automatic instantiation)
|
|
195
|
+
|
|
196
|
+
Define nested objects as separate classes, then reference them:
|
|
228
197
|
|
|
229
198
|
```ruby
|
|
199
|
+
class Address
|
|
200
|
+
include EasyTalk::Model
|
|
201
|
+
|
|
202
|
+
define_schema do
|
|
203
|
+
property :street, String
|
|
204
|
+
property :city, String
|
|
205
|
+
end
|
|
206
|
+
end
|
|
207
|
+
|
|
230
208
|
class User
|
|
231
209
|
include EasyTalk::Model
|
|
232
|
-
|
|
210
|
+
|
|
233
211
|
define_schema do
|
|
234
|
-
property :name, String
|
|
235
|
-
property :
|
|
236
|
-
property :age, Integer, minimum: 18, maximum: 120
|
|
237
|
-
property :status, String, enum: ["active", "inactive", "pending"]
|
|
212
|
+
property :name, String
|
|
213
|
+
property :address, Address
|
|
238
214
|
end
|
|
239
|
-
# Validations are automatically generated:
|
|
240
|
-
# validates :name, presence: true, length: { minimum: 2, maximum: 50 }
|
|
241
|
-
# validates :email, presence: true, format: { with: URI::MailTo::EMAIL_REGEXP }
|
|
242
|
-
# validates :age, presence: true, numericality: { greater_than_or_equal_to: 18, less_than_or_equal_to: 120 }
|
|
243
|
-
# validates :status, presence: true, inclusion: { in: ["active", "inactive", "pending"] }
|
|
244
215
|
end
|
|
245
216
|
|
|
246
|
-
user = User.new(
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
#
|
|
217
|
+
user = User.new(
|
|
218
|
+
name: "John",
|
|
219
|
+
address: { street: "123 Main St", city: "Boston" } # Hash becomes Address automatically
|
|
220
|
+
)
|
|
221
|
+
|
|
222
|
+
user.address.class # => Address
|
|
252
223
|
```
|
|
253
224
|
|
|
254
|
-
|
|
255
|
-
You can still add manual validations alongside automatic ones:
|
|
225
|
+
Nested models inside arrays work too:
|
|
256
226
|
|
|
257
227
|
```ruby
|
|
258
|
-
class
|
|
228
|
+
class Order
|
|
259
229
|
include EasyTalk::Model
|
|
260
|
-
|
|
261
|
-
# Custom validation in addition to automatic ones
|
|
262
|
-
validates :email, uniqueness: true
|
|
263
|
-
validate :complex_business_rule
|
|
264
|
-
|
|
230
|
+
|
|
265
231
|
define_schema do
|
|
266
|
-
property :
|
|
267
|
-
property :email, String, format: "email"
|
|
268
|
-
property :age, Integer, minimum: 18
|
|
269
|
-
end
|
|
270
|
-
|
|
271
|
-
private
|
|
272
|
-
|
|
273
|
-
def complex_business_rule
|
|
274
|
-
# Custom validation logic
|
|
232
|
+
property :line_items, T::Array[Address], min_items: 1
|
|
275
233
|
end
|
|
276
234
|
end
|
|
277
235
|
```
|
|
278
236
|
|
|
279
|
-
|
|
237
|
+
---
|
|
280
238
|
|
|
281
|
-
###
|
|
282
|
-
|
|
239
|
+
### Tuple arrays (fixed-position types)
|
|
240
|
+
|
|
241
|
+
Use `T::Tuple` for arrays where each position has a specific type (e.g., coordinates, CSV rows, database records):
|
|
283
242
|
|
|
284
243
|
```ruby
|
|
285
|
-
class
|
|
244
|
+
class GeoLocation
|
|
286
245
|
include EasyTalk::Model
|
|
287
246
|
|
|
288
247
|
define_schema do
|
|
289
|
-
title "Person"
|
|
290
248
|
property :name, String
|
|
291
|
-
|
|
249
|
+
# Fixed: [latitude, longitude]
|
|
250
|
+
property :coordinates, T::Tuple[Float, Float]
|
|
292
251
|
end
|
|
293
252
|
end
|
|
294
|
-
```
|
|
295
|
-
|
|
296
|
-
### Property Definitions
|
|
297
|
-
Properties are defined using the `property` method, which takes a name, a type, and optional constraints:
|
|
298
253
|
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
254
|
+
location = GeoLocation.new(
|
|
255
|
+
name: 'Office',
|
|
256
|
+
coordinates: [40.7128, -74.0060]
|
|
257
|
+
)
|
|
302
258
|
```
|
|
303
259
|
|
|
304
|
-
|
|
305
|
-
Arrays can be defined using the `T::Array` type:
|
|
260
|
+
**Generated JSON Schema:**
|
|
306
261
|
|
|
307
|
-
```
|
|
308
|
-
|
|
309
|
-
|
|
262
|
+
```json
|
|
263
|
+
{
|
|
264
|
+
"properties": {
|
|
265
|
+
"coordinates": {
|
|
266
|
+
"type": "array",
|
|
267
|
+
"items": [
|
|
268
|
+
{ "type": "number" },
|
|
269
|
+
{ "type": "number" }
|
|
270
|
+
]
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
}
|
|
310
274
|
```
|
|
311
275
|
|
|
312
|
-
|
|
276
|
+
**Mixed-type tuples:**
|
|
313
277
|
|
|
314
278
|
```ruby
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
### Constraints and Automatic Validations
|
|
319
|
-
Constraints are added to properties and are used for both schema generation and automatic validation generation:
|
|
279
|
+
class DataRow
|
|
280
|
+
include EasyTalk::Model
|
|
320
281
|
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
property :category, String, enum: ["A", "B", "C"]
|
|
326
|
-
property :score, Float, minimum: 0.0, maximum: 100.0
|
|
327
|
-
property :tags, T::Array[String], min_items: 1, max_items: 10
|
|
282
|
+
define_schema do
|
|
283
|
+
# Fixed: [name, age, active]
|
|
284
|
+
property :row, T::Tuple[String, Integer, T::Boolean]
|
|
285
|
+
end
|
|
328
286
|
end
|
|
329
|
-
# Automatically generates equivalent ActiveModel validations:
|
|
330
|
-
# validates :name, presence: true, length: { minimum: 2, maximum: 50 }
|
|
331
|
-
# validates :email, presence: true, format: { with: URI::MailTo::EMAIL_REGEXP }
|
|
332
|
-
# validates :category, presence: true, inclusion: { in: ["A", "B", "C"] }
|
|
333
|
-
# validates :score, presence: true, numericality: { greater_than_or_equal_to: 0.0, less_than_or_equal_to: 100.0 }
|
|
334
|
-
# validates :tags, presence: true, length: { minimum: 1, maximum: 10 }
|
|
335
287
|
```
|
|
336
288
|
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
| Constraint | Validation Generated |
|
|
340
|
-
|------------|---------------------|
|
|
341
|
-
| `min_length`, `max_length` | `length: { minimum: X, maximum: Y }` |
|
|
342
|
-
| `minimum`, `maximum` | `numericality: { greater_than_or_equal_to: X, less_than_or_equal_to: Y }` |
|
|
343
|
-
| `format: "email"` | `format: { with: URI::MailTo::EMAIL_REGEXP }` |
|
|
344
|
-
| `format: "url"` or `format: "uri"` | `format: { with: URI::regexp }` |
|
|
345
|
-
| `pattern: /regex/` | `format: { with: /regex/ }` |
|
|
346
|
-
| `enum: [...]` | `inclusion: { in: [...] }` |
|
|
347
|
-
| `min_items`, `max_items` (arrays) | `length: { minimum: X, maximum: Y }` |
|
|
348
|
-
| `optional: true` | Skips presence validation |
|
|
349
|
-
| `T.nilable(Type)` | Allows nil values, skips presence validation |
|
|
350
|
-
|
|
351
|
-
### Additional Properties
|
|
352
|
-
By default, EasyTalk models do not allow additional properties beyond those defined in the schema. You can change this behavior using the `additional_properties` keyword:
|
|
289
|
+
**Controlling extra items:**
|
|
353
290
|
|
|
354
291
|
```ruby
|
|
355
292
|
define_schema do
|
|
356
|
-
|
|
357
|
-
|
|
293
|
+
# Reject extra items (strict tuple)
|
|
294
|
+
property :rgb, T::Tuple[Integer, Integer, Integer], additional_items: false
|
|
295
|
+
|
|
296
|
+
# Allow extra items of specific type
|
|
297
|
+
property :header_values, T::Tuple[String], additional_items: Integer
|
|
298
|
+
|
|
299
|
+
# Allow any extra items (default)
|
|
300
|
+
property :flexible, T::Tuple[String, Integer]
|
|
358
301
|
end
|
|
359
302
|
```
|
|
360
303
|
|
|
361
|
-
|
|
304
|
+
**Tuple validation:**
|
|
362
305
|
|
|
363
306
|
```ruby
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
307
|
+
model = GeoLocation.new(coordinates: [40.7, "invalid"])
|
|
308
|
+
model.valid? # => false
|
|
309
|
+
model.errors[:coordinates]
|
|
310
|
+
# => ["item at index 1 must be a Float"]
|
|
368
311
|
```
|
|
369
312
|
|
|
370
|
-
|
|
313
|
+
---
|
|
371
314
|
|
|
372
|
-
###
|
|
373
|
-
The `T::AnyOf` type allows a property to match any of the specified schemas:
|
|
315
|
+
### Composition (AnyOf / OneOf / AllOf)
|
|
374
316
|
|
|
375
317
|
```ruby
|
|
376
|
-
class
|
|
318
|
+
class ProductA
|
|
377
319
|
include EasyTalk::Model
|
|
378
|
-
|
|
379
320
|
define_schema do
|
|
380
|
-
property :
|
|
321
|
+
property :sku, String
|
|
322
|
+
property :weight, Float
|
|
381
323
|
end
|
|
382
324
|
end
|
|
383
|
-
```
|
|
384
325
|
|
|
385
|
-
|
|
386
|
-
|
|
326
|
+
class ProductB
|
|
327
|
+
include EasyTalk::Model
|
|
328
|
+
define_schema do
|
|
329
|
+
property :sku, String
|
|
330
|
+
property :color, String
|
|
331
|
+
end
|
|
332
|
+
end
|
|
387
333
|
|
|
388
|
-
|
|
389
|
-
class Contact
|
|
334
|
+
class Cart
|
|
390
335
|
include EasyTalk::Model
|
|
391
336
|
|
|
392
337
|
define_schema do
|
|
393
|
-
property :
|
|
338
|
+
property :items, T::Array[T::AnyOf[ProductA, ProductB]]
|
|
394
339
|
end
|
|
395
340
|
end
|
|
396
341
|
```
|
|
397
342
|
|
|
398
|
-
|
|
399
|
-
|
|
343
|
+
---
|
|
344
|
+
|
|
345
|
+
## Validations
|
|
346
|
+
|
|
347
|
+
### Automatic validations (default)
|
|
348
|
+
|
|
349
|
+
EasyTalk can generate ActiveModel validations from constraints:
|
|
400
350
|
|
|
401
351
|
```ruby
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
define_schema do
|
|
406
|
-
compose T::AllOf[VehicleIdentification, OwnerInfo, RegistrationDetails]
|
|
407
|
-
end
|
|
352
|
+
EasyTalk.configure do |config|
|
|
353
|
+
config.auto_validations = true
|
|
408
354
|
end
|
|
409
355
|
```
|
|
410
356
|
|
|
411
|
-
|
|
412
|
-
You can combine composition types to create complex schemas:
|
|
357
|
+
Disable globally:
|
|
413
358
|
|
|
414
359
|
```ruby
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
define_schema do
|
|
419
|
-
property :basic_info, BaseInfo
|
|
420
|
-
property :specific_details, T::OneOf[DetailTypeA, DetailTypeB]
|
|
421
|
-
property :metadata, T::AnyOf[AdminMetadata, UserMetadata, nil]
|
|
422
|
-
end
|
|
360
|
+
EasyTalk.configure do |config|
|
|
361
|
+
config.auto_validations = false
|
|
423
362
|
end
|
|
424
363
|
```
|
|
425
364
|
|
|
426
|
-
|
|
427
|
-
Models can reference other models to create hierarchical schemas:
|
|
365
|
+
When auto validations are off, you can still write validations manually:
|
|
428
366
|
|
|
429
367
|
```ruby
|
|
430
|
-
class
|
|
368
|
+
class User
|
|
431
369
|
include EasyTalk::Model
|
|
432
|
-
|
|
370
|
+
|
|
371
|
+
validates :name, presence: true, length: { minimum: 2 }
|
|
372
|
+
|
|
433
373
|
define_schema do
|
|
434
|
-
property :
|
|
435
|
-
property :city, String
|
|
436
|
-
property :state, String
|
|
437
|
-
property :zip, String
|
|
374
|
+
property :name, String, min_length: 2
|
|
438
375
|
end
|
|
439
376
|
end
|
|
377
|
+
```
|
|
440
378
|
|
|
441
|
-
|
|
379
|
+
### Per-model validation control
|
|
380
|
+
|
|
381
|
+
```ruby
|
|
382
|
+
class LegacyModel
|
|
442
383
|
include EasyTalk::Model
|
|
443
|
-
|
|
444
|
-
define_schema do
|
|
445
|
-
property :
|
|
446
|
-
property :address, Address
|
|
384
|
+
|
|
385
|
+
define_schema(validations: false) do
|
|
386
|
+
property :data, String, min_length: 1 # no validation generated
|
|
447
387
|
end
|
|
448
388
|
end
|
|
449
389
|
```
|
|
450
390
|
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
### Enhanced Validation System
|
|
454
|
-
EasyTalk models include comprehensive ActiveModel validation support with automatic generation:
|
|
391
|
+
### Per-property validation control
|
|
455
392
|
|
|
456
393
|
```ruby
|
|
457
394
|
class User
|
|
458
395
|
include EasyTalk::Model
|
|
459
|
-
|
|
460
|
-
# Manual validations work alongside automatic ones
|
|
461
|
-
validates :age, comparison: { greater_than: 21 } # Additional business rule
|
|
462
|
-
validates :height, numericality: { greater_than: 0 } # Overrides auto-validation
|
|
463
|
-
|
|
396
|
+
|
|
464
397
|
define_schema do
|
|
465
|
-
property :name, String, min_length: 2
|
|
466
|
-
property :
|
|
467
|
-
property :height, Float # Auto-generates presence validation (overridden above)
|
|
398
|
+
property :name, String, min_length: 2
|
|
399
|
+
property :legacy_field, String, validate: false
|
|
468
400
|
end
|
|
469
401
|
end
|
|
470
402
|
```
|
|
471
403
|
|
|
472
|
-
###
|
|
473
|
-
|
|
404
|
+
### Validation adapters
|
|
405
|
+
|
|
406
|
+
EasyTalk uses a pluggable adapter system:
|
|
474
407
|
|
|
475
408
|
```ruby
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
user.errors[:height] # => ["must be greater than 0"] # Overridden validation
|
|
409
|
+
EasyTalk.configure do |config|
|
|
410
|
+
config.validation_adapter = :active_model # default
|
|
411
|
+
# config.validation_adapter = :none # disable validation generation
|
|
412
|
+
end
|
|
481
413
|
```
|
|
482
414
|
|
|
483
|
-
|
|
484
|
-
|
|
415
|
+
---
|
|
416
|
+
|
|
417
|
+
## Error formatting
|
|
418
|
+
|
|
419
|
+
Instance helpers:
|
|
485
420
|
|
|
486
421
|
```ruby
|
|
487
|
-
user
|
|
488
|
-
user.
|
|
489
|
-
user.
|
|
490
|
-
|
|
422
|
+
user.validation_errors_flat
|
|
423
|
+
user.validation_errors_json_pointer
|
|
424
|
+
user.validation_errors_rfc7807
|
|
425
|
+
user.validation_errors_jsonapi
|
|
491
426
|
```
|
|
492
427
|
|
|
493
|
-
|
|
428
|
+
Format directly:
|
|
494
429
|
|
|
495
430
|
```ruby
|
|
496
|
-
user
|
|
431
|
+
EasyTalk::ErrorFormatter.format(user.errors, format: :rfc7807, title: "User Validation Failed")
|
|
432
|
+
```
|
|
497
433
|
|
|
498
|
-
|
|
499
|
-
class Address
|
|
500
|
-
include EasyTalk::Model
|
|
501
|
-
define_schema do
|
|
502
|
-
property :street, String
|
|
503
|
-
property :city, String
|
|
504
|
-
end
|
|
505
|
-
end
|
|
434
|
+
Global defaults:
|
|
506
435
|
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
end
|
|
436
|
+
```ruby
|
|
437
|
+
EasyTalk.configure do |config|
|
|
438
|
+
config.default_error_format = :rfc7807
|
|
439
|
+
config.error_type_base_uri = "https://api.example.com/errors"
|
|
440
|
+
config.include_error_codes = true
|
|
513
441
|
end
|
|
514
|
-
|
|
515
|
-
# Hash attributes automatically instantiate nested models
|
|
516
|
-
user = User.new(
|
|
517
|
-
name: "John",
|
|
518
|
-
address: { street: "123 Main St", city: "Boston" }
|
|
519
|
-
)
|
|
520
|
-
user.address.class # => Address (automatically instantiated)
|
|
521
|
-
user.address.street # => "123 Main St"
|
|
522
442
|
```
|
|
523
443
|
|
|
524
|
-
|
|
444
|
+
---
|
|
445
|
+
|
|
446
|
+
## Schema-only mode
|
|
525
447
|
|
|
526
|
-
|
|
527
|
-
EasyTalk provides a helper method for generating OpenAI function specifications:
|
|
448
|
+
If you want schema generation and attribute accessors **without** ActiveModel validation:
|
|
528
449
|
|
|
529
450
|
```ruby
|
|
530
|
-
class
|
|
531
|
-
include EasyTalk::
|
|
532
|
-
|
|
451
|
+
class ApiContract
|
|
452
|
+
include EasyTalk::Schema
|
|
453
|
+
|
|
533
454
|
define_schema do
|
|
534
|
-
title "
|
|
535
|
-
|
|
536
|
-
property :
|
|
537
|
-
property :unit, String, enum: ["celsius", "fahrenheit"], default: "fahrenheit"
|
|
455
|
+
title "API Contract"
|
|
456
|
+
property :name, String, min_length: 2
|
|
457
|
+
property :age, Integer, minimum: 0
|
|
538
458
|
end
|
|
539
459
|
end
|
|
540
460
|
|
|
541
|
-
|
|
461
|
+
ApiContract.json_schema
|
|
462
|
+
contract = ApiContract.new(name: "Test", age: 25)
|
|
463
|
+
|
|
464
|
+
# No validations available:
|
|
465
|
+
# contract.valid? # => NoMethodError
|
|
542
466
|
```
|
|
543
467
|
|
|
544
|
-
|
|
468
|
+
Use this for documentation, OpenAPI generation, or when validation happens elsewhere.
|
|
545
469
|
|
|
546
|
-
|
|
547
|
-
|
|
470
|
+
---
|
|
471
|
+
|
|
472
|
+
## Configuration highlights
|
|
548
473
|
|
|
549
474
|
```ruby
|
|
550
|
-
|
|
551
|
-
|
|
475
|
+
EasyTalk.configure do |config|
|
|
476
|
+
# Schema behavior
|
|
477
|
+
config.default_additional_properties = false
|
|
478
|
+
config.nilable_is_optional = false
|
|
479
|
+
config.schema_version = :none
|
|
480
|
+
config.schema_id = nil
|
|
481
|
+
config.use_refs = false
|
|
482
|
+
config.base_schema_uri = nil # Base URI for auto-generating $id
|
|
483
|
+
config.auto_generate_ids = false # Auto-generate $id from base_schema_uri
|
|
484
|
+
config.prefer_external_refs = false # Use external URI in $ref when available
|
|
485
|
+
config.property_naming_strategy = :identity # :snake_case, :camel_case, :pascal_case
|
|
552
486
|
|
|
553
|
-
#
|
|
554
|
-
|
|
487
|
+
# Validations
|
|
488
|
+
config.auto_validations = true
|
|
489
|
+
config.validation_adapter = :active_model
|
|
555
490
|
|
|
556
|
-
#
|
|
557
|
-
|
|
491
|
+
# Error formatting
|
|
492
|
+
config.default_error_format = :flat # :flat, :json_pointer, :rfc7807, :jsonapi
|
|
493
|
+
config.error_type_base_uri = "about:blank"
|
|
494
|
+
config.include_error_codes = true
|
|
495
|
+
end
|
|
558
496
|
```
|
|
559
497
|
|
|
560
|
-
|
|
561
|
-
EasyTalk performs basic type checking during schema definition:
|
|
562
|
-
|
|
563
|
-
```ruby
|
|
564
|
-
# This will raise an error because "minimum" should be used with numeric types
|
|
565
|
-
property :name, String, minimum: 1 # Error!
|
|
566
|
-
|
|
567
|
-
# This will raise an error because enum values must match the property type
|
|
568
|
-
property :age, Integer, enum: ["young", "old"] # Error!
|
|
569
|
-
```
|
|
498
|
+
---
|
|
570
499
|
|
|
571
|
-
|
|
572
|
-
For advanced use cases, you can create custom type builders:
|
|
500
|
+
## Advanced topics
|
|
573
501
|
|
|
574
|
-
|
|
575
|
-
module EasyTalk
|
|
576
|
-
module Builders
|
|
577
|
-
class MyCustomTypeBuilder < BaseBuilder
|
|
578
|
-
# Custom implementation
|
|
579
|
-
end
|
|
580
|
-
end
|
|
581
|
-
end
|
|
582
|
-
```
|
|
502
|
+
For more detailed documentation, see the [full API reference on RubyDoc](https://rubydoc.info/gems/easy_talk).
|
|
583
503
|
|
|
584
|
-
|
|
504
|
+
### JSON Schema drafts, `$id`, and `$ref`
|
|
585
505
|
|
|
586
|
-
|
|
587
|
-
You can configure EasyTalk globally:
|
|
506
|
+
EasyTalk can emit `$schema` for multiple drafts (Draft-04 through 2020-12), supports `$id`, and can use `$ref`/`$defs` for reusable definitions:
|
|
588
507
|
|
|
589
508
|
```ruby
|
|
590
509
|
EasyTalk.configure do |config|
|
|
591
|
-
|
|
592
|
-
config.
|
|
593
|
-
config.
|
|
594
|
-
config.auto_validations = true # Automatically generate ActiveModel validations
|
|
595
|
-
config.schema_version = :none # JSON Schema version for $schema keyword
|
|
596
|
-
# Options: :none, :draft202012, :draft201909, :draft7, :draft6, :draft4
|
|
597
|
-
config.schema_id = nil # Base URI for $id keyword (nil = no $id)
|
|
598
|
-
config.use_refs = false # Use $ref for nested models instead of inlining
|
|
510
|
+
config.schema_version = :draft202012
|
|
511
|
+
config.schema_id = "https://example.com/schemas/user.json"
|
|
512
|
+
config.use_refs = true # Use $ref/$defs for nested models
|
|
599
513
|
end
|
|
600
514
|
```
|
|
601
515
|
|
|
602
|
-
|
|
603
|
-
|
|
516
|
+
#### External schema references
|
|
517
|
+
|
|
518
|
+
Use external URIs in `$ref` for modular, reusable schemas:
|
|
604
519
|
|
|
605
520
|
```ruby
|
|
606
|
-
# Disable automatic validations globally
|
|
607
521
|
EasyTalk.configure do |config|
|
|
608
|
-
config.
|
|
522
|
+
config.use_refs = true
|
|
523
|
+
config.prefer_external_refs = true
|
|
524
|
+
config.base_schema_uri = 'https://example.com/schemas'
|
|
525
|
+
config.auto_generate_ids = true
|
|
609
526
|
end
|
|
610
527
|
|
|
611
|
-
|
|
612
|
-
class User
|
|
528
|
+
class Address
|
|
613
529
|
include EasyTalk::Model
|
|
614
|
-
|
|
615
|
-
validates :name, presence: true, length: { minimum: 2 }
|
|
616
|
-
validates :age, presence: true, numericality: { greater_than_or_equal_to: 18 }
|
|
617
|
-
|
|
530
|
+
|
|
618
531
|
define_schema do
|
|
619
|
-
property :
|
|
620
|
-
property :
|
|
532
|
+
property :street, String
|
|
533
|
+
property :city, String
|
|
621
534
|
end
|
|
622
535
|
end
|
|
623
|
-
```
|
|
624
536
|
|
|
625
|
-
|
|
626
|
-
You can configure additional properties for individual models:
|
|
627
|
-
|
|
628
|
-
```ruby
|
|
629
|
-
class User
|
|
537
|
+
class Customer
|
|
630
538
|
include EasyTalk::Model
|
|
631
|
-
|
|
539
|
+
|
|
632
540
|
define_schema do
|
|
633
|
-
title "User"
|
|
634
|
-
additional_properties true # Allow arbitrary additional properties on this model
|
|
635
541
|
property :name, String
|
|
636
|
-
property :
|
|
542
|
+
property :address, Address
|
|
637
543
|
end
|
|
638
544
|
end
|
|
639
|
-
```
|
|
640
545
|
|
|
641
|
-
|
|
546
|
+
Customer.json_schema
|
|
547
|
+
# =>
|
|
548
|
+
# {
|
|
549
|
+
# "properties": {
|
|
550
|
+
# "address": { "$ref": "https://example.com/schemas/address" }
|
|
551
|
+
# },
|
|
552
|
+
# "$defs": {
|
|
553
|
+
# "Address": {
|
|
554
|
+
# "$id": "https://example.com/schemas/address",
|
|
555
|
+
# "properties": { "street": {...}, "city": {...} }
|
|
556
|
+
# }
|
|
557
|
+
# }
|
|
558
|
+
# }
|
|
559
|
+
```
|
|
642
560
|
|
|
643
|
-
|
|
561
|
+
**Explicit schema IDs:**
|
|
644
562
|
|
|
645
563
|
```ruby
|
|
646
|
-
class
|
|
564
|
+
class Address
|
|
647
565
|
include EasyTalk::Model
|
|
648
566
|
|
|
649
|
-
# Additional custom validations beyond automatic ones
|
|
650
|
-
validates :email, uniqueness: true
|
|
651
|
-
validates :password, confirmation: true
|
|
652
|
-
|
|
653
567
|
define_schema do
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
property :name, String, min_length: 2, max_length: 100, description: "User's full name"
|
|
657
|
-
property :email, String, format: "email", description: "User's email address"
|
|
658
|
-
property :password, String, min_length: 8, max_length: 128, description: "User's password"
|
|
659
|
-
property :notify, T::Boolean, optional: true, description: "Whether to send notifications"
|
|
568
|
+
schema_id 'https://example.com/schemas/address'
|
|
569
|
+
property :street, String
|
|
660
570
|
end
|
|
661
|
-
# Auto-generated validations:
|
|
662
|
-
# validates :name, presence: true, length: { minimum: 2, maximum: 100 }
|
|
663
|
-
# validates :email, presence: true, format: { with: URI::MailTo::EMAIL_REGEXP }
|
|
664
|
-
# validates :password, presence: true, length: { minimum: 8, maximum: 128 }
|
|
665
|
-
# validates :notify, inclusion: { in: [true, false] } - only if present (optional: true)
|
|
666
571
|
end
|
|
667
|
-
|
|
668
|
-
# Usage with automatic validation
|
|
669
|
-
user = User.new(
|
|
670
|
-
name: "John Doe",
|
|
671
|
-
email: "john@example.com",
|
|
672
|
-
password: "secretpassword123",
|
|
673
|
-
notify: true
|
|
674
|
-
)
|
|
675
|
-
user.valid? # => true (assuming email is unique)
|
|
676
|
-
|
|
677
|
-
# Invalid data triggers auto-generated validations
|
|
678
|
-
invalid_user = User.new(
|
|
679
|
-
name: "J", # Too short
|
|
680
|
-
email: "invalid-email", # Invalid format
|
|
681
|
-
password: "123" # Too short
|
|
682
|
-
)
|
|
683
|
-
invalid_user.valid? # => false
|
|
684
|
-
invalid_user.errors.full_messages
|
|
685
|
-
# => ["Name is too short (minimum is 2 characters)",
|
|
686
|
-
# "Email is invalid",
|
|
687
|
-
# "Password is too short (minimum is 8 characters)"]
|
|
688
572
|
```
|
|
689
573
|
|
|
690
|
-
|
|
574
|
+
**Per-property ref control:**
|
|
691
575
|
|
|
692
576
|
```ruby
|
|
693
|
-
class
|
|
577
|
+
class Customer
|
|
694
578
|
include EasyTalk::Model
|
|
695
579
|
|
|
696
580
|
define_schema do
|
|
697
|
-
property :
|
|
698
|
-
property :
|
|
699
|
-
property :CardExpMonth, Integer, minimum: 1, maximum: 12
|
|
700
|
-
property :CardExpYear, Integer, minimum: Date.today.year, maximum: Date.today.year + 10
|
|
701
|
-
property :CardCVV, String, pattern: '^[0-9]{3,4}$'
|
|
702
|
-
additional_properties false
|
|
581
|
+
property :address, Address, ref: false # Inline instead of ref
|
|
582
|
+
property :billing, Address # Uses ref (global setting)
|
|
703
583
|
end
|
|
704
584
|
end
|
|
585
|
+
```
|
|
705
586
|
|
|
706
|
-
|
|
707
|
-
include EasyTalk::Model
|
|
587
|
+
### Additional properties with types
|
|
708
588
|
|
|
709
|
-
|
|
710
|
-
property :PaypalEmail, String, format: 'email'
|
|
711
|
-
property :PaypalPasswordEncrypted, String
|
|
712
|
-
additional_properties false
|
|
713
|
-
end
|
|
714
|
-
end
|
|
589
|
+
Beyond boolean values, `additional_properties` now supports type constraints for dynamic properties:
|
|
715
590
|
|
|
716
|
-
|
|
591
|
+
```ruby
|
|
592
|
+
class Config
|
|
717
593
|
include EasyTalk::Model
|
|
718
594
|
|
|
719
595
|
define_schema do
|
|
720
|
-
property :
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
additional_properties false
|
|
596
|
+
property :name, String
|
|
597
|
+
|
|
598
|
+
# Allow any string-typed additional properties
|
|
599
|
+
additional_properties String
|
|
725
600
|
end
|
|
726
601
|
end
|
|
727
602
|
|
|
728
|
-
|
|
603
|
+
config = Config.new(name: 'app')
|
|
604
|
+
config.label = 'Production' # Dynamic property
|
|
605
|
+
config.as_json
|
|
606
|
+
# => { 'name' => 'app', 'label' => 'Production' }
|
|
607
|
+
```
|
|
608
|
+
|
|
609
|
+
**With constraints:**
|
|
610
|
+
|
|
611
|
+
```ruby
|
|
612
|
+
class StrictConfig
|
|
729
613
|
include EasyTalk::Model
|
|
730
614
|
|
|
731
615
|
define_schema do
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
property :Details, T::AnyOf[CreditCard, Paypal, BankTransfer]
|
|
616
|
+
property :id, Integer
|
|
617
|
+
# Integer values between 0 and 100 only
|
|
618
|
+
additional_properties Integer, minimum: 0, maximum: 100
|
|
736
619
|
end
|
|
737
620
|
end
|
|
621
|
+
|
|
622
|
+
StrictConfig.json_schema
|
|
623
|
+
# =>
|
|
624
|
+
# {
|
|
625
|
+
# "properties": { "id": { "type": "integer" } },
|
|
626
|
+
# "additionalProperties": {
|
|
627
|
+
# "type": "integer",
|
|
628
|
+
# "minimum": 0,
|
|
629
|
+
# "maximum": 100
|
|
630
|
+
# }
|
|
631
|
+
# }
|
|
738
632
|
```
|
|
739
633
|
|
|
740
|
-
|
|
634
|
+
**Nested models as additional properties:**
|
|
741
635
|
|
|
742
636
|
```ruby
|
|
743
|
-
class
|
|
637
|
+
class Person
|
|
744
638
|
include EasyTalk::Model
|
|
745
639
|
|
|
746
640
|
define_schema do
|
|
747
|
-
property :
|
|
748
|
-
|
|
749
|
-
property :state, String
|
|
750
|
-
property :zip, String, pattern: '^[0-9]{5}(?:-[0-9]{4})?$'
|
|
641
|
+
property :name, String
|
|
642
|
+
additional_properties Address # All additional properties must be Address objects
|
|
751
643
|
end
|
|
752
644
|
end
|
|
645
|
+
```
|
|
646
|
+
|
|
647
|
+
### Object-level constraints
|
|
753
648
|
|
|
754
|
-
|
|
649
|
+
Apply schema-wide constraints to limit or validate object structure:
|
|
650
|
+
|
|
651
|
+
```ruby
|
|
652
|
+
class StrictObject
|
|
755
653
|
include EasyTalk::Model
|
|
756
654
|
|
|
757
655
|
define_schema do
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
property :
|
|
761
|
-
property :
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
656
|
+
property :required1, String
|
|
657
|
+
property :required2, String
|
|
658
|
+
property :optional1, String, optional: true
|
|
659
|
+
property :optional2, String, optional: true
|
|
660
|
+
|
|
661
|
+
# Require at least 2 properties
|
|
662
|
+
min_properties 2
|
|
663
|
+
# Allow at most 3 properties
|
|
664
|
+
max_properties 3
|
|
766
665
|
end
|
|
767
666
|
end
|
|
768
667
|
|
|
769
|
-
|
|
668
|
+
obj = StrictObject.new(required1: 'a')
|
|
669
|
+
obj.valid? # => false (only 1 property, needs at least 2)
|
|
670
|
+
```
|
|
671
|
+
|
|
672
|
+
**Pattern properties:**
|
|
673
|
+
|
|
674
|
+
```ruby
|
|
675
|
+
class DynamicConfig
|
|
770
676
|
include EasyTalk::Model
|
|
771
677
|
|
|
772
678
|
define_schema do
|
|
773
|
-
title 'Company'
|
|
774
679
|
property :name, String
|
|
775
|
-
|
|
680
|
+
|
|
681
|
+
# Properties matching /^env_/ must be strings
|
|
682
|
+
pattern_properties(
|
|
683
|
+
'^env_' => { type: 'string' }
|
|
684
|
+
)
|
|
776
685
|
end
|
|
777
686
|
end
|
|
778
687
|
```
|
|
779
688
|
|
|
780
|
-
|
|
689
|
+
**Dependent required:**
|
|
781
690
|
|
|
782
691
|
```ruby
|
|
783
|
-
|
|
784
|
-
class Api::UsersController < ApplicationController
|
|
785
|
-
def create
|
|
786
|
-
schema = User.json_schema
|
|
787
|
-
|
|
788
|
-
# Validate incoming request against the schema
|
|
789
|
-
validation_result = JSONSchemer.schema(schema).valid?(params.to_json)
|
|
790
|
-
|
|
791
|
-
if validation_result
|
|
792
|
-
user = User.new(user_params)
|
|
793
|
-
if user.save
|
|
794
|
-
render json: user, status: :created
|
|
795
|
-
else
|
|
796
|
-
render json: { errors: user.errors }, status: :unprocessable_entity
|
|
797
|
-
end
|
|
798
|
-
else
|
|
799
|
-
render json: { errors: "Invalid request" }, status: :bad_request
|
|
800
|
-
end
|
|
801
|
-
end
|
|
802
|
-
|
|
803
|
-
private
|
|
804
|
-
|
|
805
|
-
def user_params
|
|
806
|
-
params.require(:user).permit(:name, :email, :password)
|
|
807
|
-
end
|
|
808
|
-
end
|
|
809
|
-
```
|
|
810
|
-
|
|
811
|
-
## Troubleshooting
|
|
812
|
-
|
|
813
|
-
### Common Errors
|
|
814
|
-
|
|
815
|
-
#### "Invalid property name"
|
|
816
|
-
Property names must start with a letter or underscore and can only contain letters, numbers, and underscores:
|
|
817
|
-
|
|
818
|
-
```ruby
|
|
819
|
-
# Invalid
|
|
820
|
-
property "1name", String # Starts with a number
|
|
821
|
-
property "name!", String # Contains a special character
|
|
822
|
-
|
|
823
|
-
# Valid
|
|
824
|
-
property :name, String
|
|
825
|
-
property :user_name, String
|
|
826
|
-
```
|
|
827
|
-
|
|
828
|
-
#### "Property type is missing"
|
|
829
|
-
You must specify a type for each property:
|
|
830
|
-
|
|
831
|
-
```ruby
|
|
832
|
-
# Invalid
|
|
833
|
-
property :name
|
|
834
|
-
|
|
835
|
-
# Valid
|
|
836
|
-
property :name, String
|
|
837
|
-
```
|
|
838
|
-
|
|
839
|
-
#### "Unknown option"
|
|
840
|
-
You specified an option that is not valid for the property type:
|
|
841
|
-
|
|
842
|
-
```ruby
|
|
843
|
-
# Invalid (min_length is for strings, not integers)
|
|
844
|
-
property :age, Integer, min_length: 2
|
|
845
|
-
|
|
846
|
-
# Valid
|
|
847
|
-
property :age, Integer, minimum: 18
|
|
848
|
-
```
|
|
849
|
-
|
|
850
|
-
### Schema Validation Issues
|
|
851
|
-
If you're having issues with validation:
|
|
852
|
-
|
|
853
|
-
1. Make sure you've defined ActiveModel validations for your model
|
|
854
|
-
2. Check for mismatches between schema constraints and validations
|
|
855
|
-
3. Verify that required properties are present
|
|
856
|
-
|
|
857
|
-
### Type Errors
|
|
858
|
-
Type errors usually occur when there's a mismatch between a property type and its constraints:
|
|
859
|
-
|
|
860
|
-
```ruby
|
|
861
|
-
# Error: enum values must be strings for a string property
|
|
862
|
-
property :status, String, enum: [1, 2, 3]
|
|
863
|
-
|
|
864
|
-
# Correct
|
|
865
|
-
property :status, String, enum: ["active", "inactive", "pending"]
|
|
866
|
-
```
|
|
867
|
-
|
|
868
|
-
### Best Practices
|
|
869
|
-
|
|
870
|
-
1. **Define clear property names and descriptions** for better documentation
|
|
871
|
-
2. **Use appropriate types** for each property with proper constraints
|
|
872
|
-
3. **Leverage automatic validations** by defining schema constraints instead of manual validations
|
|
873
|
-
4. **Keep schemas focused and modular** - extract nested objects to separate classes
|
|
874
|
-
5. **Reuse models when appropriate** instead of duplicating schema definitions
|
|
875
|
-
6. **Use explicit types** instead of relying on inference
|
|
876
|
-
7. **Test your schemas with sample data** to ensure validations work as expected
|
|
877
|
-
8. **Configure auto-validations globally** to maintain consistency across your application
|
|
878
|
-
9. **Use nullable_optional_property** for fields that can be omitted or null
|
|
879
|
-
10. **Document breaking changes** when updating schema definitions
|
|
880
|
-
|
|
881
|
-
# Nullable vs Optional Properties in EasyTalk
|
|
882
|
-
|
|
883
|
-
One of the most important distinctions when defining schemas is understanding the difference between **nullable** properties and **optional** properties. This guide explains these concepts and how to use them effectively in EasyTalk.
|
|
884
|
-
|
|
885
|
-
## Key Concepts
|
|
886
|
-
|
|
887
|
-
| Concept | Description | JSON Schema Effect | EasyTalk Syntax |
|
|
888
|
-
|---------|-------------|-------------------|-----------------|
|
|
889
|
-
| **Nullable** | Property can have a `null` value | Adds `"null"` to the type array | `T.nilable(Type)` |
|
|
890
|
-
| **Optional** | Property doesn't have to exist | Omits property from `"required"` array | `optional: true` constraint |
|
|
891
|
-
|
|
892
|
-
## Nullable Properties
|
|
893
|
-
|
|
894
|
-
A **nullable** property can contain a `null` value, but the property itself must still be present in the object:
|
|
895
|
-
|
|
896
|
-
```ruby
|
|
897
|
-
property :age, T.nilable(Integer)
|
|
898
|
-
```
|
|
899
|
-
|
|
900
|
-
This produces the following JSON Schema:
|
|
901
|
-
|
|
902
|
-
```json
|
|
903
|
-
{
|
|
904
|
-
"properties": {
|
|
905
|
-
"age": { "type": ["integer", "null"] }
|
|
906
|
-
},
|
|
907
|
-
"required": ["age"]
|
|
908
|
-
}
|
|
909
|
-
```
|
|
910
|
-
|
|
911
|
-
In this case, the following data would be valid:
|
|
912
|
-
- `{ "age": 25 }`
|
|
913
|
-
- `{ "age": null }`
|
|
914
|
-
|
|
915
|
-
But this would be invalid:
|
|
916
|
-
- `{ }` (missing the age property entirely)
|
|
917
|
-
|
|
918
|
-
## Optional Properties
|
|
919
|
-
|
|
920
|
-
An **optional** property doesn't have to be present in the object at all:
|
|
921
|
-
|
|
922
|
-
```ruby
|
|
923
|
-
property :nickname, String, optional: true
|
|
924
|
-
```
|
|
925
|
-
|
|
926
|
-
This produces:
|
|
927
|
-
|
|
928
|
-
```json
|
|
929
|
-
{
|
|
930
|
-
"properties": {
|
|
931
|
-
"nickname": { "type": "string" }
|
|
932
|
-
}
|
|
933
|
-
// Note: "nickname" is not in the "required" array
|
|
934
|
-
}
|
|
935
|
-
```
|
|
936
|
-
|
|
937
|
-
In this case, the following data would be valid:
|
|
938
|
-
- `{ "nickname": "Joe" }`
|
|
939
|
-
- `{ }` (omitting nickname entirely)
|
|
940
|
-
|
|
941
|
-
But this would be invalid:
|
|
942
|
-
- `{ "nickname": null }` (null is not allowed because the property isn't nullable)
|
|
943
|
-
|
|
944
|
-
## Nullable AND Optional Properties
|
|
945
|
-
|
|
946
|
-
For properties that should be both nullable and optional (can be omitted or null), you need to combine both approaches:
|
|
947
|
-
|
|
948
|
-
```ruby
|
|
949
|
-
property :bio, T.nilable(String), optional: true
|
|
950
|
-
```
|
|
951
|
-
|
|
952
|
-
This produces:
|
|
953
|
-
|
|
954
|
-
```json
|
|
955
|
-
{
|
|
956
|
-
"properties": {
|
|
957
|
-
"bio": { "type": ["string", "null"] }
|
|
958
|
-
}
|
|
959
|
-
// Note: "bio" is not in the "required" array
|
|
960
|
-
}
|
|
961
|
-
```
|
|
962
|
-
|
|
963
|
-
For convenience, EasyTalk also provides a helper method:
|
|
964
|
-
|
|
965
|
-
```ruby
|
|
966
|
-
nullable_optional_property :bio, String
|
|
967
|
-
```
|
|
968
|
-
|
|
969
|
-
Which is equivalent to the above.
|
|
970
|
-
|
|
971
|
-
## Configuration Options
|
|
972
|
-
|
|
973
|
-
By default, nullable properties are still required. You can change this global behavior:
|
|
974
|
-
|
|
975
|
-
```ruby
|
|
976
|
-
EasyTalk.configure do |config|
|
|
977
|
-
config.nilable_is_optional = true # Makes all T.nilable properties also optional
|
|
978
|
-
end
|
|
979
|
-
```
|
|
980
|
-
|
|
981
|
-
With this configuration, any property defined with `T.nilable(Type)` will be treated as both nullable and optional.
|
|
982
|
-
|
|
983
|
-
## Practical Examples
|
|
984
|
-
|
|
985
|
-
### User Profile Schema
|
|
986
|
-
|
|
987
|
-
```ruby
|
|
988
|
-
class UserProfile
|
|
989
|
-
include EasyTalk::Model
|
|
990
|
-
|
|
991
|
-
define_schema do
|
|
992
|
-
# Required properties (must exist, cannot be null)
|
|
993
|
-
property :id, String
|
|
994
|
-
property :name, String
|
|
995
|
-
|
|
996
|
-
# Required but nullable (must exist, can be null)
|
|
997
|
-
property :age, T.nilable(Integer)
|
|
998
|
-
|
|
999
|
-
# Optional but not nullable (can be omitted, cannot be null if present)
|
|
1000
|
-
property :email, String, optional: true
|
|
1001
|
-
|
|
1002
|
-
# Optional and nullable (can be omitted, can be null if present)
|
|
1003
|
-
nullable_optional_property :bio, String
|
|
1004
|
-
end
|
|
1005
|
-
end
|
|
1006
|
-
```
|
|
1007
|
-
|
|
1008
|
-
This creates clear expectations for data validation:
|
|
1009
|
-
- `id` and `name` must be present and cannot be null
|
|
1010
|
-
- `age` must be present but can be null
|
|
1011
|
-
- `email` doesn't have to be present, but if it is, it cannot be null
|
|
1012
|
-
- `bio` doesn't have to be present, and if it is, it can be null
|
|
1013
|
-
|
|
1014
|
-
## Common Gotchas
|
|
1015
|
-
|
|
1016
|
-
### Misconception: Nullable Implies Optional
|
|
1017
|
-
|
|
1018
|
-
A common mistake is assuming that `T.nilable(Type)` makes a property optional. By default, it only allows the property to have a null value - the property itself is still required to exist in the object.
|
|
1019
|
-
|
|
1020
|
-
### Misconception: Optional Properties Accept Null
|
|
1021
|
-
|
|
1022
|
-
An optional property (defined with `optional: true`) can be omitted entirely, but if it is present, it must conform to its type constraint. If you want to allow null values, you must also make it nullable with `T.nilable(Type)`.
|
|
1023
|
-
|
|
1024
|
-
## Migration from Earlier Versions
|
|
1025
|
-
|
|
1026
|
-
If you're upgrading from EasyTalk version 1.0.1 or earlier, be aware that the handling of nullable vs optional properties has been improved for clarity.
|
|
1027
|
-
|
|
1028
|
-
To maintain backward compatibility with your existing code, you can use:
|
|
1029
|
-
|
|
1030
|
-
```ruby
|
|
1031
|
-
EasyTalk.configure do |config|
|
|
1032
|
-
config.nilable_is_optional = true # Makes T.nilable properties behave as they did before
|
|
1033
|
-
end
|
|
1034
|
-
```
|
|
1035
|
-
|
|
1036
|
-
We recommend updating your schema definitions to explicitly declare which properties are optional using the `optional: true` constraint, as this makes your intent clearer.
|
|
1037
|
-
|
|
1038
|
-
## Best Practices
|
|
1039
|
-
|
|
1040
|
-
1. **Be explicit about intent**: Always clarify whether properties should be nullable, optional, or both
|
|
1041
|
-
2. **Use the helper method**: For properties that are both nullable and optional, use `nullable_optional_property`
|
|
1042
|
-
3. **Document expectations**: Use comments to clarify validation requirements for complex schemas
|
|
1043
|
-
4. **Consider validation implications**: Remember that ActiveModel validations operate independently of the schema definition
|
|
1044
|
-
|
|
1045
|
-
## JSON Schema Comparison
|
|
1046
|
-
|
|
1047
|
-
| EasyTalk Definition | Required | Nullable | JSON Schema Equivalent |
|
|
1048
|
-
|--------------------|----------|----------|------------------------|
|
|
1049
|
-
| `property :p, String` | Yes | No | `{ "properties": { "p": { "type": "string" } }, "required": ["p"] }` |
|
|
1050
|
-
| `property :p, T.nilable(String)` | Yes | Yes | `{ "properties": { "p": { "type": ["string", "null"] } }, "required": ["p"] }` |
|
|
1051
|
-
| `property :p, String, optional: true` | No | No | `{ "properties": { "p": { "type": "string" } } }` |
|
|
1052
|
-
| `nullable_optional_property :p, String` | No | Yes | `{ "properties": { "p": { "type": ["string", "null"] } } }` |
|
|
1053
|
-
|
|
1054
|
-
## Migration Guide from v1.x to v2.0
|
|
1055
|
-
|
|
1056
|
-
### Breaking Changes Summary
|
|
1057
|
-
|
|
1058
|
-
1. **Removed Block-Style Sub-Schemas**: Hash-based nested definitions are no longer supported
|
|
1059
|
-
2. **Enhanced Validation System**: Automatic validation generation is now enabled by default
|
|
1060
|
-
3. **Improved Model Initialization**: Better support for nested model instantiation
|
|
1061
|
-
|
|
1062
|
-
### Migration Steps
|
|
1063
|
-
|
|
1064
|
-
#### 1. Replace Hash-based Nested Schemas
|
|
1065
|
-
|
|
1066
|
-
```ruby
|
|
1067
|
-
# OLD (v1.x) - No longer works
|
|
1068
|
-
class User
|
|
1069
|
-
include EasyTalk::Model
|
|
1070
|
-
define_schema do
|
|
1071
|
-
property :address, Hash do
|
|
1072
|
-
property :street, String
|
|
1073
|
-
property :city, String
|
|
1074
|
-
end
|
|
1075
|
-
end
|
|
1076
|
-
end
|
|
1077
|
-
|
|
1078
|
-
# NEW (v2.x) - Extract to separate classes
|
|
1079
|
-
class Address
|
|
1080
|
-
include EasyTalk::Model
|
|
1081
|
-
define_schema do
|
|
1082
|
-
property :street, String
|
|
1083
|
-
property :city, String
|
|
1084
|
-
end
|
|
1085
|
-
end
|
|
1086
|
-
|
|
1087
|
-
class User
|
|
1088
|
-
include EasyTalk::Model
|
|
1089
|
-
define_schema do
|
|
1090
|
-
property :address, Address
|
|
1091
|
-
end
|
|
1092
|
-
end
|
|
1093
|
-
```
|
|
1094
|
-
|
|
1095
|
-
#### 2. Review Automatic Validations
|
|
1096
|
-
|
|
1097
|
-
With `auto_validations: true` (default), you may need to remove redundant manual validations:
|
|
1098
|
-
|
|
1099
|
-
```ruby
|
|
1100
|
-
# OLD (v1.x) - Manual validations required
|
|
1101
|
-
class User
|
|
1102
|
-
include EasyTalk::Model
|
|
1103
|
-
|
|
1104
|
-
validates :name, presence: true, length: { minimum: 2 }
|
|
1105
|
-
validates :email, presence: true, format: { with: URI::MailTo::EMAIL_REGEXP }
|
|
1106
|
-
|
|
1107
|
-
define_schema do
|
|
1108
|
-
property :name, String
|
|
1109
|
-
property :email, String
|
|
1110
|
-
end
|
|
1111
|
-
end
|
|
1112
|
-
|
|
1113
|
-
# NEW (v2.x) - Automatic validations from constraints
|
|
1114
|
-
class User
|
|
1115
|
-
include EasyTalk::Model
|
|
1116
|
-
|
|
1117
|
-
# Only add validations not covered by schema constraints
|
|
1118
|
-
validates :email, uniqueness: true
|
|
1119
|
-
|
|
1120
|
-
define_schema do
|
|
1121
|
-
property :name, String, min_length: 2 # Auto-generates presence + length
|
|
1122
|
-
property :email, String, format: "email" # Auto-generates presence + format
|
|
1123
|
-
end
|
|
1124
|
-
end
|
|
1125
|
-
```
|
|
1126
|
-
|
|
1127
|
-
#### 3. Configuration Updates
|
|
1128
|
-
|
|
1129
|
-
Review your configuration for new options:
|
|
1130
|
-
|
|
1131
|
-
```ruby
|
|
1132
|
-
EasyTalk.configure do |config|
|
|
1133
|
-
# New option in v2.0
|
|
1134
|
-
config.auto_validations = true # Enable/disable automatic validation generation
|
|
1135
|
-
|
|
1136
|
-
# Existing options (unchanged)
|
|
1137
|
-
config.nilable_is_optional = false
|
|
1138
|
-
config.default_additional_properties = false
|
|
1139
|
-
# ... other existing config
|
|
1140
|
-
end
|
|
1141
|
-
```
|
|
1142
|
-
|
|
1143
|
-
### Compatibility Notes
|
|
1144
|
-
|
|
1145
|
-
- **Ruby Version**: Still requires Ruby 3.2+
|
|
1146
|
-
- **Dependencies**: Core dependencies remain the same
|
|
1147
|
-
- **JSON Schema Output**: No changes to generated schemas
|
|
1148
|
-
- **ActiveModel Integration**: Fully backward compatible
|
|
1149
|
-
|
|
1150
|
-
## Development and Contributing
|
|
1151
|
-
|
|
1152
|
-
### Setting Up the Development Environment
|
|
1153
|
-
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that lets you experiment.
|
|
1154
|
-
|
|
1155
|
-
To install this gem onto your local machine, run:
|
|
1156
|
-
|
|
1157
|
-
```bash
|
|
1158
|
-
bundle exec rake install
|
|
1159
|
-
```
|
|
1160
|
-
|
|
1161
|
-
### Running Tests
|
|
1162
|
-
Run the test suite with:
|
|
1163
|
-
|
|
1164
|
-
```bash
|
|
1165
|
-
bundle exec rake spec
|
|
1166
|
-
```
|
|
1167
|
-
|
|
1168
|
-
### Code Quality
|
|
1169
|
-
Run the linter:
|
|
1170
|
-
|
|
1171
|
-
```bash
|
|
1172
|
-
bundle exec rubocop
|
|
1173
|
-
```
|
|
1174
|
-
|
|
1175
|
-
### Contributing Guidelines
|
|
1176
|
-
Bug reports and pull requests are welcome on GitHub at https://github.com/sergiobayona/easy_talk.
|
|
1177
|
-
|
|
1178
|
-
## JSON Schema Version (`$schema` Keyword)
|
|
1179
|
-
|
|
1180
|
-
The `$schema` keyword declares which JSON Schema dialect (draft version) a schema conforms to. EasyTalk supports configuring this at both the global and per-model level.
|
|
1181
|
-
|
|
1182
|
-
### Why Use `$schema`?
|
|
1183
|
-
|
|
1184
|
-
The `$schema` keyword:
|
|
1185
|
-
- Declares the JSON Schema version your schema is written against
|
|
1186
|
-
- Helps validators understand which specification to use
|
|
1187
|
-
- Enables tooling to provide appropriate validation and autocomplete
|
|
1188
|
-
- Documents the schema dialect for consumers of your API
|
|
1189
|
-
|
|
1190
|
-
### Supported Draft Versions
|
|
1191
|
-
|
|
1192
|
-
EasyTalk supports the following JSON Schema draft versions:
|
|
1193
|
-
|
|
1194
|
-
| Symbol | JSON Schema Version | URI |
|
|
1195
|
-
|--------|---------------------|-----|
|
|
1196
|
-
| `:draft202012` | Draft 2020-12 (latest) | `https://json-schema.org/draft/2020-12/schema` |
|
|
1197
|
-
| `:draft201909` | Draft 2019-09 | `https://json-schema.org/draft/2019-09/schema` |
|
|
1198
|
-
| `:draft7` | Draft-07 | `http://json-schema.org/draft-07/schema#` |
|
|
1199
|
-
| `:draft6` | Draft-06 | `http://json-schema.org/draft-06/schema#` |
|
|
1200
|
-
| `:draft4` | Draft-04 | `http://json-schema.org/draft-04/schema#` |
|
|
1201
|
-
| `:none` | No `$schema` output (default) | N/A |
|
|
1202
|
-
|
|
1203
|
-
### Global Configuration
|
|
1204
|
-
|
|
1205
|
-
Configure the schema version globally to apply to all models:
|
|
1206
|
-
|
|
1207
|
-
```ruby
|
|
1208
|
-
EasyTalk.configure do |config|
|
|
1209
|
-
config.schema_version = :draft202012 # Use JSON Schema Draft 2020-12
|
|
1210
|
-
end
|
|
1211
|
-
```
|
|
1212
|
-
|
|
1213
|
-
With this configuration, all models will include `$schema` in their output:
|
|
1214
|
-
|
|
1215
|
-
```ruby
|
|
1216
|
-
class User
|
|
692
|
+
class ShippingInfo
|
|
1217
693
|
include EasyTalk::Model
|
|
1218
694
|
|
|
1219
695
|
define_schema do
|
|
1220
696
|
property :name, String
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
User.json_schema
|
|
1225
|
-
# => {
|
|
1226
|
-
# "$schema" => "https://json-schema.org/draft/2020-12/schema",
|
|
1227
|
-
# "type" => "object",
|
|
1228
|
-
# "properties" => { "name" => { "type" => "string" } },
|
|
1229
|
-
# "required" => ["name"],
|
|
1230
|
-
# "additionalProperties" => false
|
|
1231
|
-
# }
|
|
1232
|
-
```
|
|
1233
|
-
|
|
1234
|
-
### Per-Model Configuration
|
|
1235
|
-
|
|
1236
|
-
Override the global setting for individual models using the `schema_version` keyword in the schema definition:
|
|
1237
|
-
|
|
1238
|
-
```ruby
|
|
1239
|
-
class LegacyModel
|
|
1240
|
-
include EasyTalk::Model
|
|
697
|
+
property :credit_card, String, optional: true
|
|
698
|
+
property :billing_address, String, optional: true
|
|
1241
699
|
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
700
|
+
# If credit_card is present, billing_address is required
|
|
701
|
+
dependent_required(
|
|
702
|
+
'credit_card' => ['billing_address']
|
|
703
|
+
)
|
|
1245
704
|
end
|
|
1246
705
|
end
|
|
1247
|
-
|
|
1248
|
-
LegacyModel.json_schema
|
|
1249
|
-
# => {
|
|
1250
|
-
# "$schema" => "http://json-schema.org/draft-07/schema#",
|
|
1251
|
-
# "type" => "object",
|
|
1252
|
-
# ...
|
|
1253
|
-
# }
|
|
1254
706
|
```
|
|
1255
707
|
|
|
1256
|
-
###
|
|
708
|
+
### Custom type builders
|
|
1257
709
|
|
|
1258
|
-
|
|
710
|
+
Register custom types with their own schema builders:
|
|
1259
711
|
|
|
1260
712
|
```ruby
|
|
1261
713
|
EasyTalk.configure do |config|
|
|
1262
|
-
config.
|
|
1263
|
-
end
|
|
1264
|
-
|
|
1265
|
-
class InternalModel
|
|
1266
|
-
include EasyTalk::Model
|
|
1267
|
-
|
|
1268
|
-
define_schema do
|
|
1269
|
-
schema_version :none # No $schema for this model
|
|
1270
|
-
property :data, String
|
|
1271
|
-
end
|
|
714
|
+
config.register_type(Money, MoneySchemaBuilder)
|
|
1272
715
|
end
|
|
1273
716
|
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
# "type" => "object",
|
|
1277
|
-
# "properties" => { "data" => { "type" => "string" } },
|
|
1278
|
-
# ...
|
|
1279
|
-
# }
|
|
1280
|
-
# Note: No "$schema" key present
|
|
717
|
+
# Or directly:
|
|
718
|
+
EasyTalk::Builders::Registry.register(Money, MoneySchemaBuilder)
|
|
1281
719
|
```
|
|
1282
720
|
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
You can also specify a custom URI if you're using a custom meta-schema or a different schema registry:
|
|
1286
|
-
|
|
1287
|
-
```ruby
|
|
1288
|
-
class CustomModel
|
|
1289
|
-
include EasyTalk::Model
|
|
1290
|
-
|
|
1291
|
-
define_schema do
|
|
1292
|
-
schema_version 'https://my-company.com/schemas/v1/meta-schema.json'
|
|
1293
|
-
property :id, String
|
|
1294
|
-
end
|
|
1295
|
-
end
|
|
1296
|
-
```
|
|
1297
|
-
|
|
1298
|
-
### Nested Models
|
|
1299
|
-
|
|
1300
|
-
The `$schema` keyword only appears at the root level of the schema. When you have nested EasyTalk models, only the top-level model's `json_schema` output will include `$schema`:
|
|
1301
|
-
|
|
1302
|
-
```ruby
|
|
1303
|
-
EasyTalk.configure do |config|
|
|
1304
|
-
config.schema_version = :draft202012
|
|
1305
|
-
end
|
|
1306
|
-
|
|
1307
|
-
class Address
|
|
1308
|
-
include EasyTalk::Model
|
|
1309
|
-
define_schema do
|
|
1310
|
-
property :city, String
|
|
1311
|
-
end
|
|
1312
|
-
end
|
|
1313
|
-
|
|
1314
|
-
class User
|
|
1315
|
-
include EasyTalk::Model
|
|
1316
|
-
define_schema do
|
|
1317
|
-
property :name, String
|
|
1318
|
-
property :address, Address
|
|
1319
|
-
end
|
|
1320
|
-
end
|
|
1321
|
-
|
|
1322
|
-
User.json_schema
|
|
1323
|
-
# => {
|
|
1324
|
-
# "$schema" => "https://json-schema.org/draft/2020-12/schema", # Only at root
|
|
1325
|
-
# "type" => "object",
|
|
1326
|
-
# "properties" => {
|
|
1327
|
-
# "name" => { "type" => "string" },
|
|
1328
|
-
# "address" => {
|
|
1329
|
-
# "type" => "object", # No $schema here
|
|
1330
|
-
# "properties" => { "city" => { "type" => "string" } },
|
|
1331
|
-
# ...
|
|
1332
|
-
# }
|
|
1333
|
-
# },
|
|
1334
|
-
# ...
|
|
1335
|
-
# }
|
|
1336
|
-
```
|
|
1337
|
-
|
|
1338
|
-
### Default Behavior
|
|
1339
|
-
|
|
1340
|
-
By default, `schema_version` is set to `:none`, meaning no `$schema` keyword is included in the generated schemas. This maintains backward compatibility with previous versions of EasyTalk.
|
|
1341
|
-
|
|
1342
|
-
### Best Practices
|
|
1343
|
-
|
|
1344
|
-
1. **Choose a version appropriate for your validators**: If you're using a specific JSON Schema validator, check which drafts it supports.
|
|
1345
|
-
|
|
1346
|
-
2. **Use Draft 2020-12 for new projects**: It's the latest stable version with the most features.
|
|
1347
|
-
|
|
1348
|
-
3. **Be consistent**: Use global configuration for consistency across your application, and only override per-model when necessary.
|
|
1349
|
-
|
|
1350
|
-
4. **Consider your consumers**: If your schemas are consumed by external systems, ensure they support the draft version you're using.
|
|
1351
|
-
|
|
1352
|
-
## Schema Identifier (`$id` Keyword)
|
|
1353
|
-
|
|
1354
|
-
The `$id` keyword provides a unique identifier for your JSON Schema document. EasyTalk supports configuring this at both the global and per-model level.
|
|
1355
|
-
|
|
1356
|
-
### Why Use `$id`?
|
|
1357
|
-
|
|
1358
|
-
The `$id` keyword:
|
|
1359
|
-
- Establishes a unique URI identifier for the schema
|
|
1360
|
-
- Enables referencing schemas from other documents via `$ref`
|
|
1361
|
-
- Provides a base URI for resolving relative references within the schema
|
|
1362
|
-
- Documents the canonical location of the schema
|
|
1363
|
-
|
|
1364
|
-
### Global Configuration
|
|
1365
|
-
|
|
1366
|
-
Configure the schema ID globally to apply to all models:
|
|
1367
|
-
|
|
1368
|
-
```ruby
|
|
1369
|
-
EasyTalk.configure do |config|
|
|
1370
|
-
config.schema_id = 'https://example.com/schemas/base.json'
|
|
1371
|
-
end
|
|
1372
|
-
```
|
|
1373
|
-
|
|
1374
|
-
With this configuration, all models will include `$id` in their output:
|
|
1375
|
-
|
|
1376
|
-
```ruby
|
|
1377
|
-
class User
|
|
1378
|
-
include EasyTalk::Model
|
|
1379
|
-
|
|
1380
|
-
define_schema do
|
|
1381
|
-
property :name, String
|
|
1382
|
-
end
|
|
1383
|
-
end
|
|
1384
|
-
|
|
1385
|
-
User.json_schema
|
|
1386
|
-
# => {
|
|
1387
|
-
# "$id" => "https://example.com/schemas/base.json",
|
|
1388
|
-
# "type" => "object",
|
|
1389
|
-
# "properties" => { "name" => { "type" => "string" } },
|
|
1390
|
-
# "required" => ["name"],
|
|
1391
|
-
# "additionalProperties" => false
|
|
1392
|
-
# }
|
|
1393
|
-
```
|
|
1394
|
-
|
|
1395
|
-
### Per-Model Configuration
|
|
1396
|
-
|
|
1397
|
-
Override the global setting for individual models using the `schema_id` keyword in the schema definition:
|
|
1398
|
-
|
|
1399
|
-
```ruby
|
|
1400
|
-
class User
|
|
1401
|
-
include EasyTalk::Model
|
|
1402
|
-
|
|
1403
|
-
define_schema do
|
|
1404
|
-
schema_id 'https://example.com/schemas/user.schema.json'
|
|
1405
|
-
property :name, String
|
|
1406
|
-
property :email, String
|
|
1407
|
-
end
|
|
1408
|
-
end
|
|
1409
|
-
|
|
1410
|
-
User.json_schema
|
|
1411
|
-
# => {
|
|
1412
|
-
# "$id" => "https://example.com/schemas/user.schema.json",
|
|
1413
|
-
# "type" => "object",
|
|
1414
|
-
# ...
|
|
1415
|
-
# }
|
|
1416
|
-
```
|
|
1417
|
-
|
|
1418
|
-
### Disabling `$id` for Specific Models
|
|
1419
|
-
|
|
1420
|
-
If you have a global schema ID configured but want to exclude `$id` from a specific model, use `:none`:
|
|
1421
|
-
|
|
1422
|
-
```ruby
|
|
1423
|
-
EasyTalk.configure do |config|
|
|
1424
|
-
config.schema_id = 'https://example.com/schemas/default.json'
|
|
1425
|
-
end
|
|
1426
|
-
|
|
1427
|
-
class InternalModel
|
|
1428
|
-
include EasyTalk::Model
|
|
1429
|
-
|
|
1430
|
-
define_schema do
|
|
1431
|
-
schema_id :none # No $id for this model
|
|
1432
|
-
property :data, String
|
|
1433
|
-
end
|
|
1434
|
-
end
|
|
1435
|
-
|
|
1436
|
-
InternalModel.json_schema
|
|
1437
|
-
# => {
|
|
1438
|
-
# "type" => "object",
|
|
1439
|
-
# "properties" => { "data" => { "type" => "string" } },
|
|
1440
|
-
# ...
|
|
1441
|
-
# }
|
|
1442
|
-
# Note: No "$id" key present
|
|
1443
|
-
```
|
|
1444
|
-
|
|
1445
|
-
### Combining `$schema` and `$id`
|
|
1446
|
-
|
|
1447
|
-
When both `$schema` and `$id` are configured, they appear in the standard order (`$schema` first, then `$id`):
|
|
1448
|
-
|
|
1449
|
-
```ruby
|
|
1450
|
-
class Product
|
|
1451
|
-
include EasyTalk::Model
|
|
1452
|
-
|
|
1453
|
-
define_schema do
|
|
1454
|
-
schema_version :draft202012
|
|
1455
|
-
schema_id 'https://example.com/schemas/product.schema.json'
|
|
1456
|
-
property :name, String
|
|
1457
|
-
property :price, Float
|
|
1458
|
-
end
|
|
1459
|
-
end
|
|
1460
|
-
|
|
1461
|
-
Product.json_schema
|
|
1462
|
-
# => {
|
|
1463
|
-
# "$schema" => "https://json-schema.org/draft/2020-12/schema",
|
|
1464
|
-
# "$id" => "https://example.com/schemas/product.schema.json",
|
|
1465
|
-
# "type" => "object",
|
|
1466
|
-
# ...
|
|
1467
|
-
# }
|
|
1468
|
-
```
|
|
1469
|
-
|
|
1470
|
-
### Nested Models
|
|
1471
|
-
|
|
1472
|
-
The `$id` keyword only appears at the root level of the schema. When you have nested EasyTalk models, only the top-level model's `json_schema` output will include `$id`:
|
|
1473
|
-
|
|
1474
|
-
```ruby
|
|
1475
|
-
EasyTalk.configure do |config|
|
|
1476
|
-
config.schema_id = 'https://example.com/schemas/user.json'
|
|
1477
|
-
end
|
|
1478
|
-
|
|
1479
|
-
class Address
|
|
1480
|
-
include EasyTalk::Model
|
|
1481
|
-
define_schema do
|
|
1482
|
-
property :city, String
|
|
1483
|
-
end
|
|
1484
|
-
end
|
|
1485
|
-
|
|
1486
|
-
class User
|
|
1487
|
-
include EasyTalk::Model
|
|
1488
|
-
define_schema do
|
|
1489
|
-
property :name, String
|
|
1490
|
-
property :address, Address
|
|
1491
|
-
end
|
|
1492
|
-
end
|
|
1493
|
-
|
|
1494
|
-
User.json_schema
|
|
1495
|
-
# => {
|
|
1496
|
-
# "$id" => "https://example.com/schemas/user.json", # Only at root
|
|
1497
|
-
# "type" => "object",
|
|
1498
|
-
# "properties" => {
|
|
1499
|
-
# "name" => { "type" => "string" },
|
|
1500
|
-
# "address" => {
|
|
1501
|
-
# "type" => "object", # No $id here
|
|
1502
|
-
# "properties" => { "city" => { "type" => "string" } },
|
|
1503
|
-
# ...
|
|
1504
|
-
# }
|
|
1505
|
-
# },
|
|
1506
|
-
# ...
|
|
1507
|
-
# }
|
|
1508
|
-
```
|
|
1509
|
-
|
|
1510
|
-
### URI Formats
|
|
1511
|
-
|
|
1512
|
-
The `$id` accepts various URI formats:
|
|
1513
|
-
|
|
1514
|
-
```ruby
|
|
1515
|
-
# Absolute URI (recommended for published schemas)
|
|
1516
|
-
schema_id 'https://example.com/schemas/user.schema.json'
|
|
1517
|
-
|
|
1518
|
-
# Relative URI
|
|
1519
|
-
schema_id 'user.schema.json'
|
|
1520
|
-
|
|
1521
|
-
# URN format
|
|
1522
|
-
schema_id 'urn:example:user-schema'
|
|
1523
|
-
```
|
|
1524
|
-
|
|
1525
|
-
### Default Behavior
|
|
1526
|
-
|
|
1527
|
-
By default, `schema_id` is set to `nil`, meaning no `$id` keyword is included in the generated schemas. This maintains backward compatibility with previous versions of EasyTalk.
|
|
1528
|
-
|
|
1529
|
-
### Best Practices
|
|
1530
|
-
|
|
1531
|
-
1. **Use absolute URIs for published schemas**: This ensures global uniqueness and enables external references.
|
|
1532
|
-
|
|
1533
|
-
2. **Follow a consistent naming convention**: For example, `https://yourdomain.com/schemas/{model-name}.schema.json`.
|
|
1534
|
-
|
|
1535
|
-
3. **Keep IDs stable**: Once published, avoid changing schema IDs as external systems may reference them.
|
|
1536
|
-
|
|
1537
|
-
4. **Combine with `$schema`**: When publishing schemas, include both `$schema` (for validation) and `$id` (for identification).
|
|
1538
|
-
|
|
1539
|
-
## Schema References (`$ref` and `$defs`)
|
|
1540
|
-
|
|
1541
|
-
The `$ref` keyword allows you to reference reusable schema definitions, reducing duplication when the same model is used in multiple places. EasyTalk supports automatic `$ref` generation for nested models.
|
|
1542
|
-
|
|
1543
|
-
### Why Use `$ref`?
|
|
1544
|
-
|
|
1545
|
-
The `$ref` keyword:
|
|
1546
|
-
- Reduces schema duplication when the same model appears multiple times
|
|
1547
|
-
- Produces cleaner, more organized schemas
|
|
1548
|
-
- Improves schema readability and maintainability
|
|
1549
|
-
- Aligns with JSON Schema best practices for reusable definitions
|
|
1550
|
-
|
|
1551
|
-
### Default Behavior (Inline Schemas)
|
|
1552
|
-
|
|
1553
|
-
By default, EasyTalk inlines nested model schemas directly:
|
|
1554
|
-
|
|
1555
|
-
```ruby
|
|
1556
|
-
class Address
|
|
1557
|
-
include EasyTalk::Model
|
|
1558
|
-
define_schema do
|
|
1559
|
-
property :street, String
|
|
1560
|
-
property :city, String
|
|
1561
|
-
end
|
|
1562
|
-
end
|
|
1563
|
-
|
|
1564
|
-
class Person
|
|
1565
|
-
include EasyTalk::Model
|
|
1566
|
-
define_schema do
|
|
1567
|
-
property :name, String
|
|
1568
|
-
property :address, Address
|
|
1569
|
-
end
|
|
1570
|
-
end
|
|
1571
|
-
|
|
1572
|
-
Person.json_schema
|
|
1573
|
-
# => {
|
|
1574
|
-
# "type" => "object",
|
|
1575
|
-
# "properties" => {
|
|
1576
|
-
# "name" => { "type" => "string" },
|
|
1577
|
-
# "address" => {
|
|
1578
|
-
# "type" => "object",
|
|
1579
|
-
# "properties" => {
|
|
1580
|
-
# "street" => { "type" => "string" },
|
|
1581
|
-
# "city" => { "type" => "string" }
|
|
1582
|
-
# },
|
|
1583
|
-
# ...
|
|
1584
|
-
# }
|
|
1585
|
-
# },
|
|
1586
|
-
# ...
|
|
1587
|
-
# }
|
|
1588
|
-
```
|
|
1589
|
-
|
|
1590
|
-
### Enabling `$ref` References
|
|
1591
|
-
|
|
1592
|
-
#### Global Configuration
|
|
1593
|
-
|
|
1594
|
-
Enable `$ref` generation for all nested models:
|
|
1595
|
-
|
|
1596
|
-
```ruby
|
|
1597
|
-
EasyTalk.configure do |config|
|
|
1598
|
-
config.use_refs = true
|
|
1599
|
-
end
|
|
1600
|
-
```
|
|
1601
|
-
|
|
1602
|
-
With this configuration, nested models are referenced via `$ref` and their definitions are placed in `$defs`:
|
|
1603
|
-
|
|
1604
|
-
```ruby
|
|
1605
|
-
Person.json_schema
|
|
1606
|
-
# => {
|
|
1607
|
-
# "type" => "object",
|
|
1608
|
-
# "properties" => {
|
|
1609
|
-
# "name" => { "type" => "string" },
|
|
1610
|
-
# "address" => { "$ref" => "#/$defs/Address" }
|
|
1611
|
-
# },
|
|
1612
|
-
# "$defs" => {
|
|
1613
|
-
# "Address" => {
|
|
1614
|
-
# "type" => "object",
|
|
1615
|
-
# "properties" => {
|
|
1616
|
-
# "street" => { "type" => "string" },
|
|
1617
|
-
# "city" => { "type" => "string" }
|
|
1618
|
-
# },
|
|
1619
|
-
# ...
|
|
1620
|
-
# }
|
|
1621
|
-
# },
|
|
1622
|
-
# ...
|
|
1623
|
-
# }
|
|
1624
|
-
```
|
|
1625
|
-
|
|
1626
|
-
#### Per-Property Configuration
|
|
1627
|
-
|
|
1628
|
-
You can also enable `$ref` for specific properties using the `ref: true` constraint:
|
|
1629
|
-
|
|
1630
|
-
```ruby
|
|
1631
|
-
class Person
|
|
1632
|
-
include EasyTalk::Model
|
|
1633
|
-
define_schema do
|
|
1634
|
-
property :name, String
|
|
1635
|
-
property :address, Address, ref: true # Use $ref for this property
|
|
1636
|
-
end
|
|
1637
|
-
end
|
|
1638
|
-
```
|
|
1639
|
-
|
|
1640
|
-
Or disable `$ref` for specific properties when it's enabled globally:
|
|
1641
|
-
|
|
1642
|
-
```ruby
|
|
1643
|
-
EasyTalk.configure do |config|
|
|
1644
|
-
config.use_refs = true
|
|
1645
|
-
end
|
|
1646
|
-
|
|
1647
|
-
class Person
|
|
1648
|
-
include EasyTalk::Model
|
|
1649
|
-
define_schema do
|
|
1650
|
-
property :name, String
|
|
1651
|
-
property :address, Address, ref: false # Inline this property despite global setting
|
|
1652
|
-
end
|
|
1653
|
-
end
|
|
1654
|
-
```
|
|
1655
|
-
|
|
1656
|
-
### Arrays of Models
|
|
1657
|
-
|
|
1658
|
-
When using `$ref` with arrays of models, the `$ref` applies to the array items:
|
|
1659
|
-
|
|
1660
|
-
```ruby
|
|
1661
|
-
EasyTalk.configure do |config|
|
|
1662
|
-
config.use_refs = true
|
|
1663
|
-
end
|
|
1664
|
-
|
|
1665
|
-
class Company
|
|
1666
|
-
include EasyTalk::Model
|
|
1667
|
-
define_schema do
|
|
1668
|
-
property :name, String
|
|
1669
|
-
property :addresses, T::Array[Address]
|
|
1670
|
-
end
|
|
1671
|
-
end
|
|
1672
|
-
|
|
1673
|
-
Company.json_schema
|
|
1674
|
-
# => {
|
|
1675
|
-
# "type" => "object",
|
|
1676
|
-
# "properties" => {
|
|
1677
|
-
# "name" => { "type" => "string" },
|
|
1678
|
-
# "addresses" => {
|
|
1679
|
-
# "type" => "array",
|
|
1680
|
-
# "items" => { "$ref" => "#/$defs/Address" }
|
|
1681
|
-
# }
|
|
1682
|
-
# },
|
|
1683
|
-
# "$defs" => {
|
|
1684
|
-
# "Address" => { ... }
|
|
1685
|
-
# },
|
|
1686
|
-
# ...
|
|
1687
|
-
# }
|
|
1688
|
-
```
|
|
1689
|
-
|
|
1690
|
-
You can also use the per-property `ref` constraint with arrays:
|
|
1691
|
-
|
|
1692
|
-
```ruby
|
|
1693
|
-
property :addresses, T::Array[Address], ref: true
|
|
1694
|
-
```
|
|
1695
|
-
|
|
1696
|
-
### Nilable Models with `$ref`
|
|
1697
|
-
|
|
1698
|
-
When using `$ref` with nilable model types, EasyTalk uses `anyOf` to combine the reference with the null type:
|
|
1699
|
-
|
|
1700
|
-
```ruby
|
|
1701
|
-
EasyTalk.configure do |config|
|
|
1702
|
-
config.use_refs = true
|
|
1703
|
-
end
|
|
1704
|
-
|
|
1705
|
-
class Person
|
|
1706
|
-
include EasyTalk::Model
|
|
1707
|
-
define_schema do
|
|
1708
|
-
property :name, String
|
|
1709
|
-
property :address, T.nilable(Address)
|
|
1710
|
-
end
|
|
1711
|
-
end
|
|
1712
|
-
|
|
1713
|
-
Person.json_schema
|
|
1714
|
-
# => {
|
|
1715
|
-
# "type" => "object",
|
|
1716
|
-
# "properties" => {
|
|
1717
|
-
# "name" => { "type" => "string" },
|
|
1718
|
-
# "address" => {
|
|
1719
|
-
# "anyOf" => [
|
|
1720
|
-
# { "$ref" => "#/$defs/Address" },
|
|
1721
|
-
# { "type" => "null" }
|
|
1722
|
-
# ]
|
|
1723
|
-
# }
|
|
1724
|
-
# },
|
|
1725
|
-
# "$defs" => {
|
|
1726
|
-
# "Address" => { ... }
|
|
1727
|
-
# },
|
|
1728
|
-
# ...
|
|
1729
|
-
# }
|
|
1730
|
-
```
|
|
1731
|
-
|
|
1732
|
-
### Multiple References to the Same Model
|
|
1733
|
-
|
|
1734
|
-
When the same model is used multiple times, it only appears once in `$defs`:
|
|
1735
|
-
|
|
1736
|
-
```ruby
|
|
1737
|
-
class Person
|
|
1738
|
-
include EasyTalk::Model
|
|
1739
|
-
define_schema do
|
|
1740
|
-
property :name, String
|
|
1741
|
-
property :home_address, Address, ref: true
|
|
1742
|
-
property :work_address, Address, ref: true
|
|
1743
|
-
property :shipping_addresses, T::Array[Address], ref: true
|
|
1744
|
-
end
|
|
1745
|
-
end
|
|
1746
|
-
|
|
1747
|
-
Person.json_schema
|
|
1748
|
-
# => {
|
|
1749
|
-
# "type" => "object",
|
|
1750
|
-
# "properties" => {
|
|
1751
|
-
# "name" => { "type" => "string" },
|
|
1752
|
-
# "home_address" => { "$ref" => "#/$defs/Address" },
|
|
1753
|
-
# "work_address" => { "$ref" => "#/$defs/Address" },
|
|
1754
|
-
# "shipping_addresses" => {
|
|
1755
|
-
# "type" => "array",
|
|
1756
|
-
# "items" => { "$ref" => "#/$defs/Address" }
|
|
1757
|
-
# }
|
|
1758
|
-
# },
|
|
1759
|
-
# "$defs" => {
|
|
1760
|
-
# "Address" => { ... } # Only defined once
|
|
1761
|
-
# },
|
|
1762
|
-
# ...
|
|
1763
|
-
# }
|
|
1764
|
-
```
|
|
1765
|
-
|
|
1766
|
-
### Combining `$ref` with Other Constraints
|
|
1767
|
-
|
|
1768
|
-
You can add additional constraints alongside `$ref`:
|
|
1769
|
-
|
|
1770
|
-
```ruby
|
|
1771
|
-
class Person
|
|
1772
|
-
include EasyTalk::Model
|
|
1773
|
-
define_schema do
|
|
1774
|
-
property :address, Address, ref: true, description: "Primary address", title: "Main Address"
|
|
1775
|
-
end
|
|
1776
|
-
end
|
|
1777
|
-
|
|
1778
|
-
Person.json_schema["properties"]["address"]
|
|
1779
|
-
# => {
|
|
1780
|
-
# "$ref" => "#/$defs/Address",
|
|
1781
|
-
# "description" => "Primary address",
|
|
1782
|
-
# "title" => "Main Address"
|
|
1783
|
-
# }
|
|
1784
|
-
```
|
|
1785
|
-
|
|
1786
|
-
### Interaction with `compose`
|
|
1787
|
-
|
|
1788
|
-
When using `compose` with `T::AllOf`, `T::AnyOf`, or `T::OneOf`, the composed models are also placed in `$defs`:
|
|
1789
|
-
|
|
1790
|
-
```ruby
|
|
1791
|
-
class Employee
|
|
1792
|
-
include EasyTalk::Model
|
|
1793
|
-
define_schema do
|
|
1794
|
-
compose T::AllOf[Person, EmployeeDetails]
|
|
1795
|
-
property :badge_number, String
|
|
1796
|
-
end
|
|
1797
|
-
end
|
|
1798
|
-
```
|
|
1799
|
-
|
|
1800
|
-
If you also have properties using `$ref`, both the composed models and property models will appear in `$defs`.
|
|
1801
|
-
|
|
1802
|
-
### Best Practices
|
|
1803
|
-
|
|
1804
|
-
1. **Use global configuration for consistency**: If you prefer `$ref` style, enable it globally rather than per-property.
|
|
1805
|
-
|
|
1806
|
-
2. **Consider schema consumers**: Some JSON Schema validators and tools work better with inlined schemas, while others prefer `$ref`. Choose based on your use case.
|
|
1807
|
-
|
|
1808
|
-
3. **Use `$ref` for frequently reused models**: If a model appears in many places, `$ref` reduces schema size and improves maintainability.
|
|
721
|
+
See the [Custom Type Builders documentation](https://rubydoc.info/gems/easy_talk/EasyTalk/Builders/Registry) for details on creating builders.
|
|
1809
722
|
|
|
1810
|
-
|
|
723
|
+
---
|
|
1811
724
|
|
|
1812
|
-
|
|
725
|
+
## Known limitations
|
|
1813
726
|
|
|
1814
|
-
|
|
727
|
+
EasyTalk aims to produce broadly compatible JSON Schema, but:
|
|
728
|
+
- Some draft-specific keywords/features may require manual schema tweaks
|
|
729
|
+
- Custom formats are limited (extend via custom builders when needed)
|
|
730
|
+
- Extremely complex composition can outgrow “auto validations” and may need manual validations or external schema validators
|
|
1815
731
|
|
|
1816
|
-
|
|
732
|
+
---
|
|
1817
733
|
|
|
1818
|
-
|
|
1819
|
-
EasyTalk supports generating schemas compatible with JSON Schema Draft-04 through Draft 2020-12. Use the `schema_version` configuration option to declare which version your schemas conform to (see [JSON Schema Version](#json-schema-version-schema-keyword) above).
|
|
734
|
+
## Contributing
|
|
1820
735
|
|
|
1821
|
-
|
|
736
|
+
- Run `bin/setup`
|
|
737
|
+
- Run specs: `bundle exec rake spec`
|
|
738
|
+
- Run lint: `bundle exec rubocop`
|
|
1822
739
|
|
|
1823
|
-
|
|
1824
|
-
To learn about current capabilities, see the [spec/easy_talk/examples](https://github.com/sergiobayona/easy_talk/tree/main/spec/easy_talk/examples) folder. The examples illustrate how EasyTalk generates JSON Schema in different scenarios.
|
|
740
|
+
Bug reports and PRs welcome.
|
|
1825
741
|
|
|
1826
|
-
|
|
1827
|
-
- Limited support for custom formats
|
|
1828
|
-
- Some draft-specific keywords may not be supported
|
|
1829
|
-
- Complex composition scenarios may require manual adjustment
|
|
742
|
+
---
|
|
1830
743
|
|
|
1831
744
|
## License
|
|
1832
745
|
|
|
1833
|
-
|
|
746
|
+
MIT
|