easy_talk 3.3.0 → 3.3.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +41 -0
- data/README.md +191 -31
- data/easy_talk.gemspec +42 -0
- data/examples/ruby_llm/Gemfile +12 -0
- data/examples/ruby_llm/structured_output.rb +47 -0
- data/examples/ruby_llm/tools_integration.rb +49 -0
- data/lib/easy_talk/builders/composition_builder.rb +3 -0
- data/lib/easy_talk/builders/null_builder.rb +5 -3
- data/lib/easy_talk/builders/object_builder.rb +5 -31
- data/lib/easy_talk/builders/union_builder.rb +3 -0
- data/lib/easy_talk/errors_helper.rb +4 -4
- data/lib/easy_talk/extensions/ruby_llm_compatibility.rb +58 -0
- data/lib/easy_talk/model.rb +33 -167
- data/lib/easy_talk/property.rb +2 -3
- data/lib/easy_talk/schema.rb +19 -129
- data/lib/easy_talk/schema_base.rb +181 -0
- data/lib/easy_talk/schema_definition.rb +1 -1
- data/lib/easy_talk/type_introspection.rb +9 -0
- data/lib/easy_talk/validation_adapters/active_model_adapter.rb +37 -37
- data/lib/easy_talk/validation_adapters/base.rb +7 -39
- data/lib/easy_talk/version.rb +1 -1
- data/lib/easy_talk.rb +23 -1
- metadata +7 -1
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module EasyTalk
|
|
4
|
+
module Extensions
|
|
5
|
+
# Class methods for RubyLLM compatibility.
|
|
6
|
+
# These are added to the model class via `extend`.
|
|
7
|
+
module RubyLLMCompatibility
|
|
8
|
+
# Returns a Hash representing the schema in a format compatible with RubyLLM.
|
|
9
|
+
# RubyLLM expects an object that responds to #to_json_schema and returns
|
|
10
|
+
# a hash with :name, :description, and :schema keys.
|
|
11
|
+
#
|
|
12
|
+
# @return [Hash] The RubyLLM-compatible schema representation
|
|
13
|
+
def to_json_schema
|
|
14
|
+
{
|
|
15
|
+
name: name,
|
|
16
|
+
description: schema_definition.schema[:description] || "Schema for #{name}",
|
|
17
|
+
schema: json_schema
|
|
18
|
+
}
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# Overrides for classes that inherit from RubyLLM::Tool.
|
|
23
|
+
# Only overrides schema-related methods, allowing all other RubyLLM::Tool
|
|
24
|
+
# functionality (halt, call, etc.) to work normally.
|
|
25
|
+
#
|
|
26
|
+
# Usage:
|
|
27
|
+
# class WeatherTool < RubyLLM::Tool
|
|
28
|
+
# include EasyTalk::Model
|
|
29
|
+
#
|
|
30
|
+
# define_schema do
|
|
31
|
+
# description 'Gets current weather'
|
|
32
|
+
# property :latitude, String
|
|
33
|
+
# property :longitude, String
|
|
34
|
+
# end
|
|
35
|
+
#
|
|
36
|
+
# def execute(latitude:, longitude:)
|
|
37
|
+
# # Can use halt() since we inherit from RubyLLM::Tool
|
|
38
|
+
# halt "Weather at #{latitude}, #{longitude}"
|
|
39
|
+
# end
|
|
40
|
+
# end
|
|
41
|
+
module RubyLLMToolOverrides
|
|
42
|
+
# Override to use EasyTalk's schema description.
|
|
43
|
+
#
|
|
44
|
+
# @return [String] The tool description from EasyTalk schema
|
|
45
|
+
def description
|
|
46
|
+
schema_def = self.class.schema_definition
|
|
47
|
+
schema_def.schema[:description] || "Tool: #{self.class.name}"
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Override to use EasyTalk's JSON schema for parameters.
|
|
51
|
+
#
|
|
52
|
+
# @return [Hash] The JSON schema for parameters
|
|
53
|
+
def params_schema
|
|
54
|
+
self.class.json_schema
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
data/lib/easy_talk/model.rb
CHANGED
|
@@ -8,10 +8,10 @@ require 'active_support/time'
|
|
|
8
8
|
require 'active_support/concern'
|
|
9
9
|
require 'active_support/json'
|
|
10
10
|
require 'active_model'
|
|
11
|
-
require_relative '
|
|
12
|
-
require_relative 'schema_definition'
|
|
11
|
+
require_relative 'schema_base'
|
|
13
12
|
require_relative 'validation_builder'
|
|
14
13
|
require_relative 'error_formatter'
|
|
14
|
+
require_relative 'extensions/ruby_llm_compatibility'
|
|
15
15
|
|
|
16
16
|
module EasyTalk
|
|
17
17
|
# The `Model` module is a mixin that provides functionality for defining and accessing the schema of a model.
|
|
@@ -38,150 +38,45 @@ module EasyTalk
|
|
|
38
38
|
module Model
|
|
39
39
|
def self.included(base)
|
|
40
40
|
base.extend(ClassMethods)
|
|
41
|
+
base.extend(EasyTalk::Extensions::RubyLLMCompatibility) # Add class-level methods
|
|
41
42
|
|
|
42
43
|
base.include ActiveModel::API
|
|
43
44
|
base.include ActiveModel::Validations
|
|
44
45
|
base.extend ActiveModel::Callbacks
|
|
45
46
|
base.include(InstanceMethods)
|
|
46
47
|
base.include(ErrorFormatter::InstanceMethods)
|
|
48
|
+
|
|
49
|
+
# If inheriting from RubyLLM::Tool, override schema methods to use EasyTalk's schema
|
|
50
|
+
return unless defined?(RubyLLM::Tool) && base < RubyLLM::Tool
|
|
51
|
+
|
|
52
|
+
base.include(EasyTalk::Extensions::RubyLLMToolOverrides)
|
|
47
53
|
end
|
|
48
54
|
|
|
49
55
|
# Instance methods mixed into models that include EasyTalk::Model
|
|
50
56
|
module InstanceMethods
|
|
57
|
+
include SchemaBase::InstanceMethods
|
|
58
|
+
|
|
51
59
|
def initialize(attributes = {})
|
|
52
60
|
@additional_properties = {}
|
|
53
61
|
provided_keys = attributes.keys.to_set(&:to_sym)
|
|
54
62
|
|
|
55
|
-
super # Perform initial mass assignment
|
|
63
|
+
super # Perform initial mass assignment via ActiveModel::API
|
|
56
64
|
|
|
57
|
-
|
|
58
|
-
return unless schema_def.respond_to?(:schema) && schema_def.schema.is_a?(Hash)
|
|
59
|
-
|
|
60
|
-
(schema_def.schema[:properties] || {}).each do |prop_name, prop_definition|
|
|
61
|
-
process_property_initialization(prop_name, prop_definition, provided_keys)
|
|
62
|
-
end
|
|
65
|
+
initialize_schema_properties(provided_keys)
|
|
63
66
|
end
|
|
64
67
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
apply_default_value(prop_name, prop_definition, provided_keys)
|
|
72
|
-
|
|
73
|
-
current_value = public_send(prop_name)
|
|
74
|
-
return if nilable_type && current_value.nil?
|
|
75
|
-
|
|
76
|
-
defined_type = T::Utils::Nilable.get_underlying_type(defined_type) if nilable_type
|
|
77
|
-
instantiate_nested_models(prop_name, defined_type, current_value)
|
|
78
|
-
end
|
|
79
|
-
|
|
80
|
-
def apply_default_value(prop_name, prop_definition, provided_keys)
|
|
81
|
-
return if provided_keys.include?(prop_name)
|
|
82
|
-
|
|
83
|
-
default_value = prop_definition.dig(:constraints, :default)
|
|
84
|
-
public_send("#{prop_name}=", default_value) unless default_value.nil?
|
|
85
|
-
end
|
|
86
|
-
|
|
87
|
-
def instantiate_nested_models(prop_name, defined_type, current_value)
|
|
88
|
-
# Single nested model: convert Hash to model instance
|
|
89
|
-
if defined_type.is_a?(Class) && defined_type.include?(EasyTalk::Model) && current_value.is_a?(Hash)
|
|
90
|
-
public_send("#{prop_name}=", defined_type.new(current_value))
|
|
91
|
-
return
|
|
92
|
-
end
|
|
93
|
-
|
|
94
|
-
# Array of nested models: convert Hash items to model instances
|
|
95
|
-
instantiate_array_items(prop_name, defined_type, current_value)
|
|
96
|
-
end
|
|
97
|
-
|
|
98
|
-
def instantiate_array_items(prop_name, defined_type, current_value)
|
|
99
|
-
return unless defined_type.is_a?(T::Types::TypedArray) && current_value.is_a?(Array)
|
|
100
|
-
|
|
101
|
-
item_type = defined_type.type.respond_to?(:raw_type) ? defined_type.type.raw_type : nil
|
|
102
|
-
return unless item_type.is_a?(Class) && item_type.include?(EasyTalk::Model)
|
|
103
|
-
|
|
104
|
-
instantiated = current_value.map { |item| item.is_a?(Hash) ? item_type.new(item) : item }
|
|
105
|
-
public_send("#{prop_name}=", instantiated)
|
|
106
|
-
end
|
|
107
|
-
|
|
108
|
-
public
|
|
109
|
-
|
|
110
|
-
def method_missing(method_name, *args)
|
|
111
|
-
method_string = method_name.to_s
|
|
112
|
-
if method_string.end_with?('=')
|
|
113
|
-
property_name = method_string.chomp('=')
|
|
114
|
-
if self.class.additional_properties_allowed?
|
|
115
|
-
@additional_properties[property_name] = args.first
|
|
116
|
-
else
|
|
117
|
-
super
|
|
118
|
-
end
|
|
119
|
-
elsif self.class.additional_properties_allowed? && @additional_properties.key?(method_string)
|
|
120
|
-
@additional_properties[method_string]
|
|
121
|
-
else
|
|
122
|
-
super
|
|
123
|
-
end
|
|
124
|
-
end
|
|
125
|
-
|
|
126
|
-
def respond_to_missing?(method_name, include_private = false)
|
|
127
|
-
return super unless self.class.additional_properties_allowed?
|
|
128
|
-
|
|
129
|
-
method_string = method_name.to_s
|
|
130
|
-
method_string.end_with?('=') || @additional_properties.key?(method_string) || super
|
|
131
|
-
end
|
|
132
|
-
|
|
133
|
-
# Add to_hash method to convert defined properties to hash
|
|
134
|
-
def to_hash
|
|
135
|
-
properties_to_include = (self.class.schema_definition.schema[:properties] || {}).keys
|
|
136
|
-
return {} if properties_to_include.empty?
|
|
137
|
-
|
|
138
|
-
properties_to_include.each_with_object({}) do |prop, hash|
|
|
139
|
-
hash[prop.to_s] = send(prop)
|
|
140
|
-
end
|
|
141
|
-
end
|
|
142
|
-
|
|
143
|
-
# Override as_json to include both defined and additional properties
|
|
144
|
-
def as_json(_options = {})
|
|
145
|
-
to_hash.merge(@additional_properties)
|
|
146
|
-
end
|
|
147
|
-
|
|
148
|
-
# to_h includes both defined and additional properties
|
|
149
|
-
def to_h
|
|
150
|
-
to_hash.merge(@additional_properties)
|
|
151
|
-
end
|
|
152
|
-
|
|
153
|
-
# Allow comparison with hashes
|
|
154
|
-
def ==(other)
|
|
155
|
-
case other
|
|
156
|
-
when Hash
|
|
157
|
-
# Convert both to comparable format for comparison
|
|
158
|
-
self_hash = (self.class.schema_definition.schema[:properties] || {}).keys.each_with_object({}) do |prop, hash|
|
|
159
|
-
hash[prop] = send(prop)
|
|
160
|
-
end
|
|
161
|
-
|
|
162
|
-
# Handle both symbol and string keys in the other hash
|
|
163
|
-
other_normalized = other.transform_keys(&:to_sym)
|
|
164
|
-
self_hash == other_normalized
|
|
165
|
-
else
|
|
166
|
-
super
|
|
167
|
-
end
|
|
68
|
+
# Returns a Hash representing the schema in a format compatible with RubyLLM.
|
|
69
|
+
# Delegates to the class method. Required for RubyLLM's with_schema method.
|
|
70
|
+
#
|
|
71
|
+
# @return [Hash] The RubyLLM-compatible schema representation
|
|
72
|
+
def to_json_schema
|
|
73
|
+
self.class.to_json_schema
|
|
168
74
|
end
|
|
169
75
|
end
|
|
170
76
|
|
|
171
77
|
# Module containing class-level methods for defining and accessing the schema of a model.
|
|
172
78
|
module ClassMethods
|
|
173
|
-
include
|
|
174
|
-
|
|
175
|
-
# Returns the schema for the model.
|
|
176
|
-
#
|
|
177
|
-
# @return [Schema] The schema for the model.
|
|
178
|
-
def schema
|
|
179
|
-
@schema ||= if defined?(@schema_definition) && @schema_definition
|
|
180
|
-
build_schema(@schema_definition)
|
|
181
|
-
else
|
|
182
|
-
{}
|
|
183
|
-
end
|
|
184
|
-
end
|
|
79
|
+
include SchemaBase::ClassMethods
|
|
185
80
|
|
|
186
81
|
# Define the schema for the model using the provided block.
|
|
187
82
|
#
|
|
@@ -205,22 +100,11 @@ module EasyTalk
|
|
|
205
100
|
# property :name, String
|
|
206
101
|
# end
|
|
207
102
|
def define_schema(options = {}, &)
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
@schema_definition = SchemaDefinition.new(name)
|
|
211
|
-
@schema_definition.klass = self # Pass the model class to the schema definition
|
|
212
|
-
@schema_definition.instance_eval(&)
|
|
103
|
+
super(&)
|
|
213
104
|
|
|
214
105
|
# Store validation options for this model
|
|
215
106
|
@validation_options = normalize_validation_options(options)
|
|
216
107
|
|
|
217
|
-
# Define accessors immediately based on schema_definition
|
|
218
|
-
defined_properties = (@schema_definition.schema[:properties] || {}).keys
|
|
219
|
-
attr_accessor(*defined_properties)
|
|
220
|
-
|
|
221
|
-
# Track which properties have had validations applied
|
|
222
|
-
@validated_properties ||= Set.new
|
|
223
|
-
|
|
224
108
|
# Initialize mutex eagerly for thread-safe schema-level validation application
|
|
225
109
|
@schema_level_validation_lock = Mutex.new
|
|
226
110
|
|
|
@@ -232,6 +116,19 @@ module EasyTalk
|
|
|
232
116
|
|
|
233
117
|
private
|
|
234
118
|
|
|
119
|
+
# Reset all memoized schema state and clear previously registered
|
|
120
|
+
# ActiveModel validators so a second define_schema call is never ignored.
|
|
121
|
+
def clear_schema_state!
|
|
122
|
+
super
|
|
123
|
+
@schema_level_validations_applied = false
|
|
124
|
+
@validated_properties = Set.new
|
|
125
|
+
|
|
126
|
+
return unless @schema_definition
|
|
127
|
+
|
|
128
|
+
reset_callbacks(:validate)
|
|
129
|
+
_validators.clear
|
|
130
|
+
end
|
|
131
|
+
|
|
235
132
|
# Normalize validation options from various input formats.
|
|
236
133
|
#
|
|
237
134
|
# @param options [Hash] The options hash from define_schema
|
|
@@ -298,37 +195,6 @@ module EasyTalk
|
|
|
298
195
|
@schema_level_validations_applied = true
|
|
299
196
|
end
|
|
300
197
|
end
|
|
301
|
-
|
|
302
|
-
public
|
|
303
|
-
|
|
304
|
-
# Returns the unvalidated schema definition for the model.
|
|
305
|
-
#
|
|
306
|
-
# @return [SchemaDefinition] The unvalidated schema definition for the model.
|
|
307
|
-
def schema_definition
|
|
308
|
-
@schema_definition ||= {}
|
|
309
|
-
end
|
|
310
|
-
|
|
311
|
-
def additional_properties_allowed?
|
|
312
|
-
ap = @schema_definition&.schema&.fetch(:additional_properties, false)
|
|
313
|
-
# Allow if true, or if it's a schema object (Class or Hash with type)
|
|
314
|
-
ap == true || ap.is_a?(Class) || ap.is_a?(Hash)
|
|
315
|
-
end
|
|
316
|
-
|
|
317
|
-
# Returns the property names defined in the schema
|
|
318
|
-
#
|
|
319
|
-
# @return [Array<Symbol>] Array of property names as symbols
|
|
320
|
-
def properties
|
|
321
|
-
(@schema_definition&.schema&.dig(:properties) || {}).keys
|
|
322
|
-
end
|
|
323
|
-
|
|
324
|
-
# Builds the schema using the provided schema definition.
|
|
325
|
-
# This is the convergence point for all schema generation.
|
|
326
|
-
#
|
|
327
|
-
# @param schema_definition [SchemaDefinition] The schema definition.
|
|
328
|
-
# @return [Schema] The validated schema.
|
|
329
|
-
def build_schema(schema_definition)
|
|
330
|
-
Builders::ObjectBuilder.new(schema_definition).build
|
|
331
|
-
end
|
|
332
198
|
end
|
|
333
199
|
end
|
|
334
200
|
end
|
data/lib/easy_talk/property.rb
CHANGED
|
@@ -112,9 +112,8 @@ module EasyTalk
|
|
|
112
112
|
args = is_collection ? [name, type, constraints] : [name, constraints]
|
|
113
113
|
builder_class.new(*args).build
|
|
114
114
|
elsif type.respond_to?(:schema)
|
|
115
|
-
#
|
|
116
|
-
|
|
117
|
-
type.schema.merge!(constraints)
|
|
115
|
+
# deep_dup so nested hashes in the cached schema aren't shared with the result
|
|
116
|
+
EasyTalk.deep_dup(type.schema).merge(constraints)
|
|
118
117
|
else
|
|
119
118
|
raise UnknownTypeError,
|
|
120
119
|
"Unknown type '#{type.inspect}' for property '#{name}'. " \
|
data/lib/easy_talk/schema.rb
CHANGED
|
@@ -7,8 +7,7 @@ require 'active_support/core_ext'
|
|
|
7
7
|
require 'active_support/time'
|
|
8
8
|
require 'active_support/concern'
|
|
9
9
|
require 'active_support/json'
|
|
10
|
-
require_relative '
|
|
11
|
-
require_relative 'schema_definition'
|
|
10
|
+
require_relative 'schema_base'
|
|
12
11
|
|
|
13
12
|
module EasyTalk
|
|
14
13
|
# A lightweight module for schema generation without ActiveModel validations.
|
|
@@ -50,150 +49,41 @@ module EasyTalk
|
|
|
50
49
|
|
|
51
50
|
# Instance methods for schema-only models.
|
|
52
51
|
module InstanceMethods
|
|
52
|
+
include SchemaBase::InstanceMethods
|
|
53
|
+
|
|
53
54
|
# Initialize the schema object with attributes.
|
|
55
|
+
# Performs manual attribute assignment (no ActiveModel) then applies
|
|
56
|
+
# defaults and nested model instantiation via the shared base.
|
|
54
57
|
#
|
|
55
58
|
# @param attributes [Hash] The attributes to set
|
|
56
59
|
def initialize(attributes = {})
|
|
57
60
|
@additional_properties = {}
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
return unless schema_def.respond_to?(:schema) && schema_def.schema.is_a?(Hash)
|
|
61
|
-
|
|
62
|
-
(schema_def.schema[:properties] || {}).each do |prop_name, prop_definition|
|
|
63
|
-
value = attributes[prop_name] || attributes[prop_name.to_s]
|
|
64
|
-
|
|
65
|
-
# Handle default values
|
|
66
|
-
if value.nil? && !attributes.key?(prop_name) && !attributes.key?(prop_name.to_s)
|
|
67
|
-
default_value = prop_definition.dig(:constraints, :default)
|
|
68
|
-
value = default_value unless default_value.nil?
|
|
69
|
-
end
|
|
70
|
-
|
|
71
|
-
# Handle nested EasyTalk::Schema or EasyTalk::Model objects
|
|
72
|
-
defined_type = prop_definition[:type]
|
|
73
|
-
nilable_type = defined_type.respond_to?(:nilable?) && defined_type.nilable?
|
|
74
|
-
defined_type = T::Utils::Nilable.get_underlying_type(defined_type) if nilable_type
|
|
75
|
-
|
|
76
|
-
if defined_type.is_a?(Class) &&
|
|
77
|
-
(defined_type.include?(EasyTalk::Schema) || defined_type.include?(EasyTalk::Model)) &&
|
|
78
|
-
value.is_a?(Hash)
|
|
79
|
-
value = defined_type.new(value)
|
|
80
|
-
end
|
|
81
|
-
|
|
82
|
-
instance_variable_set("@#{prop_name}", value)
|
|
83
|
-
end
|
|
84
|
-
end
|
|
61
|
+
provided_keys = Set.new
|
|
85
62
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
# @return [Hash] The properties as a hash
|
|
89
|
-
def to_hash
|
|
90
|
-
properties_to_include = (self.class.schema_definition.schema[:properties] || {}).keys
|
|
91
|
-
return {} if properties_to_include.empty?
|
|
92
|
-
|
|
93
|
-
properties_to_include.each_with_object({}) do |prop, hash|
|
|
94
|
-
hash[prop.to_s] = send(prop)
|
|
95
|
-
end
|
|
63
|
+
assign_schema_attributes(attributes, provided_keys)
|
|
64
|
+
initialize_schema_properties(provided_keys)
|
|
96
65
|
end
|
|
97
66
|
|
|
98
|
-
|
|
99
|
-
#
|
|
100
|
-
# @param _options [Hash] JSON options (ignored)
|
|
101
|
-
# @return [Hash] The combined hash
|
|
102
|
-
def as_json(_options = {})
|
|
103
|
-
to_hash.merge(@additional_properties)
|
|
104
|
-
end
|
|
67
|
+
private
|
|
105
68
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
# @return [Hash] The combined hash
|
|
109
|
-
def to_h
|
|
110
|
-
to_hash.merge(@additional_properties)
|
|
111
|
-
end
|
|
69
|
+
def assign_schema_attributes(attributes, provided_keys)
|
|
70
|
+
defined_properties = self.class.properties.to_set
|
|
112
71
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
self_hash = (self.class.schema_definition.schema[:properties] || {}).keys.each_with_object({}) do |prop, hash|
|
|
121
|
-
hash[prop] = send(prop)
|
|
72
|
+
attributes.each do |key, value|
|
|
73
|
+
prop_name = key.to_sym
|
|
74
|
+
if defined_properties.include?(prop_name)
|
|
75
|
+
provided_keys << prop_name
|
|
76
|
+
public_send("#{prop_name}=", value)
|
|
77
|
+
elsif self.class.additional_properties_allowed?
|
|
78
|
+
@additional_properties[key.to_s] = value
|
|
122
79
|
end
|
|
123
|
-
other_normalized = other.transform_keys(&:to_sym)
|
|
124
|
-
self_hash == other_normalized
|
|
125
|
-
else
|
|
126
|
-
super
|
|
127
80
|
end
|
|
128
81
|
end
|
|
129
82
|
end
|
|
130
83
|
|
|
131
84
|
# Class methods for schema-only models.
|
|
132
85
|
module ClassMethods
|
|
133
|
-
include
|
|
134
|
-
|
|
135
|
-
# Returns the schema for the model.
|
|
136
|
-
#
|
|
137
|
-
# @return [Hash] The schema for the model.
|
|
138
|
-
def schema
|
|
139
|
-
@schema ||= if defined?(@schema_definition) && @schema_definition
|
|
140
|
-
build_schema(@schema_definition)
|
|
141
|
-
else
|
|
142
|
-
{}
|
|
143
|
-
end
|
|
144
|
-
end
|
|
145
|
-
|
|
146
|
-
# Define the schema for the model using the provided block.
|
|
147
|
-
# Unlike EasyTalk::Model, this does NOT apply any validations.
|
|
148
|
-
#
|
|
149
|
-
# @yield The block to define the schema.
|
|
150
|
-
# @raise [ArgumentError] If the class does not have a name.
|
|
151
|
-
def define_schema(&)
|
|
152
|
-
raise ArgumentError, 'The class must have a name' unless name.present?
|
|
153
|
-
|
|
154
|
-
@schema_definition = SchemaDefinition.new(name)
|
|
155
|
-
@schema_definition.klass = self
|
|
156
|
-
@schema_definition.instance_eval(&)
|
|
157
|
-
|
|
158
|
-
# Define accessors for all properties
|
|
159
|
-
defined_properties = (@schema_definition.schema[:properties] || {}).keys
|
|
160
|
-
attr_accessor(*defined_properties)
|
|
161
|
-
|
|
162
|
-
# NO validations are applied - this is schema-only
|
|
163
|
-
|
|
164
|
-
@schema_definition
|
|
165
|
-
end
|
|
166
|
-
|
|
167
|
-
# Returns the schema definition for the model.
|
|
168
|
-
#
|
|
169
|
-
# @return [SchemaDefinition] The schema definition.
|
|
170
|
-
def schema_definition
|
|
171
|
-
@schema_definition ||= {}
|
|
172
|
-
end
|
|
173
|
-
|
|
174
|
-
# Check if additional properties are allowed.
|
|
175
|
-
#
|
|
176
|
-
# @return [Boolean] True if additional properties are allowed.
|
|
177
|
-
def additional_properties_allowed?
|
|
178
|
-
@schema_definition&.schema&.fetch(:additional_properties, false)
|
|
179
|
-
end
|
|
180
|
-
|
|
181
|
-
# Returns the property names defined in the schema.
|
|
182
|
-
#
|
|
183
|
-
# @return [Array<Symbol>] Array of property names as symbols.
|
|
184
|
-
def properties
|
|
185
|
-
(@schema_definition&.schema&.dig(:properties) || {}).keys
|
|
186
|
-
end
|
|
187
|
-
|
|
188
|
-
private
|
|
189
|
-
|
|
190
|
-
# Builds the schema using the provided schema definition.
|
|
191
|
-
#
|
|
192
|
-
# @param schema_definition [SchemaDefinition] The schema definition.
|
|
193
|
-
# @return [Hash] The built schema.
|
|
194
|
-
def build_schema(schema_definition)
|
|
195
|
-
Builders::ObjectBuilder.new(schema_definition).build
|
|
196
|
-
end
|
|
86
|
+
include SchemaBase::ClassMethods
|
|
197
87
|
end
|
|
198
88
|
end
|
|
199
89
|
end
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
# typed: true
|
|
3
|
+
|
|
4
|
+
require_relative 'builders/object_builder'
|
|
5
|
+
require_relative 'schema_definition'
|
|
6
|
+
|
|
7
|
+
module EasyTalk
|
|
8
|
+
# Shared foundation for both EasyTalk::Schema and EasyTalk::Model.
|
|
9
|
+
#
|
|
10
|
+
# This module extracts the common instance and class methods so that
|
|
11
|
+
# Schema (lightweight, no validations) and Model (full ActiveModel
|
|
12
|
+
# validations) stay in sync without code duplication.
|
|
13
|
+
module SchemaBase
|
|
14
|
+
# Instance methods shared by Schema and Model.
|
|
15
|
+
#
|
|
16
|
+
# Each including module provides its own `initialize` that:
|
|
17
|
+
# 1. Sets `@additional_properties = {}`
|
|
18
|
+
# 2. Performs attribute assignment (manually or via ActiveModel)
|
|
19
|
+
# 3. Calls `initialize_schema_properties(provided_keys)`
|
|
20
|
+
module InstanceMethods
|
|
21
|
+
private
|
|
22
|
+
|
|
23
|
+
def initialize_schema_properties(provided_keys)
|
|
24
|
+
schema_def = self.class.schema_definition
|
|
25
|
+
return unless schema_def.respond_to?(:schema) && schema_def.schema.is_a?(Hash)
|
|
26
|
+
|
|
27
|
+
(schema_def.schema[:properties] || {}).each do |prop_name, prop_definition|
|
|
28
|
+
process_property_initialization(prop_name, prop_definition, provided_keys)
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def process_property_initialization(prop_name, prop_definition, provided_keys)
|
|
33
|
+
defined_type = prop_definition[:type]
|
|
34
|
+
nilable_type = defined_type.respond_to?(:nilable?) && defined_type.nilable?
|
|
35
|
+
|
|
36
|
+
apply_default_value(prop_name, prop_definition, provided_keys)
|
|
37
|
+
|
|
38
|
+
current_value = public_send(prop_name)
|
|
39
|
+
return if nilable_type && current_value.nil?
|
|
40
|
+
|
|
41
|
+
defined_type = T::Utils::Nilable.get_underlying_type(defined_type) if nilable_type
|
|
42
|
+
instantiate_nested_models(prop_name, defined_type, current_value)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def apply_default_value(prop_name, prop_definition, provided_keys)
|
|
46
|
+
return if provided_keys.include?(prop_name)
|
|
47
|
+
|
|
48
|
+
default_value = prop_definition.dig(:constraints, :default)
|
|
49
|
+
public_send("#{prop_name}=", EasyTalk.deep_dup(default_value)) unless default_value.nil?
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def instantiate_nested_models(prop_name, defined_type, current_value)
|
|
53
|
+
if easy_talk_class?(defined_type) && current_value.is_a?(Hash)
|
|
54
|
+
public_send("#{prop_name}=", defined_type.new(current_value))
|
|
55
|
+
return
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
instantiate_array_items(prop_name, defined_type, current_value)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def easy_talk_class?(type)
|
|
62
|
+
type.is_a?(Class) && (
|
|
63
|
+
type.include?(EasyTalk::Model) || type.include?(EasyTalk::Schema)
|
|
64
|
+
)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def instantiate_array_items(prop_name, defined_type, current_value)
|
|
68
|
+
return unless defined_type.is_a?(T::Types::TypedArray) && current_value.is_a?(Array)
|
|
69
|
+
|
|
70
|
+
item_type = defined_type.type.respond_to?(:raw_type) ? defined_type.type.raw_type : nil
|
|
71
|
+
return unless easy_talk_class?(item_type)
|
|
72
|
+
|
|
73
|
+
instantiated = current_value.map { |item| item.is_a?(Hash) ? item_type.new(item) : item }
|
|
74
|
+
public_send("#{prop_name}=", instantiated)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
public
|
|
78
|
+
|
|
79
|
+
def method_missing(method_name, *args)
|
|
80
|
+
method_string = method_name.to_s
|
|
81
|
+
if method_string.end_with?('=')
|
|
82
|
+
property_name = method_string.chomp('=')
|
|
83
|
+
if self.class.additional_properties_allowed?
|
|
84
|
+
@additional_properties[property_name] = args.first
|
|
85
|
+
else
|
|
86
|
+
super
|
|
87
|
+
end
|
|
88
|
+
elsif self.class.additional_properties_allowed? && @additional_properties.key?(method_string)
|
|
89
|
+
@additional_properties[method_string]
|
|
90
|
+
else
|
|
91
|
+
super
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def respond_to_missing?(method_name, include_private = false)
|
|
96
|
+
return super unless self.class.additional_properties_allowed?
|
|
97
|
+
|
|
98
|
+
method_string = method_name.to_s
|
|
99
|
+
method_string.end_with?('=') || @additional_properties.key?(method_string) || super
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def to_hash
|
|
103
|
+
properties_to_include = (self.class.schema_definition.schema[:properties] || {}).keys
|
|
104
|
+
return {} if properties_to_include.empty?
|
|
105
|
+
|
|
106
|
+
properties_to_include.to_h { |prop| [prop.to_s, send(prop)] }
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def as_json(_options = {})
|
|
110
|
+
to_hash.merge(@additional_properties)
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def to_h
|
|
114
|
+
to_hash.merge(@additional_properties)
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def ==(other)
|
|
118
|
+
case other
|
|
119
|
+
when Hash
|
|
120
|
+
self_hash = (self.class.schema_definition.schema[:properties] || {}).keys.to_h { |prop| [prop, send(prop)] }
|
|
121
|
+
other_normalized = other.transform_keys(&:to_sym)
|
|
122
|
+
self_hash == other_normalized
|
|
123
|
+
else
|
|
124
|
+
super
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
# Class methods shared by Schema and Model.
|
|
130
|
+
module ClassMethods
|
|
131
|
+
include SchemaMethods
|
|
132
|
+
|
|
133
|
+
def schema
|
|
134
|
+
@schema ||= if defined?(@schema_definition) && @schema_definition
|
|
135
|
+
build_schema(@schema_definition)
|
|
136
|
+
else
|
|
137
|
+
{}
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def define_schema(&)
|
|
142
|
+
raise ArgumentError, 'The class must have a name' unless name.present?
|
|
143
|
+
|
|
144
|
+
clear_schema_state!
|
|
145
|
+
|
|
146
|
+
@schema_definition = SchemaDefinition.new(name)
|
|
147
|
+
@schema_definition.klass = self
|
|
148
|
+
@schema_definition.instance_eval(&)
|
|
149
|
+
|
|
150
|
+
defined_properties = (@schema_definition.schema[:properties] || {}).keys
|
|
151
|
+
attr_accessor(*defined_properties)
|
|
152
|
+
|
|
153
|
+
@schema_definition
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
def schema_definition
|
|
157
|
+
@schema_definition ||= {}
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
def additional_properties_allowed?
|
|
161
|
+
ap = @schema_definition&.schema&.fetch(:additional_properties, false)
|
|
162
|
+
ap == true || ap.is_a?(Class) || ap.is_a?(Hash)
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
def properties
|
|
166
|
+
(@schema_definition&.schema&.dig(:properties) || {}).keys
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
private
|
|
170
|
+
|
|
171
|
+
def clear_schema_state!
|
|
172
|
+
@schema = nil
|
|
173
|
+
@json_schema = nil
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
def build_schema(schema_definition)
|
|
177
|
+
Builders::ObjectBuilder.new(schema_definition).build
|
|
178
|
+
end
|
|
179
|
+
end
|
|
180
|
+
end
|
|
181
|
+
end
|