easy_talk 1.0.3 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.rubocop.yml +97 -17
- data/CHANGELOG.md +51 -0
- data/README.md +496 -60
- data/easy_talk.gemspec +38 -0
- data/lib/easy_talk/active_record_schema_builder.rb +8 -1
- data/lib/easy_talk/builders/composition_builder.rb +24 -1
- data/lib/easy_talk/builders/object_builder.rb +11 -20
- data/lib/easy_talk/builders/temporal_builder.rb +49 -0
- data/lib/easy_talk/configuration.rb +6 -3
- data/lib/easy_talk/errors_helper.rb +1 -0
- data/lib/easy_talk/model.rb +75 -14
- data/lib/easy_talk/property.rb +116 -44
- data/lib/easy_talk/schema_definition.rb +28 -16
- data/lib/easy_talk/types/composer.rb +89 -0
- data/lib/easy_talk/validation_builder.rb +339 -0
- data/lib/easy_talk/version.rb +1 -1
- data/lib/easy_talk.rb +5 -3
- metadata +13 -158
- data/lib/easy_talk/builders/all_of_builder.rb +0 -11
- data/lib/easy_talk/builders/any_of_builder.rb +0 -11
- data/lib/easy_talk/builders/date_builder.rb +0 -18
- data/lib/easy_talk/builders/datetime_builder.rb +0 -18
- data/lib/easy_talk/builders/one_of_builder.rb +0 -11
- data/lib/easy_talk/builders/time_builder.rb +0 -16
- data/lib/easy_talk/types/all_of.rb +0 -32
- data/lib/easy_talk/types/any_of.rb +0 -39
- data/lib/easy_talk/types/one_of.rb +0 -31
data/easy_talk.gemspec
ADDED
@@ -0,0 +1,38 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'lib/easy_talk/version'
|
4
|
+
|
5
|
+
Gem::Specification.new do |spec|
|
6
|
+
spec.name = 'easy_talk'
|
7
|
+
spec.version = EasyTalk::VERSION
|
8
|
+
spec.authors = ['Sergio Bayona']
|
9
|
+
spec.email = ['bayona.sergio@gmail.com']
|
10
|
+
|
11
|
+
spec.summary = 'Generate json-schema from Ruby classes.'
|
12
|
+
spec.description = 'Generate json-schema from plain Ruby classes.'
|
13
|
+
spec.homepage = 'https://github.com/sergiobayona/easy_talk'
|
14
|
+
spec.license = 'MIT'
|
15
|
+
spec.required_ruby_version = '>= 3.2'
|
16
|
+
|
17
|
+
spec.metadata['allowed_push_host'] = 'https://rubygems.org'
|
18
|
+
|
19
|
+
spec.metadata['homepage_uri'] = spec.homepage
|
20
|
+
spec.metadata['changelog_uri'] = 'https://github.com/sergiobayona/easy_talk/blob/main/CHANGELOG.md'
|
21
|
+
|
22
|
+
# Specify which files should be added to the gem when it is released.
|
23
|
+
# The `git ls-files -z` loads the files in the RubyGem that have been added into git.
|
24
|
+
spec.files = Dir.chdir(__dir__) do
|
25
|
+
`git ls-files -z`.split("\x0").reject do |f|
|
26
|
+
(File.expand_path(f) == __FILE__) ||
|
27
|
+
f.start_with?(*%w[bin/ spec/ .git .github Gemfile])
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
spec.require_paths = ['lib']
|
32
|
+
|
33
|
+
spec.add_dependency 'activemodel', '~> 7.0'
|
34
|
+
spec.add_dependency 'activesupport', '~> 7.0'
|
35
|
+
spec.add_dependency 'sorbet-runtime', '~> 0.5'
|
36
|
+
|
37
|
+
spec.metadata['rubygems_mfa_required'] = 'true'
|
38
|
+
end
|
@@ -98,6 +98,9 @@ module EasyTalk
|
|
98
98
|
# Map the database type to Ruby type
|
99
99
|
ruby_type = COLUMN_TYPE_MAP.fetch(column.type, String)
|
100
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
|
+
|
101
104
|
# Build constraints hash for this column
|
102
105
|
constraints = build_column_constraints(column, column_enhancements)
|
103
106
|
|
@@ -133,7 +136,11 @@ module EasyTalk
|
|
133
136
|
end
|
134
137
|
|
135
138
|
# Add default value if present and not a proc
|
136
|
-
|
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
|
137
144
|
|
138
145
|
# Remove nil values
|
139
146
|
constraints.compact
|
@@ -50,7 +50,18 @@ module EasyTalk
|
|
50
50
|
# @return [Array<Hash>] The array of schemas.
|
51
51
|
def schemas
|
52
52
|
items.map do |type|
|
53
|
-
type.respond_to?(:schema)
|
53
|
+
if type.respond_to?(:schema)
|
54
|
+
type.schema
|
55
|
+
else
|
56
|
+
# Map Float type to 'number' in JSON Schema
|
57
|
+
json_type = case type.to_s
|
58
|
+
when 'Float', 'BigDecimal'
|
59
|
+
'number'
|
60
|
+
else
|
61
|
+
type.to_s.downcase
|
62
|
+
end
|
63
|
+
{ type: json_type }
|
64
|
+
end
|
54
65
|
end
|
55
66
|
end
|
56
67
|
|
@@ -60,6 +71,18 @@ module EasyTalk
|
|
60
71
|
def items
|
61
72
|
@type.items
|
62
73
|
end
|
74
|
+
|
75
|
+
# Builder class for AllOf composition.
|
76
|
+
class AllOfBuilder < CompositionBuilder
|
77
|
+
end
|
78
|
+
|
79
|
+
# Builder class for AnyOf composition.
|
80
|
+
class AnyOfBuilder < CompositionBuilder
|
81
|
+
end
|
82
|
+
|
83
|
+
# Builder class for OneOf composition.
|
84
|
+
class OneOfBuilder < CompositionBuilder
|
85
|
+
end
|
63
86
|
end
|
64
87
|
end
|
65
88
|
end
|
@@ -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
|
@@ -118,6 +118,9 @@ module EasyTalk
|
|
118
118
|
# Check constraints[:optional]
|
119
119
|
return true if prop_options.dig(:constraints, :optional)
|
120
120
|
|
121
|
+
# Check for nil_optional config to determine if nilable should also mean optional
|
122
|
+
return EasyTalk.configuration.nilable_is_optional if prop_options[:type].respond_to?(:nilable?) && prop_options[:type].nilable?
|
123
|
+
|
121
124
|
false
|
122
125
|
end
|
123
126
|
|
@@ -129,24 +132,12 @@ module EasyTalk
|
|
129
132
|
@property_cache ||= {}
|
130
133
|
|
131
134
|
# Memoize so we only build each property once
|
132
|
-
@property_cache[prop_name] ||=
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
# Normal property: e.g. { type: String, constraints: {...} }
|
139
|
-
Property.new(prop_name, prop_options[:type], constraints)
|
140
|
-
end
|
141
|
-
end
|
142
|
-
|
143
|
-
##
|
144
|
-
# Build a child schema by calling another ObjectBuilder on the nested SchemaDefinition.
|
145
|
-
#
|
146
|
-
def nested_schema_builder(prop_options)
|
147
|
-
child_schema_def = prop_options[:properties]
|
148
|
-
# If user used T.nilable(...) with a block, unwrap the nilable
|
149
|
-
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
|
150
141
|
end
|
151
142
|
|
152
143
|
##
|
@@ -0,0 +1,49 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'string_builder'
|
4
|
+
|
5
|
+
module EasyTalk
|
6
|
+
module Builders
|
7
|
+
# Builder class for temporal properties (date, datetime, time).
|
8
|
+
class TemporalBuilder < StringBuilder
|
9
|
+
# Initializes a new instance of the TemporalBuilder class.
|
10
|
+
#
|
11
|
+
# @param property_name [Symbol] The name of the property.
|
12
|
+
# @param options [Hash] The options for the builder.
|
13
|
+
# @param format [String] The format of the temporal property (date, date-time, time).
|
14
|
+
def initialize(property_name, options = {}, format = nil)
|
15
|
+
super(property_name, options)
|
16
|
+
@format = format
|
17
|
+
end
|
18
|
+
|
19
|
+
# Modifies the schema to include the format constraint for a temporal property.
|
20
|
+
sig { returns(T::Hash[Symbol, T.untyped]) }
|
21
|
+
def schema
|
22
|
+
super.tap do |schema|
|
23
|
+
schema[:format] = @format if @format
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
# Builder class for date properties.
|
28
|
+
class DateBuilder < TemporalBuilder
|
29
|
+
def initialize(property_name, options = {})
|
30
|
+
super(property_name, options, 'date')
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
# Builder class for datetime properties.
|
35
|
+
class DatetimeBuilder < TemporalBuilder
|
36
|
+
def initialize(property_name, options = {})
|
37
|
+
super(property_name, options, 'date-time')
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
# Builder class for time properties.
|
42
|
+
class TimeBuilder < TemporalBuilder
|
43
|
+
def initialize(property_name, options = {})
|
44
|
+
super(property_name, options, 'time')
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
@@ -3,15 +3,18 @@
|
|
3
3
|
module EasyTalk
|
4
4
|
class Configuration
|
5
5
|
attr_accessor :exclude_foreign_keys, :exclude_associations, :excluded_columns,
|
6
|
-
:exclude_primary_key, :exclude_timestamps, :default_additional_properties
|
6
|
+
:exclude_primary_key, :exclude_timestamps, :default_additional_properties,
|
7
|
+
:nilable_is_optional, :auto_validations
|
7
8
|
|
8
9
|
def initialize
|
9
10
|
@exclude_foreign_keys = true
|
10
11
|
@exclude_associations = true
|
11
12
|
@excluded_columns = []
|
12
|
-
@exclude_primary_key = true
|
13
|
-
@exclude_timestamps = true
|
13
|
+
@exclude_primary_key = true
|
14
|
+
@exclude_timestamps = true
|
14
15
|
@default_additional_properties = false
|
16
|
+
@nilable_is_optional = false
|
17
|
+
@auto_validations = true # New option: enable validations by default
|
15
18
|
end
|
16
19
|
end
|
17
20
|
|
@@ -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
@@ -10,6 +10,7 @@ require 'active_model'
|
|
10
10
|
require_relative 'builders/object_builder'
|
11
11
|
require_relative 'schema_definition'
|
12
12
|
require_relative 'active_record_schema_builder'
|
13
|
+
require_relative 'validation_builder'
|
13
14
|
|
14
15
|
module EasyTalk
|
15
16
|
# The `Model` module is a mixin that provides functionality for defining and accessing the schema of a model.
|
@@ -47,10 +48,35 @@ module EasyTalk
|
|
47
48
|
base.extend(ActiveRecordClassMethods)
|
48
49
|
end
|
49
50
|
|
51
|
+
# Instance methods mixed into models that include EasyTalk::Model
|
50
52
|
module InstanceMethods
|
51
53
|
def initialize(attributes = {})
|
52
54
|
@additional_properties = {}
|
53
|
-
super
|
55
|
+
super # Perform initial mass assignment
|
56
|
+
|
57
|
+
# After initial assignment, instantiate nested EasyTalk::Model objects
|
58
|
+
# Get the appropriate schema definition based on model type
|
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
|
64
|
+
|
65
|
+
# Only proceed if we have a valid schema definition
|
66
|
+
return unless schema_def.respond_to?(:schema) && schema_def.schema.is_a?(Hash)
|
67
|
+
|
68
|
+
(schema_def.schema[:properties] || {}).each do |prop_name, prop_definition|
|
69
|
+
# Get the defined type and the currently assigned value
|
70
|
+
defined_type = prop_definition[:type]
|
71
|
+
current_value = public_send(prop_name)
|
72
|
+
|
73
|
+
# Check if the type is another EasyTalk::Model and the value is a Hash
|
74
|
+
next unless defined_type.is_a?(Class) && defined_type.include?(EasyTalk::Model) && current_value.is_a?(Hash)
|
75
|
+
|
76
|
+
# Instantiate the nested model and assign it back
|
77
|
+
nested_instance = defined_type.new(current_value)
|
78
|
+
public_send("#{prop_name}=", nested_instance)
|
79
|
+
end
|
54
80
|
end
|
55
81
|
|
56
82
|
def method_missing(method_name, *args)
|
@@ -77,9 +103,10 @@ module EasyTalk
|
|
77
103
|
|
78
104
|
# Add to_hash method to convert defined properties to hash
|
79
105
|
def to_hash
|
80
|
-
|
106
|
+
properties_to_include = (self.class.schema_definition.schema[:properties] || {}).keys
|
107
|
+
return {} if properties_to_include.empty?
|
81
108
|
|
82
|
-
|
109
|
+
properties_to_include.each_with_object({}) do |prop, hash|
|
83
110
|
hash[prop.to_s] = send(prop)
|
84
111
|
end
|
85
112
|
end
|
@@ -88,6 +115,23 @@ module EasyTalk
|
|
88
115
|
def as_json(_options = {})
|
89
116
|
to_hash.merge(@additional_properties)
|
90
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.
|
@@ -115,14 +159,6 @@ module EasyTalk
|
|
115
159
|
"#/$defs/#{name}"
|
116
160
|
end
|
117
161
|
|
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
162
|
# Returns the JSON schema for the model.
|
127
163
|
#
|
128
164
|
# @return [Hash] The JSON schema for the model.
|
@@ -134,12 +170,30 @@ module EasyTalk
|
|
134
170
|
#
|
135
171
|
# @yield The block to define the schema.
|
136
172
|
# @raise [ArgumentError] If the class does not have a name.
|
137
|
-
def define_schema(&
|
173
|
+
def define_schema(&)
|
138
174
|
raise ArgumentError, 'The class must have a name' unless name.present?
|
139
175
|
|
140
176
|
@schema_definition = SchemaDefinition.new(name)
|
141
|
-
@schema_definition.
|
142
|
-
|
177
|
+
@schema_definition.klass = self # Pass the model class to the schema definition
|
178
|
+
@schema_definition.instance_eval(&)
|
179
|
+
|
180
|
+
# Define accessors immediately based on schema_definition
|
181
|
+
defined_properties = (@schema_definition.schema[:properties] || {}).keys
|
182
|
+
attr_accessor(*defined_properties)
|
183
|
+
|
184
|
+
# Track which properties have had validations applied
|
185
|
+
@validated_properties ||= Set.new
|
186
|
+
|
187
|
+
# Apply auto-validations immediately after definition
|
188
|
+
if EasyTalk.configuration.auto_validations
|
189
|
+
(@schema_definition.schema[:properties] || {}).each do |prop_name, prop_def|
|
190
|
+
# Only apply validations if they haven't been applied yet
|
191
|
+
unless @validated_properties.include?(prop_name)
|
192
|
+
ValidationBuilder.build_validations(self, prop_name, prop_def[:type], prop_def[:constraints])
|
193
|
+
@validated_properties.add(prop_name)
|
194
|
+
end
|
195
|
+
end
|
196
|
+
end
|
143
197
|
|
144
198
|
@schema_definition
|
145
199
|
end
|
@@ -155,6 +209,13 @@ module EasyTalk
|
|
155
209
|
@schema_definition&.schema&.fetch(:additional_properties, false)
|
156
210
|
end
|
157
211
|
|
212
|
+
# Returns the property names defined in the schema
|
213
|
+
#
|
214
|
+
# @return [Array<Symbol>] Array of property names as symbols
|
215
|
+
def properties
|
216
|
+
(@schema_definition&.schema&.dig(:properties) || {}).keys
|
217
|
+
end
|
218
|
+
|
158
219
|
# Builds the schema using the provided schema definition.
|
159
220
|
# This is the convergence point for all schema generation.
|
160
221
|
#
|
data/lib/easy_talk/property.rb
CHANGED
@@ -6,55 +6,81 @@ require_relative 'builders/number_builder'
|
|
6
6
|
require_relative 'builders/boolean_builder'
|
7
7
|
require_relative 'builders/null_builder'
|
8
8
|
require_relative 'builders/string_builder'
|
9
|
-
require_relative 'builders/
|
10
|
-
require_relative 'builders/
|
11
|
-
require_relative 'builders/time_builder'
|
12
|
-
require_relative 'builders/any_of_builder'
|
13
|
-
require_relative 'builders/all_of_builder'
|
14
|
-
require_relative 'builders/one_of_builder'
|
9
|
+
require_relative 'builders/temporal_builder'
|
10
|
+
require_relative 'builders/composition_builder'
|
15
11
|
require_relative 'builders/typed_array_builder'
|
16
12
|
require_relative 'builders/union_builder'
|
17
13
|
|
18
|
-
#
|
19
|
-
|
20
|
-
#
|
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.
|
21
19
|
#
|
22
|
-
#
|
23
|
-
#
|
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}
|
24
23
|
#
|
25
|
-
#
|
26
|
-
#
|
27
|
-
#
|
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"]}
|
28
27
|
#
|
29
28
|
# @see EasyTalk::Property
|
29
|
+
# @see https://json-schema.org/ JSON Schema Documentation
|
30
30
|
module EasyTalk
|
31
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.
|
32
36
|
class Property
|
33
37
|
extend T::Sig
|
34
|
-
attr_reader :name, :type, :constraints
|
35
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
|
36
52
|
TYPE_TO_BUILDER = {
|
37
53
|
'String' => Builders::StringBuilder,
|
38
54
|
'Integer' => Builders::IntegerBuilder,
|
39
55
|
'Float' => Builders::NumberBuilder,
|
40
56
|
'BigDecimal' => Builders::NumberBuilder,
|
41
57
|
'T::Boolean' => Builders::BooleanBuilder,
|
58
|
+
'TrueClass' => Builders::BooleanBuilder,
|
42
59
|
'NilClass' => Builders::NullBuilder,
|
43
|
-
'Date' => Builders::DateBuilder,
|
44
|
-
'DateTime' => Builders::DatetimeBuilder,
|
45
|
-
'Time' => Builders::TimeBuilder,
|
46
|
-
'anyOf' => Builders::AnyOfBuilder,
|
47
|
-
'allOf' => Builders::AllOfBuilder,
|
48
|
-
'oneOf' => Builders::OneOfBuilder,
|
60
|
+
'Date' => Builders::TemporalBuilder::DateBuilder,
|
61
|
+
'DateTime' => Builders::TemporalBuilder::DatetimeBuilder,
|
62
|
+
'Time' => Builders::TemporalBuilder::TimeBuilder,
|
63
|
+
'anyOf' => Builders::CompositionBuilder::AnyOfBuilder,
|
64
|
+
'allOf' => Builders::CompositionBuilder::AllOfBuilder,
|
65
|
+
'oneOf' => Builders::CompositionBuilder::OneOfBuilder,
|
49
66
|
'T::Types::TypedArray' => Builders::TypedArrayBuilder,
|
50
67
|
'T::Types::Union' => Builders::UnionBuilder
|
51
68
|
}.freeze
|
52
69
|
|
53
70
|
# Initializes a new instance of the Property class.
|
54
|
-
#
|
55
|
-
# @param
|
56
|
-
# @param
|
57
|
-
# @
|
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
|
58
84
|
sig do
|
59
85
|
params(name: Symbol, type: T.any(String, Object),
|
60
86
|
constraints: T::Hash[Symbol, T.untyped]).void
|
@@ -63,18 +89,31 @@ module EasyTalk
|
|
63
89
|
@name = name
|
64
90
|
@type = type
|
65
91
|
@constraints = constraints
|
66
|
-
|
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?
|
67
97
|
end
|
68
98
|
|
69
|
-
# Builds the property based on
|
99
|
+
# Builds the property schema based on its type and constraints.
|
70
100
|
#
|
71
|
-
#
|
72
|
-
#
|
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
|
73
106
|
#
|
74
|
-
#
|
75
|
-
# 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
|
76
108
|
#
|
77
|
-
# @
|
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
|
78
117
|
def build
|
79
118
|
if nilable_type?
|
80
119
|
build_nilable_schema
|
@@ -90,43 +129,76 @@ module EasyTalk
|
|
90
129
|
end
|
91
130
|
end
|
92
131
|
|
93
|
-
# 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
|
94
138
|
#
|
95
|
-
# @
|
96
|
-
# @
|
139
|
+
# @see #build
|
140
|
+
# @see https://ruby-doc.org/stdlib-2.7.2/libdoc/json/rdoc/JSON.html#as_json-method
|
97
141
|
def as_json(*_args)
|
98
142
|
build.as_json
|
99
143
|
end
|
100
144
|
|
101
|
-
# Returns the builder associated with the property type.
|
145
|
+
# Returns the builder class associated with the property type.
|
102
146
|
#
|
103
|
-
# The builder is responsible for
|
104
|
-
# 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.
|
105
148
|
#
|
106
|
-
# @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
|
107
151
|
def builder
|
108
|
-
@builder ||=
|
152
|
+
@builder ||= find_builder_for_type
|
109
153
|
end
|
110
154
|
|
111
155
|
private
|
112
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
|
113
176
|
def nilable_type?
|
114
|
-
return unless type.respond_to?(:types)
|
115
|
-
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) }
|
116
179
|
|
117
180
|
type.types.any? { |t| t.raw_type == NilClass }
|
118
181
|
end
|
119
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"]}
|
120
190
|
def build_nilable_schema
|
121
191
|
# Extract the non-nil type from the Union
|
122
|
-
actual_type = type.types.find { |t| t != NilClass }
|
192
|
+
actual_type = type.types.find { |t| t.raw_type != NilClass }
|
193
|
+
|
194
|
+
return { type: 'null' } unless actual_type
|
123
195
|
|
124
196
|
# Create a property with the actual type
|
125
197
|
non_nil_schema = Property.new(name, actual_type, constraints).build
|
126
198
|
|
127
199
|
# Merge the types into an array
|
128
200
|
non_nil_schema.merge(
|
129
|
-
type: [non_nil_schema[:type], 'null']
|
201
|
+
type: [non_nil_schema[:type], 'null'].compact
|
130
202
|
)
|
131
203
|
end
|
132
204
|
end
|