easy_talk 3.0.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 (54) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +4 -0
  3. data/.yardopts +13 -0
  4. data/CHANGELOG.md +105 -0
  5. data/README.md +1268 -40
  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 +119 -10
  18. data/lib/easy_talk/builders/registry.rb +168 -0
  19. data/lib/easy_talk/builders/typed_array_builder.rb +20 -6
  20. data/lib/easy_talk/configuration.rb +51 -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/keywords.rb +2 -0
  32. data/lib/easy_talk/model.rb +125 -41
  33. data/lib/easy_talk/model_helper.rb +13 -0
  34. data/lib/easy_talk/naming_strategies.rb +20 -0
  35. data/lib/easy_talk/property.rb +32 -44
  36. data/lib/easy_talk/ref_helper.rb +27 -0
  37. data/lib/easy_talk/schema.rb +198 -0
  38. data/lib/easy_talk/schema_definition.rb +7 -1
  39. data/lib/easy_talk/schema_methods.rb +80 -0
  40. data/lib/easy_talk/tools/function_builder.rb +1 -1
  41. data/lib/easy_talk/type_introspection.rb +178 -0
  42. data/lib/easy_talk/types/base_composer.rb +2 -1
  43. data/lib/easy_talk/types/composer.rb +4 -0
  44. data/lib/easy_talk/validation_adapters/active_model_adapter.rb +329 -0
  45. data/lib/easy_talk/validation_adapters/base.rb +144 -0
  46. data/lib/easy_talk/validation_adapters/none_adapter.rb +36 -0
  47. data/lib/easy_talk/validation_adapters/registry.rb +87 -0
  48. data/lib/easy_talk/validation_builder.rb +28 -309
  49. data/lib/easy_talk/version.rb +1 -1
  50. data/lib/easy_talk.rb +41 -0
  51. metadata +28 -6
  52. data/docs/404.html +0 -25
  53. data/docs/_posts/2024-05-07-welcome-to-jekyll.markdown +0 -29
  54. 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,
@@ -2,6 +2,8 @@
2
2
 
3
3
  module EasyTalk
4
4
  KEYWORDS = %i[
5
+ schema_id
6
+ schema_version
5
7
  description
6
8
  type
7
9
  title
@@ -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
+
64
+ private
63
65
 
64
- next if nilable_type && current_value.nil?
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?
65
69
 
66
- defined_type = T::Utils::Nilable.get_underlying_type(defined_type) if nilable_type
70
+ apply_default_value(prop_name, prop_definition, provided_keys)
67
71
 
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)
72
+ current_value = public_send(prop_name)
73
+ return if nilable_type && current_value.nil?
70
74
 
71
- # Instantiate the nested model and assign it back
72
- nested_instance = defined_type.new(current_value)
73
- public_send("#{prop_name}=", nested_instance)
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
78
+
79
+ def apply_default_value(prop_name, prop_definition, provided_keys)
80
+ return if provided_keys.include?(prop_name)
81
+
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)
75
95
  end
76
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)
105
+ end
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,31 +182,37 @@ module EasyTalk
147
182
  end
148
183
  end
149
184
 
150
- # Returns the reference template for the model.
151
- #
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
- #
159
- # @return [Hash] The JSON schema for the model.
160
- def json_schema
161
- @json_schema ||= schema.as_json
162
- end
163
-
164
185
  # Define the schema for the model using the provided block.
165
186
  #
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
166
194
  # @yield The block to define the schema.
167
195
  # @raise [ArgumentError] If the class does not have a name.
168
- def define_schema(&)
196
+ #
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 = {}, &)
169
207
  raise ArgumentError, 'The class must have a name' unless name.present?
170
208
 
171
209
  @schema_definition = SchemaDefinition.new(name)
172
210
  @schema_definition.klass = self # Pass the model class to the schema definition
173
211
  @schema_definition.instance_eval(&)
174
212
 
213
+ # Store validation options for this model
214
+ @validation_options = normalize_validation_options(options)
215
+
175
216
  # Define accessors immediately based on schema_definition
176
217
  defined_properties = (@schema_definition.schema[:properties] || {}).keys
177
218
  attr_accessor(*defined_properties)
@@ -179,20 +220,63 @@ module EasyTalk
179
220
  # Track which properties have had validations applied
180
221
  @validated_properties ||= Set.new
181
222
 
182
- # Apply auto-validations immediately after definition
183
- if EasyTalk.configuration.auto_validations
184
- (@schema_definition.schema[:properties] || {}).each do |prop_name, prop_def|
185
- # Only apply validations if they haven't been applied yet
186
- unless @validated_properties.include?(prop_name)
187
- ValidationBuilder.build_validations(self, prop_name, prop_def[:type], prop_def[:constraints])
188
- @validated_properties.add(prop_name)
189
- end
190
- end
191
- end
223
+ # Apply validations using the adapter system
224
+ apply_schema_validations
192
225
 
193
226
  @schema_definition
194
227
  end
195
228
 
229
+ private
230
+
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 }
252
+ else
253
+ raise ArgumentError, "Invalid validations option: #{validations.inspect}. " \
254
+ "Expected true, false, Symbol, or Class."
255
+ end
256
+ end
257
+
258
+ # Apply validations to all schema properties using the configured adapter.
259
+ #
260
+ # @return [void]
261
+ def apply_schema_validations
262
+ return unless @validation_options[:enabled]
263
+
264
+ adapter = ValidationAdapters::Registry.resolve(@validation_options[:adapter])
265
+
266
+ (@schema_definition.schema[:properties] || {}).each do |prop_name, prop_def|
267
+ # Skip if already validated
268
+ next if @validated_properties.include?(prop_name)
269
+
270
+ # Skip if property has validate: false
271
+ next if prop_def[:constraints][:validate] == false
272
+
273
+ adapter.build_validations(self, prop_name, prop_def[:type], prop_def[:constraints])
274
+ @validated_properties.add(prop_name)
275
+ end
276
+ end
277
+
278
+ public
279
+
196
280
  # Returns the unvalidated schema definition for the model.
197
281
  #
198
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