easy_talk 1.0.4 → 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 +40 -0
- data/README.md +323 -68
- data/easy_talk.gemspec +38 -0
- data/lib/easy_talk/active_record_schema_builder.rb +5 -1
- data/lib/easy_talk/builders/composition_builder.rb +12 -1
- data/lib/easy_talk/builders/object_builder.rb +9 -23
- data/lib/easy_talk/configuration.rb +4 -3
- data/lib/easy_talk/errors_helper.rb +1 -0
- data/lib/easy_talk/model.rb +75 -14
- data/lib/easy_talk/property.rb +108 -32
- data/lib/easy_talk/schema_definition.rb +13 -18
- data/lib/easy_talk/types/composer.rb +7 -6
- data/lib/easy_talk/validation_builder.rb +339 -0
- data/lib/easy_talk/version.rb +1 -1
- metadata +11 -149
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:
|
225
|
+
|
226
|
+
```ruby
|
227
|
+
class User
|
228
|
+
include EasyTalk::Model
|
229
|
+
|
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:
|
189
253
|
|
190
254
|
```ruby
|
191
255
|
class User
|
192
256
|
include EasyTalk::Model
|
193
257
|
|
194
|
-
|
195
|
-
validates :
|
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,16 @@ 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
|
862
1022
|
|
863
1023
|
# Nullable vs Optional Properties in EasyTalk
|
864
1024
|
|
@@ -983,14 +1143,6 @@ class UserProfile
|
|
983
1143
|
|
984
1144
|
# Optional and nullable (can be omitted, can be null if present)
|
985
1145
|
nullable_optional_property :bio, String
|
986
|
-
|
987
|
-
# Nested object with mixed property types
|
988
|
-
property :address, Hash do
|
989
|
-
property :street, String # Required
|
990
|
-
property :city, String # Required
|
991
|
-
property :state, String, optional: true # Optional
|
992
|
-
nullable_optional_property :zip, String # Optional and nullable
|
993
|
-
end
|
994
1146
|
end
|
995
1147
|
end
|
996
1148
|
```
|
@@ -1041,6 +1193,102 @@ We recommend updating your schema definitions to explicitly declare which proper
|
|
1041
1193
|
| `property :p, String, optional: true` | No | No | `{ "properties": { "p": { "type": "string" } } }` |
|
1042
1194
|
| `nullable_optional_property :p, String` | No | Yes | `{ "properties": { "p": { "type": ["string", "null"] } } }` |
|
1043
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
|
1291
|
+
|
1044
1292
|
## Development and Contributing
|
1045
1293
|
|
1046
1294
|
### Setting Up the Development Environment
|
@@ -1059,6 +1307,13 @@ Run the test suite with:
|
|
1059
1307
|
bundle exec rake spec
|
1060
1308
|
```
|
1061
1309
|
|
1310
|
+
### Code Quality
|
1311
|
+
Run the linter:
|
1312
|
+
|
1313
|
+
```bash
|
1314
|
+
bundle exec rubocop
|
1315
|
+
```
|
1316
|
+
|
1062
1317
|
### Contributing Guidelines
|
1063
1318
|
Bug reports and pull requests are welcome on GitHub at https://github.com/sergiobayona/easy_talk.
|
1064
1319
|
|
data/easy_talk.gemspec
ADDED
@@ -0,0 +1,38 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'lib/easy_talk/version'
|
4
|
+
|
5
|
+
Gem::Specification.new do |spec|
|
6
|
+
spec.name = 'easy_talk'
|
7
|
+
spec.version = EasyTalk::VERSION
|
8
|
+
spec.authors = ['Sergio Bayona']
|
9
|
+
spec.email = ['bayona.sergio@gmail.com']
|
10
|
+
|
11
|
+
spec.summary = 'Generate json-schema from Ruby classes.'
|
12
|
+
spec.description = 'Generate json-schema from plain Ruby classes.'
|
13
|
+
spec.homepage = 'https://github.com/sergiobayona/easy_talk'
|
14
|
+
spec.license = 'MIT'
|
15
|
+
spec.required_ruby_version = '>= 3.2'
|
16
|
+
|
17
|
+
spec.metadata['allowed_push_host'] = 'https://rubygems.org'
|
18
|
+
|
19
|
+
spec.metadata['homepage_uri'] = spec.homepage
|
20
|
+
spec.metadata['changelog_uri'] = 'https://github.com/sergiobayona/easy_talk/blob/main/CHANGELOG.md'
|
21
|
+
|
22
|
+
# Specify which files should be added to the gem when it is released.
|
23
|
+
# The `git ls-files -z` loads the files in the RubyGem that have been added into git.
|
24
|
+
spec.files = Dir.chdir(__dir__) do
|
25
|
+
`git ls-files -z`.split("\x0").reject do |f|
|
26
|
+
(File.expand_path(f) == __FILE__) ||
|
27
|
+
f.start_with?(*%w[bin/ spec/ .git .github Gemfile])
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
spec.require_paths = ['lib']
|
32
|
+
|
33
|
+
spec.add_dependency 'activemodel', '~> 7.0'
|
34
|
+
spec.add_dependency 'activesupport', '~> 7.0'
|
35
|
+
spec.add_dependency 'sorbet-runtime', '~> 0.5'
|
36
|
+
|
37
|
+
spec.metadata['rubygems_mfa_required'] = 'true'
|
38
|
+
end
|
@@ -136,7 +136,11 @@ module EasyTalk
|
|
136
136
|
end
|
137
137
|
|
138
138
|
# Add default value if present and not a proc
|
139
|
-
|
139
|
+
if column.default && !column.default.is_a?(Proc) && column.type == :boolean
|
140
|
+
constraints[:default] = ActiveModel::Type::Boolean.new.cast(column.default)
|
141
|
+
elsif column.default && !column.default.is_a?(Proc)
|
142
|
+
constraints[:default] = column.default
|
143
|
+
end
|
140
144
|
|
141
145
|
# Remove nil values
|
142
146
|
constraints.compact
|
@@ -50,7 +50,18 @@ module EasyTalk
|
|
50
50
|
# @return [Array<Hash>] The array of schemas.
|
51
51
|
def schemas
|
52
52
|
items.map do |type|
|
53
|
-
type.respond_to?(:schema)
|
53
|
+
if type.respond_to?(:schema)
|
54
|
+
type.schema
|
55
|
+
else
|
56
|
+
# Map Float type to 'number' in JSON Schema
|
57
|
+
json_type = case type.to_s
|
58
|
+
when 'Float', 'BigDecimal'
|
59
|
+
'number'
|
60
|
+
else
|
61
|
+
type.to_s.downcase
|
62
|
+
end
|
63
|
+
{ type: json_type }
|
64
|
+
end
|
54
65
|
end
|
55
66
|
end
|
56
67
|
|
@@ -8,7 +8,7 @@ module EasyTalk
|
|
8
8
|
# ObjectBuilder is responsible for turning a SchemaDefinition of an "object" type
|
9
9
|
# into a validated JSON Schema hash. It:
|
10
10
|
#
|
11
|
-
# 1) Recursively processes the schema
|
11
|
+
# 1) Recursively processes the schema's :properties,
|
12
12
|
# 2) Determines which properties are required (unless optional),
|
13
13
|
# 3) Handles sub-schema composition (allOf, anyOf, oneOf, not),
|
14
14
|
# 4) Produces the final object-level schema hash.
|
@@ -55,7 +55,7 @@ module EasyTalk
|
|
55
55
|
|
56
56
|
##
|
57
57
|
# Main aggregator: merges the top-level schema keys (like :properties, :subschemas)
|
58
|
-
# into a single hash that we
|
58
|
+
# into a single hash that we'll feed to BaseBuilder.
|
59
59
|
def build_options_hash
|
60
60
|
# Start with a copy of the raw schema
|
61
61
|
merged = @original_schema.dup
|
@@ -119,9 +119,7 @@ module EasyTalk
|
|
119
119
|
return true if prop_options.dig(:constraints, :optional)
|
120
120
|
|
121
121
|
# Check for nil_optional config to determine if nilable should also mean optional
|
122
|
-
if prop_options[:type].respond_to?(:nilable?) && prop_options[:type].nilable?
|
123
|
-
return EasyTalk.configuration.nilable_is_optional
|
124
|
-
end
|
122
|
+
return EasyTalk.configuration.nilable_is_optional if prop_options[:type].respond_to?(:nilable?) && prop_options[:type].nilable?
|
125
123
|
|
126
124
|
false
|
127
125
|
end
|
@@ -134,24 +132,12 @@ module EasyTalk
|
|
134
132
|
@property_cache ||= {}
|
135
133
|
|
136
134
|
# Memoize so we only build each property once
|
137
|
-
@property_cache[prop_name] ||=
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
# Normal property: e.g. { type: String, constraints: {...} }
|
144
|
-
Property.new(prop_name, prop_options[:type], constraints)
|
145
|
-
end
|
146
|
-
end
|
147
|
-
|
148
|
-
##
|
149
|
-
# Build a child schema by calling another ObjectBuilder on the nested SchemaDefinition.
|
150
|
-
#
|
151
|
-
def nested_schema_builder(prop_options)
|
152
|
-
child_schema_def = prop_options[:properties]
|
153
|
-
# If user used T.nilable(...) with a block, unwrap the nilable
|
154
|
-
ObjectBuilder.new(child_schema_def).build
|
135
|
+
@property_cache[prop_name] ||= begin
|
136
|
+
# Remove optional constraints from the property
|
137
|
+
constraints = prop_options[:constraints].except(:optional)
|
138
|
+
# Normal property: e.g. { type: String, constraints: {...} }
|
139
|
+
Property.new(prop_name, prop_options[:type], constraints)
|
140
|
+
end
|
155
141
|
end
|
156
142
|
|
157
143
|
##
|