easy_talk 3.1.0 → 3.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +4 -0
  3. data/.yardopts +13 -0
  4. data/CHANGELOG.md +75 -0
  5. data/README.md +616 -35
  6. data/Rakefile +27 -0
  7. data/docs/.gitignore +1 -0
  8. data/docs/about.markdown +28 -8
  9. data/docs/getting-started.markdown +102 -0
  10. data/docs/index.markdown +51 -4
  11. data/docs/json_schema_compliance.md +55 -0
  12. data/docs/nested-models.markdown +216 -0
  13. data/docs/property-types.markdown +212 -0
  14. data/docs/schema-definition.markdown +180 -0
  15. data/lib/easy_talk/builders/base_builder.rb +4 -2
  16. data/lib/easy_talk/builders/composition_builder.rb +10 -12
  17. data/lib/easy_talk/builders/object_builder.rb +45 -30
  18. data/lib/easy_talk/builders/registry.rb +168 -0
  19. data/lib/easy_talk/builders/typed_array_builder.rb +15 -4
  20. data/lib/easy_talk/configuration.rb +31 -1
  21. data/lib/easy_talk/error_formatter/base.rb +100 -0
  22. data/lib/easy_talk/error_formatter/error_code_mapper.rb +82 -0
  23. data/lib/easy_talk/error_formatter/flat.rb +38 -0
  24. data/lib/easy_talk/error_formatter/json_pointer.rb +38 -0
  25. data/lib/easy_talk/error_formatter/jsonapi.rb +64 -0
  26. data/lib/easy_talk/error_formatter/path_converter.rb +53 -0
  27. data/lib/easy_talk/error_formatter/rfc7807.rb +69 -0
  28. data/lib/easy_talk/error_formatter.rb +143 -0
  29. data/lib/easy_talk/errors.rb +2 -0
  30. data/lib/easy_talk/errors_helper.rb +63 -34
  31. data/lib/easy_talk/model.rb +123 -90
  32. data/lib/easy_talk/model_helper.rb +13 -0
  33. data/lib/easy_talk/naming_strategies.rb +20 -0
  34. data/lib/easy_talk/property.rb +16 -94
  35. data/lib/easy_talk/ref_helper.rb +27 -0
  36. data/lib/easy_talk/schema.rb +198 -0
  37. data/lib/easy_talk/schema_definition.rb +7 -1
  38. data/lib/easy_talk/schema_methods.rb +80 -0
  39. data/lib/easy_talk/tools/function_builder.rb +1 -1
  40. data/lib/easy_talk/type_introspection.rb +178 -0
  41. data/lib/easy_talk/types/base_composer.rb +2 -1
  42. data/lib/easy_talk/types/composer.rb +4 -0
  43. data/lib/easy_talk/validation_adapters/active_model_adapter.rb +329 -0
  44. data/lib/easy_talk/validation_adapters/base.rb +144 -0
  45. data/lib/easy_talk/validation_adapters/none_adapter.rb +36 -0
  46. data/lib/easy_talk/validation_adapters/registry.rb +87 -0
  47. data/lib/easy_talk/validation_builder.rb +28 -309
  48. data/lib/easy_talk/version.rb +1 -1
  49. data/lib/easy_talk.rb +41 -0
  50. metadata +26 -4
  51. data/docs/404.html +0 -25
  52. data/docs/_posts/2024-05-07-welcome-to-jekyll.markdown +0 -29
  53. 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
@@ -3,3 +3,4 @@ _site
3
3
  .jekyll-cache
4
4
  .jekyll-metadata
5
5
  vendor
6
+ api/
data/docs/about.markdown CHANGED
@@ -4,15 +4,35 @@ title: About
4
4
  permalink: /about/
5
5
  ---
6
6
 
7
- This is the base Jekyll theme. You can find out more info about customizing your Jekyll theme, as well as basic Jekyll usage documentation at [jekyllrb.com](https://jekyllrb.com/)
7
+ # About EasyTalk
8
8
 
9
- You can find the source code for Minima at GitHub:
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
- You can find the source code for Jekyll at GitHub:
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
- [jekyll-organization]: https://github.com/jekyll
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
- EasyTalk is a Ruby library that simplifies defining and generating JSON Schema documents, and validates that JSON data conforms to these schemas.
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,55 @@
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
+ ## Compliance Strategy
26
+
27
+ The goal is to incrementally improve compliance by enabling more tests and fixing the underlying issues in `EasyTalk`.
28
+
29
+ ### 1. Identify Gaps
30
+ The current test runner skips many tests (marked as `pending` or explicit `skip`).
31
+ * **Root Primitives**: `EasyTalk` models are Objects. Tests for root integers/strings are skipped.
32
+ * **Strict Naming**: Tests with property names that are invalid Ruby identifiers (e.g., `foo-bar`, `123`, `constructor`) are currently skipped.
33
+ * **Missing Keywords**: Keywords like `patternProperties`, `const`, and `oneOf` (in specific contexts) may fail.
34
+
35
+ ### 2. Workflow for Improvements
36
+ 1. **Select a Feature**: Pick a specific file (e.g., `properties.json`, `required.json`) or a skipped section.
37
+ 2. **Un-skip Tests**: Remove the `skip` logic in `spec/integration/json_schema_compliance_spec.rb` for that feature.
38
+ 3. **Run & Analyze**: Run the specific test file.
39
+ ```bash
40
+ bundle exec rspec --tag json_schema_compliance
41
+ ```
42
+ 4. **Implement Fix**: Modify `EasyTalk` internals (e.g., `keywords.rb`, `schema_definition.rb`) to support the feature.
43
+ 5. **Sanitize Inputs**: Update `JsonSchemaConverter` if the test case requires adaptation (e.g., mapping a JSON key to a safe Ruby method name via `as:`) without changing the underlying validation logic.
44
+
45
+ ### 3. Known Critical Issues
46
+ * **Reserved Words**: Properties like `method`, `class`, `constructor` conflict with Ruby. Fix requires a robust proxy or sanitization layer in `EasyTalk::Model`.
47
+ * **Boolean Schemas**: `properties: { foo: false }` is valid JSON Schema (property forbidden) but not currently supported by `EasyTalk`.
48
+ * **Strict Property Validation**: `EasyTalk` raises errors for invalid property names at definition time. Compliance requires allowing arbitrary property keys (perhaps via `validates_with` logic instead of metaprogramming methods).
49
+
50
+ ## Contributing
51
+
52
+ When adding support for a new JSON Schema keyword:
53
+ 1. Check if a test file exists in `spec/fixtures/json_schema_test_suite/tests/draft7/`.
54
+ 2. Add the filename to the `FOCUS_FILES` list in `spec/integration/json_schema_compliance_spec.rb`.
55
+ 3. Implement the feature and verify pass.
@@ -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