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.
- checksums.yaml +4 -4
- data/.rubocop.yml +4 -0
- data/.yardopts +13 -0
- data/CHANGELOG.md +75 -0
- data/README.md +616 -35
- data/Rakefile +27 -0
- data/docs/.gitignore +1 -0
- data/docs/about.markdown +28 -8
- data/docs/getting-started.markdown +102 -0
- data/docs/index.markdown +51 -4
- data/docs/json_schema_compliance.md +55 -0
- data/docs/nested-models.markdown +216 -0
- data/docs/property-types.markdown +212 -0
- data/docs/schema-definition.markdown +180 -0
- data/lib/easy_talk/builders/base_builder.rb +4 -2
- data/lib/easy_talk/builders/composition_builder.rb +10 -12
- data/lib/easy_talk/builders/object_builder.rb +45 -30
- data/lib/easy_talk/builders/registry.rb +168 -0
- data/lib/easy_talk/builders/typed_array_builder.rb +15 -4
- data/lib/easy_talk/configuration.rb +31 -1
- data/lib/easy_talk/error_formatter/base.rb +100 -0
- data/lib/easy_talk/error_formatter/error_code_mapper.rb +82 -0
- data/lib/easy_talk/error_formatter/flat.rb +38 -0
- data/lib/easy_talk/error_formatter/json_pointer.rb +38 -0
- data/lib/easy_talk/error_formatter/jsonapi.rb +64 -0
- data/lib/easy_talk/error_formatter/path_converter.rb +53 -0
- data/lib/easy_talk/error_formatter/rfc7807.rb +69 -0
- data/lib/easy_talk/error_formatter.rb +143 -0
- data/lib/easy_talk/errors.rb +2 -0
- data/lib/easy_talk/errors_helper.rb +63 -34
- data/lib/easy_talk/model.rb +123 -90
- data/lib/easy_talk/model_helper.rb +13 -0
- data/lib/easy_talk/naming_strategies.rb +20 -0
- data/lib/easy_talk/property.rb +16 -94
- data/lib/easy_talk/ref_helper.rb +27 -0
- data/lib/easy_talk/schema.rb +198 -0
- data/lib/easy_talk/schema_definition.rb +7 -1
- data/lib/easy_talk/schema_methods.rb +80 -0
- data/lib/easy_talk/tools/function_builder.rb +1 -1
- data/lib/easy_talk/type_introspection.rb +178 -0
- data/lib/easy_talk/types/base_composer.rb +2 -1
- data/lib/easy_talk/types/composer.rb +4 -0
- data/lib/easy_talk/validation_adapters/active_model_adapter.rb +329 -0
- data/lib/easy_talk/validation_adapters/base.rb +144 -0
- data/lib/easy_talk/validation_adapters/none_adapter.rb +36 -0
- data/lib/easy_talk/validation_adapters/registry.rb +87 -0
- data/lib/easy_talk/validation_builder.rb +28 -309
- data/lib/easy_talk/version.rb +1 -1
- data/lib/easy_talk.rb +41 -0
- metadata +26 -4
- data/docs/404.html +0 -25
- data/docs/_posts/2024-05-07-welcome-to-jekyll.markdown +0 -29
- 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
|
data/lib/easy_talk/errors.rb
CHANGED
|
@@ -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)
|
|
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
|
-
#
|
|
48
|
-
|
|
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
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
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
|
|
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
|
|
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,
|
data/lib/easy_talk/model.rb
CHANGED
|
@@ -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
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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
|
-
|
|
64
|
+
private
|
|
65
65
|
|
|
66
|
-
|
|
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
|
-
|
|
69
|
-
|
|
79
|
+
def apply_default_value(prop_name, prop_definition, provided_keys)
|
|
80
|
+
return if provided_keys.include?(prop_name)
|
|
70
81
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
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?('=')
|
|
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
|
-
#
|
|
185
|
+
# Define the schema for the model using the provided block.
|
|
151
186
|
#
|
|
152
|
-
# @
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
#
|
|
158
|
-
#
|
|
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
|
-
# @
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
end
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
#
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
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
|
-
|
|
174
|
-
|
|
175
|
-
|
|
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
|
-
|
|
213
|
+
# Store validation options for this model
|
|
214
|
+
@validation_options = normalize_validation_options(options)
|
|
179
215
|
|
|
180
|
-
|
|
181
|
-
|
|
216
|
+
# Define accessors immediately based on schema_definition
|
|
217
|
+
defined_properties = (@schema_definition.schema[:properties] || {}).keys
|
|
218
|
+
attr_accessor(*defined_properties)
|
|
182
219
|
|
|
183
|
-
|
|
184
|
-
|
|
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
|
-
|
|
188
|
-
|
|
189
|
-
return nil if model_version == :none
|
|
223
|
+
# Apply validations using the adapter system
|
|
224
|
+
apply_schema_validations
|
|
190
225
|
|
|
191
|
-
|
|
192
|
-
else
|
|
193
|
-
# Fall back to global configuration
|
|
194
|
-
EasyTalk.configuration.schema_uri
|
|
195
|
-
end
|
|
226
|
+
@schema_definition
|
|
196
227
|
end
|
|
197
228
|
|
|
198
|
-
|
|
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
|
-
|
|
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
|
-
|
|
209
|
-
|
|
253
|
+
raise ArgumentError, "Invalid validations option: #{validations.inspect}. " \
|
|
254
|
+
"Expected true, false, Symbol, or Class."
|
|
210
255
|
end
|
|
211
256
|
end
|
|
212
257
|
|
|
213
|
-
|
|
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
|
-
# @
|
|
218
|
-
|
|
219
|
-
|
|
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
|
-
|
|
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
|
-
|
|
227
|
-
|
|
228
|
-
|
|
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
|
-
|
|
231
|
-
|
|
270
|
+
# Skip if property has validate: false
|
|
271
|
+
next if prop_def[:constraints][:validate] == false
|
|
232
272
|
|
|
233
|
-
|
|
234
|
-
|
|
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
|