easy_talk 3.1.0 → 3.3.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.
Files changed (68) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +15 -39
  3. data/.yardopts +13 -0
  4. data/CHANGELOG.md +164 -0
  5. data/README.md +442 -1529
  6. data/Rakefile +27 -0
  7. data/docs/.gitignore +1 -0
  8. data/docs/about.markdown +28 -8
  9. data/docs/getting-started.markdown +102 -0
  10. data/docs/index.markdown +51 -4
  11. data/docs/json_schema_compliance.md +169 -0
  12. data/docs/nested-models.markdown +216 -0
  13. data/docs/primitive-schema-rfc.md +894 -0
  14. data/docs/property-types.markdown +212 -0
  15. data/docs/schema-definition.markdown +180 -0
  16. data/lib/easy_talk/builders/base_builder.rb +6 -3
  17. data/lib/easy_talk/builders/boolean_builder.rb +2 -1
  18. data/lib/easy_talk/builders/collection_helpers.rb +4 -0
  19. data/lib/easy_talk/builders/composition_builder.rb +16 -13
  20. data/lib/easy_talk/builders/integer_builder.rb +2 -1
  21. data/lib/easy_talk/builders/null_builder.rb +4 -1
  22. data/lib/easy_talk/builders/number_builder.rb +4 -1
  23. data/lib/easy_talk/builders/object_builder.rb +109 -33
  24. data/lib/easy_talk/builders/registry.rb +182 -0
  25. data/lib/easy_talk/builders/string_builder.rb +3 -1
  26. data/lib/easy_talk/builders/temporal_builder.rb +7 -0
  27. data/lib/easy_talk/builders/tuple_builder.rb +89 -0
  28. data/lib/easy_talk/builders/typed_array_builder.rb +19 -6
  29. data/lib/easy_talk/builders/union_builder.rb +5 -1
  30. data/lib/easy_talk/configuration.rb +47 -2
  31. data/lib/easy_talk/error_formatter/base.rb +100 -0
  32. data/lib/easy_talk/error_formatter/error_code_mapper.rb +82 -0
  33. data/lib/easy_talk/error_formatter/flat.rb +38 -0
  34. data/lib/easy_talk/error_formatter/json_pointer.rb +38 -0
  35. data/lib/easy_talk/error_formatter/jsonapi.rb +64 -0
  36. data/lib/easy_talk/error_formatter/path_converter.rb +53 -0
  37. data/lib/easy_talk/error_formatter/rfc7807.rb +69 -0
  38. data/lib/easy_talk/error_formatter.rb +143 -0
  39. data/lib/easy_talk/errors.rb +3 -0
  40. data/lib/easy_talk/errors_helper.rb +66 -34
  41. data/lib/easy_talk/json_schema_equality.rb +46 -0
  42. data/lib/easy_talk/keywords.rb +0 -1
  43. data/lib/easy_talk/model.rb +148 -89
  44. data/lib/easy_talk/model_helper.rb +17 -0
  45. data/lib/easy_talk/naming_strategies.rb +24 -0
  46. data/lib/easy_talk/property.rb +23 -94
  47. data/lib/easy_talk/ref_helper.rb +33 -0
  48. data/lib/easy_talk/schema.rb +199 -0
  49. data/lib/easy_talk/schema_definition.rb +57 -5
  50. data/lib/easy_talk/schema_methods.rb +111 -0
  51. data/lib/easy_talk/sorbet_extension.rb +1 -0
  52. data/lib/easy_talk/tools/function_builder.rb +1 -1
  53. data/lib/easy_talk/type_introspection.rb +222 -0
  54. data/lib/easy_talk/types/base_composer.rb +2 -1
  55. data/lib/easy_talk/types/composer.rb +4 -0
  56. data/lib/easy_talk/types/tuple.rb +77 -0
  57. data/lib/easy_talk/validation_adapters/active_model_adapter.rb +617 -0
  58. data/lib/easy_talk/validation_adapters/active_model_schema_validation.rb +106 -0
  59. data/lib/easy_talk/validation_adapters/base.rb +156 -0
  60. data/lib/easy_talk/validation_adapters/none_adapter.rb +45 -0
  61. data/lib/easy_talk/validation_adapters/registry.rb +87 -0
  62. data/lib/easy_talk/validation_builder.rb +29 -309
  63. data/lib/easy_talk/version.rb +1 -1
  64. data/lib/easy_talk.rb +42 -0
  65. metadata +38 -7
  66. data/docs/404.html +0 -25
  67. data/docs/_posts/2024-05-07-welcome-to-jekyll.markdown +0 -29
  68. data/easy_talk.gemspec +0 -39
@@ -1,8 +1,11 @@
1
1
  # frozen_string_literal: true
2
+ # typed: true
2
3
 
3
4
  module EasyTalk
4
5
  # Helper module for generating consistent error messages
5
6
  module ErrorHelper
7
+ extend T::Sig
8
+
6
9
  def self.raise_constraint_error(property_name:, constraint_name:, expected:, got:)
7
10
  message = "Error in property '#{property_name}': Constraint '#{constraint_name}' expects #{expected}, " \
8
11
  "but received #{got.inspect} (#{got.class})."
@@ -27,7 +30,7 @@ module EasyTalk
27
30
  if type_info.respond_to?(:type) && type_info.type.respond_to?(:raw_type)
28
31
  type_info.type.raw_type
29
32
  # special boolean handling
30
- elsif type_info.try(:type).try(:name) == 'T::Boolean'
33
+ elsif TypeIntrospection.boolean_type?(type_info.try(:type))
31
34
  T::Boolean
32
35
  elsif type_info.respond_to?(:type_parameter)
33
36
  type_info.type_parameter
@@ -44,47 +47,77 @@ module EasyTalk
44
47
  end
45
48
 
46
49
  def self.validate_typed_array_values(property_name:, constraint_name:, type_info:, array_value:)
47
- # Skip validation if it's not actually an array
48
- return unless array_value.is_a?(Array)
50
+ # Raise error if value is not an array but type expects one
51
+ unless array_value.is_a?(Array)
52
+ inner_type = extract_inner_type(type_info)
53
+ expected_desc = TypeIntrospection.boolean_type?(inner_type) ? 'Boolean (true or false)' : inner_type.to_s
54
+ raise_constraint_error(
55
+ property_name: property_name,
56
+ constraint_name: constraint_name,
57
+ expected: expected_desc,
58
+ got: array_value
59
+ )
60
+ end
49
61
 
50
- # Extract the inner type from the array type definition
51
62
  inner_type = extract_inner_type(type_info)
52
-
53
- # Check each element of the array
54
63
  array_value.each_with_index do |element, index|
55
- if inner_type.is_a?(Array)
56
- # For union types, check if the element matches any of the allowed types
57
- unless inner_type.any? { |t| element.is_a?(t) }
58
- expected = inner_type.join(' or ')
59
- raise_array_constraint_error(
60
- property_name: property_name,
61
- constraint_name: constraint_name,
62
- index: index,
63
- expected: expected,
64
- got: element
65
- )
66
- end
67
- else
68
- # For single types, just check against that type
69
- next if [true, false].include?(element)
64
+ validate_array_element(
65
+ property_name: property_name,
66
+ constraint_name: constraint_name,
67
+ inner_type: inner_type,
68
+ element: element,
69
+ index: index
70
+ )
71
+ end
72
+ end
70
73
 
71
- unless element.is_a?(inner_type)
72
- raise_array_constraint_error(
73
- property_name: property_name,
74
- constraint_name: constraint_name,
75
- index: index,
76
- expected: inner_type,
77
- got: element
78
- )
79
- end
80
- end
74
+ def self.validate_array_element(property_name:, constraint_name:, inner_type:, element:, index:)
75
+ if inner_type.is_a?(Array)
76
+ validate_union_element(property_name, constraint_name, inner_type, element, index)
77
+ else
78
+ validate_single_type_element(property_name, constraint_name, inner_type, element, index)
79
+ end
80
+ end
81
+
82
+ def self.validate_union_element(property_name, constraint_name, inner_type, element, index)
83
+ return if inner_type.any? { |t| element.is_a?(t) }
84
+
85
+ raise_array_constraint_error(
86
+ property_name: property_name,
87
+ constraint_name: constraint_name,
88
+ index: index,
89
+ expected: inner_type.join(' or '),
90
+ got: element
91
+ )
92
+ end
93
+
94
+ def self.validate_single_type_element(property_name, constraint_name, inner_type, element, index)
95
+ # Skip if element is a boolean (booleans are valid in many contexts)
96
+ return if [true, false].include?(element)
97
+
98
+ if TypeIntrospection.boolean_type?(inner_type)
99
+ raise_array_constraint_error(
100
+ property_name: property_name,
101
+ constraint_name: constraint_name,
102
+ index: index,
103
+ expected: 'Boolean (true or false)',
104
+ got: element
105
+ )
106
+ elsif !element.is_a?(inner_type)
107
+ raise_array_constraint_error(
108
+ property_name: property_name,
109
+ constraint_name: constraint_name,
110
+ index: index,
111
+ expected: inner_type,
112
+ got: element
113
+ )
81
114
  end
82
115
  end
83
116
 
84
117
  def self.validate_constraint_value(property_name:, constraint_name:, value_type:, value:)
85
118
  return if value.nil?
86
119
 
87
- if value_type.to_s.include?('Boolean')
120
+ if TypeIntrospection.boolean_type?(value_type)
88
121
  return if value.is_a?(Array) && value.all? { |v| [true, false].include?(v) }
89
122
 
90
123
  unless [true, false].include?(value)
@@ -109,8 +142,7 @@ module EasyTalk
109
142
  )
110
143
  end
111
144
  # Handle array types specifically
112
- elsif value_type.class.name.include?('TypedArray') ||
113
- (value_type.respond_to?(:to_s) && value_type.to_s.include?('T::Array'))
145
+ elsif TypeIntrospection.typed_array?(value_type)
114
146
  # This is an array type, validate it
115
147
  validate_typed_array_values(
116
148
  property_name: property_name,
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ module EasyTalk
4
+ # Implements JSON Schema equality semantics for comparing values.
5
+ #
6
+ # Per JSON Schema specification:
7
+ # - Objects with same keys/values in different order are equal
8
+ # - Numbers that are mathematically equal are equal (1 == 1.0)
9
+ # - Type matters for non-numbers (true != 1, false != 0)
10
+ module JsonSchemaEquality
11
+ # Maximum nesting depth to prevent SystemStackError on deeply nested structures
12
+ MAX_DEPTH = 100
13
+
14
+ class << self
15
+ # Check if an array contains duplicate values using JSON Schema equality.
16
+ # Uses a Set for O(n) performance and early termination on first duplicate.
17
+ def duplicates?(array)
18
+ seen = Set.new
19
+ array.any? { |item| !seen.add?(normalize(item)) }
20
+ end
21
+
22
+ # Normalize a value for JSON Schema equality comparison
23
+ # @param value [Object] The value to normalize
24
+ # @param depth [Integer] Current recursion depth (for stack overflow protection)
25
+ # @raise [ArgumentError] if nesting depth exceeds MAX_DEPTH
26
+ def normalize(value, depth = 0)
27
+ raise ArgumentError, "Nesting depth exceeds maximum of #{MAX_DEPTH}" if depth > MAX_DEPTH
28
+
29
+ case value
30
+ when Hash
31
+ # Convert keys to strings before sorting to handle mixed key types (Symbol/String)
32
+ # and ensure consistent, order-independent comparison (JSON only has string keys)
33
+ value.map { |k, v| [k.to_s, normalize(v, depth + 1)] }.sort
34
+ when Array
35
+ value.map { |item| normalize(item, depth + 1) }
36
+ when Integer, Float
37
+ # Normalize numbers to a canonical form for mathematical equality
38
+ value.to_r
39
+ else
40
+ # Booleans, strings, nil - preserve as-is (type matters)
41
+ value
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
@@ -7,7 +7,6 @@ module EasyTalk
7
7
  description
8
8
  type
9
9
  title
10
- property
11
10
  required
12
11
  items
13
12
  additional_items
@@ -1,4 +1,5 @@
1
1
  # frozen_string_literal: true
2
+ # typed: true
2
3
 
3
4
  require 'json'
4
5
  require 'active_support'
@@ -10,6 +11,7 @@ require 'active_model'
10
11
  require_relative 'builders/object_builder'
11
12
  require_relative 'schema_definition'
12
13
  require_relative 'validation_builder'
14
+ require_relative 'error_formatter'
13
15
 
14
16
  module EasyTalk
15
17
  # The `Model` module is a mixin that provides functionality for defining and accessing the schema of a model.
@@ -41,39 +43,70 @@ module EasyTalk
41
43
  base.include ActiveModel::Validations
42
44
  base.extend ActiveModel::Callbacks
43
45
  base.include(InstanceMethods)
46
+ base.include(ErrorFormatter::InstanceMethods)
44
47
  end
45
48
 
46
49
  # Instance methods mixed into models that include EasyTalk::Model
47
50
  module InstanceMethods
48
51
  def initialize(attributes = {})
49
52
  @additional_properties = {}
53
+ provided_keys = attributes.keys.to_set(&:to_sym)
54
+
50
55
  super # Perform initial mass assignment
51
56
 
52
- # After initial assignment, instantiate nested EasyTalk::Model objects
53
57
  schema_def = self.class.schema_definition
54
-
55
- # Only proceed if we have a valid schema definition
56
58
  return unless schema_def.respond_to?(:schema) && schema_def.schema.is_a?(Hash)
57
59
 
58
60
  (schema_def.schema[:properties] || {}).each do |prop_name, prop_definition|
59
- # Get the defined type and the currently assigned value
60
- defined_type = prop_definition[:type]
61
- current_value = public_send(prop_name)
62
- nilable_type = defined_type.respond_to?(:nilable?) && defined_type.nilable?
61
+ process_property_initialization(prop_name, prop_definition, provided_keys)
62
+ end
63
+ end
64
+
65
+ private
66
+
67
+ def process_property_initialization(prop_name, prop_definition, provided_keys)
68
+ defined_type = prop_definition[:type]
69
+ nilable_type = defined_type.respond_to?(:nilable?) && defined_type.nilable?
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?
63
75
 
64
- next if nilable_type && current_value.nil?
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
65
79
 
66
- defined_type = T::Utils::Nilable.get_underlying_type(defined_type) if nilable_type
80
+ def apply_default_value(prop_name, prop_definition, provided_keys)
81
+ return if provided_keys.include?(prop_name)
67
82
 
68
- # Check if the type is another EasyTalk::Model and the value is a Hash
69
- next unless defined_type.is_a?(Class) && defined_type.include?(EasyTalk::Model) && current_value.is_a?(Hash)
83
+ default_value = prop_definition.dig(:constraints, :default)
84
+ public_send("#{prop_name}=", default_value) unless default_value.nil?
85
+ end
70
86
 
71
- # Instantiate the nested model and assign it back
72
- nested_instance = defined_type.new(current_value)
73
- public_send("#{prop_name}=", nested_instance)
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
74
92
  end
93
+
94
+ # Array of nested models: convert Hash items to model instances
95
+ instantiate_array_items(prop_name, defined_type, current_value)
75
96
  end
76
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
+
77
110
  def method_missing(method_name, *args)
78
111
  method_string = method_name.to_s
79
112
  if method_string.end_with?('=')
@@ -91,9 +124,10 @@ module EasyTalk
91
124
  end
92
125
 
93
126
  def respond_to_missing?(method_name, include_private = false)
127
+ return super unless self.class.additional_properties_allowed?
128
+
94
129
  method_string = method_name.to_s
95
- method_string.end_with?('=') ? method_string.chomp('=') : method_string
96
- self.class.additional_properties_allowed? || super
130
+ method_string.end_with?('=') || @additional_properties.key?(method_string) || super
97
131
  end
98
132
 
99
133
  # Add to_hash method to convert defined properties to hash
@@ -136,6 +170,8 @@ module EasyTalk
136
170
 
137
171
  # Module containing class-level methods for defining and accessing the schema of a model.
138
172
  module ClassMethods
173
+ include SchemaMethods
174
+
139
175
  # Returns the schema for the model.
140
176
  #
141
177
  # @return [Schema] The schema for the model.
@@ -147,103 +183,124 @@ module EasyTalk
147
183
  end
148
184
  end
149
185
 
150
- # Returns the reference template for the model.
186
+ # Define the schema for the model using the provided block.
151
187
  #
152
- # @return [String] The reference template for the model.
153
- def ref_template
154
- "#/$defs/#{name}"
155
- end
156
-
157
- # Returns the JSON schema for the model.
158
- # This is the final output that includes the $schema keyword if configured.
188
+ # @param options [Hash] Options for schema definition
189
+ # @option options [Boolean, Symbol, Class] :validations Controls validation behavior:
190
+ # - true: Enable validations using the configured adapter (default behavior)
191
+ # - false: Disable validations for this model
192
+ # - :none: Use the NoneAdapter (no validations)
193
+ # - :active_model: Use the ActiveModelAdapter
194
+ # - CustomAdapter: Use a custom adapter class
195
+ # @yield The block to define the schema.
196
+ # @raise [ArgumentError] If the class does not have a name.
159
197
  #
160
- # @return [Hash] The JSON schema for the model.
161
- def json_schema
162
- @json_schema ||= build_json_schema
163
- end
164
-
165
- private
198
+ # @example Disable validations for a model
199
+ # define_schema(validations: false) do
200
+ # property :name, String
201
+ # end
202
+ #
203
+ # @example Use a custom adapter
204
+ # define_schema(validations: MyCustomAdapter) do
205
+ # property :name, String
206
+ # end
207
+ def define_schema(options = {}, &)
208
+ raise ArgumentError, 'The class must have a name' unless name.present?
166
209
 
167
- # Builds the final JSON schema with optional $schema and $id keywords.
168
- def build_json_schema
169
- result = schema.as_json
170
- schema_uri = resolve_schema_uri
171
- id_uri = resolve_schema_id
210
+ @schema_definition = SchemaDefinition.new(name)
211
+ @schema_definition.klass = self # Pass the model class to the schema definition
212
+ @schema_definition.instance_eval(&)
172
213
 
173
- # Build prefix hash with $schema and $id (in that order per JSON Schema convention)
174
- prefix = {}
175
- prefix['$schema'] = schema_uri if schema_uri
176
- prefix['$id'] = id_uri if id_uri
214
+ # Store validation options for this model
215
+ @validation_options = normalize_validation_options(options)
177
216
 
178
- return result if prefix.empty?
217
+ # Define accessors immediately based on schema_definition
218
+ defined_properties = (@schema_definition.schema[:properties] || {}).keys
219
+ attr_accessor(*defined_properties)
179
220
 
180
- prefix.merge(result)
181
- end
221
+ # Track which properties have had validations applied
222
+ @validated_properties ||= Set.new
182
223
 
183
- # Resolves the schema URI from per-model setting or global config.
184
- def resolve_schema_uri
185
- model_version = @schema_definition&.schema&.dig(:schema_version)
224
+ # Initialize mutex eagerly for thread-safe schema-level validation application
225
+ @schema_level_validation_lock = Mutex.new
186
226
 
187
- if model_version
188
- # Per-model override - :none means explicitly no $schema
189
- return nil if model_version == :none
227
+ # Apply validations using the adapter system
228
+ apply_schema_validations
190
229
 
191
- Configuration::SCHEMA_VERSIONS[model_version] || model_version.to_s
192
- else
193
- # Fall back to global configuration
194
- EasyTalk.configuration.schema_uri
195
- end
230
+ @schema_definition
196
231
  end
197
232
 
198
- # Resolves the schema ID from per-model setting or global config.
199
- def resolve_schema_id
200
- model_id = @schema_definition&.schema&.dig(:schema_id)
201
-
202
- if model_id
203
- # Per-model override - :none means explicitly no $id
204
- return nil if model_id == :none
233
+ private
205
234
 
206
- model_id.to_s
235
+ # Normalize validation options from various input formats.
236
+ #
237
+ # @param options [Hash] The options hash from define_schema
238
+ # @return [Hash] Normalized options with :enabled and :adapter keys
239
+ def normalize_validation_options(options)
240
+ validations = options.fetch(:validations, nil)
241
+
242
+ case validations
243
+ when nil
244
+ # Use global configuration
245
+ { enabled: EasyTalk.configuration.auto_validations,
246
+ adapter: EasyTalk.configuration.validation_adapter }
247
+ when false
248
+ # Explicitly disabled
249
+ { enabled: false, adapter: :none }
250
+ when true
251
+ # Explicitly enabled with configured adapter
252
+ { enabled: true, adapter: EasyTalk.configuration.validation_adapter }
253
+ when Symbol, Class
254
+ # Specific adapter specified
255
+ { enabled: true, adapter: validations }
207
256
  else
208
- # Fall back to global configuration
209
- EasyTalk.configuration.schema_id
257
+ raise ArgumentError, "Invalid validations option: #{validations.inspect}. " \
258
+ "Expected true, false, Symbol, or Class."
210
259
  end
211
260
  end
212
261
 
213
- public
214
-
215
- # Define the schema for the model using the provided block.
262
+ # Apply validations to all schema properties using the configured adapter.
216
263
  #
217
- # @yield The block to define the schema.
218
- # @raise [ArgumentError] If the class does not have a name.
219
- def define_schema(&)
220
- raise ArgumentError, 'The class must have a name' unless name.present?
264
+ # @return [void]
265
+ def apply_schema_validations
266
+ return unless @validation_options[:enabled]
221
267
 
222
- @schema_definition = SchemaDefinition.new(name)
223
- @schema_definition.klass = self # Pass the model class to the schema definition
224
- @schema_definition.instance_eval(&)
268
+ adapter = ValidationAdapters::Registry.resolve(@validation_options[:adapter])
225
269
 
226
- # Define accessors immediately based on schema_definition
227
- defined_properties = (@schema_definition.schema[:properties] || {}).keys
228
- attr_accessor(*defined_properties)
270
+ (@schema_definition.schema[:properties] || {}).each do |prop_name, prop_def|
271
+ # Skip if already validated
272
+ next if @validated_properties.include?(prop_name)
229
273
 
230
- # Track which properties have had validations applied
231
- @validated_properties ||= Set.new
274
+ # Skip if property has validate: false
275
+ next if prop_def[:constraints][:validate] == false
232
276
 
233
- # Apply auto-validations immediately after definition
234
- if EasyTalk.configuration.auto_validations
235
- (@schema_definition.schema[:properties] || {}).each do |prop_name, prop_def|
236
- # Only apply validations if they haven't been applied yet
237
- unless @validated_properties.include?(prop_name)
238
- ValidationBuilder.build_validations(self, prop_name, prop_def[:type], prop_def[:constraints])
239
- @validated_properties.add(prop_name)
240
- end
241
- end
277
+ adapter.build_validations(self, prop_name, prop_def[:type], prop_def[:constraints])
278
+ @validated_properties.add(prop_name)
242
279
  end
243
280
 
244
- @schema_definition
281
+ # Apply schema-level validations (min_properties, max_properties, dependent_required)
282
+ apply_schema_level_validations(adapter)
245
283
  end
246
284
 
285
+ # Apply schema-level validations for object-level constraints.
286
+ # Uses double-checked locking for thread safety.
287
+ # The mutex is initialized eagerly in define_schema.
288
+ #
289
+ # @param adapter [Class] The validation adapter class
290
+ # @return [void]
291
+ def apply_schema_level_validations(adapter)
292
+ return if @schema_level_validations_applied
293
+
294
+ @schema_level_validation_lock.synchronize do
295
+ return if @schema_level_validations_applied
296
+
297
+ adapter.build_schema_validations(self, @schema_definition.schema)
298
+ @schema_level_validations_applied = true
299
+ end
300
+ end
301
+
302
+ public
303
+
247
304
  # Returns the unvalidated schema definition for the model.
248
305
  #
249
306
  # @return [SchemaDefinition] The unvalidated schema definition for the model.
@@ -252,7 +309,9 @@ module EasyTalk
252
309
  end
253
310
 
254
311
  def additional_properties_allowed?
255
- @schema_definition&.schema&.fetch(:additional_properties, false)
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)
256
315
  end
257
316
 
258
317
  # Returns the property names defined in the schema
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+ # typed: true
3
+
4
+ module EasyTalk
5
+ module ModelHelper
6
+ extend T::Sig
7
+
8
+ sig { params(type: T.untyped).returns(T::Boolean) }
9
+ def self.easytalk_model?(type)
10
+ type.is_a?(Class) &&
11
+ type.respond_to?(:schema) &&
12
+ type.respond_to?(:ref_template) &&
13
+ defined?(EasyTalk::Model) &&
14
+ type.include?(EasyTalk::Model)
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+ # typed: true
3
+
4
+ module EasyTalk
5
+ module NamingStrategies
6
+ extend T::Sig
7
+
8
+ IDENTITY = lambda(&:to_sym)
9
+ SNAKE_CASE = ->(property_name) { property_name.to_s.underscore.to_sym }
10
+ CAMEL_CASE = ->(property_name) { property_name.to_s.tr('-', '_').camelize(:lower).to_sym }
11
+ PASCAL_CASE = ->(property_name) { property_name.to_s.tr('-', '_').camelize.to_sym }
12
+
13
+ sig { params(strategy: T.any(Symbol, T.proc.params(arg0: T.untyped).returns(Symbol))).returns(T.proc.params(arg0: T.untyped).returns(Symbol)) }
14
+ def self.derive_strategy(strategy)
15
+ if strategy.is_a?(Symbol)
16
+ "EasyTalk::NamingStrategies::#{strategy.to_s.upcase}".constantize
17
+ elsif strategy.is_a?(Proc)
18
+ strategy
19
+ else
20
+ raise ArgumentError, 'Invalid property naming strategy. Must be a Symbol or a Proc.'
21
+ end
22
+ end
23
+ end
24
+ end