easy_talk 1.0.3 → 2.0.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 +97 -17
- data/CHANGELOG.md +51 -0
- data/README.md +496 -60
- data/easy_talk.gemspec +38 -0
- data/lib/easy_talk/active_record_schema_builder.rb +8 -1
- data/lib/easy_talk/builders/composition_builder.rb +24 -1
- data/lib/easy_talk/builders/object_builder.rb +11 -20
- data/lib/easy_talk/builders/temporal_builder.rb +49 -0
- data/lib/easy_talk/configuration.rb +6 -3
- data/lib/easy_talk/errors_helper.rb +1 -0
- data/lib/easy_talk/model.rb +75 -14
- data/lib/easy_talk/property.rb +116 -44
- data/lib/easy_talk/schema_definition.rb +28 -16
- data/lib/easy_talk/types/composer.rb +89 -0
- data/lib/easy_talk/validation_builder.rb +339 -0
- data/lib/easy_talk/version.rb +1 -1
- data/lib/easy_talk.rb +5 -3
- metadata +13 -158
- data/lib/easy_talk/builders/all_of_builder.rb +0 -11
- data/lib/easy_talk/builders/any_of_builder.rb +0 -11
- data/lib/easy_talk/builders/date_builder.rb +0 -18
- data/lib/easy_talk/builders/datetime_builder.rb +0 -18
- data/lib/easy_talk/builders/one_of_builder.rb +0 -11
- data/lib/easy_talk/builders/time_builder.rb +0 -16
- data/lib/easy_talk/types/all_of.rb +0 -32
- data/lib/easy_talk/types/any_of.rb +0 -39
- data/lib/easy_talk/types/one_of.rb +0 -31
data/README.md
CHANGED
@@ -1,5 +1,8 @@
|
|
1
1
|
# EasyTalk
|
2
2
|
|
3
|
+
[](https://badge.fury.io/rb/easy_talk)
|
4
|
+
[](https://github.com/sergiobayona/easy_talk/actions/workflows/dev-build.yml)
|
5
|
+
|
3
6
|
## Introduction
|
4
7
|
|
5
8
|
### What is EasyTalk?
|
@@ -7,10 +10,12 @@ EasyTalk is a Ruby library that simplifies defining and generating JSON Schema.
|
|
7
10
|
|
8
11
|
### Key Features
|
9
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).
|
10
14
|
* **Works for plain Ruby classes and ActiveRecord models**: Integrate with existing code or build from scratch.
|
11
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.
|
12
16
|
* **Schema Composition**: Define EasyTalk models and reference them in other EasyTalk models to create complex schemas.
|
13
|
-
* **
|
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.
|
14
19
|
|
15
20
|
### Use Cases
|
16
21
|
- API request/response validation
|
@@ -26,8 +31,39 @@ Inspired by Python's Pydantic library, EasyTalk brings similar functionality to
|
|
26
31
|
|
27
32
|
### Requirements
|
28
33
|
- Ruby 3.2 or higher
|
29
|
-
|
30
|
-
|
34
|
+
|
35
|
+
### Version 2.0.0 Breaking Changes
|
36
|
+
|
37
|
+
⚠️ **IMPORTANT**: Version 2.0.0 includes breaking changes. Please review before upgrading:
|
38
|
+
|
39
|
+
- **Removed**: Block-style nested object definitions (using `Hash do ... end`)
|
40
|
+
- **Migration**: Use class references instead of inline Hash definitions
|
41
|
+
|
42
|
+
```ruby
|
43
|
+
# ❌ No longer supported (v1.x style)
|
44
|
+
define_schema do
|
45
|
+
property :address, Hash do
|
46
|
+
property :street, String
|
47
|
+
property :city, String
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
# ✅ New approach (v2.x style)
|
52
|
+
class Address
|
53
|
+
include EasyTalk::Model
|
54
|
+
define_schema do
|
55
|
+
property :street, String
|
56
|
+
property :city, String
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
class User
|
61
|
+
include EasyTalk::Model
|
62
|
+
define_schema do
|
63
|
+
property :address, Address # Reference the class directly
|
64
|
+
end
|
65
|
+
end
|
66
|
+
```
|
31
67
|
|
32
68
|
### Installation Steps
|
33
69
|
Add EasyTalk to your application's Gemfile:
|
@@ -109,10 +145,11 @@ Creating and validating an instance of your model:
|
|
109
145
|
|
110
146
|
```ruby
|
111
147
|
user = User.new(name: "John Doe", email: "john@example.com", age: 25)
|
112
|
-
user.valid? # => true
|
148
|
+
user.valid? # => true (automatically validates based on schema constraints)
|
113
149
|
|
114
150
|
user.age = 17
|
115
|
-
user.valid? # => false
|
151
|
+
user.valid? # => false (violates minimum: 18 constraint)
|
152
|
+
user.errors[:age] # => ["must be greater than or equal to 18"]
|
116
153
|
```
|
117
154
|
|
118
155
|
## Core Concepts
|
@@ -143,7 +180,6 @@ EasyTalk supports standard Ruby types directly:
|
|
143
180
|
- `Float`: Floating-point numbers
|
144
181
|
- `Date`: Date values
|
145
182
|
- `DateTime`: Date and time values
|
146
|
-
- `Hash`: Object/dictionary values
|
147
183
|
|
148
184
|
#### Sorbet-Style Types
|
149
185
|
For complex types, EasyTalk uses Sorbet-style type notation:
|
@@ -184,25 +220,57 @@ end
|
|
184
220
|
|
185
221
|
In this example, `name` is required but `middle_name` is optional.
|
186
222
|
|
187
|
-
###
|
188
|
-
EasyTalk
|
223
|
+
### Automatic Validation Generation
|
224
|
+
EasyTalk automatically generates ActiveModel validations from your schema constraints. This feature is enabled by default but can be configured:
|
189
225
|
|
190
226
|
```ruby
|
191
227
|
class User
|
192
228
|
include EasyTalk::Model
|
193
229
|
|
194
|
-
|
195
|
-
|
230
|
+
define_schema do
|
231
|
+
property :name, String, min_length: 2, max_length: 50
|
232
|
+
property :email, String, format: "email"
|
233
|
+
property :age, Integer, minimum: 18, maximum: 120
|
234
|
+
property :status, String, enum: ["active", "inactive", "pending"]
|
235
|
+
end
|
236
|
+
# Validations are automatically generated:
|
237
|
+
# validates :name, presence: true, length: { minimum: 2, maximum: 50 }
|
238
|
+
# validates :email, presence: true, format: { with: URI::MailTo::EMAIL_REGEXP }
|
239
|
+
# validates :age, presence: true, numericality: { greater_than_or_equal_to: 18, less_than_or_equal_to: 120 }
|
240
|
+
# validates :status, presence: true, inclusion: { in: ["active", "inactive", "pending"] }
|
241
|
+
end
|
242
|
+
|
243
|
+
user = User.new(name: "Jo", email: "invalid-email", age: 17)
|
244
|
+
user.valid? # => false
|
245
|
+
user.errors.full_messages
|
246
|
+
# => ["Name is too short (minimum is 2 characters)",
|
247
|
+
# "Email is invalid",
|
248
|
+
# "Age must be greater than or equal to 18"]
|
249
|
+
```
|
250
|
+
|
251
|
+
### Manual Validation Overrides
|
252
|
+
You can still add manual validations alongside automatic ones:
|
253
|
+
|
254
|
+
```ruby
|
255
|
+
class User
|
256
|
+
include EasyTalk::Model
|
257
|
+
|
258
|
+
# Custom validation in addition to automatic ones
|
259
|
+
validates :email, uniqueness: true
|
260
|
+
validate :complex_business_rule
|
196
261
|
|
197
262
|
define_schema do
|
198
263
|
property :name, String
|
264
|
+
property :email, String, format: "email"
|
199
265
|
property :age, Integer, minimum: 18
|
200
266
|
end
|
267
|
+
|
268
|
+
private
|
269
|
+
|
270
|
+
def complex_business_rule
|
271
|
+
# Custom validation logic
|
272
|
+
end
|
201
273
|
end
|
202
|
-
|
203
|
-
user = User.new(name: "John", age: 17)
|
204
|
-
user.valid? # => false
|
205
|
-
user.errors.full_messages # => ["Age must be greater than or equal to 18"]
|
206
274
|
```
|
207
275
|
|
208
276
|
## Defining Schemas
|
@@ -230,16 +298,6 @@ property :name, String, description: "The person's name", title: "Full Name"
|
|
230
298
|
property :age, Integer, minimum: 0, maximum: 120, description: "The person's age"
|
231
299
|
```
|
232
300
|
|
233
|
-
### Nested Objects
|
234
|
-
You can define nested objects using a block:
|
235
|
-
|
236
|
-
```ruby
|
237
|
-
property :email, Hash do
|
238
|
-
property :address, String, format: "email"
|
239
|
-
property :verified, T::Boolean
|
240
|
-
end
|
241
|
-
```
|
242
|
-
|
243
301
|
### Arrays and Collections
|
244
302
|
Arrays can be defined using the `T::Array` type:
|
245
303
|
|
@@ -254,22 +312,38 @@ You can also define arrays of complex types:
|
|
254
312
|
property :addresses, T::Array[Address], description: "List of addresses"
|
255
313
|
```
|
256
314
|
|
257
|
-
### Constraints and Validations
|
258
|
-
Constraints
|
315
|
+
### Constraints and Automatic Validations
|
316
|
+
Constraints are added to properties and are used for both schema generation and automatic validation generation:
|
259
317
|
|
260
318
|
```ruby
|
261
|
-
|
262
|
-
property :
|
263
|
-
property :
|
319
|
+
define_schema do
|
320
|
+
property :name, String, min_length: 2, max_length: 50
|
321
|
+
property :email, String, format: "email"
|
322
|
+
property :category, String, enum: ["A", "B", "C"]
|
323
|
+
property :score, Float, minimum: 0.0, maximum: 100.0
|
324
|
+
property :tags, T::Array[String], min_items: 1, max_items: 10
|
325
|
+
end
|
326
|
+
# Automatically generates equivalent ActiveModel validations:
|
327
|
+
# validates :name, presence: true, length: { minimum: 2, maximum: 50 }
|
328
|
+
# validates :email, presence: true, format: { with: URI::MailTo::EMAIL_REGEXP }
|
329
|
+
# validates :category, presence: true, inclusion: { in: ["A", "B", "C"] }
|
330
|
+
# validates :score, presence: true, numericality: { greater_than_or_equal_to: 0.0, less_than_or_equal_to: 100.0 }
|
331
|
+
# validates :tags, presence: true, length: { minimum: 1, maximum: 10 }
|
264
332
|
```
|
265
333
|
|
266
|
-
|
334
|
+
### Supported Constraint-to-Validation Mappings
|
267
335
|
|
268
|
-
|
269
|
-
|
270
|
-
|
271
|
-
|
272
|
-
|
336
|
+
| Constraint | Validation Generated |
|
337
|
+
|------------|---------------------|
|
338
|
+
| `min_length`, `max_length` | `length: { minimum: X, maximum: Y }` |
|
339
|
+
| `minimum`, `maximum` | `numericality: { greater_than_or_equal_to: X, less_than_or_equal_to: Y }` |
|
340
|
+
| `format: "email"` | `format: { with: URI::MailTo::EMAIL_REGEXP }` |
|
341
|
+
| `format: "url"` or `format: "uri"` | `format: { with: URI::regexp }` |
|
342
|
+
| `pattern: /regex/` | `format: { with: /regex/ }` |
|
343
|
+
| `enum: [...]` | `inclusion: { in: [...] }` |
|
344
|
+
| `min_items`, `max_items` (arrays) | `length: { minimum: X, maximum: Y }` |
|
345
|
+
| `optional: true` | Skips presence validation |
|
346
|
+
| `T.nilable(Type)` | Allows nil values, skips presence validation |
|
273
347
|
|
274
348
|
### Additional Properties
|
275
349
|
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:
|
@@ -373,20 +447,21 @@ end
|
|
373
447
|
|
374
448
|
## ActiveModel Integration
|
375
449
|
|
376
|
-
###
|
377
|
-
EasyTalk models include ActiveModel
|
450
|
+
### Enhanced Validation System
|
451
|
+
EasyTalk models include comprehensive ActiveModel validation support with automatic generation:
|
378
452
|
|
379
453
|
```ruby
|
380
454
|
class User
|
381
455
|
include EasyTalk::Model
|
382
456
|
|
383
|
-
|
384
|
-
validates :
|
457
|
+
# Manual validations work alongside automatic ones
|
458
|
+
validates :age, comparison: { greater_than: 21 } # Additional business rule
|
459
|
+
validates :height, numericality: { greater_than: 0 } # Overrides auto-validation
|
385
460
|
|
386
461
|
define_schema do
|
387
|
-
property :name, String
|
388
|
-
property :age, Integer
|
389
|
-
property :height, Float
|
462
|
+
property :name, String, min_length: 2 # Auto-generates presence + length validations
|
463
|
+
property :age, Integer, minimum: 18 # Auto-generates presence + numericality validations
|
464
|
+
property :height, Float # Auto-generates presence validation (overridden above)
|
390
465
|
end
|
391
466
|
end
|
392
467
|
```
|
@@ -395,10 +470,11 @@ end
|
|
395
470
|
You can access validation errors using the standard ActiveModel methods:
|
396
471
|
|
397
472
|
```ruby
|
398
|
-
user = User.new(name: "
|
473
|
+
user = User.new(name: "J", age: 18, height: -5.9)
|
399
474
|
user.valid? # => false
|
400
|
-
user.errors[:
|
401
|
-
user.errors[:
|
475
|
+
user.errors[:name] # => ["is too short (minimum is 2 characters)"]
|
476
|
+
user.errors[:age] # => ["must be greater than 21"] # Custom validation
|
477
|
+
user.errors[:height] # => ["must be greater than 0"] # Overridden validation
|
402
478
|
```
|
403
479
|
|
404
480
|
### Model Attributes
|
@@ -411,10 +487,35 @@ user.age = 30
|
|
411
487
|
puts user.name # => "John"
|
412
488
|
```
|
413
489
|
|
414
|
-
You can also initialize a model with a hash of attributes:
|
490
|
+
You can also initialize a model with a hash of attributes, including nested EasyTalk models:
|
415
491
|
|
416
492
|
```ruby
|
417
493
|
user = User.new(name: "John", age: 30, height: 5.9)
|
494
|
+
|
495
|
+
# NEW in v2.0.0: Automatic nested model instantiation
|
496
|
+
class Address
|
497
|
+
include EasyTalk::Model
|
498
|
+
define_schema do
|
499
|
+
property :street, String
|
500
|
+
property :city, String
|
501
|
+
end
|
502
|
+
end
|
503
|
+
|
504
|
+
class User
|
505
|
+
include EasyTalk::Model
|
506
|
+
define_schema do
|
507
|
+
property :name, String
|
508
|
+
property :address, Address
|
509
|
+
end
|
510
|
+
end
|
511
|
+
|
512
|
+
# Hash attributes automatically instantiate nested models
|
513
|
+
user = User.new(
|
514
|
+
name: "John",
|
515
|
+
address: { street: "123 Main St", city: "Boston" }
|
516
|
+
)
|
517
|
+
user.address.class # => Address (automatically instantiated)
|
518
|
+
user.address.street # => "123 Main St"
|
418
519
|
```
|
419
520
|
|
420
521
|
## ActiveRecord Integration
|
@@ -597,12 +698,42 @@ You can configure EasyTalk globally:
|
|
597
698
|
|
598
699
|
```ruby
|
599
700
|
EasyTalk.configure do |config|
|
701
|
+
# ActiveRecord integration options
|
600
702
|
config.excluded_columns = [:created_at, :updated_at, :deleted_at]
|
601
703
|
config.exclude_foreign_keys = true
|
602
704
|
config.exclude_primary_key = true
|
603
705
|
config.exclude_timestamps = true
|
604
706
|
config.exclude_associations = false
|
707
|
+
|
708
|
+
# Schema behavior options
|
605
709
|
config.default_additional_properties = false
|
710
|
+
config.nilable_is_optional = false # Makes T.nilable properties also optional
|
711
|
+
|
712
|
+
# NEW in v2.0.0: Automatic validation generation
|
713
|
+
config.auto_validations = true # Automatically generate ActiveModel validations
|
714
|
+
end
|
715
|
+
```
|
716
|
+
|
717
|
+
### Automatic Validation Configuration
|
718
|
+
The new `auto_validations` option (enabled by default) automatically generates ActiveModel validations from your schema constraints:
|
719
|
+
|
720
|
+
```ruby
|
721
|
+
# Disable automatic validations globally
|
722
|
+
EasyTalk.configure do |config|
|
723
|
+
config.auto_validations = false
|
724
|
+
end
|
725
|
+
|
726
|
+
# Now you must manually define validations
|
727
|
+
class User
|
728
|
+
include EasyTalk::Model
|
729
|
+
|
730
|
+
validates :name, presence: true, length: { minimum: 2 }
|
731
|
+
validates :age, presence: true, numericality: { greater_than_or_equal_to: 18 }
|
732
|
+
|
733
|
+
define_schema do
|
734
|
+
property :name, String, min_length: 2
|
735
|
+
property :age, Integer, minimum: 18
|
736
|
+
end
|
606
737
|
end
|
607
738
|
```
|
608
739
|
|
@@ -651,25 +782,51 @@ end
|
|
651
782
|
|
652
783
|
## Examples
|
653
784
|
|
654
|
-
### User Registration
|
785
|
+
### User Registration (with Auto-Validations)
|
655
786
|
|
656
787
|
```ruby
|
657
788
|
class User
|
658
789
|
include EasyTalk::Model
|
659
790
|
|
660
|
-
|
661
|
-
validates :
|
662
|
-
validates :
|
791
|
+
# Additional custom validations beyond automatic ones
|
792
|
+
validates :email, uniqueness: true
|
793
|
+
validates :password, confirmation: true
|
663
794
|
|
664
795
|
define_schema do
|
665
796
|
title "User Registration"
|
666
797
|
description "User registration information"
|
667
|
-
property :name, String, description: "User's full name"
|
798
|
+
property :name, String, min_length: 2, max_length: 100, description: "User's full name"
|
668
799
|
property :email, String, format: "email", description: "User's email address"
|
669
|
-
property :password, String, min_length: 8, description: "User's password"
|
670
|
-
property :notify, T::Boolean,
|
800
|
+
property :password, String, min_length: 8, max_length: 128, description: "User's password"
|
801
|
+
property :notify, T::Boolean, optional: true, description: "Whether to send notifications"
|
671
802
|
end
|
803
|
+
# Auto-generated validations:
|
804
|
+
# validates :name, presence: true, length: { minimum: 2, maximum: 100 }
|
805
|
+
# validates :email, presence: true, format: { with: URI::MailTo::EMAIL_REGEXP }
|
806
|
+
# validates :password, presence: true, length: { minimum: 8, maximum: 128 }
|
807
|
+
# validates :notify, inclusion: { in: [true, false] } - only if present (optional: true)
|
672
808
|
end
|
809
|
+
|
810
|
+
# Usage with automatic validation
|
811
|
+
user = User.new(
|
812
|
+
name: "John Doe",
|
813
|
+
email: "john@example.com",
|
814
|
+
password: "secretpassword123",
|
815
|
+
notify: true
|
816
|
+
)
|
817
|
+
user.valid? # => true (assuming email is unique)
|
818
|
+
|
819
|
+
# Invalid data triggers auto-generated validations
|
820
|
+
invalid_user = User.new(
|
821
|
+
name: "J", # Too short
|
822
|
+
email: "invalid-email", # Invalid format
|
823
|
+
password: "123" # Too short
|
824
|
+
)
|
825
|
+
invalid_user.valid? # => false
|
826
|
+
invalid_user.errors.full_messages
|
827
|
+
# => ["Name is too short (minimum is 2 characters)",
|
828
|
+
# "Email is invalid",
|
829
|
+
# "Password is too short (minimum is 8 characters)"]
|
673
830
|
```
|
674
831
|
|
675
832
|
### Payment Processing
|
@@ -852,13 +1009,285 @@ property :status, String, enum: ["active", "inactive", "pending"]
|
|
852
1009
|
|
853
1010
|
### Best Practices
|
854
1011
|
|
855
|
-
1. Define clear property names and descriptions
|
856
|
-
2. Use appropriate types for each property
|
857
|
-
3.
|
858
|
-
4. Keep schemas focused and modular
|
859
|
-
5. Reuse models when appropriate
|
860
|
-
6. Use explicit types instead of relying on inference
|
861
|
-
7. Test your schemas with sample data
|
1012
|
+
1. **Define clear property names and descriptions** for better documentation
|
1013
|
+
2. **Use appropriate types** for each property with proper constraints
|
1014
|
+
3. **Leverage automatic validations** by defining schema constraints instead of manual validations
|
1015
|
+
4. **Keep schemas focused and modular** - extract nested objects to separate classes
|
1016
|
+
5. **Reuse models when appropriate** instead of duplicating schema definitions
|
1017
|
+
6. **Use explicit types** instead of relying on inference
|
1018
|
+
7. **Test your schemas with sample data** to ensure validations work as expected
|
1019
|
+
8. **Configure auto-validations globally** to maintain consistency across your application
|
1020
|
+
9. **Use nullable_optional_property** for fields that can be omitted or null
|
1021
|
+
10. **Document breaking changes** when updating schema definitions
|
1022
|
+
|
1023
|
+
# Nullable vs Optional Properties in EasyTalk
|
1024
|
+
|
1025
|
+
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.
|
1026
|
+
|
1027
|
+
## Key Concepts
|
1028
|
+
|
1029
|
+
| Concept | Description | JSON Schema Effect | EasyTalk Syntax |
|
1030
|
+
|---------|-------------|-------------------|-----------------|
|
1031
|
+
| **Nullable** | Property can have a `null` value | Adds `"null"` to the type array | `T.nilable(Type)` |
|
1032
|
+
| **Optional** | Property doesn't have to exist | Omits property from `"required"` array | `optional: true` constraint |
|
1033
|
+
|
1034
|
+
## Nullable Properties
|
1035
|
+
|
1036
|
+
A **nullable** property can contain a `null` value, but the property itself must still be present in the object:
|
1037
|
+
|
1038
|
+
```ruby
|
1039
|
+
property :age, T.nilable(Integer)
|
1040
|
+
```
|
1041
|
+
|
1042
|
+
This produces the following JSON Schema:
|
1043
|
+
|
1044
|
+
```json
|
1045
|
+
{
|
1046
|
+
"properties": {
|
1047
|
+
"age": { "type": ["integer", "null"] }
|
1048
|
+
},
|
1049
|
+
"required": ["age"]
|
1050
|
+
}
|
1051
|
+
```
|
1052
|
+
|
1053
|
+
In this case, the following data would be valid:
|
1054
|
+
- `{ "age": 25 }`
|
1055
|
+
- `{ "age": null }`
|
1056
|
+
|
1057
|
+
But this would be invalid:
|
1058
|
+
- `{ }` (missing the age property entirely)
|
1059
|
+
|
1060
|
+
## Optional Properties
|
1061
|
+
|
1062
|
+
An **optional** property doesn't have to be present in the object at all:
|
1063
|
+
|
1064
|
+
```ruby
|
1065
|
+
property :nickname, String, optional: true
|
1066
|
+
```
|
1067
|
+
|
1068
|
+
This produces:
|
1069
|
+
|
1070
|
+
```json
|
1071
|
+
{
|
1072
|
+
"properties": {
|
1073
|
+
"nickname": { "type": "string" }
|
1074
|
+
}
|
1075
|
+
// Note: "nickname" is not in the "required" array
|
1076
|
+
}
|
1077
|
+
```
|
1078
|
+
|
1079
|
+
In this case, the following data would be valid:
|
1080
|
+
- `{ "nickname": "Joe" }`
|
1081
|
+
- `{ }` (omitting nickname entirely)
|
1082
|
+
|
1083
|
+
But this would be invalid:
|
1084
|
+
- `{ "nickname": null }` (null is not allowed because the property isn't nullable)
|
1085
|
+
|
1086
|
+
## Nullable AND Optional Properties
|
1087
|
+
|
1088
|
+
For properties that should be both nullable and optional (can be omitted or null), you need to combine both approaches:
|
1089
|
+
|
1090
|
+
```ruby
|
1091
|
+
property :bio, T.nilable(String), optional: true
|
1092
|
+
```
|
1093
|
+
|
1094
|
+
This produces:
|
1095
|
+
|
1096
|
+
```json
|
1097
|
+
{
|
1098
|
+
"properties": {
|
1099
|
+
"bio": { "type": ["string", "null"] }
|
1100
|
+
}
|
1101
|
+
// Note: "bio" is not in the "required" array
|
1102
|
+
}
|
1103
|
+
```
|
1104
|
+
|
1105
|
+
For convenience, EasyTalk also provides a helper method:
|
1106
|
+
|
1107
|
+
```ruby
|
1108
|
+
nullable_optional_property :bio, String
|
1109
|
+
```
|
1110
|
+
|
1111
|
+
Which is equivalent to the above.
|
1112
|
+
|
1113
|
+
## Configuration Options
|
1114
|
+
|
1115
|
+
By default, nullable properties are still required. You can change this global behavior:
|
1116
|
+
|
1117
|
+
```ruby
|
1118
|
+
EasyTalk.configure do |config|
|
1119
|
+
config.nilable_is_optional = true # Makes all T.nilable properties also optional
|
1120
|
+
end
|
1121
|
+
```
|
1122
|
+
|
1123
|
+
With this configuration, any property defined with `T.nilable(Type)` will be treated as both nullable and optional.
|
1124
|
+
|
1125
|
+
## Practical Examples
|
1126
|
+
|
1127
|
+
### User Profile Schema
|
1128
|
+
|
1129
|
+
```ruby
|
1130
|
+
class UserProfile
|
1131
|
+
include EasyTalk::Model
|
1132
|
+
|
1133
|
+
define_schema do
|
1134
|
+
# Required properties (must exist, cannot be null)
|
1135
|
+
property :id, String
|
1136
|
+
property :name, String
|
1137
|
+
|
1138
|
+
# Required but nullable (must exist, can be null)
|
1139
|
+
property :age, T.nilable(Integer)
|
1140
|
+
|
1141
|
+
# Optional but not nullable (can be omitted, cannot be null if present)
|
1142
|
+
property :email, String, optional: true
|
1143
|
+
|
1144
|
+
# Optional and nullable (can be omitted, can be null if present)
|
1145
|
+
nullable_optional_property :bio, String
|
1146
|
+
end
|
1147
|
+
end
|
1148
|
+
```
|
1149
|
+
|
1150
|
+
This creates clear expectations for data validation:
|
1151
|
+
- `id` and `name` must be present and cannot be null
|
1152
|
+
- `age` must be present but can be null
|
1153
|
+
- `email` doesn't have to be present, but if it is, it cannot be null
|
1154
|
+
- `bio` doesn't have to be present, and if it is, it can be null
|
1155
|
+
|
1156
|
+
## Common Gotchas
|
1157
|
+
|
1158
|
+
### Misconception: Nullable Implies Optional
|
1159
|
+
|
1160
|
+
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.
|
1161
|
+
|
1162
|
+
### Misconception: Optional Properties Accept Null
|
1163
|
+
|
1164
|
+
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)`.
|
1165
|
+
|
1166
|
+
## Migration from Earlier Versions
|
1167
|
+
|
1168
|
+
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.
|
1169
|
+
|
1170
|
+
To maintain backward compatibility with your existing code, you can use:
|
1171
|
+
|
1172
|
+
```ruby
|
1173
|
+
EasyTalk.configure do |config|
|
1174
|
+
config.nilable_is_optional = true # Makes T.nilable properties behave as they did before
|
1175
|
+
end
|
1176
|
+
```
|
1177
|
+
|
1178
|
+
We recommend updating your schema definitions to explicitly declare which properties are optional using the `optional: true` constraint, as this makes your intent clearer.
|
1179
|
+
|
1180
|
+
## Best Practices
|
1181
|
+
|
1182
|
+
1. **Be explicit about intent**: Always clarify whether properties should be nullable, optional, or both
|
1183
|
+
2. **Use the helper method**: For properties that are both nullable and optional, use `nullable_optional_property`
|
1184
|
+
3. **Document expectations**: Use comments to clarify validation requirements for complex schemas
|
1185
|
+
4. **Consider validation implications**: Remember that ActiveModel validations operate independently of the schema definition
|
1186
|
+
|
1187
|
+
## JSON Schema Comparison
|
1188
|
+
|
1189
|
+
| EasyTalk Definition | Required | Nullable | JSON Schema Equivalent |
|
1190
|
+
|--------------------|----------|----------|------------------------|
|
1191
|
+
| `property :p, String` | Yes | No | `{ "properties": { "p": { "type": "string" } }, "required": ["p"] }` |
|
1192
|
+
| `property :p, T.nilable(String)` | Yes | Yes | `{ "properties": { "p": { "type": ["string", "null"] } }, "required": ["p"] }` |
|
1193
|
+
| `property :p, String, optional: true` | No | No | `{ "properties": { "p": { "type": "string" } } }` |
|
1194
|
+
| `nullable_optional_property :p, String` | No | Yes | `{ "properties": { "p": { "type": ["string", "null"] } } }` |
|
1195
|
+
|
1196
|
+
## Migration Guide from v1.x to v2.0
|
1197
|
+
|
1198
|
+
### Breaking Changes Summary
|
1199
|
+
|
1200
|
+
1. **Removed Block-Style Sub-Schemas**: Hash-based nested definitions are no longer supported
|
1201
|
+
2. **Enhanced Validation System**: Automatic validation generation is now enabled by default
|
1202
|
+
3. **Improved Model Initialization**: Better support for nested model instantiation
|
1203
|
+
|
1204
|
+
### Migration Steps
|
1205
|
+
|
1206
|
+
#### 1. Replace Hash-based Nested Schemas
|
1207
|
+
|
1208
|
+
```ruby
|
1209
|
+
# OLD (v1.x) - No longer works
|
1210
|
+
class User
|
1211
|
+
include EasyTalk::Model
|
1212
|
+
define_schema do
|
1213
|
+
property :address, Hash do
|
1214
|
+
property :street, String
|
1215
|
+
property :city, String
|
1216
|
+
end
|
1217
|
+
end
|
1218
|
+
end
|
1219
|
+
|
1220
|
+
# NEW (v2.x) - Extract to separate classes
|
1221
|
+
class Address
|
1222
|
+
include EasyTalk::Model
|
1223
|
+
define_schema do
|
1224
|
+
property :street, String
|
1225
|
+
property :city, String
|
1226
|
+
end
|
1227
|
+
end
|
1228
|
+
|
1229
|
+
class User
|
1230
|
+
include EasyTalk::Model
|
1231
|
+
define_schema do
|
1232
|
+
property :address, Address
|
1233
|
+
end
|
1234
|
+
end
|
1235
|
+
```
|
1236
|
+
|
1237
|
+
#### 2. Review Automatic Validations
|
1238
|
+
|
1239
|
+
With `auto_validations: true` (default), you may need to remove redundant manual validations:
|
1240
|
+
|
1241
|
+
```ruby
|
1242
|
+
# OLD (v1.x) - Manual validations required
|
1243
|
+
class User
|
1244
|
+
include EasyTalk::Model
|
1245
|
+
|
1246
|
+
validates :name, presence: true, length: { minimum: 2 }
|
1247
|
+
validates :email, presence: true, format: { with: URI::MailTo::EMAIL_REGEXP }
|
1248
|
+
|
1249
|
+
define_schema do
|
1250
|
+
property :name, String
|
1251
|
+
property :email, String
|
1252
|
+
end
|
1253
|
+
end
|
1254
|
+
|
1255
|
+
# NEW (v2.x) - Automatic validations from constraints
|
1256
|
+
class User
|
1257
|
+
include EasyTalk::Model
|
1258
|
+
|
1259
|
+
# Only add validations not covered by schema constraints
|
1260
|
+
validates :email, uniqueness: true
|
1261
|
+
|
1262
|
+
define_schema do
|
1263
|
+
property :name, String, min_length: 2 # Auto-generates presence + length
|
1264
|
+
property :email, String, format: "email" # Auto-generates presence + format
|
1265
|
+
end
|
1266
|
+
end
|
1267
|
+
```
|
1268
|
+
|
1269
|
+
#### 3. Configuration Updates
|
1270
|
+
|
1271
|
+
Review your configuration for new options:
|
1272
|
+
|
1273
|
+
```ruby
|
1274
|
+
EasyTalk.configure do |config|
|
1275
|
+
# New option in v2.0
|
1276
|
+
config.auto_validations = true # Enable/disable automatic validation generation
|
1277
|
+
|
1278
|
+
# Existing options (unchanged)
|
1279
|
+
config.nilable_is_optional = false
|
1280
|
+
config.exclude_foreign_keys = true
|
1281
|
+
# ... other existing config
|
1282
|
+
end
|
1283
|
+
```
|
1284
|
+
|
1285
|
+
### Compatibility Notes
|
1286
|
+
|
1287
|
+
- **Ruby Version**: Still requires Ruby 3.2+
|
1288
|
+
- **Dependencies**: Core dependencies remain the same
|
1289
|
+
- **JSON Schema Output**: No changes to generated schemas
|
1290
|
+
- **ActiveRecord Integration**: Fully backward compatible
|
862
1291
|
|
863
1292
|
## Development and Contributing
|
864
1293
|
|
@@ -878,6 +1307,13 @@ Run the test suite with:
|
|
878
1307
|
bundle exec rake spec
|
879
1308
|
```
|
880
1309
|
|
1310
|
+
### Code Quality
|
1311
|
+
Run the linter:
|
1312
|
+
|
1313
|
+
```bash
|
1314
|
+
bundle exec rubocop
|
1315
|
+
```
|
1316
|
+
|
881
1317
|
### Contributing Guidelines
|
882
1318
|
Bug reports and pull requests are welcome on GitHub at https://github.com/sergiobayona/easy_talk.
|
883
1319
|
|