easy_talk 1.0.4 → 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/.rubocop.yml +97 -17
- data/CHANGELOG.md +66 -0
- data/README.md +316 -210
- data/easy_talk.gemspec +39 -0
- data/lib/easy_talk/builders/composition_builder.rb +12 -1
- data/lib/easy_talk/builders/integer_builder.rb +1 -0
- data/lib/easy_talk/builders/object_builder.rb +9 -23
- data/lib/easy_talk/builders/string_builder.rb +9 -0
- data/lib/easy_talk/configuration.rb +2 -8
- data/lib/easy_talk/errors_helper.rb +1 -0
- data/lib/easy_talk/model.rb +83 -56
- data/lib/easy_talk/property.rb +108 -32
- data/lib/easy_talk/schema_definition.rb +13 -18
- data/lib/easy_talk/types/composer.rb +7 -6
- data/lib/easy_talk/validation_builder.rb +327 -0
- data/lib/easy_talk/version.rb +1 -1
- metadata +28 -140
- data/lib/easy_talk/active_record_schema_builder.rb +0 -295
data/easy_talk.gemspec
ADDED
@@ -0,0 +1,39 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'lib/easy_talk/version'
|
4
|
+
|
5
|
+
Gem::Specification.new do |spec|
|
6
|
+
spec.name = 'easy_talk'
|
7
|
+
spec.version = EasyTalk::VERSION
|
8
|
+
spec.authors = ['Sergio Bayona']
|
9
|
+
spec.email = ['bayona.sergio@gmail.com']
|
10
|
+
|
11
|
+
spec.summary = 'Generate json-schema from Ruby classes with ActiveModel integration.'
|
12
|
+
spec.description = 'Generate json-schema from plain Ruby classes with ActiveModel integration for validations and serialization.'
|
13
|
+
spec.homepage = 'https://github.com/sergiobayona/easy_talk'
|
14
|
+
spec.license = 'MIT'
|
15
|
+
spec.required_ruby_version = '>= 3.2'
|
16
|
+
|
17
|
+
spec.metadata['allowed_push_host'] = 'https://rubygems.org'
|
18
|
+
|
19
|
+
spec.metadata['homepage_uri'] = spec.homepage
|
20
|
+
spec.metadata['changelog_uri'] = 'https://github.com/sergiobayona/easy_talk/blob/main/CHANGELOG.md'
|
21
|
+
|
22
|
+
# Specify which files should be added to the gem when it is released.
|
23
|
+
# The `git ls-files -z` loads the files in the RubyGem that have been added into git.
|
24
|
+
spec.files = Dir.chdir(__dir__) do
|
25
|
+
`git ls-files -z`.split("\x0").reject do |f|
|
26
|
+
(File.expand_path(f) == __FILE__) ||
|
27
|
+
f.start_with?(*%w[bin/ spec/ .git .github Gemfile])
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
spec.require_paths = ['lib']
|
32
|
+
|
33
|
+
spec.add_dependency 'activemodel', ['>= 7.0', '< 9.0']
|
34
|
+
spec.add_dependency 'activesupport', ['>= 7.0', '< 9.0']
|
35
|
+
spec.add_dependency 'js_regex', '~> 3.0'
|
36
|
+
spec.add_dependency 'sorbet-runtime', '~> 0.5'
|
37
|
+
|
38
|
+
spec.metadata['rubygems_mfa_required'] = 'true'
|
39
|
+
end
|
@@ -50,7 +50,18 @@ module EasyTalk
|
|
50
50
|
# @return [Array<Hash>] The array of schemas.
|
51
51
|
def schemas
|
52
52
|
items.map do |type|
|
53
|
-
type.respond_to?(:schema)
|
53
|
+
if type.respond_to?(:schema)
|
54
|
+
type.schema
|
55
|
+
else
|
56
|
+
# Map Float type to 'number' in JSON Schema
|
57
|
+
json_type = case type.to_s
|
58
|
+
when 'Float', 'BigDecimal'
|
59
|
+
'number'
|
60
|
+
else
|
61
|
+
type.to_s.downcase
|
62
|
+
end
|
63
|
+
{ type: json_type }
|
64
|
+
end
|
54
65
|
end
|
55
66
|
end
|
56
67
|
|
@@ -8,7 +8,7 @@ module EasyTalk
|
|
8
8
|
# ObjectBuilder is responsible for turning a SchemaDefinition of an "object" type
|
9
9
|
# into a validated JSON Schema hash. It:
|
10
10
|
#
|
11
|
-
# 1) Recursively processes the schema
|
11
|
+
# 1) Recursively processes the schema's :properties,
|
12
12
|
# 2) Determines which properties are required (unless optional),
|
13
13
|
# 3) Handles sub-schema composition (allOf, anyOf, oneOf, not),
|
14
14
|
# 4) Produces the final object-level schema hash.
|
@@ -55,7 +55,7 @@ module EasyTalk
|
|
55
55
|
|
56
56
|
##
|
57
57
|
# Main aggregator: merges the top-level schema keys (like :properties, :subschemas)
|
58
|
-
# into a single hash that we
|
58
|
+
# into a single hash that we'll feed to BaseBuilder.
|
59
59
|
def build_options_hash
|
60
60
|
# Start with a copy of the raw schema
|
61
61
|
merged = @original_schema.dup
|
@@ -119,9 +119,7 @@ module EasyTalk
|
|
119
119
|
return true if prop_options.dig(:constraints, :optional)
|
120
120
|
|
121
121
|
# Check for nil_optional config to determine if nilable should also mean optional
|
122
|
-
if prop_options[:type].respond_to?(:nilable?) && prop_options[:type].nilable?
|
123
|
-
return EasyTalk.configuration.nilable_is_optional
|
124
|
-
end
|
122
|
+
return EasyTalk.configuration.nilable_is_optional if prop_options[:type].respond_to?(:nilable?) && prop_options[:type].nilable?
|
125
123
|
|
126
124
|
false
|
127
125
|
end
|
@@ -134,24 +132,12 @@ module EasyTalk
|
|
134
132
|
@property_cache ||= {}
|
135
133
|
|
136
134
|
# Memoize so we only build each property once
|
137
|
-
@property_cache[prop_name] ||=
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
# Normal property: e.g. { type: String, constraints: {...} }
|
144
|
-
Property.new(prop_name, prop_options[:type], constraints)
|
145
|
-
end
|
146
|
-
end
|
147
|
-
|
148
|
-
##
|
149
|
-
# Build a child schema by calling another ObjectBuilder on the nested SchemaDefinition.
|
150
|
-
#
|
151
|
-
def nested_schema_builder(prop_options)
|
152
|
-
child_schema_def = prop_options[:properties]
|
153
|
-
# If user used T.nilable(...) with a block, unwrap the nilable
|
154
|
-
ObjectBuilder.new(child_schema_def).build
|
135
|
+
@property_cache[prop_name] ||= begin
|
136
|
+
# Remove optional constraints from the property
|
137
|
+
constraints = prop_options[:constraints].except(:optional)
|
138
|
+
# Normal property: e.g. { type: String, constraints: {...} }
|
139
|
+
Property.new(prop_name, prop_options[:type], constraints)
|
140
|
+
end
|
155
141
|
end
|
156
142
|
|
157
143
|
##
|
@@ -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,18 +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
|
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 # New option, defaulting to true
|
14
|
-
@exclude_timestamps = true # New option, defaulting to true
|
15
8
|
@default_additional_properties = false
|
16
9
|
@nilable_is_optional = false
|
10
|
+
@auto_validations = true
|
17
11
|
end
|
18
12
|
end
|
19
13
|
|
@@ -1,6 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
module EasyTalk
|
4
|
+
# Helper module for generating consistent error messages
|
4
5
|
module ErrorHelper
|
5
6
|
def self.raise_constraint_error(property_name:, constraint_name:, expected:, got:)
|
6
7
|
message = "Error in property '#{property_name}': Constraint '#{constraint_name}' expects #{expected}, " \
|
data/lib/easy_talk/model.rb
CHANGED
@@ -9,7 +9,7 @@ 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 '
|
12
|
+
require_relative 'validation_builder'
|
13
13
|
|
14
14
|
module EasyTalk
|
15
15
|
# The `Model` module is a mixin that provides functionality for defining and accessing the schema of a model.
|
@@ -35,22 +35,43 @@ module EasyTalk
|
|
35
35
|
# @see SchemaDefinition
|
36
36
|
module Model
|
37
37
|
def self.included(base)
|
38
|
-
base.
|
38
|
+
base.extend(ClassMethods)
|
39
|
+
|
40
|
+
base.include ActiveModel::API
|
39
41
|
base.include ActiveModel::Validations
|
40
42
|
base.extend ActiveModel::Callbacks
|
41
|
-
base.extend(ClassMethods)
|
42
43
|
base.include(InstanceMethods)
|
43
|
-
|
44
|
-
# Apply ActiveRecord-specific functionality if appropriate
|
45
|
-
return unless defined?(ActiveRecord) && base.ancestors.include?(ActiveRecord::Base)
|
46
|
-
|
47
|
-
base.extend(ActiveRecordClassMethods)
|
48
44
|
end
|
49
45
|
|
46
|
+
# Instance methods mixed into models that include EasyTalk::Model
|
50
47
|
module InstanceMethods
|
51
48
|
def initialize(attributes = {})
|
52
49
|
@additional_properties = {}
|
53
|
-
super
|
50
|
+
super # Perform initial mass assignment
|
51
|
+
|
52
|
+
# After initial assignment, instantiate nested EasyTalk::Model objects
|
53
|
+
schema_def = self.class.schema_definition
|
54
|
+
|
55
|
+
# Only proceed if we have a valid schema definition
|
56
|
+
return unless schema_def.respond_to?(:schema) && schema_def.schema.is_a?(Hash)
|
57
|
+
|
58
|
+
(schema_def.schema[:properties] || {}).each do |prop_name, prop_definition|
|
59
|
+
# Get the defined type and the currently assigned value
|
60
|
+
defined_type = prop_definition[:type]
|
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
|
67
|
+
|
68
|
+
# Check if the type is another EasyTalk::Model and the value is a Hash
|
69
|
+
next unless defined_type.is_a?(Class) && defined_type.include?(EasyTalk::Model) && current_value.is_a?(Hash)
|
70
|
+
|
71
|
+
# Instantiate the nested model and assign it back
|
72
|
+
nested_instance = defined_type.new(current_value)
|
73
|
+
public_send("#{prop_name}=", nested_instance)
|
74
|
+
end
|
54
75
|
end
|
55
76
|
|
56
77
|
def method_missing(method_name, *args)
|
@@ -77,9 +98,10 @@ module EasyTalk
|
|
77
98
|
|
78
99
|
# Add to_hash method to convert defined properties to hash
|
79
100
|
def to_hash
|
80
|
-
|
101
|
+
properties_to_include = (self.class.schema_definition.schema[:properties] || {}).keys
|
102
|
+
return {} if properties_to_include.empty?
|
81
103
|
|
82
|
-
|
104
|
+
properties_to_include.each_with_object({}) do |prop, hash|
|
83
105
|
hash[prop.to_s] = send(prop)
|
84
106
|
end
|
85
107
|
end
|
@@ -88,6 +110,28 @@ module EasyTalk
|
|
88
110
|
def as_json(_options = {})
|
89
111
|
to_hash.merge(@additional_properties)
|
90
112
|
end
|
113
|
+
|
114
|
+
# to_h includes both defined and additional properties
|
115
|
+
def to_h
|
116
|
+
to_hash.merge(@additional_properties)
|
117
|
+
end
|
118
|
+
|
119
|
+
# Allow comparison with hashes
|
120
|
+
def ==(other)
|
121
|
+
case other
|
122
|
+
when Hash
|
123
|
+
# Convert both to comparable format for comparison
|
124
|
+
self_hash = (self.class.schema_definition.schema[:properties] || {}).keys.each_with_object({}) do |prop, hash|
|
125
|
+
hash[prop] = send(prop)
|
126
|
+
end
|
127
|
+
|
128
|
+
# Handle both symbol and string keys in the other hash
|
129
|
+
other_normalized = other.transform_keys(&:to_sym)
|
130
|
+
self_hash == other_normalized
|
131
|
+
else
|
132
|
+
super
|
133
|
+
end
|
134
|
+
end
|
91
135
|
end
|
92
136
|
|
93
137
|
# Module containing class-level methods for defining and accessing the schema of a model.
|
@@ -97,13 +141,8 @@ module EasyTalk
|
|
97
141
|
# @return [Schema] The schema for the model.
|
98
142
|
def schema
|
99
143
|
@schema ||= if defined?(@schema_definition) && @schema_definition
|
100
|
-
# Schema defined explicitly via define_schema
|
101
144
|
build_schema(@schema_definition)
|
102
|
-
elsif respond_to?(:active_record_schema_definition)
|
103
|
-
# ActiveRecord model without explicit schema definition
|
104
|
-
build_schema(active_record_schema_definition)
|
105
145
|
else
|
106
|
-
# Default case - empty schema
|
107
146
|
{}
|
108
147
|
end
|
109
148
|
end
|
@@ -115,14 +154,6 @@ module EasyTalk
|
|
115
154
|
"#/$defs/#{name}"
|
116
155
|
end
|
117
156
|
|
118
|
-
def properties
|
119
|
-
@properties ||= begin
|
120
|
-
return unless schema[:properties].present?
|
121
|
-
|
122
|
-
schema[:properties].keys.map(&:to_sym)
|
123
|
-
end
|
124
|
-
end
|
125
|
-
|
126
157
|
# Returns the JSON schema for the model.
|
127
158
|
#
|
128
159
|
# @return [Hash] The JSON schema for the model.
|
@@ -134,12 +165,30 @@ module EasyTalk
|
|
134
165
|
#
|
135
166
|
# @yield The block to define the schema.
|
136
167
|
# @raise [ArgumentError] If the class does not have a name.
|
137
|
-
def define_schema(&
|
168
|
+
def define_schema(&)
|
138
169
|
raise ArgumentError, 'The class must have a name' unless name.present?
|
139
170
|
|
140
171
|
@schema_definition = SchemaDefinition.new(name)
|
141
|
-
@schema_definition.
|
142
|
-
|
172
|
+
@schema_definition.klass = self # Pass the model class to the schema definition
|
173
|
+
@schema_definition.instance_eval(&)
|
174
|
+
|
175
|
+
# Define accessors immediately based on schema_definition
|
176
|
+
defined_properties = (@schema_definition.schema[:properties] || {}).keys
|
177
|
+
attr_accessor(*defined_properties)
|
178
|
+
|
179
|
+
# Track which properties have had validations applied
|
180
|
+
@validated_properties ||= Set.new
|
181
|
+
|
182
|
+
# Apply auto-validations immediately after definition
|
183
|
+
if EasyTalk.configuration.auto_validations
|
184
|
+
(@schema_definition.schema[:properties] || {}).each do |prop_name, prop_def|
|
185
|
+
# Only apply validations if they haven't been applied yet
|
186
|
+
unless @validated_properties.include?(prop_name)
|
187
|
+
ValidationBuilder.build_validations(self, prop_name, prop_def[:type], prop_def[:constraints])
|
188
|
+
@validated_properties.add(prop_name)
|
189
|
+
end
|
190
|
+
end
|
191
|
+
end
|
143
192
|
|
144
193
|
@schema_definition
|
145
194
|
end
|
@@ -155,6 +204,13 @@ module EasyTalk
|
|
155
204
|
@schema_definition&.schema&.fetch(:additional_properties, false)
|
156
205
|
end
|
157
206
|
|
207
|
+
# Returns the property names defined in the schema
|
208
|
+
#
|
209
|
+
# @return [Array<Symbol>] Array of property names as symbols
|
210
|
+
def properties
|
211
|
+
(@schema_definition&.schema&.dig(:properties) || {}).keys
|
212
|
+
end
|
213
|
+
|
158
214
|
# Builds the schema using the provided schema definition.
|
159
215
|
# This is the convergence point for all schema generation.
|
160
216
|
#
|
@@ -164,34 +220,5 @@ module EasyTalk
|
|
164
220
|
Builders::ObjectBuilder.new(schema_definition).build
|
165
221
|
end
|
166
222
|
end
|
167
|
-
|
168
|
-
# Module containing ActiveRecord-specific methods for schema generation
|
169
|
-
module ActiveRecordClassMethods
|
170
|
-
# Gets a SchemaDefinition that's built from the ActiveRecord database schema
|
171
|
-
#
|
172
|
-
# @return [SchemaDefinition] A schema definition built from the database
|
173
|
-
def active_record_schema_definition
|
174
|
-
@active_record_schema_definition ||= ActiveRecordSchemaBuilder.new(self).build_schema_definition
|
175
|
-
end
|
176
|
-
|
177
|
-
# Store enhancements to be applied to the schema
|
178
|
-
#
|
179
|
-
# @return [Hash] The schema enhancements
|
180
|
-
def schema_enhancements
|
181
|
-
@schema_enhancements ||= {}
|
182
|
-
end
|
183
|
-
|
184
|
-
# Enhance the generated schema with additional information
|
185
|
-
#
|
186
|
-
# @param enhancements [Hash] The schema enhancements
|
187
|
-
# @return [void]
|
188
|
-
def enhance_schema(enhancements)
|
189
|
-
@schema_enhancements = enhancements
|
190
|
-
# Clear cached values to force regeneration
|
191
|
-
@active_record_schema_definition = nil
|
192
|
-
@schema = nil
|
193
|
-
@json_schema = nil
|
194
|
-
end
|
195
|
-
end
|
196
223
|
end
|
197
224
|
end
|
data/lib/easy_talk/property.rb
CHANGED
@@ -11,30 +11,51 @@ require_relative 'builders/composition_builder'
|
|
11
11
|
require_relative 'builders/typed_array_builder'
|
12
12
|
require_relative 'builders/union_builder'
|
13
13
|
|
14
|
-
#
|
15
|
-
|
16
|
-
#
|
14
|
+
# EasyTalk module provides a DSL for building JSON Schema definitions.
|
15
|
+
#
|
16
|
+
# This module contains classes and utilities for easily creating valid JSON Schema
|
17
|
+
# documents with a Ruby-native syntax. The `Property` class serves as the main entry
|
18
|
+
# point for defining schema properties.
|
17
19
|
#
|
18
|
-
#
|
19
|
-
#
|
20
|
+
# @example Basic property definition
|
21
|
+
# property = EasyTalk::Property.new(:name, String, minLength: 3, maxLength: 50)
|
22
|
+
# property.build # => {"type"=>"string", "minLength"=>3, "maxLength"=>50}
|
20
23
|
#
|
21
|
-
#
|
22
|
-
#
|
23
|
-
#
|
24
|
+
# @example Using with nilable types
|
25
|
+
# nilable_prop = EasyTalk::Property.new(:optional_field, T::Types::Union.new(String, NilClass))
|
26
|
+
# nilable_prop.build # => {"type"=>["string", "null"]}
|
24
27
|
#
|
25
28
|
# @see EasyTalk::Property
|
29
|
+
# @see https://json-schema.org/ JSON Schema Documentation
|
26
30
|
module EasyTalk
|
27
31
|
# Property class for building a JSON schema property.
|
32
|
+
#
|
33
|
+
# This class handles the conversion from Ruby types to JSON Schema property definitions,
|
34
|
+
# and provides support for common constraints like minimum/maximum values, string patterns,
|
35
|
+
# and custom validators.
|
28
36
|
class Property
|
29
37
|
extend T::Sig
|
30
|
-
attr_reader :name, :type, :constraints
|
31
38
|
|
39
|
+
# @return [Symbol] The name of the property
|
40
|
+
attr_reader :name
|
41
|
+
|
42
|
+
# @return [Object] The type definition of the property
|
43
|
+
attr_reader :type
|
44
|
+
|
45
|
+
# @return [Hash<Symbol, Object>] Additional constraints applied to the property
|
46
|
+
attr_reader :constraints
|
47
|
+
|
48
|
+
# Mapping of Ruby type names to their corresponding schema builder classes.
|
49
|
+
# Each builder knows how to convert a specific Ruby type to JSON Schema.
|
50
|
+
#
|
51
|
+
# @api private
|
32
52
|
TYPE_TO_BUILDER = {
|
33
53
|
'String' => Builders::StringBuilder,
|
34
54
|
'Integer' => Builders::IntegerBuilder,
|
35
55
|
'Float' => Builders::NumberBuilder,
|
36
56
|
'BigDecimal' => Builders::NumberBuilder,
|
37
57
|
'T::Boolean' => Builders::BooleanBuilder,
|
58
|
+
'TrueClass' => Builders::BooleanBuilder,
|
38
59
|
'NilClass' => Builders::NullBuilder,
|
39
60
|
'Date' => Builders::TemporalBuilder::DateBuilder,
|
40
61
|
'DateTime' => Builders::TemporalBuilder::DatetimeBuilder,
|
@@ -47,10 +68,19 @@ module EasyTalk
|
|
47
68
|
}.freeze
|
48
69
|
|
49
70
|
# Initializes a new instance of the Property class.
|
50
|
-
#
|
51
|
-
# @param
|
52
|
-
# @param
|
53
|
-
# @
|
71
|
+
#
|
72
|
+
# @param name [Symbol] The name of the property
|
73
|
+
# @param type [Object] The type of the property (Ruby class, string name, or Sorbet type)
|
74
|
+
# @param constraints [Hash<Symbol, Object>] Additional constraints for the property
|
75
|
+
# (e.g., minLength, pattern, format)
|
76
|
+
#
|
77
|
+
# @example String property with constraints
|
78
|
+
# Property.new(:username, 'String', minLength: 3, maxLength: 20, pattern: '^[a-z0-9_]+$')
|
79
|
+
#
|
80
|
+
# @example Integer property with range
|
81
|
+
# Property.new(:age, 'Integer', minimum: 0, maximum: 120)
|
82
|
+
#
|
83
|
+
# @raise [ArgumentError] If the property type is missing or empty
|
54
84
|
sig do
|
55
85
|
params(name: Symbol, type: T.any(String, Object),
|
56
86
|
constraints: T::Hash[Symbol, T.untyped]).void
|
@@ -59,18 +89,31 @@ module EasyTalk
|
|
59
89
|
@name = name
|
60
90
|
@type = type
|
61
91
|
@constraints = constraints
|
62
|
-
|
92
|
+
if type.nil? || (type.respond_to?(:empty?) && type.is_a?(String) && type.strip.empty?)
|
93
|
+
raise ArgumentError,
|
94
|
+
'property type is missing'
|
95
|
+
end
|
96
|
+
raise ArgumentError, 'property type is not supported' if type.is_a?(Array) && type.empty?
|
63
97
|
end
|
64
98
|
|
65
|
-
# Builds the property based on
|
99
|
+
# Builds the property schema based on its type and constraints.
|
66
100
|
#
|
67
|
-
#
|
68
|
-
#
|
101
|
+
# This method handles different types of properties:
|
102
|
+
# - Nilable types (can be null)
|
103
|
+
# - Types with dedicated builders
|
104
|
+
# - Types that implement their own schema method
|
105
|
+
# - Default fallback to 'object' type
|
69
106
|
#
|
70
|
-
#
|
71
|
-
# The arguments passed to the builder depend on whether the builder is a collection type or not.
|
107
|
+
# @return [Hash] The complete JSON Schema property definition
|
72
108
|
#
|
73
|
-
# @
|
109
|
+
# @example Simple string property
|
110
|
+
# property = Property.new(:name, 'String')
|
111
|
+
# property.build # => {"type"=>"string"}
|
112
|
+
#
|
113
|
+
# @example Complex nested schema
|
114
|
+
# address = Address.new # A class with a .schema method
|
115
|
+
# property = Property.new(:shipping_address, address, description: "Shipping address")
|
116
|
+
# property.build # => Address schema merged with the description constraint
|
74
117
|
def build
|
75
118
|
if nilable_type?
|
76
119
|
build_nilable_schema
|
@@ -86,43 +129,76 @@ module EasyTalk
|
|
86
129
|
end
|
87
130
|
end
|
88
131
|
|
89
|
-
# Converts the
|
132
|
+
# Converts the property definition to a JSON-compatible format.
|
133
|
+
#
|
134
|
+
# This method enables seamless integration with Ruby's JSON library.
|
135
|
+
#
|
136
|
+
# @param _args [Array] Optional arguments passed to #as_json (ignored)
|
137
|
+
# @return [Hash] The JSON-compatible representation of the property schema
|
90
138
|
#
|
91
|
-
# @
|
92
|
-
# @
|
139
|
+
# @see #build
|
140
|
+
# @see https://ruby-doc.org/stdlib-2.7.2/libdoc/json/rdoc/JSON.html#as_json-method
|
93
141
|
def as_json(*_args)
|
94
142
|
build.as_json
|
95
143
|
end
|
96
144
|
|
97
|
-
# Returns the builder associated with the property type.
|
145
|
+
# Returns the builder class associated with the property type.
|
98
146
|
#
|
99
|
-
# The builder is responsible for
|
100
|
-
# It looks up the builder based on the type's class name or name.
|
147
|
+
# The builder is responsible for converting the Ruby type to a JSON Schema type.
|
101
148
|
#
|
102
|
-
# @return [
|
149
|
+
# @return [Class, nil] The builder class for this property's type, or nil if no dedicated builder exists
|
150
|
+
# @see #find_builder_for_type
|
103
151
|
def builder
|
104
|
-
@builder ||=
|
152
|
+
@builder ||= find_builder_for_type
|
105
153
|
end
|
106
154
|
|
107
155
|
private
|
108
156
|
|
157
|
+
# Finds the appropriate builder for the current type.
|
158
|
+
#
|
159
|
+
# First checks if there's a builder for the class name, then falls back
|
160
|
+
# to checking if there's a builder for the type's name (if it responds to :name).
|
161
|
+
#
|
162
|
+
# @return [Class, nil] The builder class for this type, or nil if none matches
|
163
|
+
# @api private
|
164
|
+
def find_builder_for_type
|
165
|
+
TYPE_TO_BUILDER[type.class.name.to_s] ||
|
166
|
+
(type.respond_to?(:name) ? TYPE_TO_BUILDER[type.name.to_s] : nil)
|
167
|
+
end
|
168
|
+
|
169
|
+
# Determines if the type is nilable (can be nil).
|
170
|
+
#
|
171
|
+
# A type is nilable if it's a union type that includes NilClass.
|
172
|
+
# This is typically represented as T.nilable(Type) in Sorbet.
|
173
|
+
#
|
174
|
+
# @return [Boolean] true if the type is nilable, false otherwise
|
175
|
+
# @api private
|
109
176
|
def nilable_type?
|
110
|
-
return unless type.respond_to?(:types)
|
111
|
-
return unless type.types.all? { |t| t.respond_to?(:raw_type) }
|
177
|
+
return false unless type.respond_to?(:types)
|
178
|
+
return false unless type.types.all? { |t| t.respond_to?(:raw_type) }
|
112
179
|
|
113
180
|
type.types.any? { |t| t.raw_type == NilClass }
|
114
181
|
end
|
115
182
|
|
183
|
+
# Builds a schema for a nilable type, which can be either the actual type or null.
|
184
|
+
#
|
185
|
+
# @return [Hash] A schema with both the actual type and null type
|
186
|
+
# @api private
|
187
|
+
# @example
|
188
|
+
# # For a T.nilable(String) type:
|
189
|
+
# {"type"=>["string", "null"]}
|
116
190
|
def build_nilable_schema
|
117
191
|
# Extract the non-nil type from the Union
|
118
|
-
actual_type = type
|
192
|
+
actual_type = T::Utils::Nilable.get_underlying_type(type)
|
193
|
+
|
194
|
+
return { type: 'null' } unless actual_type
|
119
195
|
|
120
196
|
# Create a property with the actual type
|
121
197
|
non_nil_schema = Property.new(name, actual_type, constraints).build
|
122
198
|
|
123
199
|
# Merge the types into an array
|
124
200
|
non_nil_schema.merge(
|
125
|
-
type: [non_nil_schema[:type], 'null']
|
201
|
+
type: [non_nil_schema[:type], 'null'].compact
|
126
202
|
)
|
127
203
|
end
|
128
204
|
end
|