easy_talk 3.1.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 +75 -0
- data/README.md +616 -35
- 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 +45 -30
- data/lib/easy_talk/builders/registry.rb +168 -0
- data/lib/easy_talk/builders/typed_array_builder.rb +15 -4
- data/lib/easy_talk/configuration.rb +31 -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/model.rb +123 -90
- data/lib/easy_talk/model_helper.rb +13 -0
- data/lib/easy_talk/naming_strategies.rb +20 -0
- data/lib/easy_talk/property.rb +16 -94
- 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 +26 -4
- 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
|
|
@@ -124,17 +102,20 @@ module EasyTalk
|
|
|
124
102
|
def build
|
|
125
103
|
if nilable_type?
|
|
126
104
|
build_nilable_schema
|
|
127
|
-
elsif should_use_ref?
|
|
128
|
-
build_ref_schema
|
|
129
|
-
elsif
|
|
130
|
-
|
|
131
|
-
|
|
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
|
|
132
111
|
elsif type.respond_to?(:schema)
|
|
133
112
|
# merge the top-level constraints from *this* property
|
|
134
113
|
# e.g. :title, :description, :default, etc
|
|
135
114
|
type.schema.merge!(constraints)
|
|
136
115
|
else
|
|
137
|
-
|
|
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.'
|
|
138
119
|
end
|
|
139
120
|
end
|
|
140
121
|
|
|
@@ -151,28 +132,18 @@ module EasyTalk
|
|
|
151
132
|
build.as_json
|
|
152
133
|
end
|
|
153
134
|
|
|
154
|
-
# Returns the builder class associated with the property type.
|
|
155
|
-
#
|
|
156
|
-
# The builder is responsible for converting the Ruby type to a JSON Schema type.
|
|
157
|
-
#
|
|
158
|
-
# @return [Class, nil] The builder class for this property's type, or nil if no dedicated builder exists
|
|
159
|
-
# @see #find_builder_for_type
|
|
160
|
-
def builder
|
|
161
|
-
@builder ||= find_builder_for_type
|
|
162
|
-
end
|
|
163
|
-
|
|
164
135
|
private
|
|
165
136
|
|
|
166
|
-
# Finds the appropriate builder for the current type.
|
|
137
|
+
# Finds the appropriate builder for the current type using the Builders::Registry.
|
|
167
138
|
#
|
|
168
|
-
#
|
|
169
|
-
#
|
|
139
|
+
# The registry is checked for a matching builder based on the type's class name
|
|
140
|
+
# or the type's own name.
|
|
170
141
|
#
|
|
171
|
-
# @return [Class, nil]
|
|
142
|
+
# @return [Array(Class, Boolean), nil] A tuple of [builder_class, is_collection] or nil if none matches
|
|
172
143
|
# @api private
|
|
144
|
+
# @see Builders::Registry.resolve
|
|
173
145
|
def find_builder_for_type
|
|
174
|
-
|
|
175
|
-
(type.respond_to?(:name) ? TYPE_TO_BUILDER[type.name.to_s] : nil)
|
|
146
|
+
Builders::Registry.resolve(type)
|
|
176
147
|
end
|
|
177
148
|
|
|
178
149
|
# Determines if the type is nilable (can be nil).
|
|
@@ -203,7 +174,7 @@ module EasyTalk
|
|
|
203
174
|
return { type: 'null' } unless actual_type
|
|
204
175
|
|
|
205
176
|
# Check if the underlying type is an EasyTalk model that should use $ref
|
|
206
|
-
if
|
|
177
|
+
if RefHelper.should_use_ref_for_type?(actual_type, constraints)
|
|
207
178
|
# Use anyOf with $ref and null type
|
|
208
179
|
ref_constraints = constraints.except(:ref, :optional)
|
|
209
180
|
schema = { anyOf: [{ '$ref': actual_type.ref_template }, { type: 'null' }] }
|
|
@@ -218,54 +189,5 @@ module EasyTalk
|
|
|
218
189
|
type: [non_nil_schema[:type], 'null'].compact
|
|
219
190
|
)
|
|
220
191
|
end
|
|
221
|
-
|
|
222
|
-
# Determines if $ref should be used for the current type.
|
|
223
|
-
#
|
|
224
|
-
# @return [Boolean] true if $ref should be used, false otherwise
|
|
225
|
-
# @api private
|
|
226
|
-
def should_use_ref?
|
|
227
|
-
return false unless easytalk_model?(type)
|
|
228
|
-
|
|
229
|
-
should_use_ref_for_type?(type)
|
|
230
|
-
end
|
|
231
|
-
|
|
232
|
-
# Determines if $ref should be used for a given type based on constraints and config.
|
|
233
|
-
#
|
|
234
|
-
# @param check_type [Class] The type to check
|
|
235
|
-
# @return [Boolean] true if $ref should be used, false otherwise
|
|
236
|
-
# @api private
|
|
237
|
-
def should_use_ref_for_type?(check_type)
|
|
238
|
-
return false unless easytalk_model?(check_type)
|
|
239
|
-
|
|
240
|
-
# Per-property constraint takes precedence
|
|
241
|
-
return constraints[:ref] if constraints.key?(:ref)
|
|
242
|
-
|
|
243
|
-
# Fall back to global configuration
|
|
244
|
-
EasyTalk.configuration.use_refs
|
|
245
|
-
end
|
|
246
|
-
|
|
247
|
-
# Checks if a type is an EasyTalk model.
|
|
248
|
-
#
|
|
249
|
-
# @param check_type [Object] The type to check
|
|
250
|
-
# @return [Boolean] true if the type is an EasyTalk model
|
|
251
|
-
# @api private
|
|
252
|
-
def easytalk_model?(check_type)
|
|
253
|
-
check_type.is_a?(Class) &&
|
|
254
|
-
check_type.respond_to?(:schema) &&
|
|
255
|
-
check_type.respond_to?(:ref_template) &&
|
|
256
|
-
defined?(EasyTalk::Model) &&
|
|
257
|
-
check_type.include?(EasyTalk::Model)
|
|
258
|
-
end
|
|
259
|
-
|
|
260
|
-
# Builds a $ref schema for an EasyTalk model.
|
|
261
|
-
#
|
|
262
|
-
# @return [Hash] A schema with $ref pointing to the model's definition
|
|
263
|
-
# @api private
|
|
264
|
-
def build_ref_schema
|
|
265
|
-
# Remove ref and optional from constraints as they're not JSON Schema keywords
|
|
266
|
-
ref_constraints = constraints.except(:ref, :optional)
|
|
267
|
-
schema = { '$ref': type.ref_template }
|
|
268
|
-
ref_constraints.empty? ? schema : schema.merge(ref_constraints)
|
|
269
|
-
end
|
|
270
192
|
end
|
|
271
193
|
end
|
|
@@ -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
|