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.
Files changed (53) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +4 -0
  3. data/.yardopts +13 -0
  4. data/CHANGELOG.md +75 -0
  5. data/README.md +616 -35
  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 +55 -0
  12. data/docs/nested-models.markdown +216 -0
  13. data/docs/property-types.markdown +212 -0
  14. data/docs/schema-definition.markdown +180 -0
  15. data/lib/easy_talk/builders/base_builder.rb +4 -2
  16. data/lib/easy_talk/builders/composition_builder.rb +10 -12
  17. data/lib/easy_talk/builders/object_builder.rb +45 -30
  18. data/lib/easy_talk/builders/registry.rb +168 -0
  19. data/lib/easy_talk/builders/typed_array_builder.rb +15 -4
  20. data/lib/easy_talk/configuration.rb +31 -1
  21. data/lib/easy_talk/error_formatter/base.rb +100 -0
  22. data/lib/easy_talk/error_formatter/error_code_mapper.rb +82 -0
  23. data/lib/easy_talk/error_formatter/flat.rb +38 -0
  24. data/lib/easy_talk/error_formatter/json_pointer.rb +38 -0
  25. data/lib/easy_talk/error_formatter/jsonapi.rb +64 -0
  26. data/lib/easy_talk/error_formatter/path_converter.rb +53 -0
  27. data/lib/easy_talk/error_formatter/rfc7807.rb +69 -0
  28. data/lib/easy_talk/error_formatter.rb +143 -0
  29. data/lib/easy_talk/errors.rb +2 -0
  30. data/lib/easy_talk/errors_helper.rb +63 -34
  31. data/lib/easy_talk/model.rb +123 -90
  32. data/lib/easy_talk/model_helper.rb +13 -0
  33. data/lib/easy_talk/naming_strategies.rb +20 -0
  34. data/lib/easy_talk/property.rb +16 -94
  35. data/lib/easy_talk/ref_helper.rb +27 -0
  36. data/lib/easy_talk/schema.rb +198 -0
  37. data/lib/easy_talk/schema_definition.rb +7 -1
  38. data/lib/easy_talk/schema_methods.rb +80 -0
  39. data/lib/easy_talk/tools/function_builder.rb +1 -1
  40. data/lib/easy_talk/type_introspection.rb +178 -0
  41. data/lib/easy_talk/types/base_composer.rb +2 -1
  42. data/lib/easy_talk/types/composer.rb +4 -0
  43. data/lib/easy_talk/validation_adapters/active_model_adapter.rb +329 -0
  44. data/lib/easy_talk/validation_adapters/base.rb +144 -0
  45. data/lib/easy_talk/validation_adapters/none_adapter.rb +36 -0
  46. data/lib/easy_talk/validation_adapters/registry.rb +87 -0
  47. data/lib/easy_talk/validation_builder.rb +28 -309
  48. data/lib/easy_talk/version.rb +1 -1
  49. data/lib/easy_talk.rb +41 -0
  50. metadata +26 -4
  51. data/docs/404.html +0 -25
  52. data/docs/_posts/2024-05-07-welcome-to-jekyll.markdown +0 -29
  53. data/easy_talk.gemspec +0 -39
@@ -0,0 +1,69 @@
1
+ # frozen_string_literal: true
2
+
3
+ module EasyTalk
4
+ module ErrorFormatter
5
+ # Formats validation errors according to RFC 7807 (Problem Details for HTTP APIs).
6
+ #
7
+ # RFC 7807 defines a standard format for describing errors in HTTP APIs.
8
+ # This formatter produces a Problem Details object with validation errors
9
+ # in an extended "errors" array.
10
+ #
11
+ # @see https://tools.ietf.org/html/rfc7807
12
+ #
13
+ # @example Output
14
+ # {
15
+ # "type" => "https://example.com/validation-error",
16
+ # "title" => "Validation Failed",
17
+ # "status" => 422,
18
+ # "detail" => "The request contains invalid parameters",
19
+ # "errors" => [
20
+ # { "pointer" => "/properties/name", "detail" => "can't be blank", "code" => "blank" }
21
+ # ]
22
+ # }
23
+ #
24
+ class Rfc7807 < Base
25
+ # Default values for RFC 7807 fields
26
+ DEFAULT_TITLE = 'Validation Failed'
27
+ DEFAULT_STATUS = 422
28
+ DEFAULT_DETAIL = 'The request contains invalid parameters'
29
+
30
+ # Format the errors as an RFC 7807 Problem Details object.
31
+ #
32
+ # @return [Hash] The Problem Details object
33
+ def format
34
+ {
35
+ 'type' => error_type_uri,
36
+ 'title' => options.fetch(:title, DEFAULT_TITLE),
37
+ 'status' => options.fetch(:status, DEFAULT_STATUS),
38
+ 'detail' => options.fetch(:detail, DEFAULT_DETAIL),
39
+ 'errors' => build_errors_array
40
+ }
41
+ end
42
+
43
+ private
44
+
45
+ def error_type_uri
46
+ base_uri = options.fetch(:type_base_uri, EasyTalk.configuration.error_type_base_uri)
47
+ type_suffix = options.fetch(:type, 'validation-error')
48
+ return type_suffix if base_uri.nil? || base_uri == 'about:blank'
49
+
50
+ "#{base_uri.chomp('/')}/#{type_suffix}"
51
+ end
52
+
53
+ def build_errors_array
54
+ error_entries.map do |entry|
55
+ build_error_object(entry)
56
+ end
57
+ end
58
+
59
+ def build_error_object(entry)
60
+ error = {
61
+ 'pointer' => PathConverter.to_json_pointer(entry[:attribute]),
62
+ 'detail' => entry[:message]
63
+ }
64
+ error['code'] = ErrorCodeMapper.map(entry[:type]) if include_codes? && entry[:type]
65
+ error
66
+ end
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,143 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'error_formatter/error_code_mapper'
4
+ require_relative 'error_formatter/path_converter'
5
+ require_relative 'error_formatter/base'
6
+ require_relative 'error_formatter/flat'
7
+ require_relative 'error_formatter/json_pointer'
8
+ require_relative 'error_formatter/rfc7807'
9
+ require_relative 'error_formatter/jsonapi'
10
+
11
+ module EasyTalk
12
+ # Module for formatting ActiveModel validation errors into standardized formats.
13
+ #
14
+ # Provides multiple output formats for API responses:
15
+ # - `:flat` - Simple flat array of field/message/code objects
16
+ # - `:json_pointer` - Array with JSON Pointer (RFC 6901) paths
17
+ # - `:rfc7807` - RFC 7807 Problem Details format
18
+ # - `:jsonapi` - JSON:API specification error format
19
+ #
20
+ # @example Using via model instance methods
21
+ # user = User.new(name: '')
22
+ # user.valid?
23
+ #
24
+ # user.validation_errors # Uses default format from config
25
+ # user.validation_errors_flat # Flat format
26
+ # user.validation_errors_json_pointer # JSON Pointer format
27
+ # user.validation_errors_rfc7807 # RFC 7807 format
28
+ # user.validation_errors_jsonapi # JSON:API format
29
+ #
30
+ # @example Using the format method directly
31
+ # EasyTalk::ErrorFormatter.format(user.errors, format: :rfc7807, title: 'Custom Title')
32
+ #
33
+ module ErrorFormatter
34
+ # Map of format symbols to formatter classes
35
+ FORMATTERS = {
36
+ flat: Flat,
37
+ json_pointer: JsonPointer,
38
+ rfc7807: Rfc7807,
39
+ jsonapi: Jsonapi
40
+ }.freeze
41
+
42
+ class << self
43
+ # Format validation errors using the specified format.
44
+ #
45
+ # @param errors [ActiveModel::Errors] The errors object to format
46
+ # @param format [Symbol] The output format (:flat, :json_pointer, :rfc7807, :jsonapi)
47
+ # @param options [Hash] Format-specific options
48
+ # @return [Hash, Array] The formatted errors
49
+ # @raise [ArgumentError] If the format is not recognized
50
+ #
51
+ # @example
52
+ # EasyTalk::ErrorFormatter.format(user.errors, format: :flat)
53
+ # EasyTalk::ErrorFormatter.format(user.errors, format: :rfc7807, title: 'Validation Error')
54
+ def format(errors, format: nil, **options)
55
+ format ||= EasyTalk.configuration.default_error_format
56
+ formatter_class = FORMATTERS[format.to_sym]
57
+
58
+ raise ArgumentError, "Unknown error format: #{format}. Valid formats: #{FORMATTERS.keys.join(', ')}" unless formatter_class
59
+
60
+ formatter_class.new(errors, options).format
61
+ end
62
+ end
63
+
64
+ # Instance methods mixed into EasyTalk::Model classes.
65
+ #
66
+ # Provides convenient methods for formatting validation errors
67
+ # on model instances.
68
+ module InstanceMethods
69
+ # Format validation errors using the default or specified format.
70
+ #
71
+ # @param format [Symbol] The output format (optional, uses config default)
72
+ # @param options [Hash] Format-specific options
73
+ # @return [Hash, Array] The formatted errors
74
+ #
75
+ # @example
76
+ # user.validation_errors
77
+ # user.validation_errors(format: :rfc7807, title: 'User Error')
78
+ def validation_errors(format: nil, **)
79
+ ErrorFormatter.format(errors, format: format, **)
80
+ end
81
+
82
+ # Format validation errors as a flat array.
83
+ #
84
+ # @param options [Hash] Format options
85
+ # @option options [Boolean] :include_codes Whether to include error codes
86
+ # @return [Array<Hash>] Array of error objects
87
+ #
88
+ # @example
89
+ # user.validation_errors_flat
90
+ # # => [{ "field" => "name", "message" => "can't be blank", "code" => "blank" }]
91
+ def validation_errors_flat(**)
92
+ ErrorFormatter.format(errors, format: :flat, **)
93
+ end
94
+
95
+ # Format validation errors with JSON Pointer paths.
96
+ #
97
+ # @param options [Hash] Format options
98
+ # @option options [Boolean] :include_codes Whether to include error codes
99
+ # @return [Array<Hash>] Array of error objects with pointer paths
100
+ #
101
+ # @example
102
+ # user.validation_errors_json_pointer
103
+ # # => [{ "pointer" => "/properties/name", "message" => "can't be blank", "code" => "blank" }]
104
+ def validation_errors_json_pointer(**)
105
+ ErrorFormatter.format(errors, format: :json_pointer, **)
106
+ end
107
+
108
+ # Format validation errors as RFC 7807 Problem Details.
109
+ #
110
+ # @param options [Hash] Format options
111
+ # @option options [String] :title The problem title
112
+ # @option options [Integer] :status The HTTP status code
113
+ # @option options [String] :detail The problem detail message
114
+ # @option options [String] :type_base_uri Base URI for error type
115
+ # @option options [String] :type The error type suffix
116
+ # @option options [Boolean] :include_codes Whether to include error codes
117
+ # @return [Hash] The Problem Details object
118
+ #
119
+ # @example
120
+ # user.validation_errors_rfc7807
121
+ # user.validation_errors_rfc7807(title: 'User Validation Failed', status: 400)
122
+ def validation_errors_rfc7807(**)
123
+ ErrorFormatter.format(errors, format: :rfc7807, **)
124
+ end
125
+
126
+ # Format validation errors according to JSON:API specification.
127
+ #
128
+ # @param options [Hash] Format options
129
+ # @option options [String] :status The HTTP status code (as string)
130
+ # @option options [String] :title The error title
131
+ # @option options [String] :source_prefix The source pointer prefix
132
+ # @option options [Boolean] :include_codes Whether to include error codes
133
+ # @return [Hash] The JSON:API error object
134
+ #
135
+ # @example
136
+ # user.validation_errors_jsonapi
137
+ # user.validation_errors_jsonapi(title: 'Validation Error', source_prefix: '/data')
138
+ def validation_errors_jsonapi(**)
139
+ ErrorFormatter.format(errors, format: :jsonapi, **)
140
+ end
141
+ end
142
+ end
143
+ end
@@ -4,5 +4,7 @@ module EasyTalk
4
4
  class Error < StandardError; end
5
5
  class ConstraintError < Error; end
6
6
  class UnknownOptionError < Error; end
7
+ class UnknownTypeError < Error; end
8
+ class InvalidInstructionsError < Error; end
7
9
  class InvalidPropertyNameError < Error; end
8
10
  end
@@ -27,7 +27,7 @@ module EasyTalk
27
27
  if type_info.respond_to?(:type) && type_info.type.respond_to?(:raw_type)
28
28
  type_info.type.raw_type
29
29
  # special boolean handling
30
- elsif type_info.try(:type).try(:name) == 'T::Boolean'
30
+ elsif TypeIntrospection.boolean_type?(type_info.try(:type))
31
31
  T::Boolean
32
32
  elsif type_info.respond_to?(:type_parameter)
33
33
  type_info.type_parameter
@@ -44,47 +44,77 @@ module EasyTalk
44
44
  end
45
45
 
46
46
  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)
47
+ # Raise error if value is not an array but type expects one
48
+ unless array_value.is_a?(Array)
49
+ inner_type = extract_inner_type(type_info)
50
+ expected_desc = TypeIntrospection.boolean_type?(inner_type) ? 'Boolean (true or false)' : inner_type.to_s
51
+ raise_constraint_error(
52
+ property_name: property_name,
53
+ constraint_name: constraint_name,
54
+ expected: expected_desc,
55
+ got: array_value
56
+ )
57
+ end
49
58
 
50
- # Extract the inner type from the array type definition
51
59
  inner_type = extract_inner_type(type_info)
52
-
53
- # Check each element of the array
54
60
  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)
61
+ validate_array_element(
62
+ property_name: property_name,
63
+ constraint_name: constraint_name,
64
+ inner_type: inner_type,
65
+ element: element,
66
+ index: index
67
+ )
68
+ end
69
+ end
70
70
 
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
71
+ def self.validate_array_element(property_name:, constraint_name:, inner_type:, element:, index:)
72
+ if inner_type.is_a?(Array)
73
+ validate_union_element(property_name, constraint_name, inner_type, element, index)
74
+ else
75
+ validate_single_type_element(property_name, constraint_name, inner_type, element, index)
76
+ end
77
+ end
78
+
79
+ def self.validate_union_element(property_name, constraint_name, inner_type, element, index)
80
+ return if inner_type.any? { |t| element.is_a?(t) }
81
+
82
+ raise_array_constraint_error(
83
+ property_name: property_name,
84
+ constraint_name: constraint_name,
85
+ index: index,
86
+ expected: inner_type.join(' or '),
87
+ got: element
88
+ )
89
+ end
90
+
91
+ def self.validate_single_type_element(property_name, constraint_name, inner_type, element, index)
92
+ # Skip if element is a boolean (booleans are valid in many contexts)
93
+ return if [true, false].include?(element)
94
+
95
+ if TypeIntrospection.boolean_type?(inner_type)
96
+ raise_array_constraint_error(
97
+ property_name: property_name,
98
+ constraint_name: constraint_name,
99
+ index: index,
100
+ expected: 'Boolean (true or false)',
101
+ got: element
102
+ )
103
+ elsif !element.is_a?(inner_type)
104
+ raise_array_constraint_error(
105
+ property_name: property_name,
106
+ constraint_name: constraint_name,
107
+ index: index,
108
+ expected: inner_type,
109
+ got: element
110
+ )
81
111
  end
82
112
  end
83
113
 
84
114
  def self.validate_constraint_value(property_name:, constraint_name:, value_type:, value:)
85
115
  return if value.nil?
86
116
 
87
- if value_type.to_s.include?('Boolean')
117
+ if TypeIntrospection.boolean_type?(value_type)
88
118
  return if value.is_a?(Array) && value.all? { |v| [true, false].include?(v) }
89
119
 
90
120
  unless [true, false].include?(value)
@@ -109,8 +139,7 @@ module EasyTalk
109
139
  )
110
140
  end
111
141
  # 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'))
142
+ elsif TypeIntrospection.typed_array?(value_type)
114
143
  # This is an array type, validate it
115
144
  validate_typed_array_values(
116
145
  property_name: property_name,
@@ -10,6 +10,7 @@ require 'active_model'
10
10
  require_relative 'builders/object_builder'
11
11
  require_relative 'schema_definition'
12
12
  require_relative 'validation_builder'
13
+ require_relative 'error_formatter'
13
14
 
14
15
  module EasyTalk
15
16
  # The `Model` module is a mixin that provides functionality for defining and accessing the schema of a model.
@@ -41,39 +42,70 @@ module EasyTalk
41
42
  base.include ActiveModel::Validations
42
43
  base.extend ActiveModel::Callbacks
43
44
  base.include(InstanceMethods)
45
+ base.include(ErrorFormatter::InstanceMethods)
44
46
  end
45
47
 
46
48
  # Instance methods mixed into models that include EasyTalk::Model
47
49
  module InstanceMethods
48
50
  def initialize(attributes = {})
49
51
  @additional_properties = {}
52
+ provided_keys = attributes.keys.to_set(&:to_sym)
53
+
50
54
  super # Perform initial mass assignment
51
55
 
52
- # After initial assignment, instantiate nested EasyTalk::Model objects
53
56
  schema_def = self.class.schema_definition
54
-
55
- # Only proceed if we have a valid schema definition
56
57
  return unless schema_def.respond_to?(:schema) && schema_def.schema.is_a?(Hash)
57
58
 
58
59
  (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?
60
+ process_property_initialization(prop_name, prop_definition, provided_keys)
61
+ end
62
+ end
63
63
 
64
- next if nilable_type && current_value.nil?
64
+ private
65
65
 
66
- defined_type = T::Utils::Nilable.get_underlying_type(defined_type) if nilable_type
66
+ def process_property_initialization(prop_name, prop_definition, provided_keys)
67
+ defined_type = prop_definition[:type]
68
+ nilable_type = defined_type.respond_to?(:nilable?) && defined_type.nilable?
69
+
70
+ apply_default_value(prop_name, prop_definition, provided_keys)
71
+
72
+ current_value = public_send(prop_name)
73
+ return if nilable_type && current_value.nil?
74
+
75
+ defined_type = T::Utils::Nilable.get_underlying_type(defined_type) if nilable_type
76
+ instantiate_nested_models(prop_name, defined_type, current_value)
77
+ end
67
78
 
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)
79
+ def apply_default_value(prop_name, prop_definition, provided_keys)
80
+ return if provided_keys.include?(prop_name)
70
81
 
71
- # Instantiate the nested model and assign it back
72
- nested_instance = defined_type.new(current_value)
73
- public_send("#{prop_name}=", nested_instance)
82
+ default_value = prop_definition.dig(:constraints, :default)
83
+ public_send("#{prop_name}=", default_value) unless default_value.nil?
84
+ end
85
+
86
+ def instantiate_nested_models(prop_name, defined_type, current_value)
87
+ # Single nested model: convert Hash to model instance
88
+ if defined_type.is_a?(Class) && defined_type.include?(EasyTalk::Model) && current_value.is_a?(Hash)
89
+ public_send("#{prop_name}=", defined_type.new(current_value))
90
+ return
74
91
  end
92
+
93
+ # Array of nested models: convert Hash items to model instances
94
+ instantiate_array_items(prop_name, defined_type, current_value)
95
+ end
96
+
97
+ def instantiate_array_items(prop_name, defined_type, current_value)
98
+ return unless defined_type.is_a?(T::Types::TypedArray) && current_value.is_a?(Array)
99
+
100
+ item_type = defined_type.type.respond_to?(:raw_type) ? defined_type.type.raw_type : nil
101
+ return unless item_type.is_a?(Class) && item_type.include?(EasyTalk::Model)
102
+
103
+ instantiated = current_value.map { |item| item.is_a?(Hash) ? item_type.new(item) : item }
104
+ public_send("#{prop_name}=", instantiated)
75
105
  end
76
106
 
107
+ public
108
+
77
109
  def method_missing(method_name, *args)
78
110
  method_string = method_name.to_s
79
111
  if method_string.end_with?('=')
@@ -91,9 +123,10 @@ module EasyTalk
91
123
  end
92
124
 
93
125
  def respond_to_missing?(method_name, include_private = false)
126
+ return super unless self.class.additional_properties_allowed?
127
+
94
128
  method_string = method_name.to_s
95
- method_string.end_with?('=') ? method_string.chomp('=') : method_string
96
- self.class.additional_properties_allowed? || super
129
+ method_string.end_with?('=') || @additional_properties.key?(method_string) || super
97
130
  end
98
131
 
99
132
  # Add to_hash method to convert defined properties to hash
@@ -136,6 +169,8 @@ module EasyTalk
136
169
 
137
170
  # Module containing class-level methods for defining and accessing the schema of a model.
138
171
  module ClassMethods
172
+ include SchemaMethods
173
+
139
174
  # Returns the schema for the model.
140
175
  #
141
176
  # @return [Schema] The schema for the model.
@@ -147,103 +182,101 @@ module EasyTalk
147
182
  end
148
183
  end
149
184
 
150
- # Returns the reference template for the model.
185
+ # Define the schema for the model using the provided block.
151
186
  #
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.
187
+ # @param options [Hash] Options for schema definition
188
+ # @option options [Boolean, Symbol, Class] :validations Controls validation behavior:
189
+ # - true: Enable validations using the configured adapter (default behavior)
190
+ # - false: Disable validations for this model
191
+ # - :none: Use the NoneAdapter (no validations)
192
+ # - :active_model: Use the ActiveModelAdapter
193
+ # - CustomAdapter: Use a custom adapter class
194
+ # @yield The block to define the schema.
195
+ # @raise [ArgumentError] If the class does not have a name.
159
196
  #
160
- # @return [Hash] The JSON schema for the model.
161
- def json_schema
162
- @json_schema ||= build_json_schema
163
- end
164
-
165
- private
166
-
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
197
+ # @example Disable validations for a model
198
+ # define_schema(validations: false) do
199
+ # property :name, String
200
+ # end
201
+ #
202
+ # @example Use a custom adapter
203
+ # define_schema(validations: MyCustomAdapter) do
204
+ # property :name, String
205
+ # end
206
+ def define_schema(options = {}, &)
207
+ raise ArgumentError, 'The class must have a name' unless name.present?
172
208
 
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
209
+ @schema_definition = SchemaDefinition.new(name)
210
+ @schema_definition.klass = self # Pass the model class to the schema definition
211
+ @schema_definition.instance_eval(&)
177
212
 
178
- return result if prefix.empty?
213
+ # Store validation options for this model
214
+ @validation_options = normalize_validation_options(options)
179
215
 
180
- prefix.merge(result)
181
- end
216
+ # Define accessors immediately based on schema_definition
217
+ defined_properties = (@schema_definition.schema[:properties] || {}).keys
218
+ attr_accessor(*defined_properties)
182
219
 
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)
220
+ # Track which properties have had validations applied
221
+ @validated_properties ||= Set.new
186
222
 
187
- if model_version
188
- # Per-model override - :none means explicitly no $schema
189
- return nil if model_version == :none
223
+ # Apply validations using the adapter system
224
+ apply_schema_validations
190
225
 
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
226
+ @schema_definition
196
227
  end
197
228
 
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
229
+ private
205
230
 
206
- model_id.to_s
231
+ # Normalize validation options from various input formats.
232
+ #
233
+ # @param options [Hash] The options hash from define_schema
234
+ # @return [Hash] Normalized options with :enabled and :adapter keys
235
+ def normalize_validation_options(options)
236
+ validations = options.fetch(:validations, nil)
237
+
238
+ case validations
239
+ when nil
240
+ # Use global configuration
241
+ { enabled: EasyTalk.configuration.auto_validations,
242
+ adapter: EasyTalk.configuration.validation_adapter }
243
+ when false
244
+ # Explicitly disabled
245
+ { enabled: false, adapter: :none }
246
+ when true
247
+ # Explicitly enabled with configured adapter
248
+ { enabled: true, adapter: EasyTalk.configuration.validation_adapter }
249
+ when Symbol, Class
250
+ # Specific adapter specified
251
+ { enabled: true, adapter: validations }
207
252
  else
208
- # Fall back to global configuration
209
- EasyTalk.configuration.schema_id
253
+ raise ArgumentError, "Invalid validations option: #{validations.inspect}. " \
254
+ "Expected true, false, Symbol, or Class."
210
255
  end
211
256
  end
212
257
 
213
- public
214
-
215
- # Define the schema for the model using the provided block.
258
+ # Apply validations to all schema properties using the configured adapter.
216
259
  #
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?
260
+ # @return [void]
261
+ def apply_schema_validations
262
+ return unless @validation_options[:enabled]
221
263
 
222
- @schema_definition = SchemaDefinition.new(name)
223
- @schema_definition.klass = self # Pass the model class to the schema definition
224
- @schema_definition.instance_eval(&)
264
+ adapter = ValidationAdapters::Registry.resolve(@validation_options[:adapter])
225
265
 
226
- # Define accessors immediately based on schema_definition
227
- defined_properties = (@schema_definition.schema[:properties] || {}).keys
228
- attr_accessor(*defined_properties)
266
+ (@schema_definition.schema[:properties] || {}).each do |prop_name, prop_def|
267
+ # Skip if already validated
268
+ next if @validated_properties.include?(prop_name)
229
269
 
230
- # Track which properties have had validations applied
231
- @validated_properties ||= Set.new
270
+ # Skip if property has validate: false
271
+ next if prop_def[:constraints][:validate] == false
232
272
 
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
273
+ adapter.build_validations(self, prop_name, prop_def[:type], prop_def[:constraints])
274
+ @validated_properties.add(prop_name)
242
275
  end
243
-
244
- @schema_definition
245
276
  end
246
277
 
278
+ public
279
+
247
280
  # Returns the unvalidated schema definition for the model.
248
281
  #
249
282
  # @return [SchemaDefinition] The unvalidated schema definition for the model.
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module EasyTalk
4
+ module ModelHelper
5
+ def self.easytalk_model?(type)
6
+ type.is_a?(Class) &&
7
+ type.respond_to?(:schema) &&
8
+ type.respond_to?(:ref_template) &&
9
+ defined?(EasyTalk::Model) &&
10
+ type.include?(EasyTalk::Model)
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module EasyTalk
4
+ module NamingStrategies
5
+ IDENTITY = lambda(&:to_sym)
6
+ SNAKE_CASE = ->(property_name) { property_name.to_s.underscore.to_sym }
7
+ CAMEL_CASE = ->(property_name) { property_name.to_s.tr('-', '_').camelize(:lower).to_sym }
8
+ PASCAL_CASE = ->(property_name) { property_name.to_s.tr('-', '_').camelize.to_sym }
9
+
10
+ def self.derive_strategy(strategy)
11
+ if strategy.is_a?(Symbol)
12
+ "EasyTalk::NamingStrategies::#{strategy.to_s.upcase}".constantize
13
+ elsif strategy.is_a?(Proc)
14
+ strategy
15
+ else
16
+ raise ArgumentError, 'Invalid property naming strategy. Must be a Symbol or a Proc.'
17
+ end
18
+ end
19
+ end
20
+ end