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/Rakefile
CHANGED
|
@@ -3,9 +3,36 @@
|
|
|
3
3
|
require 'bundler/gem_tasks'
|
|
4
4
|
require 'rspec/core/rake_task'
|
|
5
5
|
require 'rubocop/rake_task'
|
|
6
|
+
require 'yard'
|
|
6
7
|
|
|
7
8
|
RSpec::Core::RakeTask.new(:spec)
|
|
8
9
|
|
|
9
10
|
RuboCop::RakeTask.new
|
|
10
11
|
|
|
12
|
+
YARD::Rake::YardocTask.new(:yard) do |t|
|
|
13
|
+
t.files = ['lib/**/*.rb']
|
|
14
|
+
t.options = ['--readme', 'README.md', '--output-dir', 'docs/api']
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
namespace :docs do
|
|
18
|
+
desc 'Generate YARD API documentation'
|
|
19
|
+
task api: :yard
|
|
20
|
+
|
|
21
|
+
desc 'Serve Jekyll documentation locally'
|
|
22
|
+
task :serve do
|
|
23
|
+
Dir.chdir('docs') do
|
|
24
|
+
sh 'bundle install --quiet'
|
|
25
|
+
sh 'bundle exec jekyll serve'
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
desc 'Build all documentation (Jekyll + YARD)'
|
|
30
|
+
task build: :yard do
|
|
31
|
+
Dir.chdir('docs') do
|
|
32
|
+
sh 'bundle install --quiet'
|
|
33
|
+
sh 'bundle exec jekyll build'
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
11
38
|
task default: %i[spec rubocop]
|
data/docs/.gitignore
CHANGED
data/docs/about.markdown
CHANGED
|
@@ -4,15 +4,35 @@ title: About
|
|
|
4
4
|
permalink: /about/
|
|
5
5
|
---
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
# About EasyTalk
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
[jekyll][jekyll-organization] /
|
|
11
|
-
[minima](https://github.com/jekyll/minima)
|
|
9
|
+
EasyTalk is a Ruby library for defining and generating JSON Schema from Ruby classes. Inspired by Python's Pydantic library, it provides an ActiveModel-like interface for defining structured data models with automatic validation and JSON Schema generation.
|
|
12
10
|
|
|
13
|
-
|
|
14
|
-
[jekyll][jekyll-organization] /
|
|
15
|
-
[jekyll](https://github.com/jekyll/jekyll)
|
|
11
|
+
## Why EasyTalk?
|
|
16
12
|
|
|
13
|
+
- **Type Safety** - Define your data structures once and get validation automatically
|
|
14
|
+
- **JSON Schema Generation** - Produce standards-compliant JSON Schema from Ruby code
|
|
15
|
+
- **LLM Integration** - Generate function schemas for OpenAI and other LLM providers
|
|
16
|
+
- **ActiveModel Compatible** - Works with Rails forms, validations, and serialization
|
|
17
17
|
|
|
18
|
-
|
|
18
|
+
## Use Cases
|
|
19
|
+
|
|
20
|
+
- **API Request/Response Validation** - Define expected shapes for API payloads
|
|
21
|
+
- **LLM Function Calling** - Generate structured output schemas for AI applications
|
|
22
|
+
- **Configuration Files** - Validate configuration against a schema
|
|
23
|
+
- **Data Pipelines** - Ensure data conforms to expected structures
|
|
24
|
+
|
|
25
|
+
## Links
|
|
26
|
+
|
|
27
|
+
- [GitHub Repository](https://github.com/sergiobayona/easy_talk)
|
|
28
|
+
- [RubyGems](https://rubygems.org/gems/easy_talk)
|
|
29
|
+
- [API Documentation](api/)
|
|
30
|
+
- [Changelog](https://github.com/sergiobayona/easy_talk/blob/main/CHANGELOG.md)
|
|
31
|
+
|
|
32
|
+
## Author
|
|
33
|
+
|
|
34
|
+
Created by [Sergio Bayona](https://github.com/sergiobayona).
|
|
35
|
+
|
|
36
|
+
## License
|
|
37
|
+
|
|
38
|
+
EasyTalk is released under the [MIT License](https://github.com/sergiobayona/easy_talk/blob/main/LICENSE.txt).
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
---
|
|
2
|
+
layout: page
|
|
3
|
+
title: Getting Started
|
|
4
|
+
permalink: /getting-started/
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Getting Started with EasyTalk
|
|
8
|
+
|
|
9
|
+
## Requirements
|
|
10
|
+
|
|
11
|
+
- Ruby 3.2 or later
|
|
12
|
+
- ActiveModel/ActiveSupport 7.0-8.x
|
|
13
|
+
|
|
14
|
+
## Installation
|
|
15
|
+
|
|
16
|
+
Add EasyTalk to your Gemfile:
|
|
17
|
+
|
|
18
|
+
```ruby
|
|
19
|
+
gem 'easy_talk'
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Then run:
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
bundle install
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Or install directly:
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
gem install easy_talk
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Basic Usage
|
|
35
|
+
|
|
36
|
+
### 1. Define a Model
|
|
37
|
+
|
|
38
|
+
Include `EasyTalk::Model` in your class and use `define_schema` to declare properties:
|
|
39
|
+
|
|
40
|
+
```ruby
|
|
41
|
+
require 'easy_talk'
|
|
42
|
+
|
|
43
|
+
class Person
|
|
44
|
+
include EasyTalk::Model
|
|
45
|
+
|
|
46
|
+
define_schema do
|
|
47
|
+
title "Person"
|
|
48
|
+
description "A person record"
|
|
49
|
+
|
|
50
|
+
property :name, String
|
|
51
|
+
property :age, Integer
|
|
52
|
+
property :email, String, format: "email"
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
### 2. Generate JSON Schema
|
|
58
|
+
|
|
59
|
+
Call `.json_schema` on your class to get the JSON Schema:
|
|
60
|
+
|
|
61
|
+
```ruby
|
|
62
|
+
Person.json_schema
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
This produces:
|
|
66
|
+
|
|
67
|
+
```json
|
|
68
|
+
{
|
|
69
|
+
"type": "object",
|
|
70
|
+
"title": "Person",
|
|
71
|
+
"description": "A person record",
|
|
72
|
+
"properties": {
|
|
73
|
+
"name": { "type": "string" },
|
|
74
|
+
"age": { "type": "integer" },
|
|
75
|
+
"email": { "type": "string", "format": "email" }
|
|
76
|
+
},
|
|
77
|
+
"required": ["name", "age", "email"],
|
|
78
|
+
"additionalProperties": false
|
|
79
|
+
}
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
### 3. Create and Validate Instances
|
|
83
|
+
|
|
84
|
+
EasyTalk models work like ActiveModel objects:
|
|
85
|
+
|
|
86
|
+
```ruby
|
|
87
|
+
person = Person.new(name: "Alice", age: 30, email: "alice@example.com")
|
|
88
|
+
person.valid? # => true
|
|
89
|
+
person.name # => "Alice"
|
|
90
|
+
|
|
91
|
+
# Invalid data triggers validation errors
|
|
92
|
+
invalid = Person.new(name: "", age: -5, email: "not-an-email")
|
|
93
|
+
invalid.valid? # => false
|
|
94
|
+
invalid.errors.full_messages
|
|
95
|
+
# => ["Name is too short", "Age must be greater than or equal to 0", ...]
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
## Next Steps
|
|
99
|
+
|
|
100
|
+
- [Schema Definition](schema-definition) - Learn the full DSL
|
|
101
|
+
- [Property Types](property-types) - Explore available types and constraints
|
|
102
|
+
- [Nested Models](nested-models) - Build complex, composable schemas
|
data/docs/index.markdown
CHANGED
|
@@ -1,7 +1,54 @@
|
|
|
1
1
|
---
|
|
2
|
-
# Feel free to add content and custom Front Matter to this file.
|
|
3
|
-
# To modify the layout, see https://jekyllrb.com/docs/themes/#overriding-theme-defaults
|
|
4
|
-
|
|
5
2
|
layout: home
|
|
3
|
+
title: Home
|
|
6
4
|
---
|
|
7
|
-
|
|
5
|
+
|
|
6
|
+
# EasyTalk
|
|
7
|
+
|
|
8
|
+
EasyTalk is a Ruby library for defining and generating JSON Schema from Ruby classes. It provides an ActiveModel-like interface for defining structured data models with validation and JSON Schema generation capabilities.
|
|
9
|
+
|
|
10
|
+
## Key Features
|
|
11
|
+
|
|
12
|
+
- **Schema Definition DSL** - Define JSON Schema using a clean Ruby DSL
|
|
13
|
+
- **Type System** - Support for Ruby types and Sorbet-style generics
|
|
14
|
+
- **ActiveModel Integration** - Built-in validations from schema constraints
|
|
15
|
+
- **Nested Models** - Compose complex schemas from simple building blocks
|
|
16
|
+
- **LLM Function Calling** - Generate OpenAI-compatible function schemas
|
|
17
|
+
|
|
18
|
+
## Quick Start
|
|
19
|
+
|
|
20
|
+
```ruby
|
|
21
|
+
class User
|
|
22
|
+
include EasyTalk::Model
|
|
23
|
+
|
|
24
|
+
define_schema do
|
|
25
|
+
title "User"
|
|
26
|
+
description "A user in the system"
|
|
27
|
+
property :name, String, min_length: 2
|
|
28
|
+
property :email, String, format: "email"
|
|
29
|
+
property :age, Integer, minimum: 0, optional: true
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Generate JSON Schema
|
|
34
|
+
User.json_schema
|
|
35
|
+
# => {"type"=>"object", "title"=>"User", ...}
|
|
36
|
+
|
|
37
|
+
# Create and validate instances
|
|
38
|
+
user = User.new(name: "Alice", email: "alice@example.com")
|
|
39
|
+
user.valid? # => true
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## Documentation
|
|
43
|
+
|
|
44
|
+
- [Getting Started](getting-started) - Installation and basic usage
|
|
45
|
+
- [Schema Definition](schema-definition) - How to define schemas
|
|
46
|
+
- [Property Types](property-types) - Available types and constraints
|
|
47
|
+
- [Nested Models](nested-models) - Composing complex schemas
|
|
48
|
+
- [API Reference](api/) - Generated API documentation
|
|
49
|
+
|
|
50
|
+
## Links
|
|
51
|
+
|
|
52
|
+
- [GitHub Repository](https://github.com/sergiobayona/easy_talk)
|
|
53
|
+
- [RubyGems](https://rubygems.org/gems/easy_talk)
|
|
54
|
+
- [Changelog](https://github.com/sergiobayona/easy_talk/blob/main/CHANGELOG.md)
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
# JSON Schema Compliance Guide
|
|
2
|
+
|
|
3
|
+
This document defines the strategy for testing and improving `EasyTalk`'s compliance with the official [JSON Schema Specification](https://json-schema.org/specification).
|
|
4
|
+
|
|
5
|
+
## Test Suite Setup
|
|
6
|
+
|
|
7
|
+
`EasyTalk` integrates the standard [JSON Schema Test Suite](https://github.com/json-schema-org/JSON-Schema-Test-Suite) as a git submodule in `spec/fixtures/json_schema_test_suite`. This provides a language-agnostic set of test cases covering all aspects of the specification.
|
|
8
|
+
|
|
9
|
+
### Infrastructure
|
|
10
|
+
|
|
11
|
+
- **Submodule**: Located at `spec/fixtures/json_schema_test_suite`.
|
|
12
|
+
- **Converter**: `spec/support/json_schema_converter.rb` dynamically converts raw JSON Schema definitions into `EasyTalk::Model` classes at runtime.
|
|
13
|
+
- **Runner**: `spec/integration/json_schema_compliance_spec.rb` iterates over the test suite files, generates models, and asserts valid/invalid behavior.
|
|
14
|
+
|
|
15
|
+
## Running the Tests
|
|
16
|
+
|
|
17
|
+
The compliance tests are **optional** and excluded from the default `rspec` run to prevent noise from known incompatibilities.
|
|
18
|
+
|
|
19
|
+
To run the compliance suite:
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
bundle exec rspec --tag json_schema_compliance spec/integration/json_schema_compliance_spec.rb
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Schema Wrapping Strategy
|
|
26
|
+
|
|
27
|
+
Since `EasyTalk` models are always objects, the JSON Schema test suite's root-level primitive tests (e.g., `{"type": "integer"}` with data `5`) require adaptation.
|
|
28
|
+
|
|
29
|
+
### How It Works
|
|
30
|
+
|
|
31
|
+
The `JsonSchemaConverter` uses a **wrapper property strategy**:
|
|
32
|
+
|
|
33
|
+
1. **Detection**: `needs_wrapping?` checks if the schema is non-object (no `type: object` or `properties` key)
|
|
34
|
+
2. **Wrapping**: Non-object schemas become a `value` property on a wrapper object
|
|
35
|
+
3. **Data transformation**: Primitive test data is wrapped as `{"value": data}`
|
|
36
|
+
|
|
37
|
+
**Example transformation:**
|
|
38
|
+
|
|
39
|
+
```
|
|
40
|
+
Original JSON Schema test:
|
|
41
|
+
Schema: {"type": "integer", "minimum": 1}
|
|
42
|
+
Data: 5
|
|
43
|
+
Valid: true
|
|
44
|
+
|
|
45
|
+
Transformed for EasyTalk:
|
|
46
|
+
Schema: {
|
|
47
|
+
"type": "object",
|
|
48
|
+
"properties": { "value": {"type": "integer", "minimum": 1} },
|
|
49
|
+
"required": ["value"]
|
|
50
|
+
}
|
|
51
|
+
Data: {"value": 5}
|
|
52
|
+
Valid: true
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
This preserves validation semantics while fitting EasyTalk's object-based model.
|
|
56
|
+
|
|
57
|
+
## Current Test Results
|
|
58
|
+
|
|
59
|
+
As of the latest run:
|
|
60
|
+
|
|
61
|
+
| Metric | Count |
|
|
62
|
+
|--------|-------|
|
|
63
|
+
| Total examples | 916 |
|
|
64
|
+
| Passing | ~193 |
|
|
65
|
+
| Failing | 165 |
|
|
66
|
+
| Pending (known unsupported) | 558 |
|
|
67
|
+
|
|
68
|
+
### Known Unsupported Features
|
|
69
|
+
|
|
70
|
+
The following test files are skipped entirely via `KNOWN_FAILURES`:
|
|
71
|
+
|
|
72
|
+
| File | Reason |
|
|
73
|
+
|------|--------|
|
|
74
|
+
| `not.json` | `not` keyword not supported |
|
|
75
|
+
| `anyOf.json` | `anyOf` validation not supported |
|
|
76
|
+
| `allOf.json` | `allOf` validation not supported |
|
|
77
|
+
| `oneOf.json` | `oneOf` validation not supported |
|
|
78
|
+
| `refRemote.json` | Remote `$ref` not supported |
|
|
79
|
+
| `dependencies.json` | Dependencies not supported |
|
|
80
|
+
| `definitions.json` | `$defs`/definitions not supported |
|
|
81
|
+
| `if-then-else.json` | Conditional logic not supported |
|
|
82
|
+
| `patternProperties.json` | Pattern properties not supported |
|
|
83
|
+
| `properties.json` | Complex property interactions not supported |
|
|
84
|
+
| `propertyNames.json` | Property names validation not supported |
|
|
85
|
+
| `ref.json` | Complex `$ref` not supported |
|
|
86
|
+
| `required.json` | Complex required checks not supported |
|
|
87
|
+
| `additionalItems.json` | Additional items not supported |
|
|
88
|
+
| `additionalProperties.json` | Additional properties validation not supported |
|
|
89
|
+
| `boolean_schema.json` | Boolean schemas (`true`/`false` as schema) not supported |
|
|
90
|
+
| `const.json` | `const` keyword not supported |
|
|
91
|
+
| `default.json` | Default keyword behavior not supported |
|
|
92
|
+
| `enum.json` | Enum validation not fully supported |
|
|
93
|
+
| `infinite-loop-detection.json` | Infinite loop detection not supported |
|
|
94
|
+
| `maxProperties.json` | Max properties not supported |
|
|
95
|
+
| `minProperties.json` | Min properties not supported |
|
|
96
|
+
|
|
97
|
+
## Compliance Gaps
|
|
98
|
+
|
|
99
|
+
The 165 failing tests reveal real validation gaps in EasyTalk:
|
|
100
|
+
|
|
101
|
+
### 1. Type Coercion (Intentional Behavior)
|
|
102
|
+
|
|
103
|
+
EasyTalk uses ActiveModel's numericality validation which coerces strings to numbers:
|
|
104
|
+
|
|
105
|
+
```ruby
|
|
106
|
+
user = User.new(age: "30") # String
|
|
107
|
+
user.valid? # => true (coerced to integer 30)
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
Per JSON Schema, `"30"` should be invalid for `type: integer`. This is documented as **intentional behavior** for Rails compatibility. A `strict_types` configuration option is planned (see [#137](https://github.com/sergiobayona/easy_talk/issues/137)).
|
|
111
|
+
|
|
112
|
+
### 2. Format Validation Scope
|
|
113
|
+
|
|
114
|
+
JSON Schema specifies that format validations should only apply to strings and ignore other types. EasyTalk currently validates format on the assigned value regardless of type.
|
|
115
|
+
|
|
116
|
+
### 3. Empty String Presence
|
|
117
|
+
|
|
118
|
+
EasyTalk uses ActiveModel's presence validation for required fields, which rejects empty strings. JSON Schema considers `""` a valid string.
|
|
119
|
+
|
|
120
|
+
### 4. Array Type Validation
|
|
121
|
+
|
|
122
|
+
Array element types are not strictly validated at runtime.
|
|
123
|
+
|
|
124
|
+
### 5. Null Type
|
|
125
|
+
|
|
126
|
+
The `null` type is not fully implemented as a standalone type.
|
|
127
|
+
|
|
128
|
+
### 6. uniqueItems
|
|
129
|
+
|
|
130
|
+
Array uniqueness constraint is not enforced during validation.
|
|
131
|
+
|
|
132
|
+
## Workflow for Improvements
|
|
133
|
+
|
|
134
|
+
1. **Select a Feature**: Pick a specific file from `KNOWN_FAILURES` or analyze failing tests.
|
|
135
|
+
2. **Enable Tests**: Remove the file from `KNOWN_FAILURES` in the spec.
|
|
136
|
+
3. **Run & Analyze**:
|
|
137
|
+
```bash
|
|
138
|
+
bundle exec rspec --tag json_schema_compliance spec/integration/json_schema_compliance_spec.rb
|
|
139
|
+
```
|
|
140
|
+
4. **Implement Fix**: Modify EasyTalk internals to support the feature.
|
|
141
|
+
5. **Update Converter**: If needed, update `JsonSchemaConverter` for test adaptation.
|
|
142
|
+
|
|
143
|
+
## Critical Implementation Notes
|
|
144
|
+
|
|
145
|
+
### Reserved Words
|
|
146
|
+
|
|
147
|
+
Properties like `method`, `class`, `constructor` conflict with Ruby. The converter sanitizes these via `sanitize_property_name` and uses the `as:` option to preserve the original JSON key.
|
|
148
|
+
|
|
149
|
+
### Boolean Schemas
|
|
150
|
+
|
|
151
|
+
`properties: { foo: false }` is valid JSON Schema (property forbidden) but not currently supported.
|
|
152
|
+
|
|
153
|
+
### Strict Property Validation
|
|
154
|
+
|
|
155
|
+
EasyTalk raises `InvalidPropertyNameError` for invalid property names at definition time. Full compliance would require allowing arbitrary property keys.
|
|
156
|
+
|
|
157
|
+
## Contributing
|
|
158
|
+
|
|
159
|
+
When adding support for a new JSON Schema keyword:
|
|
160
|
+
|
|
161
|
+
1. Check if a test file exists in `spec/fixtures/json_schema_test_suite/tests/draft7/`.
|
|
162
|
+
2. Remove the filename from `KNOWN_FAILURES` in `spec/integration/json_schema_compliance_spec.rb`.
|
|
163
|
+
3. Run the tests and analyze failures.
|
|
164
|
+
4. Implement the feature in EasyTalk.
|
|
165
|
+
5. Update this document with any new findings.
|
|
166
|
+
|
|
167
|
+
## Related Issues
|
|
168
|
+
|
|
169
|
+
- [#137](https://github.com/sergiobayona/easy_talk/issues/137) - Add `strict_types` configuration option
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
---
|
|
2
|
+
layout: page
|
|
3
|
+
title: Nested Models
|
|
4
|
+
permalink: /nested-models/
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Nested Models
|
|
8
|
+
|
|
9
|
+
EasyTalk supports composing complex schemas from simpler building blocks. This enables clean, reusable data structures.
|
|
10
|
+
|
|
11
|
+
## Basic Nesting
|
|
12
|
+
|
|
13
|
+
Reference another EasyTalk model as a property type:
|
|
14
|
+
|
|
15
|
+
```ruby
|
|
16
|
+
class Address
|
|
17
|
+
include EasyTalk::Model
|
|
18
|
+
|
|
19
|
+
define_schema do
|
|
20
|
+
property :street, String
|
|
21
|
+
property :city, String
|
|
22
|
+
property :zip_code, String, pattern: /^\d{5}$/
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
class Person
|
|
27
|
+
include EasyTalk::Model
|
|
28
|
+
|
|
29
|
+
define_schema do
|
|
30
|
+
property :name, String
|
|
31
|
+
property :home_address, Address
|
|
32
|
+
property :work_address, Address, optional: true
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
### Generated Schema
|
|
38
|
+
|
|
39
|
+
```json
|
|
40
|
+
{
|
|
41
|
+
"type": "object",
|
|
42
|
+
"properties": {
|
|
43
|
+
"name": { "type": "string" },
|
|
44
|
+
"home_address": {
|
|
45
|
+
"type": "object",
|
|
46
|
+
"properties": {
|
|
47
|
+
"street": { "type": "string" },
|
|
48
|
+
"city": { "type": "string" },
|
|
49
|
+
"zip_code": { "type": "string", "pattern": "^\\d{5}$" }
|
|
50
|
+
},
|
|
51
|
+
"required": ["street", "city", "zip_code"]
|
|
52
|
+
},
|
|
53
|
+
"work_address": { ... }
|
|
54
|
+
},
|
|
55
|
+
"required": ["name", "home_address"]
|
|
56
|
+
}
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## Arrays of Models
|
|
60
|
+
|
|
61
|
+
Use `T::Array[ModelClass]` for collections:
|
|
62
|
+
|
|
63
|
+
```ruby
|
|
64
|
+
class Order
|
|
65
|
+
include EasyTalk::Model
|
|
66
|
+
|
|
67
|
+
define_schema do
|
|
68
|
+
property :id, String
|
|
69
|
+
property :items, T::Array[LineItem], min_items: 1
|
|
70
|
+
property :shipping_address, Address
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
class LineItem
|
|
75
|
+
include EasyTalk::Model
|
|
76
|
+
|
|
77
|
+
define_schema do
|
|
78
|
+
property :product_id, String
|
|
79
|
+
property :quantity, Integer, minimum: 1
|
|
80
|
+
property :price, Float, minimum: 0
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
## Auto-Instantiation
|
|
86
|
+
|
|
87
|
+
When you pass a Hash to a nested model property, EasyTalk automatically instantiates the nested model:
|
|
88
|
+
|
|
89
|
+
```ruby
|
|
90
|
+
person = Person.new(
|
|
91
|
+
name: "Alice",
|
|
92
|
+
home_address: {
|
|
93
|
+
street: "123 Main St",
|
|
94
|
+
city: "Boston",
|
|
95
|
+
zip_code: "02101"
|
|
96
|
+
}
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
person.home_address.class # => Address
|
|
100
|
+
person.home_address.city # => "Boston"
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
This works recursively for deeply nested structures.
|
|
104
|
+
|
|
105
|
+
## Using $ref for Reusability
|
|
106
|
+
|
|
107
|
+
By default, nested models are inlined. Enable `$ref` for cleaner, more reusable schemas:
|
|
108
|
+
|
|
109
|
+
```ruby
|
|
110
|
+
class Person
|
|
111
|
+
include EasyTalk::Model
|
|
112
|
+
|
|
113
|
+
define_schema do
|
|
114
|
+
property :name, String
|
|
115
|
+
property :address, Address, ref: true
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
This generates:
|
|
121
|
+
|
|
122
|
+
```json
|
|
123
|
+
{
|
|
124
|
+
"type": "object",
|
|
125
|
+
"properties": {
|
|
126
|
+
"name": { "type": "string" },
|
|
127
|
+
"address": { "$ref": "#/$defs/Address" }
|
|
128
|
+
},
|
|
129
|
+
"$defs": {
|
|
130
|
+
"Address": {
|
|
131
|
+
"type": "object",
|
|
132
|
+
"properties": { ... }
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
## Nullable Nested Models
|
|
139
|
+
|
|
140
|
+
Allow null values for nested models:
|
|
141
|
+
|
|
142
|
+
```ruby
|
|
143
|
+
property :backup_address, T.nilable(Address)
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
Or make it both nullable and optional:
|
|
147
|
+
|
|
148
|
+
```ruby
|
|
149
|
+
property :backup_address, T.nilable(Address), optional: true
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
## Composition with compose
|
|
153
|
+
|
|
154
|
+
Use `compose` to merge multiple schemas into one:
|
|
155
|
+
|
|
156
|
+
```ruby
|
|
157
|
+
class BasicInfo
|
|
158
|
+
include EasyTalk::Model
|
|
159
|
+
define_schema do
|
|
160
|
+
property :name, String
|
|
161
|
+
property :email, String
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
class ContactInfo
|
|
166
|
+
include EasyTalk::Model
|
|
167
|
+
define_schema do
|
|
168
|
+
property :phone, String
|
|
169
|
+
property :address, Address
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
class FullProfile
|
|
174
|
+
include EasyTalk::Model
|
|
175
|
+
define_schema do
|
|
176
|
+
compose T::AllOf[BasicInfo, ContactInfo]
|
|
177
|
+
end
|
|
178
|
+
end
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
## Polymorphic Types
|
|
182
|
+
|
|
183
|
+
Use `T::OneOf` or `T::AnyOf` for polymorphic properties:
|
|
184
|
+
|
|
185
|
+
```ruby
|
|
186
|
+
class EmailContact
|
|
187
|
+
include EasyTalk::Model
|
|
188
|
+
define_schema do
|
|
189
|
+
property :email, String, format: "email"
|
|
190
|
+
end
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
class PhoneContact
|
|
194
|
+
include EasyTalk::Model
|
|
195
|
+
define_schema do
|
|
196
|
+
property :phone, String
|
|
197
|
+
end
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
class User
|
|
201
|
+
include EasyTalk::Model
|
|
202
|
+
define_schema do
|
|
203
|
+
property :name, String
|
|
204
|
+
property :primary_contact, T::OneOf[EmailContact, PhoneContact]
|
|
205
|
+
end
|
|
206
|
+
end
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
This ensures the `primary_contact` matches exactly one of the specified schemas.
|
|
210
|
+
|
|
211
|
+
## Best Practices
|
|
212
|
+
|
|
213
|
+
1. **Keep models focused** - Each model should represent one concept
|
|
214
|
+
2. **Reuse models** - Define common structures (Address, Money, etc.) once
|
|
215
|
+
3. **Use $ref for large schemas** - Reduces duplication and improves readability
|
|
216
|
+
4. **Validate at boundaries** - Nested models validate automatically when the parent validates
|