easy_talk 2.0.0 → 3.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/CHANGELOG.md +26 -0
- data/README.md +14 -163
- data/easy_talk.gemspec +5 -4
- data/lib/easy_talk/builders/integer_builder.rb +1 -0
- data/lib/easy_talk/builders/string_builder.rb +9 -0
- data/lib/easy_talk/configuration.rb +2 -9
- data/lib/easy_talk/model.rb +14 -48
- data/lib/easy_talk/property.rb +1 -1
- data/lib/easy_talk/validation_builder.rb +37 -49
- data/lib/easy_talk/version.rb +1 -1
- metadata +35 -9
- data/lib/easy_talk/active_record_schema_builder.rb +0 -299
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: ce558538c73afc10d98c0c6b5e38e68ce033a79d1c212bb91fede27f10fb3523
|
4
|
+
data.tar.gz: e46a7a67b6c1a3209108a800cefca8659381236c23ce14986e971babd874908d
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: b523e6cd50edc0594d98fffdb8a9de0616e5f58a2af435a23e5f4a81ac54b25692cd0bccba71c93eca812022efd298b2a9d654b34e3b8f37301ea8e7b16a4de3
|
7
|
+
data.tar.gz: 7f287df819b0e09ddf5fce9506f045860dea876b0653cf7ede7dfb58e7a1d78e545933946921f0dfb4f1f02709357629f0b071c53e7c4ff945cdcf1908f5c2b6
|
data/CHANGELOG.md
CHANGED
@@ -1,3 +1,29 @@
|
|
1
|
+
## [3.0.0] - 2025-01-03
|
2
|
+
|
3
|
+
### BREAKING CHANGES
|
4
|
+
- **Removed ActiveRecord Support**: Completely removed ActiveRecord integration including:
|
5
|
+
- Deleted `ActiveRecordSchemaBuilder` class and database schema introspection
|
6
|
+
- Removed `enhance_schema` method for ActiveRecord models
|
7
|
+
- Removed ActiveRecord-specific configuration options (`excluded_columns`, `exclude_foreign_keys`,
|
8
|
+
`exclude_primary_key`, `exclude_timestamps`, `exclude_associations`)
|
9
|
+
- Deleted all ActiveRecord integration tests
|
10
|
+
|
11
|
+
### Changed
|
12
|
+
- **Simplified Architecture**: EasyTalk now focuses exclusively on Plain Ruby classes with ActiveModel integration
|
13
|
+
- **Unified Integration Path**: All models now follow the same integration pattern using `ActiveModel::API` and `ActiveModel::Validations`
|
14
|
+
- **Streamlined Configuration**: Removed ActiveRecord-specific configuration options, keeping only core options
|
15
|
+
- **Updated Documentation**: Removed ActiveRecord examples and configuration references from README
|
16
|
+
|
17
|
+
### Fixed
|
18
|
+
- **Code Quality**: Fixed ValidationBuilder class length violation by consolidating format validation methods
|
19
|
+
- **Documentation**: Updated all examples to use `define_schema` pattern instead of removed `enhance_schema`
|
20
|
+
|
21
|
+
### Migration Guide
|
22
|
+
If you were using EasyTalk with ActiveRecord models:
|
23
|
+
- Replace `enhance_schema` calls with `define_schema` blocks
|
24
|
+
- Manually define properties instead of relying on database schema introspection
|
25
|
+
- Remove ActiveRecord-specific configuration options from your EasyTalk.configure blocks
|
26
|
+
|
1
27
|
## [2.0.0] - 2025-06-05
|
2
28
|
|
3
29
|
### Added
|
data/README.md
CHANGED
@@ -11,7 +11,7 @@ EasyTalk is a Ruby library that simplifies defining and generating JSON Schema.
|
|
11
11
|
### Key Features
|
12
12
|
* **Intuitive Schema Definition**: Use Ruby classes and methods to define JSON Schema documents easily.
|
13
13
|
* **Automatic ActiveModel Validations**: Schema constraints automatically generate corresponding ActiveModel validations (configurable).
|
14
|
-
* **Works for plain Ruby classes and
|
14
|
+
* **Works for plain Ruby classes and ActiveModel classes**: Integrate with existing code or build from scratch.
|
15
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.
|
16
16
|
* **Schema Composition**: Define EasyTalk models and reference them in other EasyTalk models to create complex schemas.
|
17
17
|
* **Enhanced Model Integration**: Automatic instantiation of nested EasyTalk models from hash attributes.
|
@@ -518,119 +518,6 @@ user.address.class # => Address (automatically instantiated)
|
|
518
518
|
user.address.street # => "123 Main St"
|
519
519
|
```
|
520
520
|
|
521
|
-
## ActiveRecord Integration
|
522
|
-
|
523
|
-
### Automatic Schema Generation
|
524
|
-
For ActiveRecord models, EasyTalk automatically generates a schema based on the database columns:
|
525
|
-
|
526
|
-
```ruby
|
527
|
-
class Product < ActiveRecord::Base
|
528
|
-
include EasyTalk::Model
|
529
|
-
end
|
530
|
-
```
|
531
|
-
|
532
|
-
This will create a schema with properties for each column in the `products` table.
|
533
|
-
|
534
|
-
### Enhancing Generated Schemas
|
535
|
-
You can enhance the auto-generated schema with the `enhance_schema` method:
|
536
|
-
|
537
|
-
```ruby
|
538
|
-
class Product < ActiveRecord::Base
|
539
|
-
include EasyTalk::Model
|
540
|
-
|
541
|
-
enhance_schema({
|
542
|
-
title: "Retail Product",
|
543
|
-
description: "A product available for purchase",
|
544
|
-
properties: {
|
545
|
-
name: {
|
546
|
-
description: "Product display name",
|
547
|
-
title: "Product Name"
|
548
|
-
},
|
549
|
-
price: {
|
550
|
-
description: "Retail price in USD"
|
551
|
-
}
|
552
|
-
}
|
553
|
-
})
|
554
|
-
end
|
555
|
-
```
|
556
|
-
|
557
|
-
### Column Exclusion Options
|
558
|
-
EasyTalk provides several ways to exclude columns from your JSON schema:
|
559
|
-
|
560
|
-
#### 1. Global Configuration
|
561
|
-
|
562
|
-
```ruby
|
563
|
-
EasyTalk.configure do |config|
|
564
|
-
# Exclude specific columns by name from all models
|
565
|
-
config.excluded_columns = [:created_at, :updated_at, :deleted_at]
|
566
|
-
|
567
|
-
# Exclude all foreign key columns (columns ending with '_id')
|
568
|
-
config.exclude_foreign_keys = true # Default: false
|
569
|
-
|
570
|
-
# Exclude all primary key columns ('id')
|
571
|
-
config.exclude_primary_key = true # Default: true
|
572
|
-
|
573
|
-
# Exclude timestamp columns ('created_at', 'updated_at')
|
574
|
-
config.exclude_timestamps = true # Default: true
|
575
|
-
|
576
|
-
# Exclude all association properties
|
577
|
-
config.exclude_associations = true # Default: false
|
578
|
-
end
|
579
|
-
```
|
580
|
-
|
581
|
-
#### 2. Model-Specific Column Ignoring
|
582
|
-
|
583
|
-
```ruby
|
584
|
-
class Product < ActiveRecord::Base
|
585
|
-
include EasyTalk::Model
|
586
|
-
|
587
|
-
enhance_schema({
|
588
|
-
ignore: [:internal_ref_id, :legacy_code] # Model-specific exclusions
|
589
|
-
})
|
590
|
-
end
|
591
|
-
```
|
592
|
-
|
593
|
-
### Virtual Properties
|
594
|
-
You can add properties that don't exist as database columns:
|
595
|
-
|
596
|
-
```ruby
|
597
|
-
class Product < ActiveRecord::Base
|
598
|
-
include EasyTalk::Model
|
599
|
-
|
600
|
-
enhance_schema({
|
601
|
-
properties: {
|
602
|
-
full_details: {
|
603
|
-
virtual: true,
|
604
|
-
type: :string,
|
605
|
-
description: "Complete product information"
|
606
|
-
}
|
607
|
-
}
|
608
|
-
})
|
609
|
-
end
|
610
|
-
```
|
611
|
-
|
612
|
-
### Associations and Foreign Keys
|
613
|
-
By default, EasyTalk includes your model's associations in the schema:
|
614
|
-
|
615
|
-
```ruby
|
616
|
-
class Product < ActiveRecord::Base
|
617
|
-
include EasyTalk::Model
|
618
|
-
belongs_to :category
|
619
|
-
has_many :reviews
|
620
|
-
end
|
621
|
-
```
|
622
|
-
|
623
|
-
This will include `category` (as an object) and `reviews` (as an array) in the schema.
|
624
|
-
|
625
|
-
You can control this behavior with configuration:
|
626
|
-
|
627
|
-
```ruby
|
628
|
-
EasyTalk.configure do |config|
|
629
|
-
config.exclude_associations = true # Don't include associations
|
630
|
-
config.exclude_foreign_keys = true # Don't include foreign key columns
|
631
|
-
end
|
632
|
-
```
|
633
|
-
|
634
521
|
## Advanced Features
|
635
522
|
|
636
523
|
### LLM Function Generation
|
@@ -698,19 +585,10 @@ You can configure EasyTalk globally:
|
|
698
585
|
|
699
586
|
```ruby
|
700
587
|
EasyTalk.configure do |config|
|
701
|
-
# ActiveRecord integration options
|
702
|
-
config.excluded_columns = [:created_at, :updated_at, :deleted_at]
|
703
|
-
config.exclude_foreign_keys = true
|
704
|
-
config.exclude_primary_key = true
|
705
|
-
config.exclude_timestamps = true
|
706
|
-
config.exclude_associations = false
|
707
|
-
|
708
588
|
# Schema behavior options
|
709
|
-
config.default_additional_properties = false
|
710
|
-
config.nilable_is_optional = false
|
711
|
-
|
712
|
-
# NEW in v2.0.0: Automatic validation generation
|
713
|
-
config.auto_validations = true # Automatically generate ActiveModel validations
|
589
|
+
config.default_additional_properties = false # Control additional properties on all models
|
590
|
+
config.nilable_is_optional = false # Makes T.nilable properties also optional
|
591
|
+
config.auto_validations = true # Automatically generate ActiveModel validations
|
714
592
|
end
|
715
593
|
```
|
716
594
|
|
@@ -738,45 +616,18 @@ end
|
|
738
616
|
```
|
739
617
|
|
740
618
|
### Per-Model Configuration
|
741
|
-
|
742
|
-
|
743
|
-
```ruby
|
744
|
-
class Product < ActiveRecord::Base
|
745
|
-
include EasyTalk::Model
|
746
|
-
|
747
|
-
enhance_schema({
|
748
|
-
additionalProperties: true,
|
749
|
-
ignore: [:internal_ref_id, :legacy_code]
|
750
|
-
})
|
751
|
-
end
|
752
|
-
```
|
753
|
-
|
754
|
-
### Exclusion Rules
|
755
|
-
Columns are excluded based on the following rules (in order of precedence):
|
756
|
-
|
757
|
-
1. Explicitly listed in `excluded_columns` global setting
|
758
|
-
2. Listed in the model's `schema_enhancements[:ignore]` array
|
759
|
-
3. Is a primary key when `exclude_primary_key` is true (default)
|
760
|
-
4. Is a timestamp column when `exclude_timestamps` is true (default)
|
761
|
-
5. Matches a foreign key pattern when `exclude_foreign_keys` is true
|
762
|
-
|
763
|
-
### Customizing Output
|
764
|
-
You can customize the JSON Schema output by enhancing the schema:
|
619
|
+
You can configure additional properties for individual models:
|
765
620
|
|
766
621
|
```ruby
|
767
|
-
class User
|
622
|
+
class User
|
768
623
|
include EasyTalk::Model
|
769
624
|
|
770
|
-
|
771
|
-
title
|
772
|
-
|
773
|
-
|
774
|
-
|
775
|
-
|
776
|
-
description: "User's full name"
|
777
|
-
}
|
778
|
-
}
|
779
|
-
})
|
625
|
+
define_schema do
|
626
|
+
title "User"
|
627
|
+
additional_properties true # Allow arbitrary additional properties on this model
|
628
|
+
property :name, String
|
629
|
+
property :email, String, format: "email"
|
630
|
+
end
|
780
631
|
end
|
781
632
|
```
|
782
633
|
|
@@ -1277,7 +1128,7 @@ EasyTalk.configure do |config|
|
|
1277
1128
|
|
1278
1129
|
# Existing options (unchanged)
|
1279
1130
|
config.nilable_is_optional = false
|
1280
|
-
config.
|
1131
|
+
config.default_additional_properties = false
|
1281
1132
|
# ... other existing config
|
1282
1133
|
end
|
1283
1134
|
```
|
@@ -1287,7 +1138,7 @@ end
|
|
1287
1138
|
- **Ruby Version**: Still requires Ruby 3.2+
|
1288
1139
|
- **Dependencies**: Core dependencies remain the same
|
1289
1140
|
- **JSON Schema Output**: No changes to generated schemas
|
1290
|
-
- **
|
1141
|
+
- **ActiveModel Integration**: Fully backward compatible
|
1291
1142
|
|
1292
1143
|
## Development and Contributing
|
1293
1144
|
|
data/easy_talk.gemspec
CHANGED
@@ -8,8 +8,8 @@ Gem::Specification.new do |spec|
|
|
8
8
|
spec.authors = ['Sergio Bayona']
|
9
9
|
spec.email = ['bayona.sergio@gmail.com']
|
10
10
|
|
11
|
-
spec.summary = 'Generate json-schema from Ruby classes.'
|
12
|
-
spec.description = 'Generate json-schema from plain Ruby classes.'
|
11
|
+
spec.summary = 'Generate json-schema from Ruby classes with ActiveModel integration.'
|
12
|
+
spec.description = 'Generate json-schema from plain Ruby classes with ActiveModel integration for validations and serialization.'
|
13
13
|
spec.homepage = 'https://github.com/sergiobayona/easy_talk'
|
14
14
|
spec.license = 'MIT'
|
15
15
|
spec.required_ruby_version = '>= 3.2'
|
@@ -30,8 +30,9 @@ Gem::Specification.new do |spec|
|
|
30
30
|
|
31
31
|
spec.require_paths = ['lib']
|
32
32
|
|
33
|
-
spec.add_dependency 'activemodel', '
|
34
|
-
spec.add_dependency 'activesupport', '
|
33
|
+
spec.add_dependency 'activemodel', ['>= 7.0', '< 9.0']
|
34
|
+
spec.add_dependency 'activesupport', ['>= 7.0', '< 9.0']
|
35
|
+
spec.add_dependency 'js_regex', '~> 3.0'
|
35
36
|
spec.add_dependency 'sorbet-runtime', '~> 0.5'
|
36
37
|
|
37
38
|
spec.metadata['rubygems_mfa_required'] = 'true'
|
@@ -1,6 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require_relative 'base_builder'
|
4
|
+
require 'js_regex' # Compile the ruby regex to JS regex
|
4
5
|
require 'sorbet-runtime' # Add the import statement for the T module
|
5
6
|
|
6
7
|
module EasyTalk
|
@@ -8,6 +9,7 @@ module EasyTalk
|
|
8
9
|
# Builder class for string properties.
|
9
10
|
class StringBuilder < BaseBuilder
|
10
11
|
extend T::Sig
|
12
|
+
|
11
13
|
VALID_OPTIONS = {
|
12
14
|
format: { type: String, key: :format },
|
13
15
|
pattern: { type: String, key: :pattern },
|
@@ -22,6 +24,13 @@ module EasyTalk
|
|
22
24
|
def initialize(name, constraints = {})
|
23
25
|
super(name, { type: 'string' }, constraints, VALID_OPTIONS)
|
24
26
|
end
|
27
|
+
|
28
|
+
def build
|
29
|
+
super.tap do |schema|
|
30
|
+
pattern = schema[:pattern]
|
31
|
+
schema[:pattern] = JsRegex.new(pattern).source if pattern.is_a?(String)
|
32
|
+
end
|
33
|
+
end
|
25
34
|
end
|
26
35
|
end
|
27
36
|
end
|
@@ -2,19 +2,12 @@
|
|
2
2
|
|
3
3
|
module EasyTalk
|
4
4
|
class Configuration
|
5
|
-
attr_accessor :
|
6
|
-
:exclude_primary_key, :exclude_timestamps, :default_additional_properties,
|
7
|
-
:nilable_is_optional, :auto_validations
|
5
|
+
attr_accessor :default_additional_properties, :nilable_is_optional, :auto_validations
|
8
6
|
|
9
7
|
def initialize
|
10
|
-
@exclude_foreign_keys = true
|
11
|
-
@exclude_associations = true
|
12
|
-
@excluded_columns = []
|
13
|
-
@exclude_primary_key = true
|
14
|
-
@exclude_timestamps = true
|
15
8
|
@default_additional_properties = false
|
16
9
|
@nilable_is_optional = false
|
17
|
-
@auto_validations = true
|
10
|
+
@auto_validations = true
|
18
11
|
end
|
19
12
|
end
|
20
13
|
|
data/lib/easy_talk/model.rb
CHANGED
@@ -9,7 +9,6 @@ require 'active_support/json'
|
|
9
9
|
require 'active_model'
|
10
10
|
require_relative 'builders/object_builder'
|
11
11
|
require_relative 'schema_definition'
|
12
|
-
require_relative 'active_record_schema_builder'
|
13
12
|
require_relative 'validation_builder'
|
14
13
|
|
15
14
|
module EasyTalk
|
@@ -36,16 +35,12 @@ module EasyTalk
|
|
36
35
|
# @see SchemaDefinition
|
37
36
|
module Model
|
38
37
|
def self.included(base)
|
39
|
-
base.
|
38
|
+
base.extend(ClassMethods)
|
39
|
+
|
40
|
+
base.include ActiveModel::API
|
40
41
|
base.include ActiveModel::Validations
|
41
42
|
base.extend ActiveModel::Callbacks
|
42
|
-
base.extend(ClassMethods)
|
43
43
|
base.include(InstanceMethods)
|
44
|
-
|
45
|
-
# Apply ActiveRecord-specific functionality if appropriate
|
46
|
-
return unless defined?(ActiveRecord) && base.ancestors.include?(ActiveRecord::Base)
|
47
|
-
|
48
|
-
base.extend(ActiveRecordClassMethods)
|
49
44
|
end
|
50
45
|
|
51
46
|
# Instance methods mixed into models that include EasyTalk::Model
|
@@ -55,12 +50,7 @@ module EasyTalk
|
|
55
50
|
super # Perform initial mass assignment
|
56
51
|
|
57
52
|
# After initial assignment, instantiate nested EasyTalk::Model objects
|
58
|
-
|
59
|
-
schema_def = if self.class.respond_to?(:active_record_schema_definition)
|
60
|
-
self.class.active_record_schema_definition
|
61
|
-
else
|
62
|
-
self.class.schema_definition
|
63
|
-
end
|
53
|
+
schema_def = self.class.schema_definition
|
64
54
|
|
65
55
|
# Only proceed if we have a valid schema definition
|
66
56
|
return unless schema_def.respond_to?(:schema) && schema_def.schema.is_a?(Hash)
|
@@ -69,6 +59,11 @@ module EasyTalk
|
|
69
59
|
# Get the defined type and the currently assigned value
|
70
60
|
defined_type = prop_definition[:type]
|
71
61
|
current_value = public_send(prop_name)
|
62
|
+
nilable_type = defined_type.respond_to?(:nilable?) && defined_type.nilable?
|
63
|
+
|
64
|
+
next if nilable_type && current_value.nil?
|
65
|
+
|
66
|
+
defined_type = T::Utils::Nilable.get_underlying_type(defined_type) if nilable_type
|
72
67
|
|
73
68
|
# Check if the type is another EasyTalk::Model and the value is a Hash
|
74
69
|
next unless defined_type.is_a?(Class) && defined_type.include?(EasyTalk::Model) && current_value.is_a?(Hash)
|
@@ -116,6 +111,11 @@ module EasyTalk
|
|
116
111
|
to_hash.merge(@additional_properties)
|
117
112
|
end
|
118
113
|
|
114
|
+
# to_h includes both defined and additional properties
|
115
|
+
def to_h
|
116
|
+
to_hash.merge(@additional_properties)
|
117
|
+
end
|
118
|
+
|
119
119
|
# Allow comparison with hashes
|
120
120
|
def ==(other)
|
121
121
|
case other
|
@@ -141,13 +141,8 @@ module EasyTalk
|
|
141
141
|
# @return [Schema] The schema for the model.
|
142
142
|
def schema
|
143
143
|
@schema ||= if defined?(@schema_definition) && @schema_definition
|
144
|
-
# Schema defined explicitly via define_schema
|
145
144
|
build_schema(@schema_definition)
|
146
|
-
elsif respond_to?(:active_record_schema_definition)
|
147
|
-
# ActiveRecord model without explicit schema definition
|
148
|
-
build_schema(active_record_schema_definition)
|
149
145
|
else
|
150
|
-
# Default case - empty schema
|
151
146
|
{}
|
152
147
|
end
|
153
148
|
end
|
@@ -225,34 +220,5 @@ module EasyTalk
|
|
225
220
|
Builders::ObjectBuilder.new(schema_definition).build
|
226
221
|
end
|
227
222
|
end
|
228
|
-
|
229
|
-
# Module containing ActiveRecord-specific methods for schema generation
|
230
|
-
module ActiveRecordClassMethods
|
231
|
-
# Gets a SchemaDefinition that's built from the ActiveRecord database schema
|
232
|
-
#
|
233
|
-
# @return [SchemaDefinition] A schema definition built from the database
|
234
|
-
def active_record_schema_definition
|
235
|
-
@active_record_schema_definition ||= ActiveRecordSchemaBuilder.new(self).build_schema_definition
|
236
|
-
end
|
237
|
-
|
238
|
-
# Store enhancements to be applied to the schema
|
239
|
-
#
|
240
|
-
# @return [Hash] The schema enhancements
|
241
|
-
def schema_enhancements
|
242
|
-
@schema_enhancements ||= {}
|
243
|
-
end
|
244
|
-
|
245
|
-
# Enhance the generated schema with additional information
|
246
|
-
#
|
247
|
-
# @param enhancements [Hash] The schema enhancements
|
248
|
-
# @return [void]
|
249
|
-
def enhance_schema(enhancements)
|
250
|
-
@schema_enhancements = enhancements
|
251
|
-
# Clear cached values to force regeneration
|
252
|
-
@active_record_schema_definition = nil
|
253
|
-
@schema = nil
|
254
|
-
@json_schema = nil
|
255
|
-
end
|
256
|
-
end
|
257
223
|
end
|
258
224
|
end
|
data/lib/easy_talk/property.rb
CHANGED
@@ -189,7 +189,7 @@ module EasyTalk
|
|
189
189
|
# {"type"=>["string", "null"]}
|
190
190
|
def build_nilable_schema
|
191
191
|
# Extract the non-nil type from the Union
|
192
|
-
actual_type = type
|
192
|
+
actual_type = T::Utils::Nilable.get_underlying_type(type)
|
193
193
|
|
194
194
|
return { type: 'null' } unless actual_type
|
195
195
|
|
@@ -65,8 +65,8 @@ module EasyTalk
|
|
65
65
|
end
|
66
66
|
|
67
67
|
# Check if the type is nilable (e.g., T.nilable(String))
|
68
|
-
def nilable_type?
|
69
|
-
|
68
|
+
def nilable_type?(type = @type)
|
69
|
+
type.respond_to?(:nilable?) && type.nilable?
|
70
70
|
end
|
71
71
|
|
72
72
|
# Extract the inner type from a complex type like T.nilable(String)
|
@@ -118,6 +118,8 @@ module EasyTalk
|
|
118
118
|
end
|
119
119
|
elsif type.to_s.include?('T::Boolean')
|
120
120
|
[TrueClass, FalseClass] # Return both boolean classes
|
121
|
+
elsif nilable_type?(type)
|
122
|
+
extract_inner_type(type)
|
121
123
|
else
|
122
124
|
String # Default fallback
|
123
125
|
end
|
@@ -137,48 +139,30 @@ module EasyTalk
|
|
137
139
|
@klass.validates @property_name, format: { with: Regexp.new(@constraints[:pattern]) } if @constraints[:pattern]
|
138
140
|
|
139
141
|
# Handle length constraints
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
142
|
+
begin
|
143
|
+
length_options = {}
|
144
|
+
length_options[:minimum] = @constraints[:min_length] if @constraints[:min_length].is_a?(Numeric) && @constraints[:min_length] >= 0
|
145
|
+
length_options[:maximum] = @constraints[:max_length] if @constraints[:max_length].is_a?(Numeric) && @constraints[:max_length] >= 0
|
146
|
+
@klass.validates @property_name, length: length_options if length_options.any?
|
147
|
+
rescue ArgumentError
|
148
|
+
# Silently ignore invalid length constraints
|
149
|
+
end
|
144
150
|
end
|
145
151
|
|
146
152
|
# Apply format-specific validations (email, url, etc.)
|
147
153
|
def apply_format_validation(format)
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
}
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
uuid_regex = /\A[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\z/i
|
161
|
-
@klass.validates @property_name, format: {
|
162
|
-
with: uuid_regex,
|
163
|
-
message: 'must be a valid UUID'
|
164
|
-
}
|
165
|
-
when 'date'
|
166
|
-
@klass.validates @property_name, format: {
|
167
|
-
with: /\A\d{4}-\d{2}-\d{2}\z/,
|
168
|
-
message: 'must be a valid date in YYYY-MM-DD format'
|
169
|
-
}
|
170
|
-
when 'date-time'
|
171
|
-
# ISO 8601 date-time format
|
172
|
-
@klass.validates @property_name, format: {
|
173
|
-
with: /\A\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2})?\z/,
|
174
|
-
message: 'must be a valid ISO 8601 date-time'
|
175
|
-
}
|
176
|
-
when 'time'
|
177
|
-
@klass.validates @property_name, format: {
|
178
|
-
with: /\A\d{2}:\d{2}:\d{2}(?:\.\d+)?\z/,
|
179
|
-
message: 'must be a valid time in HH:MM:SS format'
|
180
|
-
}
|
181
|
-
end
|
154
|
+
format_configs = {
|
155
|
+
'email' => { with: URI::MailTo::EMAIL_REGEXP, message: 'must be a valid email address' },
|
156
|
+
'uri' => { with: URI::DEFAULT_PARSER.make_regexp, message: 'must be a valid URL' },
|
157
|
+
'url' => { with: URI::DEFAULT_PARSER.make_regexp, message: 'must be a valid URL' },
|
158
|
+
'uuid' => { with: /\A[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\z/i, message: 'must be a valid UUID' },
|
159
|
+
'date' => { with: /\A\d{4}-\d{2}-\d{2}\z/, message: 'must be a valid date in YYYY-MM-DD format' },
|
160
|
+
'date-time' => { with: /\A\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2})?\z/, message: 'must be a valid ISO 8601 date-time' },
|
161
|
+
'time' => { with: /\A\d{2}:\d{2}:\d{2}(?:\.\d+)?\z/, message: 'must be a valid time in HH:MM:SS format' }
|
162
|
+
}
|
163
|
+
|
164
|
+
config = format_configs[format.to_s]
|
165
|
+
@klass.validates @property_name, format: config if config
|
182
166
|
end
|
183
167
|
|
184
168
|
# Validate integer-specific constraints
|
@@ -193,15 +177,19 @@ module EasyTalk
|
|
193
177
|
|
194
178
|
# Apply numeric validations for integers and floats
|
195
179
|
def apply_numeric_validations(only_integer: false)
|
196
|
-
|
197
|
-
|
198
|
-
|
199
|
-
|
200
|
-
|
201
|
-
|
202
|
-
|
203
|
-
|
204
|
-
|
180
|
+
begin
|
181
|
+
options = { only_integer: only_integer }
|
182
|
+
|
183
|
+
# Add range constraints - only if they are numeric
|
184
|
+
options[:greater_than_or_equal_to] = @constraints[:minimum] if @constraints[:minimum].is_a?(Numeric)
|
185
|
+
options[:less_than_or_equal_to] = @constraints[:maximum] if @constraints[:maximum].is_a?(Numeric)
|
186
|
+
options[:greater_than] = @constraints[:exclusive_minimum] if @constraints[:exclusive_minimum].is_a?(Numeric)
|
187
|
+
options[:less_than] = @constraints[:exclusive_maximum] if @constraints[:exclusive_maximum].is_a?(Numeric)
|
188
|
+
|
189
|
+
@klass.validates @property_name, numericality: options
|
190
|
+
rescue ArgumentError
|
191
|
+
# Silently ignore invalid numeric constraints
|
192
|
+
end
|
205
193
|
|
206
194
|
# Add multiple_of validation
|
207
195
|
return unless @constraints[:multiple_of]
|
data/lib/easy_talk/version.rb
CHANGED
metadata
CHANGED
@@ -1,42 +1,68 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: easy_talk
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version:
|
4
|
+
version: 3.0.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Sergio Bayona
|
8
8
|
bindir: bin
|
9
9
|
cert_chain: []
|
10
|
-
date: 2025-
|
10
|
+
date: 2025-09-03 00:00:00.000000000 Z
|
11
11
|
dependencies:
|
12
12
|
- !ruby/object:Gem::Dependency
|
13
13
|
name: activemodel
|
14
14
|
requirement: !ruby/object:Gem::Requirement
|
15
15
|
requirements:
|
16
|
-
- - "
|
16
|
+
- - ">="
|
17
17
|
- !ruby/object:Gem::Version
|
18
18
|
version: '7.0'
|
19
|
+
- - "<"
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: '9.0'
|
19
22
|
type: :runtime
|
20
23
|
prerelease: false
|
21
24
|
version_requirements: !ruby/object:Gem::Requirement
|
22
25
|
requirements:
|
23
|
-
- - "
|
26
|
+
- - ">="
|
24
27
|
- !ruby/object:Gem::Version
|
25
28
|
version: '7.0'
|
29
|
+
- - "<"
|
30
|
+
- !ruby/object:Gem::Version
|
31
|
+
version: '9.0'
|
26
32
|
- !ruby/object:Gem::Dependency
|
27
33
|
name: activesupport
|
28
34
|
requirement: !ruby/object:Gem::Requirement
|
29
35
|
requirements:
|
30
|
-
- - "
|
36
|
+
- - ">="
|
31
37
|
- !ruby/object:Gem::Version
|
32
38
|
version: '7.0'
|
39
|
+
- - "<"
|
40
|
+
- !ruby/object:Gem::Version
|
41
|
+
version: '9.0'
|
33
42
|
type: :runtime
|
34
43
|
prerelease: false
|
35
44
|
version_requirements: !ruby/object:Gem::Requirement
|
36
45
|
requirements:
|
37
|
-
- - "
|
46
|
+
- - ">="
|
38
47
|
- !ruby/object:Gem::Version
|
39
48
|
version: '7.0'
|
49
|
+
- - "<"
|
50
|
+
- !ruby/object:Gem::Version
|
51
|
+
version: '9.0'
|
52
|
+
- !ruby/object:Gem::Dependency
|
53
|
+
name: js_regex
|
54
|
+
requirement: !ruby/object:Gem::Requirement
|
55
|
+
requirements:
|
56
|
+
- - "~>"
|
57
|
+
- !ruby/object:Gem::Version
|
58
|
+
version: '3.0'
|
59
|
+
type: :runtime
|
60
|
+
prerelease: false
|
61
|
+
version_requirements: !ruby/object:Gem::Requirement
|
62
|
+
requirements:
|
63
|
+
- - "~>"
|
64
|
+
- !ruby/object:Gem::Version
|
65
|
+
version: '3.0'
|
40
66
|
- !ruby/object:Gem::Dependency
|
41
67
|
name: sorbet-runtime
|
42
68
|
requirement: !ruby/object:Gem::Requirement
|
@@ -51,7 +77,8 @@ dependencies:
|
|
51
77
|
- - "~>"
|
52
78
|
- !ruby/object:Gem::Version
|
53
79
|
version: '0.5'
|
54
|
-
description: Generate json-schema from plain Ruby classes
|
80
|
+
description: Generate json-schema from plain Ruby classes with ActiveModel integration
|
81
|
+
for validations and serialization.
|
55
82
|
email:
|
56
83
|
- bayona.sergio@gmail.com
|
57
84
|
executables: []
|
@@ -73,7 +100,6 @@ files:
|
|
73
100
|
- docs/index.markdown
|
74
101
|
- easy_talk.gemspec
|
75
102
|
- lib/easy_talk.rb
|
76
|
-
- lib/easy_talk/active_record_schema_builder.rb
|
77
103
|
- lib/easy_talk/builders/base_builder.rb
|
78
104
|
- lib/easy_talk/builders/boolean_builder.rb
|
79
105
|
- lib/easy_talk/builders/collection_helpers.rb
|
@@ -123,5 +149,5 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
123
149
|
requirements: []
|
124
150
|
rubygems_version: 3.6.2
|
125
151
|
specification_version: 4
|
126
|
-
summary: Generate json-schema from Ruby classes.
|
152
|
+
summary: Generate json-schema from Ruby classes with ActiveModel integration.
|
127
153
|
test_files: []
|
@@ -1,299 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module EasyTalk
|
4
|
-
# This class is responsible for building a SchemaDefinition from an ActiveRecord model
|
5
|
-
# It analyzes the database schema and creates a SchemaDefinition that can be
|
6
|
-
# passed to ObjectBuilder for final schema generation
|
7
|
-
class ActiveRecordSchemaBuilder
|
8
|
-
# Mapping of ActiveRecord column types to Ruby classes
|
9
|
-
COLUMN_TYPE_MAP = {
|
10
|
-
string: String,
|
11
|
-
text: String,
|
12
|
-
integer: Integer,
|
13
|
-
bigint: Integer,
|
14
|
-
float: Float,
|
15
|
-
decimal: Float,
|
16
|
-
boolean: T::Boolean,
|
17
|
-
date: Date,
|
18
|
-
datetime: DateTime,
|
19
|
-
timestamp: DateTime,
|
20
|
-
time: Time,
|
21
|
-
json: Hash,
|
22
|
-
jsonb: Hash
|
23
|
-
}.freeze
|
24
|
-
|
25
|
-
# Mapping for format constraints based on column type
|
26
|
-
FORMAT_MAP = {
|
27
|
-
date: 'date',
|
28
|
-
datetime: 'date-time',
|
29
|
-
timestamp: 'date-time',
|
30
|
-
time: 'time'
|
31
|
-
}.freeze
|
32
|
-
|
33
|
-
attr_reader :model
|
34
|
-
|
35
|
-
# Initialize the builder with an ActiveRecord model
|
36
|
-
#
|
37
|
-
# @param model [Class] An ActiveRecord model class
|
38
|
-
# @raise [ArgumentError] If the provided class is not an ActiveRecord model
|
39
|
-
def initialize(model)
|
40
|
-
raise ArgumentError, 'Class must be an ActiveRecord model' unless model.ancestors.include?(ActiveRecord::Base)
|
41
|
-
|
42
|
-
@model = model
|
43
|
-
end
|
44
|
-
|
45
|
-
# Build a SchemaDefinition object from the ActiveRecord model
|
46
|
-
#
|
47
|
-
# @return [EasyTalk::SchemaDefinition] A schema definition built from the database structure
|
48
|
-
def build_schema_definition
|
49
|
-
schema_def = SchemaDefinition.new(model.name)
|
50
|
-
|
51
|
-
# Apply basic schema metadata
|
52
|
-
apply_schema_metadata(schema_def)
|
53
|
-
|
54
|
-
# Add all database columns as properties
|
55
|
-
add_column_properties(schema_def)
|
56
|
-
|
57
|
-
# Add model associations as properties
|
58
|
-
add_association_properties(schema_def) unless EasyTalk.configuration.exclude_associations
|
59
|
-
|
60
|
-
# Add virtual properties defined in schema_enhancements
|
61
|
-
add_virtual_properties(schema_def)
|
62
|
-
|
63
|
-
schema_def
|
64
|
-
end
|
65
|
-
|
66
|
-
private
|
67
|
-
|
68
|
-
# Set top-level schema metadata like title, description, and additionalProperties
|
69
|
-
#
|
70
|
-
# @param schema_def [EasyTalk::SchemaDefinition] The schema definition to modify
|
71
|
-
def apply_schema_metadata(schema_def)
|
72
|
-
# Set title (from enhancements or derive from model name)
|
73
|
-
title = schema_enhancements['title'] || model.name.demodulize.humanize
|
74
|
-
schema_def.title(title)
|
75
|
-
|
76
|
-
# Set description if provided
|
77
|
-
if (description = schema_enhancements['description'])
|
78
|
-
schema_def.description(description)
|
79
|
-
end
|
80
|
-
|
81
|
-
# Set additionalProperties (from enhancements or configuration default)
|
82
|
-
additional_props = if schema_enhancements.key?('additionalProperties')
|
83
|
-
schema_enhancements['additionalProperties']
|
84
|
-
else
|
85
|
-
EasyTalk.configuration.default_additional_properties
|
86
|
-
end
|
87
|
-
schema_def.additional_properties(additional_props)
|
88
|
-
end
|
89
|
-
|
90
|
-
# Add properties based on database columns
|
91
|
-
#
|
92
|
-
# @param schema_def [EasyTalk::SchemaDefinition] The schema definition to modify
|
93
|
-
def add_column_properties(schema_def)
|
94
|
-
filtered_columns.each do |column|
|
95
|
-
# Get column enhancement info if it exists
|
96
|
-
column_enhancements = schema_enhancements.dig('properties', column.name.to_s) || {}
|
97
|
-
|
98
|
-
# Map the database type to Ruby type
|
99
|
-
ruby_type = COLUMN_TYPE_MAP.fetch(column.type, String)
|
100
|
-
|
101
|
-
# If the column is nullable, wrap the type in a Union with NilClass
|
102
|
-
ruby_type = T::Types::Union.new([ruby_type, NilClass]) if column.null
|
103
|
-
|
104
|
-
# Build constraints hash for this column
|
105
|
-
constraints = build_column_constraints(column, column_enhancements)
|
106
|
-
|
107
|
-
# Add the property to schema definition
|
108
|
-
schema_def.property(column.name.to_sym, ruby_type, constraints)
|
109
|
-
end
|
110
|
-
end
|
111
|
-
|
112
|
-
# Build constraints hash for a database column
|
113
|
-
#
|
114
|
-
# @param column [ActiveRecord::ConnectionAdapters::Column] The database column
|
115
|
-
# @param enhancements [Hash] Any schema enhancements for this column
|
116
|
-
# @return [Hash] The constraints hash
|
117
|
-
def build_column_constraints(column, enhancements)
|
118
|
-
constraints = {
|
119
|
-
optional: enhancements['optional'],
|
120
|
-
description: enhancements['description'],
|
121
|
-
title: enhancements['title']
|
122
|
-
}
|
123
|
-
|
124
|
-
# Add format constraint for date/time columns
|
125
|
-
if (format = FORMAT_MAP[column.type])
|
126
|
-
constraints[:format] = format
|
127
|
-
end
|
128
|
-
|
129
|
-
# Add max_length for string columns with limits
|
130
|
-
constraints[:max_length] = column.limit if column.type == :string && column.limit
|
131
|
-
|
132
|
-
# Add precision/scale for numeric columns
|
133
|
-
if column.type == :decimal && column.precision
|
134
|
-
constraints[:precision] = column.precision
|
135
|
-
constraints[:scale] = column.scale if column.scale
|
136
|
-
end
|
137
|
-
|
138
|
-
# Add default value if present and not a proc
|
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
|
144
|
-
|
145
|
-
# Remove nil values
|
146
|
-
constraints.compact
|
147
|
-
end
|
148
|
-
|
149
|
-
# Add properties based on ActiveRecord associations
|
150
|
-
#
|
151
|
-
# @param schema_def [EasyTalk::SchemaDefinition] The schema definition to modify
|
152
|
-
def add_association_properties(schema_def)
|
153
|
-
model.reflect_on_all_associations.each do |association|
|
154
|
-
# Skip if we can't determine the class or it's in the association exclusion list
|
155
|
-
next if association_excluded?(association)
|
156
|
-
|
157
|
-
# Get association enhancement info if it exists
|
158
|
-
assoc_enhancements = schema_enhancements.dig('properties', association.name.to_s) || {}
|
159
|
-
|
160
|
-
case association.macro
|
161
|
-
when :belongs_to, :has_one
|
162
|
-
schema_def.property(
|
163
|
-
association.name,
|
164
|
-
association.klass,
|
165
|
-
{ optional: assoc_enhancements['optional'], description: assoc_enhancements['description'] }.compact
|
166
|
-
)
|
167
|
-
when :has_many, :has_and_belongs_to_many
|
168
|
-
schema_def.property(
|
169
|
-
association.name,
|
170
|
-
T::Array[association.klass],
|
171
|
-
{ optional: assoc_enhancements['optional'], description: assoc_enhancements['description'] }.compact
|
172
|
-
)
|
173
|
-
end
|
174
|
-
end
|
175
|
-
end
|
176
|
-
|
177
|
-
# Add virtual properties defined in schema_enhancements
|
178
|
-
#
|
179
|
-
# @param schema_def [EasyTalk::SchemaDefinition] The schema definition to modify
|
180
|
-
def add_virtual_properties(schema_def)
|
181
|
-
return unless schema_enhancements['properties']
|
182
|
-
|
183
|
-
schema_enhancements['properties'].each do |name, options|
|
184
|
-
next unless options['virtual']
|
185
|
-
|
186
|
-
# Map string type name to Ruby class
|
187
|
-
ruby_type = map_type_string_to_ruby_class(options['type'] || 'string')
|
188
|
-
|
189
|
-
# Build constraints for virtual property
|
190
|
-
constraints = {
|
191
|
-
description: options['description'],
|
192
|
-
title: options['title'],
|
193
|
-
optional: options['optional'],
|
194
|
-
format: options['format'],
|
195
|
-
default: options['default'],
|
196
|
-
min_length: options['minLength'],
|
197
|
-
max_length: options['maxLength'],
|
198
|
-
enum: options['enum']
|
199
|
-
}.compact
|
200
|
-
|
201
|
-
# Add the virtual property
|
202
|
-
schema_def.property(name.to_sym, ruby_type, constraints)
|
203
|
-
end
|
204
|
-
end
|
205
|
-
|
206
|
-
# Map a type string to a Ruby class
|
207
|
-
#
|
208
|
-
# @param type_str [String] The type string (e.g., 'string', 'integer')
|
209
|
-
# @return [Class] The corresponding Ruby class
|
210
|
-
def map_type_string_to_ruby_class(type_str)
|
211
|
-
case type_str.to_s.downcase
|
212
|
-
when 'string' then String
|
213
|
-
when 'integer' then Integer
|
214
|
-
when 'number' then Float
|
215
|
-
when 'boolean' then T::Boolean
|
216
|
-
when 'object' then Hash
|
217
|
-
when 'array' then Array
|
218
|
-
when 'date' then Date
|
219
|
-
when 'datetime' then DateTime
|
220
|
-
when 'time' then Time
|
221
|
-
else String # Default fallback
|
222
|
-
end
|
223
|
-
end
|
224
|
-
|
225
|
-
# Get all columns that should be included in the schema
|
226
|
-
#
|
227
|
-
# @return [Array<ActiveRecord::ConnectionAdapters::Column>] Filtered columns
|
228
|
-
def filtered_columns
|
229
|
-
model.columns.reject do |column|
|
230
|
-
config = EasyTalk.configuration
|
231
|
-
excluded_columns.include?(column.name.to_sym) ||
|
232
|
-
(config.exclude_primary_key && column.name == model.primary_key) ||
|
233
|
-
(config.exclude_timestamps && timestamp_column?(column.name)) ||
|
234
|
-
(config.exclude_foreign_keys && foreign_key_column?(column.name))
|
235
|
-
end
|
236
|
-
end
|
237
|
-
|
238
|
-
# Check if a column is a timestamp column
|
239
|
-
#
|
240
|
-
# @param column_name [String] The column name
|
241
|
-
# @return [Boolean] True if the column is a timestamp column
|
242
|
-
def timestamp_column?(column_name)
|
243
|
-
%w[created_at updated_at].include?(column_name)
|
244
|
-
end
|
245
|
-
|
246
|
-
# Check if a column is a foreign key column
|
247
|
-
#
|
248
|
-
# @param column_name [String] The column name
|
249
|
-
# @return [Boolean] True if the column is a foreign key column
|
250
|
-
def foreign_key_column?(column_name)
|
251
|
-
column_name.end_with?('_id')
|
252
|
-
end
|
253
|
-
|
254
|
-
# Check if an association should be excluded
|
255
|
-
#
|
256
|
-
# @param association [ActiveRecord::Reflection::AssociationReflection] The association
|
257
|
-
# @return [Boolean] True if the association should be excluded
|
258
|
-
def association_excluded?(association)
|
259
|
-
!association.klass ||
|
260
|
-
excluded_associations.include?(association.name.to_sym) ||
|
261
|
-
association.options[:polymorphic] # Skip polymorphic associations (complex to model)
|
262
|
-
end
|
263
|
-
|
264
|
-
# Get schema enhancements
|
265
|
-
#
|
266
|
-
# @return [Hash] Schema enhancements
|
267
|
-
def schema_enhancements
|
268
|
-
@schema_enhancements ||= if model.respond_to?(:schema_enhancements)
|
269
|
-
model.schema_enhancements.deep_transform_keys(&:to_s)
|
270
|
-
else
|
271
|
-
{}
|
272
|
-
end
|
273
|
-
end
|
274
|
-
|
275
|
-
# Get all excluded columns
|
276
|
-
#
|
277
|
-
# @return [Array<Symbol>] Excluded column names
|
278
|
-
def excluded_columns
|
279
|
-
@excluded_columns ||= begin
|
280
|
-
config = EasyTalk.configuration
|
281
|
-
global_exclusions = config.excluded_columns || []
|
282
|
-
model_exclusions = schema_enhancements['ignore'] || []
|
283
|
-
|
284
|
-
# Combine and convert to symbols for consistent comparison
|
285
|
-
(global_exclusions + model_exclusions).map(&:to_sym)
|
286
|
-
end
|
287
|
-
end
|
288
|
-
|
289
|
-
# Get all excluded associations
|
290
|
-
#
|
291
|
-
# @return [Array<Symbol>] Excluded association names
|
292
|
-
def excluded_associations
|
293
|
-
@excluded_associations ||= begin
|
294
|
-
model_exclusions = schema_enhancements['ignore_associations'] || []
|
295
|
-
model_exclusions.map(&:to_sym)
|
296
|
-
end
|
297
|
-
end
|
298
|
-
end
|
299
|
-
end
|