easy_talk 1.0.2 → 1.0.4

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.
data/README.md CHANGED
@@ -1,41 +1,85 @@
1
1
  # EasyTalk
2
2
 
3
- EasyTalk is a Ruby library that simplifies defining and generating JSON Schema documents, and validates that JSON data conforms to these schemas.
3
+ ## Introduction
4
+
5
+ ### What is EasyTalk?
6
+ EasyTalk is a Ruby library that simplifies defining and generating JSON Schema. It provides an intuitive interface for Ruby developers to define structured data models that can be used for validation and documentation.
7
+
8
+ ### Key Features
9
+ * **Intuitive Schema Definition**: Use Ruby classes and methods to define JSON Schema documents easily.
10
+ * **Works for plain Ruby classes and ActiveRecord models**: Integrate with existing code or build from scratch.
11
+ * **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
+ * **Schema Composition**: Define EasyTalk models and reference them in other EasyTalk models to create complex schemas.
13
+ * **Validation**: Write validations using ActiveModel's validations.
14
+
15
+ ### Use Cases
16
+ - API request/response validation
17
+ - LLM function definitions
18
+ - Object structure documentation
19
+ - Data validation and transformation
20
+ - Configuration schema definitions
21
+
22
+ ### Inspiration
23
+ Inspired by Python's Pydantic library, EasyTalk brings similar functionality to the Ruby ecosystem, providing a Ruby-friendly approach to JSON Schema operations.
4
24
 
5
- Key Features
6
- * Intuitive Schema Definition: Use Ruby classes and methods to define JSON Schema documents easily.
7
- * 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.
8
- * Schema Composition: Define EasyTalk models and reference them in other EasyTalk models to create complex schemas.
9
- * Validation: Write validations using ActiveModel’s validations.
25
+ ## Installation
10
26
 
11
- Inspiration
12
- Inspired by Python's Pydantic library, EasyTalk brings similar functionality to the Ruby ecosystem, providing a Ruby-friendly approach to JSON Schema operations.
27
+ ### Requirements
28
+ - Ruby 3.2 or higher
29
+ - ActiveModel 7.0 or higher
30
+ - ActiveSupport 7.0 or higher
13
31
 
14
- Example Use:
32
+ ### Installation Steps
33
+ Add EasyTalk to your application's Gemfile:
15
34
 
16
35
  ```ruby
17
- class User
36
+ gem 'easy_talk'
37
+ ```
38
+
39
+ Or install it directly:
40
+
41
+ ```bash
42
+ $ gem install easy_talk
43
+ ```
44
+
45
+ ### Verification
46
+ After installation, you can verify it's working by creating a simple model:
47
+
48
+ ```ruby
49
+ require 'easy_talk'
50
+
51
+ class Test
18
52
  include EasyTalk::Model
53
+
54
+ define_schema do
55
+ property :name, String
56
+ end
57
+ end
58
+
59
+ puts Test.json_schema
60
+ ```
19
61
 
20
- validates :name, :email, :group, presence: true
21
- validates :age, numericality: { greater_than_or_equal_to: 18, less_than_or_equal_to: 100 }
62
+ ## Quick Start
63
+
64
+ ### Minimal Example
65
+ Here's a basic example to get you started with EasyTalk:
66
+
67
+ ```ruby
68
+ class User
69
+ include EasyTalk::Model
22
70
 
23
71
  define_schema do
24
72
  title "User"
25
73
  description "A user of the system"
26
- property :name, String, description: "The user's name", title: "Full Name"
27
- property :email, Hash do
28
- property :address, String, format: "email", description: "The user's email", title: "Email Address"
29
- property :verified, T::Boolean, description: "Whether the email is verified"
30
- end
31
- property :group, Integer, enum: [1, 2, 3], default: 1, description: "The user's group"
32
- property :age, Integer, minimum: 18, maximum: 100, description: "The user's age"
33
- property :tags, T::Array[String], min_items: 1, unique_items: true, description: "The user's tags"
74
+ property :name, String, description: "The user's name"
75
+ property :email, String, format: "email"
76
+ property :age, Integer, minimum: 18
34
77
  end
35
78
  end
36
79
  ```
37
80
 
38
- Calling `User.json_schema` will return the Ruby representation of the JSON Schema for the `User` class:
81
+ ### Generated JSON Schema
82
+ Calling `User.json_schema` will generate:
39
83
 
40
84
  ```ruby
41
85
  {
@@ -44,89 +88,591 @@ Calling `User.json_schema` will return the Ruby representation of the JSON Schem
44
88
  "description" => "A user of the system",
45
89
  "properties" => {
46
90
  "name" => {
47
- "type" => "string", "title" => "Full Name", "description" => "The user's name"
91
+ "type" => "string",
92
+ "description" => "The user's name"
48
93
  },
49
94
  "email" => {
50
- "type" => "object",
51
- "properties" => {
52
- "address" => {
53
- "type" => "string", "title" => "Email Address", "description" => "The user's email", "format" => "email"
54
- },
55
- "verified" => {
56
- "type" => "boolean", "description" => "Whether the email is verified"
57
- }
58
- },
59
- "required" => ["address", "verified"]
60
- },
61
- "group" => {
62
- "type" => "integer", "description" => "The user's group", "enum" => [1, 2, 3], "default" => 1
95
+ "type" => "string",
96
+ "format" => "email"
63
97
  },
64
98
  "age" => {
65
- "type" => "integer", "description" => "The user's age", "minimum" => 18, "maximum" => 100
66
- },
67
- "tags" => {
68
- "type" => "array",
69
- "items" => { "type" => "string" },
70
- "description" => "The user's tags",
71
- "minItems" => 1,
72
- "uniqueItems" => true
99
+ "type" => "integer",
100
+ "minimum" => 18
73
101
  }
74
102
  },
75
- "required" => ["name", "email", "group", "age", "tags"]
103
+ "required" => ["name", "email", "age"]
76
104
  }
77
105
  ```
78
106
 
79
- Instantiate a User object and validate it with ActiveModel validations:
107
+ ### Basic Usage
108
+ Creating and validating an instance of your model:
80
109
 
81
110
  ```ruby
82
- user = User.new(name: "John Doe", email: { address: "john@test.com", verified: true }, group: 1, age: 25, tags: ["tag1", "tag2"])
111
+ user = User.new(name: "John Doe", email: "john@example.com", age: 25)
83
112
  user.valid? # => true
84
113
 
85
- user.name = nil
114
+ user.age = 17
86
115
  user.valid? # => false
116
+ ```
117
+
118
+ ## Core Concepts
119
+
120
+ ### Schema Definition
121
+ In EasyTalk, you define your schema by including the `EasyTalk::Model` module and using the `define_schema` method. This method takes a block where you can define the properties and constraints of your schema.
87
122
 
88
- user.errors.full_messages # => ["Name can't be blank"]
89
- user.errors["name"] # => ["can't be blank"]
123
+ ```ruby
124
+ class MyModel
125
+ include EasyTalk::Model
126
+
127
+ define_schema do
128
+ title "My Model"
129
+ description "Description of my model"
130
+ property :some_property, String
131
+ property :another_property, Integer
132
+ end
133
+ end
90
134
  ```
91
135
 
92
- ## Installation
136
+ ### Property Types
137
+
138
+ #### Ruby Types
139
+ EasyTalk supports standard Ruby types directly:
140
+
141
+ - `String`: String values
142
+ - `Integer`: Integer values
143
+ - `Float`: Floating-point numbers
144
+ - `Date`: Date values
145
+ - `DateTime`: Date and time values
146
+ - `Hash`: Object/dictionary values
147
+
148
+ #### Sorbet-Style Types
149
+ For complex types, EasyTalk uses Sorbet-style type notation:
150
+
151
+ - `T::Boolean`: Boolean values (true/false)
152
+ - `T::Array[Type]`: Arrays with items of a specific type
153
+ - `T.nilable(Type)`: Type that can also be nil
154
+
155
+ #### Custom Types
156
+ EasyTalk supports special composition types:
157
+
158
+ - `T::AnyOf[Type1, Type2, ...]`: Value can match any of the specified schemas
159
+ - `T::OneOf[Type1, Type2, ...]`: Value must match exactly one of the specified schemas
160
+ - `T::AllOf[Type1, Type2, ...]`: Value must match all of the specified schemas
161
+
162
+ ### Property Constraints
163
+ Property constraints depend on the type of property. Some common constraints include:
164
+
165
+ - `description`: A description of the property
166
+ - `title`: A title for the property
167
+ - `format`: A format hint for the property (e.g., "email", "date")
168
+ - `enum`: A list of allowed values
169
+ - `minimum`/`maximum`: Minimum/maximum values for numbers
170
+ - `min_length`/`max_length`: Minimum/maximum length for strings
171
+ - `pattern`: A regular expression pattern for strings
172
+ - `min_items`/`max_items`: Minimum/maximum number of items for arrays
173
+ - `unique_items`: Whether array items must be unique
174
+
175
+ ### Required vs Optional Properties
176
+ By default, all properties defined in an EasyTalk model are required. You can make a property optional by specifying `optional: true`:
177
+
178
+ ```ruby
179
+ define_schema do
180
+ property :name, String
181
+ property :middle_name, String, optional: true
182
+ end
183
+ ```
93
184
 
94
- install the gem by running the following command in your terminal:
185
+ In this example, `name` is required but `middle_name` is optional.
95
186
 
96
- $ gem install easy_talk
187
+ ### Schema Validation
188
+ EasyTalk models include ActiveModel validations. You can validate your models using the standard ActiveModel validation methods:
97
189
 
98
- ## Usage
190
+ ```ruby
191
+ class User
192
+ include EasyTalk::Model
193
+
194
+ validates :name, presence: true
195
+ validates :age, numericality: { greater_than_or_equal_to: 18 }
196
+
197
+ define_schema do
198
+ property :name, String
199
+ property :age, Integer, minimum: 18
200
+ end
201
+ end
99
202
 
100
- Simply include the `EasyTalk::Model` module in your Ruby class, define the schema using the `define_schema` block, and call the `json_schema` class method to generate the JSON Schema document.
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
+ ```
101
207
 
208
+ ## Defining Schemas
102
209
 
103
- ## Schema Definition
210
+ ### Basic Schema Structure
211
+ A schema definition consists of a class that includes `EasyTalk::Model` and a `define_schema` block:
104
212
 
105
- In the example above, the define_schema method adds a title and description to the schema. The property method defines properties of the schema document. property accepts:
213
+ ```ruby
214
+ class Person
215
+ include EasyTalk::Model
106
216
 
107
- * A name (symbol)
108
- * A type (generic Ruby type like String/Integer, a Sorbet type like T::Boolean, or one of the custom types like T::AnyOf[...])
109
- * A hash of constraints (e.g., minimum: 18, enum: [1, 2, 3], etc.)
217
+ define_schema do
218
+ title "Person"
219
+ property :name, String
220
+ property :age, Integer
221
+ end
222
+ end
223
+ ```
110
224
 
111
- ## Why Sortbet-style types?
225
+ ### Property Definitions
226
+ Properties are defined using the `property` method, which takes a name, a type, and optional constraints:
112
227
 
113
- Ruby doesn’t natively allow complex types like Array[String] or Array[Integer]. Sorbet-style types let you define these compound types clearly. EasyTalk uses this style to handle property types such as T::Array[String] or T::AnyOf[ClassA, ClassB].
228
+ ```ruby
229
+ property :name, String, description: "The person's name", title: "Full Name"
230
+ property :age, Integer, minimum: 0, maximum: 120, description: "The person's age"
231
+ ```
114
232
 
115
- ## Property Constraints
233
+ ### Nested Objects
234
+ You can define nested objects using a block:
116
235
 
117
- Property constraints are type-dependent. Refer to the [CONSTRAINTS.md](CONSTRAINTS.md) file for a list of constraints supported by the JSON Schema generator.
236
+ ```ruby
237
+ property :email, Hash do
238
+ property :address, String, format: "email"
239
+ property :verified, T::Boolean
240
+ end
241
+ ```
118
242
 
243
+ ### Arrays and Collections
244
+ Arrays can be defined using the `T::Array` type:
245
+
246
+ ```ruby
247
+ property :tags, T::Array[String], min_items: 1, unique_items: true
248
+ property :scores, T::Array[Integer], description: "List of scores"
249
+ ```
250
+
251
+ You can also define arrays of complex types:
252
+
253
+ ```ruby
254
+ property :addresses, T::Array[Address], description: "List of addresses"
255
+ ```
256
+
257
+ ### Constraints and Validations
258
+ Constraints can be added to properties and are used for schema generation:
259
+
260
+ ```ruby
261
+ property :name, String, min_length: 2, max_length: 50
262
+ property :email, String, format: "email"
263
+ property :category, String, enum: ["A", "B", "C"], default: "A"
264
+ ```
265
+
266
+ For validation, you can use ActiveModel validations:
267
+
268
+ ```ruby
269
+ validates :name, presence: true, length: { minimum: 2, maximum: 50 }
270
+ validates :email, format: { with: URI::MailTo::EMAIL_REGEXP }
271
+ validates :category, inclusion: { in: ["A", "B", "C"] }
272
+ ```
273
+
274
+ ### Additional Properties
275
+ 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:
276
+
277
+ ```ruby
278
+ define_schema do
279
+ property :name, String
280
+ additional_properties true
281
+ end
282
+ ```
283
+
284
+ With `additional_properties true`, you can add arbitrary properties to your model instances:
285
+
286
+ ```ruby
287
+ company = Company.new
288
+ company.name = "Acme Corp" # Defined property
289
+ company.location = "New York" # Additional property
290
+ company.employee_count = 100 # Additional property
291
+ ```
119
292
 
120
293
  ## Schema Composition
121
294
 
122
- EasyTalk supports schema composition. You can define a schema for a nested object by defining a new class that includes `EasyTalk::Model`. You can then reference the nested schema in the parent using special types:
295
+ ### Using T::AnyOf
296
+ The `T::AnyOf` type allows a property to match any of the specified schemas:
297
+
298
+ ```ruby
299
+ class Payment
300
+ include EasyTalk::Model
301
+
302
+ define_schema do
303
+ property :details, T::AnyOf[CreditCard, Paypal, BankTransfer]
304
+ end
305
+ end
306
+ ```
307
+
308
+ ### Using T::OneOf
309
+ The `T::OneOf` type requires a property to match exactly one of the specified schemas:
310
+
311
+ ```ruby
312
+ class Contact
313
+ include EasyTalk::Model
314
+
315
+ define_schema do
316
+ property :contact, T::OneOf[PhoneContact, EmailContact]
317
+ end
318
+ end
319
+ ```
320
+
321
+ ### Using T::AllOf
322
+ The `T::AllOf` type requires a property to match all of the specified schemas:
323
+
324
+ ```ruby
325
+ class VehicleRegistration
326
+ include EasyTalk::Model
327
+
328
+ define_schema do
329
+ compose T::AllOf[VehicleIdentification, OwnerInfo, RegistrationDetails]
330
+ end
331
+ end
332
+ ```
333
+
334
+ ### Complex Compositions
335
+ You can combine composition types to create complex schemas:
336
+
337
+ ```ruby
338
+ class ComplexObject
339
+ include EasyTalk::Model
340
+
341
+ define_schema do
342
+ property :basic_info, BaseInfo
343
+ property :specific_details, T::OneOf[DetailTypeA, DetailTypeB]
344
+ property :metadata, T::AnyOf[AdminMetadata, UserMetadata, nil]
345
+ end
346
+ end
347
+ ```
348
+
349
+ ### Reusing Models
350
+ Models can reference other models to create hierarchical schemas:
351
+
352
+ ```ruby
353
+ class Address
354
+ include EasyTalk::Model
355
+
356
+ define_schema do
357
+ property :street, String
358
+ property :city, String
359
+ property :state, String
360
+ property :zip, String
361
+ end
362
+ end
363
+
364
+ class User
365
+ include EasyTalk::Model
366
+
367
+ define_schema do
368
+ property :name, String
369
+ property :address, Address
370
+ end
371
+ end
372
+ ```
373
+
374
+ ## ActiveModel Integration
375
+
376
+ ### Validations
377
+ EasyTalk models include ActiveModel validations:
378
+
379
+ ```ruby
380
+ class User
381
+ include EasyTalk::Model
382
+
383
+ validates :age, comparison: { greater_than: 21 }
384
+ validates :height, presence: true, numericality: { greater_than: 0 }
385
+
386
+ define_schema do
387
+ property :name, String
388
+ property :age, Integer
389
+ property :height, Float
390
+ end
391
+ end
392
+ ```
393
+
394
+ ### Error Handling
395
+ You can access validation errors using the standard ActiveModel methods:
396
+
397
+ ```ruby
398
+ user = User.new(name: "Jim", age: 18, height: -5.9)
399
+ user.valid? # => false
400
+ user.errors[:age] # => ["must be greater than 21"]
401
+ user.errors[:height] # => ["must be greater than 0"]
402
+ ```
403
+
404
+ ### Model Attributes
405
+ EasyTalk models provide getters and setters for all defined properties:
406
+
407
+ ```ruby
408
+ user = User.new
409
+ user.name = "John"
410
+ user.age = 30
411
+ puts user.name # => "John"
412
+ ```
413
+
414
+ You can also initialize a model with a hash of attributes:
415
+
416
+ ```ruby
417
+ user = User.new(name: "John", age: 30, height: 5.9)
418
+ ```
419
+
420
+ ## ActiveRecord Integration
421
+
422
+ ### Automatic Schema Generation
423
+ For ActiveRecord models, EasyTalk automatically generates a schema based on the database columns:
424
+
425
+ ```ruby
426
+ class Product < ActiveRecord::Base
427
+ include EasyTalk::Model
428
+ end
429
+ ```
430
+
431
+ This will create a schema with properties for each column in the `products` table.
432
+
433
+ ### Enhancing Generated Schemas
434
+ You can enhance the auto-generated schema with the `enhance_schema` method:
435
+
436
+ ```ruby
437
+ class Product < ActiveRecord::Base
438
+ include EasyTalk::Model
439
+
440
+ enhance_schema({
441
+ title: "Retail Product",
442
+ description: "A product available for purchase",
443
+ properties: {
444
+ name: {
445
+ description: "Product display name",
446
+ title: "Product Name"
447
+ },
448
+ price: {
449
+ description: "Retail price in USD"
450
+ }
451
+ }
452
+ })
453
+ end
454
+ ```
455
+
456
+ ### Column Exclusion Options
457
+ EasyTalk provides several ways to exclude columns from your JSON schema:
458
+
459
+ #### 1. Global Configuration
460
+
461
+ ```ruby
462
+ EasyTalk.configure do |config|
463
+ # Exclude specific columns by name from all models
464
+ config.excluded_columns = [:created_at, :updated_at, :deleted_at]
465
+
466
+ # Exclude all foreign key columns (columns ending with '_id')
467
+ config.exclude_foreign_keys = true # Default: false
468
+
469
+ # Exclude all primary key columns ('id')
470
+ config.exclude_primary_key = true # Default: true
471
+
472
+ # Exclude timestamp columns ('created_at', 'updated_at')
473
+ config.exclude_timestamps = true # Default: true
474
+
475
+ # Exclude all association properties
476
+ config.exclude_associations = true # Default: false
477
+ end
478
+ ```
479
+
480
+ #### 2. Model-Specific Column Ignoring
481
+
482
+ ```ruby
483
+ class Product < ActiveRecord::Base
484
+ include EasyTalk::Model
485
+
486
+ enhance_schema({
487
+ ignore: [:internal_ref_id, :legacy_code] # Model-specific exclusions
488
+ })
489
+ end
490
+ ```
491
+
492
+ ### Virtual Properties
493
+ You can add properties that don't exist as database columns:
494
+
495
+ ```ruby
496
+ class Product < ActiveRecord::Base
497
+ include EasyTalk::Model
498
+
499
+ enhance_schema({
500
+ properties: {
501
+ full_details: {
502
+ virtual: true,
503
+ type: :string,
504
+ description: "Complete product information"
505
+ }
506
+ }
507
+ })
508
+ end
509
+ ```
510
+
511
+ ### Associations and Foreign Keys
512
+ By default, EasyTalk includes your model's associations in the schema:
513
+
514
+ ```ruby
515
+ class Product < ActiveRecord::Base
516
+ include EasyTalk::Model
517
+ belongs_to :category
518
+ has_many :reviews
519
+ end
520
+ ```
521
+
522
+ This will include `category` (as an object) and `reviews` (as an array) in the schema.
523
+
524
+ You can control this behavior with configuration:
525
+
526
+ ```ruby
527
+ EasyTalk.configure do |config|
528
+ config.exclude_associations = true # Don't include associations
529
+ config.exclude_foreign_keys = true # Don't include foreign key columns
530
+ end
531
+ ```
532
+
533
+ ## Advanced Features
534
+
535
+ ### LLM Function Generation
536
+ EasyTalk provides a helper method for generating OpenAI function specifications:
537
+
538
+ ```ruby
539
+ class Weather
540
+ include EasyTalk::Model
541
+
542
+ define_schema do
543
+ title "GetWeather"
544
+ description "Get the current weather in a given location"
545
+ property :location, String, description: "The city and state, e.g. San Francisco, CA"
546
+ property :unit, String, enum: ["celsius", "fahrenheit"], default: "fahrenheit"
547
+ end
548
+ end
549
+
550
+ function_spec = EasyTalk::Tools::FunctionBuilder.new(Weather)
551
+ ```
552
+
553
+ This generates a function specification compatible with OpenAI's function calling API.
554
+
555
+ ### Schema Transformation
556
+ You can transform EasyTalk schemas into various formats:
557
+
558
+ ```ruby
559
+ # Get Ruby hash representation
560
+ schema_hash = User.schema
561
+
562
+ # Get JSON Schema representation
563
+ json_schema = User.json_schema
564
+
565
+ # Convert to JSON string
566
+ json_string = User.json_schema.to_json
567
+ ```
568
+
569
+ ### Type Checking and Validation
570
+ EasyTalk performs basic type checking during schema definition:
571
+
572
+ ```ruby
573
+ # This will raise an error because "minimum" should be used with numeric types
574
+ property :name, String, minimum: 1 # Error!
575
+
576
+ # This will raise an error because enum values must match the property type
577
+ property :age, Integer, enum: ["young", "old"] # Error!
578
+ ```
579
+
580
+ ### Custom Type Builders
581
+ For advanced use cases, you can create custom type builders:
582
+
583
+ ```ruby
584
+ module EasyTalk
585
+ module Builders
586
+ class MyCustomTypeBuilder < BaseBuilder
587
+ # Custom implementation
588
+ end
589
+ end
590
+ end
591
+ ```
592
+
593
+ ## Configuration
594
+
595
+ ### Global Settings
596
+ You can configure EasyTalk globally:
123
597
 
124
- T::OneOf[Model1, Model2, ...] — The property must match at least one of the specified schemas
125
- T::AnyOf[Model1, Model2, ...] — The property can match any of the specified schemas
126
- T::AllOf[Model1, Model2, ...] — The property must match all of the specified schemas
598
+ ```ruby
599
+ EasyTalk.configure do |config|
600
+ config.excluded_columns = [:created_at, :updated_at, :deleted_at]
601
+ config.exclude_foreign_keys = true
602
+ config.exclude_primary_key = true
603
+ config.exclude_timestamps = true
604
+ config.exclude_associations = false
605
+ config.default_additional_properties = false
606
+ end
607
+ ```
608
+
609
+ ### Per-Model Configuration
610
+ Some settings can be configured per model:
611
+
612
+ ```ruby
613
+ class Product < ActiveRecord::Base
614
+ include EasyTalk::Model
615
+
616
+ enhance_schema({
617
+ additionalProperties: true,
618
+ ignore: [:internal_ref_id, :legacy_code]
619
+ })
620
+ end
621
+ ```
622
+
623
+ ### Exclusion Rules
624
+ Columns are excluded based on the following rules (in order of precedence):
625
+
626
+ 1. Explicitly listed in `excluded_columns` global setting
627
+ 2. Listed in the model's `schema_enhancements[:ignore]` array
628
+ 3. Is a primary key when `exclude_primary_key` is true (default)
629
+ 4. Is a timestamp column when `exclude_timestamps` is true (default)
630
+ 5. Matches a foreign key pattern when `exclude_foreign_keys` is true
631
+
632
+ ### Customizing Output
633
+ You can customize the JSON Schema output by enhancing the schema:
634
+
635
+ ```ruby
636
+ class User < ActiveRecord::Base
637
+ include EasyTalk::Model
638
+
639
+ enhance_schema({
640
+ title: "User Account",
641
+ description: "User account information",
642
+ properties: {
643
+ name: {
644
+ title: "Full Name",
645
+ description: "User's full name"
646
+ }
647
+ }
648
+ })
649
+ end
650
+ ```
127
651
 
128
- Example: A Payment object that can be a credit card, PayPal, or bank transfer:
652
+ ## Examples
129
653
 
654
+ ### User Registration
655
+
656
+ ```ruby
657
+ class User
658
+ include EasyTalk::Model
659
+
660
+ validates :name, :email, :password, presence: true
661
+ validates :password, length: { minimum: 8 }
662
+ validates :email, format: { with: URI::MailTo::EMAIL_REGEXP }
663
+
664
+ define_schema do
665
+ title "User Registration"
666
+ description "User registration information"
667
+ property :name, String, description: "User's full name"
668
+ 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, default: true, description: "Whether to send notifications"
671
+ end
672
+ end
673
+ ```
674
+
675
+ ### Payment Processing
130
676
 
131
677
  ```ruby
132
678
  class CreditCard
@@ -176,131 +722,328 @@ class Payment
176
722
  end
177
723
  ```
178
724
 
179
- ## Additional Properties
725
+ ### Complex Object Hierarchies
726
+
727
+ ```ruby
728
+ class Address
729
+ include EasyTalk::Model
180
730
 
181
- EasyTalk supports the JSON Schema `additionalProperties` keyword, allowing you to control whether instances of your model can accept properties beyond those explicitly defined in the schema.
731
+ define_schema do
732
+ property :street, String
733
+ property :city, String
734
+ property :state, String
735
+ property :zip, String, pattern: '^[0-9]{5}(?:-[0-9]{4})?$'
736
+ end
737
+ end
182
738
 
183
- ### Usage
739
+ class Employee
740
+ include EasyTalk::Model
184
741
 
185
- Use the `additional_properties` keyword in your schema definition to specify whether additional properties are allowed:
742
+ define_schema do
743
+ title 'Employee'
744
+ description 'Company employee'
745
+ property :name, String, title: 'Full Name'
746
+ property :gender, String, enum: %w[male female other]
747
+ property :department, T.nilable(String)
748
+ property :hire_date, Date
749
+ property :active, T::Boolean, default: true
750
+ property :addresses, T.nilable(T::Array[Address])
751
+ end
752
+ end
186
753
 
187
- ```ruby
188
754
  class Company
189
755
  include EasyTalk::Model
190
756
 
191
757
  define_schema do
758
+ title 'Company'
192
759
  property :name, String
193
- additional_properties true # Allow additional properties
760
+ property :employees, T::Array[Employee], title: 'Company Employees', description: 'A list of company employees'
194
761
  end
195
762
  end
763
+ ```
196
764
 
197
- # Additional properties are allowed
198
- company = Company.new
199
- company.name = "Acme Corp" # Defined property
200
- company.location = "New York" # Additional property
201
- company.employee_count = 100 # Additional property
765
+ ### API Integration
202
766
 
203
- company.as_json
204
- # => {
205
- # "name" => "Acme Corp",
206
- # "location" => "New York",
207
- # "employee_count" => 100
208
- # }
767
+ ```ruby
768
+ # app/controllers/api/users_controller.rb
769
+ class Api::UsersController < ApplicationController
770
+ def create
771
+ schema = User.json_schema
772
+
773
+ # Validate incoming request against the schema
774
+ validation_result = JSONSchemer.schema(schema).valid?(params.to_json)
775
+
776
+ if validation_result
777
+ user = User.new(user_params)
778
+ if user.save
779
+ render json: user, status: :created
780
+ else
781
+ render json: { errors: user.errors }, status: :unprocessable_entity
782
+ end
783
+ else
784
+ render json: { errors: "Invalid request" }, status: :bad_request
785
+ end
786
+ end
787
+
788
+ private
789
+
790
+ def user_params
791
+ params.require(:user).permit(:name, :email, :password)
792
+ end
793
+ end
209
794
  ```
210
795
 
211
- ### Behavior
796
+ ## Troubleshooting
212
797
 
213
- When `additional_properties true`:
214
- - Instances can accept properties beyond those defined in the schema
215
- - Additional properties can be set both via the constructor and direct assignment
216
- - Additional properties are included in JSON serialization
217
- - Attempting to access an undefined additional property raises NoMethodError
798
+ ### Common Errors
218
799
 
219
- ```ruby
220
- # Setting via constructor
221
- company = Company.new(
222
- name: "Acme Corp",
223
- location: "New York" # Additional property
224
- )
800
+ #### "Invalid property name"
801
+ Property names must start with a letter or underscore and can only contain letters, numbers, and underscores:
225
802
 
226
- # Setting via assignment
227
- company.rank = 1 # Additional property
803
+ ```ruby
804
+ # Invalid
805
+ property "1name", String # Starts with a number
806
+ property "name!", String # Contains a special character
228
807
 
229
- # Accessing undefined properties
230
- company.undefined_prop # Raises NoMethodError
808
+ # Valid
809
+ property :name, String
810
+ property :user_name, String
231
811
  ```
232
812
 
233
- When `additional_properties false` or not specified:
234
- - Only properties defined in the schema are allowed
235
- - Attempting to set or get undefined properties raises NoMethodError
813
+ #### "Property type is missing"
814
+ You must specify a type for each property:
236
815
 
237
816
  ```ruby
238
- class RestrictedCompany
239
- include EasyTalk::Model
817
+ # Invalid
818
+ property :name
240
819
 
241
- define_schema do
242
- property :name, String
243
- additional_properties false # Restrict to defined properties only
244
- end
245
- end
820
+ # Valid
821
+ property :name, String
822
+ ```
823
+
824
+ #### "Unknown option"
825
+ You specified an option that is not valid for the property type:
826
+
827
+ ```ruby
828
+ # Invalid (min_length is for strings, not integers)
829
+ property :age, Integer, min_length: 2
246
830
 
247
- company = RestrictedCompany.new
248
- company.name = "Acme Corp" # OK - defined property
249
- company.location = "New York" # Raises NoMethodError
831
+ # Valid
832
+ property :age, Integer, minimum: 18
250
833
  ```
251
834
 
252
- ### JSON Schema
835
+ ### Schema Validation Issues
836
+ If you're having issues with validation:
253
837
 
254
- The `additional_properties` setting is reflected in the generated JSON Schema:
838
+ 1. Make sure you've defined ActiveModel validations for your model
839
+ 2. Check for mismatches between schema constraints and validations
840
+ 3. Verify that required properties are present
841
+
842
+ ### Type Errors
843
+ Type errors usually occur when there's a mismatch between a property type and its constraints:
255
844
 
256
845
  ```ruby
257
- Company.json_schema
258
- # => {
259
- # "type" => "object",
260
- # "properties" => {
261
- # "name" => { "type" => "string" }
262
- # },
263
- # "required" => ["name"],
264
- # "additionalProperties" => true
265
- # }
846
+ # Error: enum values must be strings for a string property
847
+ property :status, String, enum: [1, 2, 3]
848
+
849
+ # Correct
850
+ property :status, String, enum: ["active", "inactive", "pending"]
266
851
  ```
267
852
 
268
853
  ### Best Practices
269
854
 
270
- 1. **Default to Restrictive**: Unless you specifically need additional properties, it's recommended to leave `additional_properties` as false (the default) to maintain schema integrity.
855
+ 1. Define clear property names and descriptions
856
+ 2. Use appropriate types for each property
857
+ 3. Add validations for important business rules
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
862
+
863
+ # Nullable vs Optional Properties in EasyTalk
271
864
 
272
- 2. **Documentation**: If you enable additional properties, document the expected additional property types and their purpose.
865
+ 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.
273
866
 
274
- 3. **Validation**: Consider implementing custom validation for additional properties if they need to conform to specific patterns or types.
867
+ ## Key Concepts
275
868
 
276
- 4. **Error Handling**: When working with instances that allow additional properties, use `respond_to?` or `try` to handle potentially undefined properties safely:
869
+ | Concept | Description | JSON Schema Effect | EasyTalk Syntax |
870
+ |---------|-------------|-------------------|-----------------|
871
+ | **Nullable** | Property can have a `null` value | Adds `"null"` to the type array | `T.nilable(Type)` |
872
+ | **Optional** | Property doesn't have to exist | Omits property from `"required"` array | `optional: true` constraint |
873
+
874
+ ## Nullable Properties
875
+
876
+ A **nullable** property can contain a `null` value, but the property itself must still be present in the object:
277
877
 
278
878
  ```ruby
279
- # Safe property access
280
- value = company.try(:optional_property)
281
- # or
282
- value = company.optional_property if company.respond_to?(:optional_property)
879
+ property :age, T.nilable(Integer)
283
880
  ```
284
881
 
285
- ## Type Checking and Schema Constraints
882
+ This produces the following JSON Schema:
286
883
 
287
- EasyTalk uses a combination of standard Ruby types (`String`, `Integer`), Sorbet types (`T::Boolean`, `T::Array[String]`, etc.), and custom Sorbet-style types (`T::AnyOf[]`, `T::OneOf[]`) to perform basic type checking. For example:
884
+ ```json
885
+ {
886
+ "properties": {
887
+ "age": { "type": ["integer", "null"] }
888
+ },
889
+ "required": ["age"]
890
+ }
891
+ ```
288
892
 
289
- If you specify `enum: [1,2,3]` but the property type is `String`, EasyTalk raises a type error.
290
- If you define `minimum: 1` on a `String` property, it raises an error because minimum applies only to numeric types.
893
+ In this case, the following data would be valid:
894
+ - `{ "age": 25 }`
895
+ - `{ "age": null }`
291
896
 
292
- ## Schema Validation
897
+ But this would be invalid:
898
+ - `{ }` (missing the age property entirely)
293
899
 
294
- You can instantiate an EasyTalk model with a hash of attributes and validate it using standard ActiveModel validations. EasyTalk does not automatically validate instances; you must explicitly define ActiveModel validations in your EasyTalk model. See [spec/easy_talk/activemodel_integration_spec.rb](ActiveModel Integration Spec) for examples.
900
+ ## Optional Properties
295
901
 
296
- ## JSON Schema Specifications
902
+ An **optional** property doesn't have to be present in the object at all:
297
903
 
298
- EasyTalk is currently loose about JSON Schema versions. It doesn’t strictly enforce or adhere to any particular version of the specification. The goal is to add more robust support for the latest JSON Schema specs in the future.
904
+ ```ruby
905
+ property :nickname, String, optional: true
906
+ ```
299
907
 
300
- To learn about current capabilities, see the [spec/easy_talk/examples](https://github.com/sergiobayona/easy_talk/tree/main/spec/easy_talk/examples) folder. The examples illustrate how EasyTalk generates JSON Schema in different scenarios.
908
+ This produces:
909
+
910
+ ```json
911
+ {
912
+ "properties": {
913
+ "nickname": { "type": "string" }
914
+ }
915
+ // Note: "nickname" is not in the "required" array
916
+ }
917
+ ```
918
+
919
+ In this case, the following data would be valid:
920
+ - `{ "nickname": "Joe" }`
921
+ - `{ }` (omitting nickname entirely)
922
+
923
+ But this would be invalid:
924
+ - `{ "nickname": null }` (null is not allowed because the property isn't nullable)
301
925
 
302
- ## Development
926
+ ## Nullable AND Optional Properties
303
927
 
928
+ For properties that should be both nullable and optional (can be omitted or null), you need to combine both approaches:
929
+
930
+ ```ruby
931
+ property :bio, T.nilable(String), optional: true
932
+ ```
933
+
934
+ This produces:
935
+
936
+ ```json
937
+ {
938
+ "properties": {
939
+ "bio": { "type": ["string", "null"] }
940
+ }
941
+ // Note: "bio" is not in the "required" array
942
+ }
943
+ ```
944
+
945
+ For convenience, EasyTalk also provides a helper method:
946
+
947
+ ```ruby
948
+ nullable_optional_property :bio, String
949
+ ```
950
+
951
+ Which is equivalent to the above.
952
+
953
+ ## Configuration Options
954
+
955
+ By default, nullable properties are still required. You can change this global behavior:
956
+
957
+ ```ruby
958
+ EasyTalk.configure do |config|
959
+ config.nilable_is_optional = true # Makes all T.nilable properties also optional
960
+ end
961
+ ```
962
+
963
+ With this configuration, any property defined with `T.nilable(Type)` will be treated as both nullable and optional.
964
+
965
+ ## Practical Examples
966
+
967
+ ### User Profile Schema
968
+
969
+ ```ruby
970
+ class UserProfile
971
+ include EasyTalk::Model
972
+
973
+ define_schema do
974
+ # Required properties (must exist, cannot be null)
975
+ property :id, String
976
+ property :name, String
977
+
978
+ # Required but nullable (must exist, can be null)
979
+ property :age, T.nilable(Integer)
980
+
981
+ # Optional but not nullable (can be omitted, cannot be null if present)
982
+ property :email, String, optional: true
983
+
984
+ # Optional and nullable (can be omitted, can be null if present)
985
+ 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
+ end
995
+ end
996
+ ```
997
+
998
+ This creates clear expectations for data validation:
999
+ - `id` and `name` must be present and cannot be null
1000
+ - `age` must be present but can be null
1001
+ - `email` doesn't have to be present, but if it is, it cannot be null
1002
+ - `bio` doesn't have to be present, and if it is, it can be null
1003
+
1004
+ ## Common Gotchas
1005
+
1006
+ ### Misconception: Nullable Implies Optional
1007
+
1008
+ 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.
1009
+
1010
+ ### Misconception: Optional Properties Accept Null
1011
+
1012
+ 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)`.
1013
+
1014
+ ## Migration from Earlier Versions
1015
+
1016
+ 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.
1017
+
1018
+ To maintain backward compatibility with your existing code, you can use:
1019
+
1020
+ ```ruby
1021
+ EasyTalk.configure do |config|
1022
+ config.nilable_is_optional = true # Makes T.nilable properties behave as they did before
1023
+ end
1024
+ ```
1025
+
1026
+ We recommend updating your schema definitions to explicitly declare which properties are optional using the `optional: true` constraint, as this makes your intent clearer.
1027
+
1028
+ ## Best Practices
1029
+
1030
+ 1. **Be explicit about intent**: Always clarify whether properties should be nullable, optional, or both
1031
+ 2. **Use the helper method**: For properties that are both nullable and optional, use `nullable_optional_property`
1032
+ 3. **Document expectations**: Use comments to clarify validation requirements for complex schemas
1033
+ 4. **Consider validation implications**: Remember that ActiveModel validations operate independently of the schema definition
1034
+
1035
+ ## JSON Schema Comparison
1036
+
1037
+ | EasyTalk Definition | Required | Nullable | JSON Schema Equivalent |
1038
+ |--------------------|----------|----------|------------------------|
1039
+ | `property :p, String` | Yes | No | `{ "properties": { "p": { "type": "string" } }, "required": ["p"] }` |
1040
+ | `property :p, T.nilable(String)` | Yes | Yes | `{ "properties": { "p": { "type": ["string", "null"] } }, "required": ["p"] }` |
1041
+ | `property :p, String, optional: true` | No | No | `{ "properties": { "p": { "type": "string" } } }` |
1042
+ | `nullable_optional_property :p, String` | No | Yes | `{ "properties": { "p": { "type": ["string", "null"] } } }` |
1043
+
1044
+ ## Development and Contributing
1045
+
1046
+ ### Setting Up the Development Environment
304
1047
  After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that lets you experiment.
305
1048
 
306
1049
  To install this gem onto your local machine, run:
@@ -309,11 +1052,29 @@ To install this gem onto your local machine, run:
309
1052
  bundle exec rake install
310
1053
  ```
311
1054
 
312
- ## Contributing
1055
+ ### Running Tests
1056
+ Run the test suite with:
1057
+
1058
+ ```bash
1059
+ bundle exec rake spec
1060
+ ```
1061
+
1062
+ ### Contributing Guidelines
1063
+ Bug reports and pull requests are welcome on GitHub at https://github.com/sergiobayona/easy_talk.
1064
+
1065
+ ## JSON Schema Compatibility
1066
+
1067
+ ### Supported Versions
1068
+ EasyTalk is currently loose about JSON Schema versions. It doesn't strictly enforce or adhere to any particular version of the specification. The goal is to add more robust support for the latest JSON Schema specs in the future.
313
1069
 
314
- Bug reports and pull requests are welcome on GitHub at https://github.com/sergiobayona/easy_talk.
1070
+ ### Specification Compliance
1071
+ To learn about current capabilities, see the [spec/easy_talk/examples](https://github.com/sergiobayona/easy_talk/tree/main/spec/easy_talk/examples) folder. The examples illustrate how EasyTalk generates JSON Schema in different scenarios.
1072
+
1073
+ ### Known Limitations
1074
+ - Limited support for custom formats
1075
+ - No direct support for JSON Schema draft 2020-12 features
1076
+ - Complex composition scenarios may require manual adjustment
315
1077
 
316
1078
  ## License
317
1079
 
318
1080
  The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
319
-