easy_talk 1.0.2 → 1.0.4
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 +6 -3
- data/CHANGELOG.md +32 -0
- data/README.md +907 -146
- data/lib/easy_talk/active_record_schema_builder.rb +295 -0
- data/lib/easy_talk/builders/base_builder.rb +16 -14
- data/lib/easy_talk/builders/composition_builder.rb +12 -0
- data/lib/easy_talk/builders/object_builder.rb +13 -2
- data/lib/easy_talk/builders/string_builder.rb +1 -2
- data/lib/easy_talk/builders/temporal_builder.rb +49 -0
- data/lib/easy_talk/configuration.rb +29 -0
- data/lib/easy_talk/errors.rb +8 -0
- data/lib/easy_talk/errors_helper.rb +147 -0
- data/lib/easy_talk/model.rb +46 -3
- data/lib/easy_talk/property.rb +8 -12
- data/lib/easy_talk/schema_definition.rb +18 -2
- data/lib/easy_talk/types/composer.rb +88 -0
- data/lib/easy_talk/version.rb +1 -1
- data/lib/easy_talk.rb +16 -6
- metadata +36 -11
- 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
@@ -0,0 +1,295 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module EasyTalk
|
4
|
+
# This class is responsible for building a SchemaDefinition from an ActiveRecord model
|
5
|
+
# It analyzes the database schema and creates a SchemaDefinition that can be
|
6
|
+
# passed to ObjectBuilder for final schema generation
|
7
|
+
class ActiveRecordSchemaBuilder
|
8
|
+
# Mapping of ActiveRecord column types to Ruby classes
|
9
|
+
COLUMN_TYPE_MAP = {
|
10
|
+
string: String,
|
11
|
+
text: String,
|
12
|
+
integer: Integer,
|
13
|
+
bigint: Integer,
|
14
|
+
float: Float,
|
15
|
+
decimal: Float,
|
16
|
+
boolean: T::Boolean,
|
17
|
+
date: Date,
|
18
|
+
datetime: DateTime,
|
19
|
+
timestamp: DateTime,
|
20
|
+
time: Time,
|
21
|
+
json: Hash,
|
22
|
+
jsonb: Hash
|
23
|
+
}.freeze
|
24
|
+
|
25
|
+
# Mapping for format constraints based on column type
|
26
|
+
FORMAT_MAP = {
|
27
|
+
date: 'date',
|
28
|
+
datetime: 'date-time',
|
29
|
+
timestamp: 'date-time',
|
30
|
+
time: 'time'
|
31
|
+
}.freeze
|
32
|
+
|
33
|
+
attr_reader :model
|
34
|
+
|
35
|
+
# Initialize the builder with an ActiveRecord model
|
36
|
+
#
|
37
|
+
# @param model [Class] An ActiveRecord model class
|
38
|
+
# @raise [ArgumentError] If the provided class is not an ActiveRecord model
|
39
|
+
def initialize(model)
|
40
|
+
raise ArgumentError, 'Class must be an ActiveRecord model' unless model.ancestors.include?(ActiveRecord::Base)
|
41
|
+
|
42
|
+
@model = model
|
43
|
+
end
|
44
|
+
|
45
|
+
# Build a SchemaDefinition object from the ActiveRecord model
|
46
|
+
#
|
47
|
+
# @return [EasyTalk::SchemaDefinition] A schema definition built from the database structure
|
48
|
+
def build_schema_definition
|
49
|
+
schema_def = SchemaDefinition.new(model.name)
|
50
|
+
|
51
|
+
# Apply basic schema metadata
|
52
|
+
apply_schema_metadata(schema_def)
|
53
|
+
|
54
|
+
# Add all database columns as properties
|
55
|
+
add_column_properties(schema_def)
|
56
|
+
|
57
|
+
# Add model associations as properties
|
58
|
+
add_association_properties(schema_def) unless EasyTalk.configuration.exclude_associations
|
59
|
+
|
60
|
+
# Add virtual properties defined in schema_enhancements
|
61
|
+
add_virtual_properties(schema_def)
|
62
|
+
|
63
|
+
schema_def
|
64
|
+
end
|
65
|
+
|
66
|
+
private
|
67
|
+
|
68
|
+
# Set top-level schema metadata like title, description, and additionalProperties
|
69
|
+
#
|
70
|
+
# @param schema_def [EasyTalk::SchemaDefinition] The schema definition to modify
|
71
|
+
def apply_schema_metadata(schema_def)
|
72
|
+
# Set title (from enhancements or derive from model name)
|
73
|
+
title = schema_enhancements['title'] || model.name.demodulize.humanize
|
74
|
+
schema_def.title(title)
|
75
|
+
|
76
|
+
# Set description if provided
|
77
|
+
if (description = schema_enhancements['description'])
|
78
|
+
schema_def.description(description)
|
79
|
+
end
|
80
|
+
|
81
|
+
# Set additionalProperties (from enhancements or configuration default)
|
82
|
+
additional_props = if schema_enhancements.key?('additionalProperties')
|
83
|
+
schema_enhancements['additionalProperties']
|
84
|
+
else
|
85
|
+
EasyTalk.configuration.default_additional_properties
|
86
|
+
end
|
87
|
+
schema_def.additional_properties(additional_props)
|
88
|
+
end
|
89
|
+
|
90
|
+
# Add properties based on database columns
|
91
|
+
#
|
92
|
+
# @param schema_def [EasyTalk::SchemaDefinition] The schema definition to modify
|
93
|
+
def add_column_properties(schema_def)
|
94
|
+
filtered_columns.each do |column|
|
95
|
+
# Get column enhancement info if it exists
|
96
|
+
column_enhancements = schema_enhancements.dig('properties', column.name.to_s) || {}
|
97
|
+
|
98
|
+
# Map the database type to Ruby type
|
99
|
+
ruby_type = COLUMN_TYPE_MAP.fetch(column.type, String)
|
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
|
+
|
104
|
+
# Build constraints hash for this column
|
105
|
+
constraints = build_column_constraints(column, column_enhancements)
|
106
|
+
|
107
|
+
# Add the property to schema definition
|
108
|
+
schema_def.property(column.name.to_sym, ruby_type, constraints)
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
# Build constraints hash for a database column
|
113
|
+
#
|
114
|
+
# @param column [ActiveRecord::ConnectionAdapters::Column] The database column
|
115
|
+
# @param enhancements [Hash] Any schema enhancements for this column
|
116
|
+
# @return [Hash] The constraints hash
|
117
|
+
def build_column_constraints(column, enhancements)
|
118
|
+
constraints = {
|
119
|
+
optional: enhancements['optional'],
|
120
|
+
description: enhancements['description'],
|
121
|
+
title: enhancements['title']
|
122
|
+
}
|
123
|
+
|
124
|
+
# Add format constraint for date/time columns
|
125
|
+
if (format = FORMAT_MAP[column.type])
|
126
|
+
constraints[:format] = format
|
127
|
+
end
|
128
|
+
|
129
|
+
# Add max_length for string columns with limits
|
130
|
+
constraints[:max_length] = column.limit if column.type == :string && column.limit
|
131
|
+
|
132
|
+
# Add precision/scale for numeric columns
|
133
|
+
if column.type == :decimal && column.precision
|
134
|
+
constraints[:precision] = column.precision
|
135
|
+
constraints[:scale] = column.scale if column.scale
|
136
|
+
end
|
137
|
+
|
138
|
+
# Add default value if present and not a proc
|
139
|
+
constraints[:default] = column.default if column.default && !column.default.is_a?(Proc)
|
140
|
+
|
141
|
+
# Remove nil values
|
142
|
+
constraints.compact
|
143
|
+
end
|
144
|
+
|
145
|
+
# Add properties based on ActiveRecord associations
|
146
|
+
#
|
147
|
+
# @param schema_def [EasyTalk::SchemaDefinition] The schema definition to modify
|
148
|
+
def add_association_properties(schema_def)
|
149
|
+
model.reflect_on_all_associations.each do |association|
|
150
|
+
# Skip if we can't determine the class or it's in the association exclusion list
|
151
|
+
next if association_excluded?(association)
|
152
|
+
|
153
|
+
# Get association enhancement info if it exists
|
154
|
+
assoc_enhancements = schema_enhancements.dig('properties', association.name.to_s) || {}
|
155
|
+
|
156
|
+
case association.macro
|
157
|
+
when :belongs_to, :has_one
|
158
|
+
schema_def.property(
|
159
|
+
association.name,
|
160
|
+
association.klass,
|
161
|
+
{ optional: assoc_enhancements['optional'], description: assoc_enhancements['description'] }.compact
|
162
|
+
)
|
163
|
+
when :has_many, :has_and_belongs_to_many
|
164
|
+
schema_def.property(
|
165
|
+
association.name,
|
166
|
+
T::Array[association.klass],
|
167
|
+
{ optional: assoc_enhancements['optional'], description: assoc_enhancements['description'] }.compact
|
168
|
+
)
|
169
|
+
end
|
170
|
+
end
|
171
|
+
end
|
172
|
+
|
173
|
+
# Add virtual properties defined in schema_enhancements
|
174
|
+
#
|
175
|
+
# @param schema_def [EasyTalk::SchemaDefinition] The schema definition to modify
|
176
|
+
def add_virtual_properties(schema_def)
|
177
|
+
return unless schema_enhancements['properties']
|
178
|
+
|
179
|
+
schema_enhancements['properties'].each do |name, options|
|
180
|
+
next unless options['virtual']
|
181
|
+
|
182
|
+
# Map string type name to Ruby class
|
183
|
+
ruby_type = map_type_string_to_ruby_class(options['type'] || 'string')
|
184
|
+
|
185
|
+
# Build constraints for virtual property
|
186
|
+
constraints = {
|
187
|
+
description: options['description'],
|
188
|
+
title: options['title'],
|
189
|
+
optional: options['optional'],
|
190
|
+
format: options['format'],
|
191
|
+
default: options['default'],
|
192
|
+
min_length: options['minLength'],
|
193
|
+
max_length: options['maxLength'],
|
194
|
+
enum: options['enum']
|
195
|
+
}.compact
|
196
|
+
|
197
|
+
# Add the virtual property
|
198
|
+
schema_def.property(name.to_sym, ruby_type, constraints)
|
199
|
+
end
|
200
|
+
end
|
201
|
+
|
202
|
+
# Map a type string to a Ruby class
|
203
|
+
#
|
204
|
+
# @param type_str [String] The type string (e.g., 'string', 'integer')
|
205
|
+
# @return [Class] The corresponding Ruby class
|
206
|
+
def map_type_string_to_ruby_class(type_str)
|
207
|
+
case type_str.to_s.downcase
|
208
|
+
when 'string' then String
|
209
|
+
when 'integer' then Integer
|
210
|
+
when 'number' then Float
|
211
|
+
when 'boolean' then T::Boolean
|
212
|
+
when 'object' then Hash
|
213
|
+
when 'array' then Array
|
214
|
+
when 'date' then Date
|
215
|
+
when 'datetime' then DateTime
|
216
|
+
when 'time' then Time
|
217
|
+
else String # Default fallback
|
218
|
+
end
|
219
|
+
end
|
220
|
+
|
221
|
+
# Get all columns that should be included in the schema
|
222
|
+
#
|
223
|
+
# @return [Array<ActiveRecord::ConnectionAdapters::Column>] Filtered columns
|
224
|
+
def filtered_columns
|
225
|
+
model.columns.reject do |column|
|
226
|
+
config = EasyTalk.configuration
|
227
|
+
excluded_columns.include?(column.name.to_sym) ||
|
228
|
+
(config.exclude_primary_key && column.name == model.primary_key) ||
|
229
|
+
(config.exclude_timestamps && timestamp_column?(column.name)) ||
|
230
|
+
(config.exclude_foreign_keys && foreign_key_column?(column.name))
|
231
|
+
end
|
232
|
+
end
|
233
|
+
|
234
|
+
# Check if a column is a timestamp column
|
235
|
+
#
|
236
|
+
# @param column_name [String] The column name
|
237
|
+
# @return [Boolean] True if the column is a timestamp column
|
238
|
+
def timestamp_column?(column_name)
|
239
|
+
%w[created_at updated_at].include?(column_name)
|
240
|
+
end
|
241
|
+
|
242
|
+
# Check if a column is a foreign key column
|
243
|
+
#
|
244
|
+
# @param column_name [String] The column name
|
245
|
+
# @return [Boolean] True if the column is a foreign key column
|
246
|
+
def foreign_key_column?(column_name)
|
247
|
+
column_name.end_with?('_id')
|
248
|
+
end
|
249
|
+
|
250
|
+
# Check if an association should be excluded
|
251
|
+
#
|
252
|
+
# @param association [ActiveRecord::Reflection::AssociationReflection] The association
|
253
|
+
# @return [Boolean] True if the association should be excluded
|
254
|
+
def association_excluded?(association)
|
255
|
+
!association.klass ||
|
256
|
+
excluded_associations.include?(association.name.to_sym) ||
|
257
|
+
association.options[:polymorphic] # Skip polymorphic associations (complex to model)
|
258
|
+
end
|
259
|
+
|
260
|
+
# Get schema enhancements
|
261
|
+
#
|
262
|
+
# @return [Hash] Schema enhancements
|
263
|
+
def schema_enhancements
|
264
|
+
@schema_enhancements ||= if model.respond_to?(:schema_enhancements)
|
265
|
+
model.schema_enhancements.deep_transform_keys(&:to_s)
|
266
|
+
else
|
267
|
+
{}
|
268
|
+
end
|
269
|
+
end
|
270
|
+
|
271
|
+
# Get all excluded columns
|
272
|
+
#
|
273
|
+
# @return [Array<Symbol>] Excluded column names
|
274
|
+
def excluded_columns
|
275
|
+
@excluded_columns ||= begin
|
276
|
+
config = EasyTalk.configuration
|
277
|
+
global_exclusions = config.excluded_columns || []
|
278
|
+
model_exclusions = schema_enhancements['ignore'] || []
|
279
|
+
|
280
|
+
# Combine and convert to symbols for consistent comparison
|
281
|
+
(global_exclusions + model_exclusions).map(&:to_sym)
|
282
|
+
end
|
283
|
+
end
|
284
|
+
|
285
|
+
# Get all excluded associations
|
286
|
+
#
|
287
|
+
# @return [Array<Symbol>] Excluded association names
|
288
|
+
def excluded_associations
|
289
|
+
@excluded_associations ||= begin
|
290
|
+
model_exclusions = schema_enhancements['ignore_associations'] || []
|
291
|
+
model_exclusions.map(&:to_sym)
|
292
|
+
end
|
293
|
+
end
|
294
|
+
end
|
295
|
+
end
|
@@ -15,11 +15,11 @@ module EasyTalk
|
|
15
15
|
optional: { type: T.nilable(T::Boolean), key: :optional }
|
16
16
|
}.freeze
|
17
17
|
|
18
|
-
attr_reader :
|
18
|
+
attr_reader :property_name, :schema, :options
|
19
19
|
|
20
20
|
sig do
|
21
21
|
params(
|
22
|
-
|
22
|
+
property_name: Symbol,
|
23
23
|
schema: T::Hash[Symbol, T.untyped],
|
24
24
|
options: T::Hash[Symbol, String],
|
25
25
|
valid_options: T::Hash[Symbol, T.untyped]
|
@@ -27,14 +27,14 @@ module EasyTalk
|
|
27
27
|
end
|
28
28
|
# Initializes a new instance of the BaseBuilder class.
|
29
29
|
#
|
30
|
-
# @param
|
30
|
+
# @param property_name [Symbol] The name of the property.
|
31
31
|
# @param schema [Hash] A hash representing a json schema object.
|
32
32
|
# @param options [Hash] The options for the builder (default: {}).
|
33
33
|
# @param valid_options [Hash] The acceptable options for the given property type (default: {}).
|
34
|
-
def initialize(
|
34
|
+
def initialize(property_name, schema, options = {}, valid_options = {})
|
35
35
|
@valid_options = COMMON_OPTIONS.merge(valid_options)
|
36
|
-
|
37
|
-
@
|
36
|
+
EasyTalk.assert_valid_property_options(property_name, options, @valid_options.keys)
|
37
|
+
@property_name = property_name
|
38
38
|
@schema = schema
|
39
39
|
@options = options
|
40
40
|
end
|
@@ -42,16 +42,18 @@ module EasyTalk
|
|
42
42
|
# Builds the schema object based on the provided options.
|
43
43
|
sig { returns(T::Hash[Symbol, T.untyped]) }
|
44
44
|
def build
|
45
|
-
@valid_options.each_with_object(schema) do |(
|
46
|
-
next if @options[
|
45
|
+
@valid_options.each_with_object(schema) do |(constraint_name, value), obj|
|
46
|
+
next if @options[constraint_name].nil?
|
47
47
|
|
48
|
-
#
|
48
|
+
# Use our centralized validation
|
49
|
+
ErrorHelper.validate_constraint_value(
|
50
|
+
property_name: property_name,
|
51
|
+
constraint_name: constraint_name,
|
52
|
+
value_type: value[:type],
|
53
|
+
value: @options[constraint_name]
|
54
|
+
)
|
49
55
|
|
50
|
-
|
51
|
-
raise TypeError, "Invalid type for #{key}"
|
52
|
-
end
|
53
|
-
|
54
|
-
obj[value[:key]] = T.let(@options[key], value[:type])
|
56
|
+
obj[value[:key]] = @options[constraint_name]
|
55
57
|
end
|
56
58
|
end
|
57
59
|
|
@@ -60,6 +60,18 @@ module EasyTalk
|
|
60
60
|
def items
|
61
61
|
@type.items
|
62
62
|
end
|
63
|
+
|
64
|
+
# Builder class for AllOf composition.
|
65
|
+
class AllOfBuilder < CompositionBuilder
|
66
|
+
end
|
67
|
+
|
68
|
+
# Builder class for AnyOf composition.
|
69
|
+
class AnyOfBuilder < CompositionBuilder
|
70
|
+
end
|
71
|
+
|
72
|
+
# Builder class for OneOf composition.
|
73
|
+
class OneOfBuilder < CompositionBuilder
|
74
|
+
end
|
63
75
|
end
|
64
76
|
end
|
65
77
|
end
|
@@ -1,5 +1,6 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require_relative 'base_builder'
|
2
|
-
require 'set'
|
3
4
|
|
4
5
|
module EasyTalk
|
5
6
|
module Builders
|
@@ -68,6 +69,9 @@ module EasyTalk
|
|
68
69
|
# Populate the final "required" array from @required_properties
|
69
70
|
merged[:required] = @required_properties.to_a if @required_properties.any?
|
70
71
|
|
72
|
+
# Add additionalProperties: false by default if not explicitly set
|
73
|
+
merged[:additional_properties] = false unless merged.key?(:additional_properties)
|
74
|
+
|
71
75
|
# Prune empty or nil values so we don't produce stuff like "properties": {} unnecessarily
|
72
76
|
merged.reject! { |_k, v| v.nil? || v == {} || v == [] }
|
73
77
|
|
@@ -114,6 +118,11 @@ module EasyTalk
|
|
114
118
|
# Check constraints[:optional]
|
115
119
|
return true if prop_options.dig(:constraints, :optional)
|
116
120
|
|
121
|
+
# Check for nil_optional config to determine if nilable should also mean optional
|
122
|
+
if prop_options[:type].respond_to?(:nilable?) && prop_options[:type].nilable?
|
123
|
+
return EasyTalk.configuration.nilable_is_optional
|
124
|
+
end
|
125
|
+
|
117
126
|
false
|
118
127
|
end
|
119
128
|
|
@@ -129,8 +138,10 @@ module EasyTalk
|
|
129
138
|
# This indicates block-style definition => nested schema
|
130
139
|
nested_schema_builder(prop_options)
|
131
140
|
else
|
141
|
+
# Remove optional constraints from the property
|
142
|
+
constraints = prop_options[:constraints].except(:optional)
|
132
143
|
# Normal property: e.g. { type: String, constraints: {...} }
|
133
|
-
Property.new(prop_name, prop_options[:type],
|
144
|
+
Property.new(prop_name, prop_options[:type], constraints)
|
134
145
|
end
|
135
146
|
end
|
136
147
|
|
@@ -15,8 +15,7 @@ module EasyTalk
|
|
15
15
|
max_length: { type: Integer, key: :maxLength },
|
16
16
|
enum: { type: T::Array[String], key: :enum },
|
17
17
|
const: { type: String, key: :const },
|
18
|
-
default: { type: String, key: :default }
|
19
|
-
optional: { type: T::Boolean, key: :optional }
|
18
|
+
default: { type: String, key: :default }
|
20
19
|
}.freeze
|
21
20
|
|
22
21
|
sig { params(name: Symbol, constraints: Hash).void }
|
@@ -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
|
@@ -0,0 +1,29 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module EasyTalk
|
4
|
+
class Configuration
|
5
|
+
attr_accessor :exclude_foreign_keys, :exclude_associations, :excluded_columns,
|
6
|
+
:exclude_primary_key, :exclude_timestamps, :default_additional_properties,
|
7
|
+
:nilable_is_optional
|
8
|
+
|
9
|
+
def initialize
|
10
|
+
@exclude_foreign_keys = true
|
11
|
+
@exclude_associations = true
|
12
|
+
@excluded_columns = []
|
13
|
+
@exclude_primary_key = true # New option, defaulting to true
|
14
|
+
@exclude_timestamps = true # New option, defaulting to true
|
15
|
+
@default_additional_properties = false
|
16
|
+
@nilable_is_optional = false
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
class << self
|
21
|
+
def configuration
|
22
|
+
@configuration ||= Configuration.new
|
23
|
+
end
|
24
|
+
|
25
|
+
def configure
|
26
|
+
yield(configuration)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,147 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module EasyTalk
|
4
|
+
module ErrorHelper
|
5
|
+
def self.raise_constraint_error(property_name:, constraint_name:, expected:, got:)
|
6
|
+
message = "Error in property '#{property_name}': Constraint '#{constraint_name}' expects #{expected}, " \
|
7
|
+
"but received #{got.inspect} (#{got.class})."
|
8
|
+
raise ConstraintError, message
|
9
|
+
end
|
10
|
+
|
11
|
+
def self.raise_array_constraint_error(property_name:, constraint_name:, index:, expected:, got:)
|
12
|
+
message = "Error in property '#{property_name}': Constraint '#{constraint_name}' at index #{index} " \
|
13
|
+
"expects #{expected}, but received #{got.inspect} (#{got.class})."
|
14
|
+
raise ConstraintError, message
|
15
|
+
end
|
16
|
+
|
17
|
+
def self.raise_unknown_option_error(property_name:, option:, valid_options:)
|
18
|
+
option = option.keys.first if option.is_a?(Hash)
|
19
|
+
message = "Unknown option '#{option}' for property '#{property_name}'. " \
|
20
|
+
"Valid options are: #{valid_options.join(', ')}."
|
21
|
+
raise UnknownOptionError, message
|
22
|
+
end
|
23
|
+
|
24
|
+
def self.extract_inner_type(type_info)
|
25
|
+
# No change needed here
|
26
|
+
if type_info.respond_to?(:type) && type_info.type.respond_to?(:raw_type)
|
27
|
+
type_info.type.raw_type
|
28
|
+
# special boolean handling
|
29
|
+
elsif type_info.try(:type).try(:name) == 'T::Boolean'
|
30
|
+
T::Boolean
|
31
|
+
elsif type_info.respond_to?(:type_parameter)
|
32
|
+
type_info.type_parameter
|
33
|
+
elsif type_info.respond_to?(:raw_a) && type_info.respond_to?(:raw_b)
|
34
|
+
# Handle SimplePairUnion types
|
35
|
+
[type_info.raw_a, type_info.raw_b]
|
36
|
+
elsif type_info.respond_to?(:types)
|
37
|
+
# Handle complex union types
|
38
|
+
type_info.types.map { |t| t.respond_to?(:raw_type) ? t.raw_type : t }
|
39
|
+
else
|
40
|
+
# Fallback to something sensible
|
41
|
+
Object
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def self.validate_typed_array_values(property_name:, constraint_name:, type_info:, array_value:)
|
46
|
+
# Skip validation if it's not actually an array
|
47
|
+
return unless array_value.is_a?(Array)
|
48
|
+
|
49
|
+
# Extract the inner type from the array type definition
|
50
|
+
inner_type = extract_inner_type(type_info)
|
51
|
+
|
52
|
+
# Check each element of the array
|
53
|
+
array_value.each_with_index do |element, index|
|
54
|
+
if inner_type.is_a?(Array)
|
55
|
+
# For union types, check if the element matches any of the allowed types
|
56
|
+
unless inner_type.any? { |t| element.is_a?(t) }
|
57
|
+
expected = inner_type.join(' or ')
|
58
|
+
raise_array_constraint_error(
|
59
|
+
property_name: property_name,
|
60
|
+
constraint_name: constraint_name,
|
61
|
+
index: index,
|
62
|
+
expected: expected,
|
63
|
+
got: element
|
64
|
+
)
|
65
|
+
end
|
66
|
+
else
|
67
|
+
# For single types, just check against that type
|
68
|
+
next if [true, false].include?(element)
|
69
|
+
|
70
|
+
unless element.is_a?(inner_type)
|
71
|
+
raise_array_constraint_error(
|
72
|
+
property_name: property_name,
|
73
|
+
constraint_name: constraint_name,
|
74
|
+
index: index,
|
75
|
+
expected: inner_type,
|
76
|
+
got: element
|
77
|
+
)
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
def self.validate_constraint_value(property_name:, constraint_name:, value_type:, value:)
|
84
|
+
return if value.nil?
|
85
|
+
|
86
|
+
if value_type.to_s.include?('Boolean')
|
87
|
+
return if value.is_a?(Array) && value.all? { |v| [true, false].include?(v) }
|
88
|
+
|
89
|
+
unless [true, false].include?(value)
|
90
|
+
raise_constraint_error(
|
91
|
+
property_name: property_name,
|
92
|
+
constraint_name: constraint_name,
|
93
|
+
expected: 'Boolean (true or false)',
|
94
|
+
got: value
|
95
|
+
)
|
96
|
+
end
|
97
|
+
return
|
98
|
+
end
|
99
|
+
|
100
|
+
# Handle simple scalar types (String, Integer, etc.)
|
101
|
+
if value_type.is_a?(Class)
|
102
|
+
unless value.is_a?(value_type)
|
103
|
+
raise_constraint_error(
|
104
|
+
property_name: property_name,
|
105
|
+
constraint_name: constraint_name,
|
106
|
+
expected: value_type,
|
107
|
+
got: value
|
108
|
+
)
|
109
|
+
end
|
110
|
+
# Handle array types specifically
|
111
|
+
elsif value_type.class.name.include?('TypedArray') ||
|
112
|
+
(value_type.respond_to?(:to_s) && value_type.to_s.include?('T::Array'))
|
113
|
+
# This is an array type, validate it
|
114
|
+
validate_typed_array_values(
|
115
|
+
property_name: property_name,
|
116
|
+
constraint_name: constraint_name,
|
117
|
+
type_info: value_type,
|
118
|
+
array_value: value
|
119
|
+
)
|
120
|
+
# Handle Sorbet type objects
|
121
|
+
elsif value_type.class.ancestors.include?(T::Types::Base)
|
122
|
+
# Extract the inner type
|
123
|
+
inner_type = extract_inner_type(value_type)
|
124
|
+
|
125
|
+
if inner_type.is_a?(Array)
|
126
|
+
# For union types, check if the value matches any of the allowed types
|
127
|
+
unless inner_type.any? { |t| value.is_a?(t) }
|
128
|
+
expected = inner_type.join(' or ')
|
129
|
+
raise_constraint_error(
|
130
|
+
property_name: property_name,
|
131
|
+
constraint_name: constraint_name,
|
132
|
+
expected: expected,
|
133
|
+
got: value
|
134
|
+
)
|
135
|
+
end
|
136
|
+
elsif !value.is_a?(inner_type)
|
137
|
+
raise_constraint_error(
|
138
|
+
property_name: property_name,
|
139
|
+
constraint_name: constraint_name,
|
140
|
+
expected: inner_type,
|
141
|
+
got: value
|
142
|
+
)
|
143
|
+
end
|
144
|
+
end
|
145
|
+
end
|
146
|
+
end
|
147
|
+
end
|