easy_talk_two 1.1.0

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