easy_talk 3.0.0 → 3.2.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 +4 -0
- data/.yardopts +13 -0
- data/CHANGELOG.md +105 -0
- data/README.md +1268 -40
- data/Rakefile +27 -0
- data/docs/.gitignore +1 -0
- data/docs/about.markdown +28 -8
- data/docs/getting-started.markdown +102 -0
- data/docs/index.markdown +51 -4
- data/docs/json_schema_compliance.md +55 -0
- data/docs/nested-models.markdown +216 -0
- data/docs/property-types.markdown +212 -0
- data/docs/schema-definition.markdown +180 -0
- data/lib/easy_talk/builders/base_builder.rb +4 -2
- data/lib/easy_talk/builders/composition_builder.rb +10 -12
- data/lib/easy_talk/builders/object_builder.rb +119 -10
- data/lib/easy_talk/builders/registry.rb +168 -0
- data/lib/easy_talk/builders/typed_array_builder.rb +20 -6
- data/lib/easy_talk/configuration.rb +51 -1
- data/lib/easy_talk/error_formatter/base.rb +100 -0
- data/lib/easy_talk/error_formatter/error_code_mapper.rb +82 -0
- data/lib/easy_talk/error_formatter/flat.rb +38 -0
- data/lib/easy_talk/error_formatter/json_pointer.rb +38 -0
- data/lib/easy_talk/error_formatter/jsonapi.rb +64 -0
- data/lib/easy_talk/error_formatter/path_converter.rb +53 -0
- data/lib/easy_talk/error_formatter/rfc7807.rb +69 -0
- data/lib/easy_talk/error_formatter.rb +143 -0
- data/lib/easy_talk/errors.rb +2 -0
- data/lib/easy_talk/errors_helper.rb +63 -34
- data/lib/easy_talk/keywords.rb +2 -0
- data/lib/easy_talk/model.rb +125 -41
- data/lib/easy_talk/model_helper.rb +13 -0
- data/lib/easy_talk/naming_strategies.rb +20 -0
- data/lib/easy_talk/property.rb +32 -44
- data/lib/easy_talk/ref_helper.rb +27 -0
- data/lib/easy_talk/schema.rb +198 -0
- data/lib/easy_talk/schema_definition.rb +7 -1
- data/lib/easy_talk/schema_methods.rb +80 -0
- data/lib/easy_talk/tools/function_builder.rb +1 -1
- data/lib/easy_talk/type_introspection.rb +178 -0
- data/lib/easy_talk/types/base_composer.rb +2 -1
- data/lib/easy_talk/types/composer.rb +4 -0
- data/lib/easy_talk/validation_adapters/active_model_adapter.rb +329 -0
- data/lib/easy_talk/validation_adapters/base.rb +144 -0
- data/lib/easy_talk/validation_adapters/none_adapter.rb +36 -0
- data/lib/easy_talk/validation_adapters/registry.rb +87 -0
- data/lib/easy_talk/validation_builder.rb +28 -309
- data/lib/easy_talk/version.rb +1 -1
- data/lib/easy_talk.rb +41 -0
- metadata +28 -6
- data/docs/404.html +0 -25
- data/docs/_posts/2024-05-07-welcome-to-jekyll.markdown +0 -29
- data/easy_talk.gemspec +0 -39
data/lib/easy_talk/property.rb
CHANGED
|
@@ -45,28 +45,6 @@ module EasyTalk
|
|
|
45
45
|
# @return [Hash<Symbol, Object>] Additional constraints applied to the property
|
|
46
46
|
attr_reader :constraints
|
|
47
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
|
|
52
|
-
TYPE_TO_BUILDER = {
|
|
53
|
-
'String' => Builders::StringBuilder,
|
|
54
|
-
'Integer' => Builders::IntegerBuilder,
|
|
55
|
-
'Float' => Builders::NumberBuilder,
|
|
56
|
-
'BigDecimal' => Builders::NumberBuilder,
|
|
57
|
-
'T::Boolean' => Builders::BooleanBuilder,
|
|
58
|
-
'TrueClass' => Builders::BooleanBuilder,
|
|
59
|
-
'NilClass' => Builders::NullBuilder,
|
|
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,
|
|
66
|
-
'T::Types::TypedArray' => Builders::TypedArrayBuilder,
|
|
67
|
-
'T::Types::Union' => Builders::UnionBuilder
|
|
68
|
-
}.freeze
|
|
69
|
-
|
|
70
48
|
# Initializes a new instance of the Property class.
|
|
71
49
|
#
|
|
72
50
|
# @param name [Symbol] The name of the property
|
|
@@ -101,31 +79,43 @@ module EasyTalk
|
|
|
101
79
|
# This method handles different types of properties:
|
|
102
80
|
# - Nilable types (can be null)
|
|
103
81
|
# - Types with dedicated builders
|
|
104
|
-
# - Types that implement their own schema method
|
|
82
|
+
# - Types that implement their own schema method (EasyTalk models)
|
|
105
83
|
# - Default fallback to 'object' type
|
|
106
84
|
#
|
|
85
|
+
# When use_refs is enabled (globally or per-property), EasyTalk models
|
|
86
|
+
# are referenced via $ref instead of being inlined.
|
|
87
|
+
#
|
|
107
88
|
# @return [Hash] The complete JSON Schema property definition
|
|
108
89
|
#
|
|
109
90
|
# @example Simple string property
|
|
110
91
|
# property = Property.new(:name, 'String')
|
|
111
92
|
# property.build # => {"type"=>"string"}
|
|
112
93
|
#
|
|
113
|
-
# @example Complex nested schema
|
|
94
|
+
# @example Complex nested schema (inlined)
|
|
114
95
|
# address = Address.new # A class with a .schema method
|
|
115
96
|
# property = Property.new(:shipping_address, address, description: "Shipping address")
|
|
116
97
|
# property.build # => Address schema merged with the description constraint
|
|
98
|
+
#
|
|
99
|
+
# @example Nested schema with $ref
|
|
100
|
+
# property = Property.new(:shipping_address, Address, ref: true)
|
|
101
|
+
# property.build # => {"$ref"=>"#/$defs/Address", ...constraints}
|
|
117
102
|
def build
|
|
118
103
|
if nilable_type?
|
|
119
104
|
build_nilable_schema
|
|
120
|
-
elsif
|
|
121
|
-
|
|
122
|
-
|
|
105
|
+
elsif RefHelper.should_use_ref?(type, constraints)
|
|
106
|
+
RefHelper.build_ref_schema(type, constraints)
|
|
107
|
+
elsif (resolved = find_builder_for_type)
|
|
108
|
+
builder_class, is_collection = resolved
|
|
109
|
+
args = is_collection ? [name, type, constraints] : [name, constraints]
|
|
110
|
+
builder_class.new(*args).build
|
|
123
111
|
elsif type.respond_to?(:schema)
|
|
124
112
|
# merge the top-level constraints from *this* property
|
|
125
113
|
# e.g. :title, :description, :default, etc
|
|
126
114
|
type.schema.merge!(constraints)
|
|
127
115
|
else
|
|
128
|
-
|
|
116
|
+
raise UnknownTypeError,
|
|
117
|
+
"Unknown type '#{type.inspect}' for property '#{name}'. " \
|
|
118
|
+
'Register a custom builder with EasyTalk.register_type or use a supported type.'
|
|
129
119
|
end
|
|
130
120
|
end
|
|
131
121
|
|
|
@@ -142,28 +132,18 @@ module EasyTalk
|
|
|
142
132
|
build.as_json
|
|
143
133
|
end
|
|
144
134
|
|
|
145
|
-
# Returns the builder class associated with the property type.
|
|
146
|
-
#
|
|
147
|
-
# The builder is responsible for converting the Ruby type to a JSON Schema type.
|
|
148
|
-
#
|
|
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
|
|
151
|
-
def builder
|
|
152
|
-
@builder ||= find_builder_for_type
|
|
153
|
-
end
|
|
154
|
-
|
|
155
135
|
private
|
|
156
136
|
|
|
157
|
-
# Finds the appropriate builder for the current type.
|
|
137
|
+
# Finds the appropriate builder for the current type using the Builders::Registry.
|
|
158
138
|
#
|
|
159
|
-
#
|
|
160
|
-
#
|
|
139
|
+
# The registry is checked for a matching builder based on the type's class name
|
|
140
|
+
# or the type's own name.
|
|
161
141
|
#
|
|
162
|
-
# @return [Class, nil]
|
|
142
|
+
# @return [Array(Class, Boolean), nil] A tuple of [builder_class, is_collection] or nil if none matches
|
|
163
143
|
# @api private
|
|
144
|
+
# @see Builders::Registry.resolve
|
|
164
145
|
def find_builder_for_type
|
|
165
|
-
|
|
166
|
-
(type.respond_to?(:name) ? TYPE_TO_BUILDER[type.name.to_s] : nil)
|
|
146
|
+
Builders::Registry.resolve(type)
|
|
167
147
|
end
|
|
168
148
|
|
|
169
149
|
# Determines if the type is nilable (can be nil).
|
|
@@ -193,6 +173,14 @@ module EasyTalk
|
|
|
193
173
|
|
|
194
174
|
return { type: 'null' } unless actual_type
|
|
195
175
|
|
|
176
|
+
# Check if the underlying type is an EasyTalk model that should use $ref
|
|
177
|
+
if RefHelper.should_use_ref_for_type?(actual_type, constraints)
|
|
178
|
+
# Use anyOf with $ref and null type
|
|
179
|
+
ref_constraints = constraints.except(:ref, :optional)
|
|
180
|
+
schema = { anyOf: [{ '$ref': actual_type.ref_template }, { type: 'null' }] }
|
|
181
|
+
return ref_constraints.empty? ? schema : schema.merge(ref_constraints)
|
|
182
|
+
end
|
|
183
|
+
|
|
196
184
|
# Create a property with the actual type
|
|
197
185
|
non_nil_schema = Property.new(name, actual_type, constraints).build
|
|
198
186
|
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'model_helper'
|
|
4
|
+
|
|
5
|
+
module EasyTalk
|
|
6
|
+
module RefHelper
|
|
7
|
+
def self.should_use_ref?(type, constraints)
|
|
8
|
+
ModelHelper.easytalk_model?(type) && should_use_ref_for_type?(type, constraints)
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def self.should_use_ref_for_type?(type, constraints)
|
|
12
|
+
return false unless ModelHelper.easytalk_model?(type)
|
|
13
|
+
|
|
14
|
+
# Per-property constraint takes precedence
|
|
15
|
+
if constraints.key?(:ref)
|
|
16
|
+
constraints[:ref]
|
|
17
|
+
else
|
|
18
|
+
EasyTalk.configuration.use_refs
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def self.build_ref_schema(type, constraints)
|
|
23
|
+
# Remove ref and optional from constraints as they're not JSON Schema keywords
|
|
24
|
+
{ '$ref': type.ref_template }.merge(constraints.except(:ref, :optional))
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'json'
|
|
4
|
+
require 'active_support'
|
|
5
|
+
require 'active_support/core_ext'
|
|
6
|
+
require 'active_support/time'
|
|
7
|
+
require 'active_support/concern'
|
|
8
|
+
require 'active_support/json'
|
|
9
|
+
require_relative 'builders/object_builder'
|
|
10
|
+
require_relative 'schema_definition'
|
|
11
|
+
|
|
12
|
+
module EasyTalk
|
|
13
|
+
# A lightweight module for schema generation without ActiveModel validations.
|
|
14
|
+
#
|
|
15
|
+
# Use this module when you need JSON Schema generation without the overhead
|
|
16
|
+
# of ActiveModel validations. This is ideal for:
|
|
17
|
+
# - API documentation and OpenAPI spec generation
|
|
18
|
+
# - Schema-first design where validation happens elsewhere
|
|
19
|
+
# - High-performance scenarios where validation overhead is unwanted
|
|
20
|
+
# - Generating schemas for external systems
|
|
21
|
+
#
|
|
22
|
+
# Unlike EasyTalk::Model, this module does NOT include ActiveModel::API or
|
|
23
|
+
# ActiveModel::Validations, so instances will not respond to `valid?` or have
|
|
24
|
+
# validation errors.
|
|
25
|
+
#
|
|
26
|
+
# @example Basic usage
|
|
27
|
+
# class ApiContract
|
|
28
|
+
# include EasyTalk::Schema
|
|
29
|
+
#
|
|
30
|
+
# define_schema do
|
|
31
|
+
# title 'API Contract'
|
|
32
|
+
# property :name, String, min_length: 2
|
|
33
|
+
# property :age, Integer, minimum: 0
|
|
34
|
+
# end
|
|
35
|
+
# end
|
|
36
|
+
#
|
|
37
|
+
# ApiContract.json_schema # => { "type" => "object", ... }
|
|
38
|
+
# contract = ApiContract.new(name: 'Test', age: 25)
|
|
39
|
+
# contract.name # => 'Test'
|
|
40
|
+
# contract.valid? # => NoMethodError (no ActiveModel)
|
|
41
|
+
#
|
|
42
|
+
# @see EasyTalk::Model For a full-featured module with validations
|
|
43
|
+
#
|
|
44
|
+
module Schema
|
|
45
|
+
def self.included(base)
|
|
46
|
+
base.extend(ClassMethods)
|
|
47
|
+
base.include(InstanceMethods)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Instance methods for schema-only models.
|
|
51
|
+
module InstanceMethods
|
|
52
|
+
# Initialize the schema object with attributes.
|
|
53
|
+
#
|
|
54
|
+
# @param attributes [Hash] The attributes to set
|
|
55
|
+
def initialize(attributes = {})
|
|
56
|
+
@additional_properties = {}
|
|
57
|
+
schema_def = self.class.schema_definition
|
|
58
|
+
|
|
59
|
+
return unless schema_def.respond_to?(:schema) && schema_def.schema.is_a?(Hash)
|
|
60
|
+
|
|
61
|
+
(schema_def.schema[:properties] || {}).each do |prop_name, prop_definition|
|
|
62
|
+
value = attributes[prop_name] || attributes[prop_name.to_s]
|
|
63
|
+
|
|
64
|
+
# Handle default values
|
|
65
|
+
if value.nil? && !attributes.key?(prop_name) && !attributes.key?(prop_name.to_s)
|
|
66
|
+
default_value = prop_definition.dig(:constraints, :default)
|
|
67
|
+
value = default_value unless default_value.nil?
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# Handle nested EasyTalk::Schema or EasyTalk::Model objects
|
|
71
|
+
defined_type = prop_definition[:type]
|
|
72
|
+
nilable_type = defined_type.respond_to?(:nilable?) && defined_type.nilable?
|
|
73
|
+
defined_type = T::Utils::Nilable.get_underlying_type(defined_type) if nilable_type
|
|
74
|
+
|
|
75
|
+
if defined_type.is_a?(Class) &&
|
|
76
|
+
(defined_type.include?(EasyTalk::Schema) || defined_type.include?(EasyTalk::Model)) &&
|
|
77
|
+
value.is_a?(Hash)
|
|
78
|
+
value = defined_type.new(value)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
instance_variable_set("@#{prop_name}", value)
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# Convert defined properties to a hash.
|
|
86
|
+
#
|
|
87
|
+
# @return [Hash] The properties as a hash
|
|
88
|
+
def to_hash
|
|
89
|
+
properties_to_include = (self.class.schema_definition.schema[:properties] || {}).keys
|
|
90
|
+
return {} if properties_to_include.empty?
|
|
91
|
+
|
|
92
|
+
properties_to_include.each_with_object({}) do |prop, hash|
|
|
93
|
+
hash[prop.to_s] = send(prop)
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# Convert to JSON-compatible hash including additional properties.
|
|
98
|
+
#
|
|
99
|
+
# @param _options [Hash] JSON options (ignored)
|
|
100
|
+
# @return [Hash] The combined hash
|
|
101
|
+
def as_json(_options = {})
|
|
102
|
+
to_hash.merge(@additional_properties)
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# Convert to hash including additional properties.
|
|
106
|
+
#
|
|
107
|
+
# @return [Hash] The combined hash
|
|
108
|
+
def to_h
|
|
109
|
+
to_hash.merge(@additional_properties)
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# Allow comparison with hashes.
|
|
113
|
+
#
|
|
114
|
+
# @param other [Object] The object to compare with
|
|
115
|
+
# @return [Boolean] True if equal
|
|
116
|
+
def ==(other)
|
|
117
|
+
case other
|
|
118
|
+
when Hash
|
|
119
|
+
self_hash = (self.class.schema_definition.schema[:properties] || {}).keys.each_with_object({}) do |prop, hash|
|
|
120
|
+
hash[prop] = send(prop)
|
|
121
|
+
end
|
|
122
|
+
other_normalized = other.transform_keys(&:to_sym)
|
|
123
|
+
self_hash == other_normalized
|
|
124
|
+
else
|
|
125
|
+
super
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
# Class methods for schema-only models.
|
|
131
|
+
module ClassMethods
|
|
132
|
+
include SchemaMethods
|
|
133
|
+
|
|
134
|
+
# Returns the schema for the model.
|
|
135
|
+
#
|
|
136
|
+
# @return [Hash] The schema for the model.
|
|
137
|
+
def schema
|
|
138
|
+
@schema ||= if defined?(@schema_definition) && @schema_definition
|
|
139
|
+
build_schema(@schema_definition)
|
|
140
|
+
else
|
|
141
|
+
{}
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
# Define the schema for the model using the provided block.
|
|
146
|
+
# Unlike EasyTalk::Model, this does NOT apply any validations.
|
|
147
|
+
#
|
|
148
|
+
# @yield The block to define the schema.
|
|
149
|
+
# @raise [ArgumentError] If the class does not have a name.
|
|
150
|
+
def define_schema(&)
|
|
151
|
+
raise ArgumentError, 'The class must have a name' unless name.present?
|
|
152
|
+
|
|
153
|
+
@schema_definition = SchemaDefinition.new(name)
|
|
154
|
+
@schema_definition.klass = self
|
|
155
|
+
@schema_definition.instance_eval(&)
|
|
156
|
+
|
|
157
|
+
# Define accessors for all properties
|
|
158
|
+
defined_properties = (@schema_definition.schema[:properties] || {}).keys
|
|
159
|
+
attr_accessor(*defined_properties)
|
|
160
|
+
|
|
161
|
+
# NO validations are applied - this is schema-only
|
|
162
|
+
|
|
163
|
+
@schema_definition
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
# Returns the schema definition for the model.
|
|
167
|
+
#
|
|
168
|
+
# @return [SchemaDefinition] The schema definition.
|
|
169
|
+
def schema_definition
|
|
170
|
+
@schema_definition ||= {}
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
# Check if additional properties are allowed.
|
|
174
|
+
#
|
|
175
|
+
# @return [Boolean] True if additional properties are allowed.
|
|
176
|
+
def additional_properties_allowed?
|
|
177
|
+
@schema_definition&.schema&.fetch(:additional_properties, false)
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
# Returns the property names defined in the schema.
|
|
181
|
+
#
|
|
182
|
+
# @return [Array<Symbol>] Array of property names as symbols.
|
|
183
|
+
def properties
|
|
184
|
+
(@schema_definition&.schema&.dig(:properties) || {}).keys
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
private
|
|
188
|
+
|
|
189
|
+
# Builds the schema using the provided schema definition.
|
|
190
|
+
#
|
|
191
|
+
# @param schema_definition [SchemaDefinition] The schema definition.
|
|
192
|
+
# @return [Hash] The built schema.
|
|
193
|
+
def build_schema(schema_definition)
|
|
194
|
+
Builders::ObjectBuilder.new(schema_definition).build
|
|
195
|
+
end
|
|
196
|
+
end
|
|
197
|
+
end
|
|
198
|
+
end
|
|
@@ -24,6 +24,7 @@ module EasyTalk
|
|
|
24
24
|
@schema[:additional_properties] = false unless schema.key?(:additional_properties)
|
|
25
25
|
@name = name
|
|
26
26
|
@klass = nil # Initialize klass to nil
|
|
27
|
+
@property_naming_strategy = EasyTalk.configuration.property_naming_strategy
|
|
27
28
|
end
|
|
28
29
|
|
|
29
30
|
EasyTalk::KEYWORDS.each do |keyword|
|
|
@@ -38,7 +39,8 @@ module EasyTalk
|
|
|
38
39
|
end
|
|
39
40
|
|
|
40
41
|
def property(name, type, constraints = {}, &)
|
|
41
|
-
|
|
42
|
+
constraints[:as] ||= @property_naming_strategy.call(name)
|
|
43
|
+
validate_property_name(constraints[:as])
|
|
42
44
|
@schema[:properties] ||= {}
|
|
43
45
|
|
|
44
46
|
if block_given?
|
|
@@ -76,5 +78,9 @@ module EasyTalk
|
|
|
76
78
|
# Call standard property method
|
|
77
79
|
property(name, nilable_type, constraints)
|
|
78
80
|
end
|
|
81
|
+
|
|
82
|
+
def property_naming_strategy(strategy)
|
|
83
|
+
@property_naming_strategy = EasyTalk::NamingStrategies.derive_strategy(strategy)
|
|
84
|
+
end
|
|
79
85
|
end
|
|
80
86
|
end
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module EasyTalk
|
|
4
|
+
# Shared methods for JSON Schema generation.
|
|
5
|
+
#
|
|
6
|
+
# This module provides common functionality for building JSON schemas,
|
|
7
|
+
# including $schema and $id resolution. It is included in both
|
|
8
|
+
# EasyTalk::Model and EasyTalk::Schema to avoid code duplication.
|
|
9
|
+
#
|
|
10
|
+
# @note Classes including this module must define:
|
|
11
|
+
# - `name` - The class name (used in ref_template)
|
|
12
|
+
# - `schema` - The built schema hash (used in json_schema)
|
|
13
|
+
# - `@schema_definition` - Instance variable with schema metadata
|
|
14
|
+
#
|
|
15
|
+
module SchemaMethods
|
|
16
|
+
# Returns the reference template for the model.
|
|
17
|
+
#
|
|
18
|
+
# @return [String] The reference template for the model.
|
|
19
|
+
def ref_template
|
|
20
|
+
"#/$defs/#{name}"
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Returns the JSON schema for the model.
|
|
24
|
+
# This is the final output that includes the $schema keyword if configured.
|
|
25
|
+
#
|
|
26
|
+
# @return [Hash] The JSON schema for the model.
|
|
27
|
+
def json_schema
|
|
28
|
+
@json_schema ||= build_json_schema
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
private
|
|
32
|
+
|
|
33
|
+
# Builds the final JSON schema with optional $schema and $id keywords.
|
|
34
|
+
#
|
|
35
|
+
# @return [Hash] The JSON schema.
|
|
36
|
+
def build_json_schema
|
|
37
|
+
result = schema.as_json
|
|
38
|
+
schema_uri = resolve_schema_uri
|
|
39
|
+
id_uri = resolve_schema_id
|
|
40
|
+
|
|
41
|
+
prefix = {}
|
|
42
|
+
prefix['$schema'] = schema_uri if schema_uri
|
|
43
|
+
prefix['$id'] = id_uri if id_uri
|
|
44
|
+
|
|
45
|
+
return result if prefix.empty?
|
|
46
|
+
|
|
47
|
+
prefix.merge(result)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Resolves the schema URI from per-model setting or global config.
|
|
51
|
+
#
|
|
52
|
+
# @return [String, nil] The schema URI.
|
|
53
|
+
def resolve_schema_uri
|
|
54
|
+
model_version = @schema_definition&.schema&.dig(:schema_version)
|
|
55
|
+
|
|
56
|
+
if model_version
|
|
57
|
+
return nil if model_version == :none
|
|
58
|
+
|
|
59
|
+
Configuration::SCHEMA_VERSIONS[model_version] || model_version.to_s
|
|
60
|
+
else
|
|
61
|
+
EasyTalk.configuration.schema_uri
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# Resolves the schema ID from per-model setting or global config.
|
|
66
|
+
#
|
|
67
|
+
# @return [String, nil] The schema ID.
|
|
68
|
+
def resolve_schema_id
|
|
69
|
+
model_id = @schema_definition&.schema&.dig(:schema_id)
|
|
70
|
+
|
|
71
|
+
if model_id
|
|
72
|
+
return nil if model_id == :none
|
|
73
|
+
|
|
74
|
+
model_id.to_s
|
|
75
|
+
else
|
|
76
|
+
EasyTalk.configuration.schema_id
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
@@ -27,7 +27,7 @@ module EasyTalk
|
|
|
27
27
|
|
|
28
28
|
def generate_function_description(model)
|
|
29
29
|
if model.respond_to?(:instructions)
|
|
30
|
-
raise
|
|
30
|
+
raise InvalidInstructionsError, 'The instructions must be a string' unless model.instructions.is_a?(String)
|
|
31
31
|
|
|
32
32
|
model.instructions
|
|
33
33
|
else
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'bigdecimal'
|
|
4
|
+
|
|
5
|
+
module EasyTalk
|
|
6
|
+
# Centralized module for robust type introspection.
|
|
7
|
+
#
|
|
8
|
+
# This module provides predicate methods for detecting types without relying
|
|
9
|
+
# on brittle string-based checks. It uses Sorbet's type system properly and
|
|
10
|
+
# handles edge cases gracefully.
|
|
11
|
+
#
|
|
12
|
+
# @example Checking if a type is boolean
|
|
13
|
+
# TypeIntrospection.boolean_type?(T::Boolean) # => true
|
|
14
|
+
# TypeIntrospection.boolean_type?(TrueClass) # => true
|
|
15
|
+
# TypeIntrospection.boolean_type?(String) # => false
|
|
16
|
+
#
|
|
17
|
+
# @example Getting JSON Schema type
|
|
18
|
+
# TypeIntrospection.json_schema_type(Integer) # => 'integer'
|
|
19
|
+
# TypeIntrospection.json_schema_type(Float) # => 'number'
|
|
20
|
+
#
|
|
21
|
+
module TypeIntrospection
|
|
22
|
+
# Mapping of Ruby classes to JSON Schema types
|
|
23
|
+
PRIMITIVE_TO_JSON_SCHEMA = {
|
|
24
|
+
String => 'string',
|
|
25
|
+
Integer => 'integer',
|
|
26
|
+
Float => 'number',
|
|
27
|
+
BigDecimal => 'number',
|
|
28
|
+
TrueClass => 'boolean',
|
|
29
|
+
FalseClass => 'boolean',
|
|
30
|
+
NilClass => 'null'
|
|
31
|
+
}.freeze
|
|
32
|
+
|
|
33
|
+
class << self
|
|
34
|
+
# Check if type represents a boolean (T::Boolean or TrueClass/FalseClass).
|
|
35
|
+
#
|
|
36
|
+
# @param type [Object] The type to check
|
|
37
|
+
# @return [Boolean] true if the type is a boolean type
|
|
38
|
+
#
|
|
39
|
+
# @example
|
|
40
|
+
# boolean_type?(T::Boolean) # => true
|
|
41
|
+
# boolean_type?(TrueClass) # => true
|
|
42
|
+
# boolean_type?(FalseClass) # => true
|
|
43
|
+
# boolean_type?(String) # => false
|
|
44
|
+
def boolean_type?(type)
|
|
45
|
+
return false if type.nil?
|
|
46
|
+
return true if [TrueClass, FalseClass].include?(type)
|
|
47
|
+
return true if type.respond_to?(:raw_type) && [TrueClass, FalseClass].include?(type.raw_type)
|
|
48
|
+
|
|
49
|
+
# Check for T::Boolean which is a TypeAlias with name 'T::Boolean'
|
|
50
|
+
return true if type.respond_to?(:name) && type.name == 'T::Boolean'
|
|
51
|
+
|
|
52
|
+
# Check for union types containing TrueClass and FalseClass
|
|
53
|
+
if type.respond_to?(:types)
|
|
54
|
+
type_classes = type.types.map { |t| t.respond_to?(:raw_type) ? t.raw_type : t }
|
|
55
|
+
return type_classes.sort_by(&:name) == [FalseClass, TrueClass].sort_by(&:name)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
false
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Check if type is a typed array (T::Array[...]).
|
|
62
|
+
#
|
|
63
|
+
# @param type [Object] The type to check
|
|
64
|
+
# @return [Boolean] true if the type is a typed array
|
|
65
|
+
#
|
|
66
|
+
# @example
|
|
67
|
+
# typed_array?(T::Array[String]) # => true
|
|
68
|
+
# typed_array?(Array) # => false
|
|
69
|
+
def typed_array?(type)
|
|
70
|
+
return false if type.nil?
|
|
71
|
+
|
|
72
|
+
type.is_a?(T::Types::TypedArray)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Check if type is nilable (T.nilable(...)).
|
|
76
|
+
#
|
|
77
|
+
# @param type [Object] The type to check
|
|
78
|
+
# @return [Boolean] true if the type is nilable
|
|
79
|
+
#
|
|
80
|
+
# @example
|
|
81
|
+
# nilable_type?(T.nilable(String)) # => true
|
|
82
|
+
# nilable_type?(String) # => false
|
|
83
|
+
def nilable_type?(type)
|
|
84
|
+
return false if type.nil?
|
|
85
|
+
|
|
86
|
+
type.respond_to?(:nilable?) && type.nilable?
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# Check if type is a primitive Ruby type.
|
|
90
|
+
#
|
|
91
|
+
# @param type [Object] The type to check
|
|
92
|
+
# @return [Boolean] true if the type is a primitive
|
|
93
|
+
def primitive_type?(type)
|
|
94
|
+
return false if type.nil?
|
|
95
|
+
|
|
96
|
+
resolved = if type.is_a?(Class)
|
|
97
|
+
type
|
|
98
|
+
elsif type.respond_to?(:raw_type)
|
|
99
|
+
type.raw_type
|
|
100
|
+
end
|
|
101
|
+
PRIMITIVE_TO_JSON_SCHEMA.key?(resolved)
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# Get JSON Schema type string for a Ruby type.
|
|
105
|
+
#
|
|
106
|
+
# @param type [Object] The type to convert
|
|
107
|
+
# @return [String] The JSON Schema type string
|
|
108
|
+
#
|
|
109
|
+
# @example
|
|
110
|
+
# json_schema_type(Integer) # => 'integer'
|
|
111
|
+
# json_schema_type(Float) # => 'number'
|
|
112
|
+
# json_schema_type(BigDecimal) # => 'number'
|
|
113
|
+
# json_schema_type(String) # => 'string'
|
|
114
|
+
def json_schema_type(type)
|
|
115
|
+
return 'object' if type.nil?
|
|
116
|
+
return 'boolean' if boolean_type?(type)
|
|
117
|
+
|
|
118
|
+
resolved_class = if type.is_a?(Class)
|
|
119
|
+
type
|
|
120
|
+
elsif type.respond_to?(:raw_type)
|
|
121
|
+
type.raw_type
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
PRIMITIVE_TO_JSON_SCHEMA[resolved_class] || resolved_class&.name&.downcase || 'object'
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
# Get the Ruby class for a type, handling Sorbet types.
|
|
128
|
+
#
|
|
129
|
+
# @param type [Object] The type to resolve
|
|
130
|
+
# @return [Class, Array<Class>, nil] The resolved class or classes
|
|
131
|
+
#
|
|
132
|
+
# @example
|
|
133
|
+
# get_type_class(String) # => String
|
|
134
|
+
# get_type_class(T::Boolean) # => [TrueClass, FalseClass]
|
|
135
|
+
# get_type_class(T::Array[String]) # => Array
|
|
136
|
+
def get_type_class(type)
|
|
137
|
+
return nil if type.nil?
|
|
138
|
+
return type if type.is_a?(Class)
|
|
139
|
+
return type.raw_type if type.respond_to?(:raw_type)
|
|
140
|
+
return Array if typed_array?(type)
|
|
141
|
+
return [TrueClass, FalseClass] if boolean_type?(type)
|
|
142
|
+
|
|
143
|
+
if nilable_type?(type)
|
|
144
|
+
inner = extract_inner_type(type)
|
|
145
|
+
return get_type_class(inner) if inner && inner != type
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
nil
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
# Extract inner type from nilable or complex types.
|
|
152
|
+
#
|
|
153
|
+
# @param type [Object] The type to unwrap
|
|
154
|
+
# @return [Object] The inner type, or the original type if not wrapped
|
|
155
|
+
#
|
|
156
|
+
# @example
|
|
157
|
+
# extract_inner_type(T.nilable(String)) # => String
|
|
158
|
+
def extract_inner_type(type)
|
|
159
|
+
return type if type.nil?
|
|
160
|
+
|
|
161
|
+
if type.respond_to?(:unwrap_nilable)
|
|
162
|
+
unwrapped = type.unwrap_nilable
|
|
163
|
+
return unwrapped.respond_to?(:raw_type) ? unwrapped.raw_type : unwrapped
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
if type.respond_to?(:types)
|
|
167
|
+
non_nil = type.types.find do |t|
|
|
168
|
+
raw = t.respond_to?(:raw_type) ? t.raw_type : t
|
|
169
|
+
raw != NilClass
|
|
170
|
+
end
|
|
171
|
+
return non_nil.respond_to?(:raw_type) ? non_nil.raw_type : non_nil if non_nil
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
type
|
|
175
|
+
end
|
|
176
|
+
end
|
|
177
|
+
end
|
|
178
|
+
end
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
module EasyTalk
|
|
4
4
|
module Types
|
|
5
5
|
# no-doc
|
|
6
|
-
class BaseComposer
|
|
6
|
+
class BaseComposer < T::Types::Base
|
|
7
7
|
extend T::Sig
|
|
8
8
|
extend T::Generic
|
|
9
9
|
|
|
@@ -16,6 +16,7 @@ module EasyTalk
|
|
|
16
16
|
#
|
|
17
17
|
# @param args [Array] the items to be assigned to the instance variable @items
|
|
18
18
|
def initialize(*args)
|
|
19
|
+
super()
|
|
19
20
|
@items = args
|
|
20
21
|
end
|
|
21
22
|
end
|