easy_talk 1.0.3 → 2.0.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 +97 -17
- data/CHANGELOG.md +51 -0
- data/README.md +496 -60
- data/easy_talk.gemspec +38 -0
- data/lib/easy_talk/active_record_schema_builder.rb +8 -1
- data/lib/easy_talk/builders/composition_builder.rb +24 -1
- data/lib/easy_talk/builders/object_builder.rb +11 -20
- data/lib/easy_talk/builders/temporal_builder.rb +49 -0
- data/lib/easy_talk/configuration.rb +6 -3
- data/lib/easy_talk/errors_helper.rb +1 -0
- data/lib/easy_talk/model.rb +75 -14
- data/lib/easy_talk/property.rb +116 -44
- data/lib/easy_talk/schema_definition.rb +28 -16
- data/lib/easy_talk/types/composer.rb +89 -0
- data/lib/easy_talk/validation_builder.rb +339 -0
- data/lib/easy_talk/version.rb +1 -1
- data/lib/easy_talk.rb +5 -3
- metadata +13 -158
- data/lib/easy_talk/builders/all_of_builder.rb +0 -11
- data/lib/easy_talk/builders/any_of_builder.rb +0 -11
- data/lib/easy_talk/builders/date_builder.rb +0 -18
- data/lib/easy_talk/builders/datetime_builder.rb +0 -18
- data/lib/easy_talk/builders/one_of_builder.rb +0 -11
- data/lib/easy_talk/builders/time_builder.rb +0 -16
- data/lib/easy_talk/types/all_of.rb +0 -32
- data/lib/easy_talk/types/any_of.rb +0 -39
- data/lib/easy_talk/types/one_of.rb +0 -31
@@ -1,6 +1,8 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require_relative 'keywords'
|
4
|
+
require_relative 'types/composer'
|
5
|
+
require_relative 'validation_builder'
|
4
6
|
|
5
7
|
module EasyTalk
|
6
8
|
#
|
@@ -15,11 +17,13 @@ module EasyTalk
|
|
15
17
|
extend T::AllOf
|
16
18
|
|
17
19
|
attr_reader :name, :schema
|
20
|
+
attr_accessor :klass # Add accessor for the model class
|
18
21
|
|
19
22
|
def initialize(name, schema = {})
|
20
23
|
@schema = schema
|
21
24
|
@schema[:additional_properties] = false unless schema.key?(:additional_properties)
|
22
25
|
@name = name
|
26
|
+
@klass = nil # Initialize klass to nil
|
23
27
|
end
|
24
28
|
|
25
29
|
EasyTalk::KEYWORDS.each do |keyword|
|
@@ -33,36 +37,44 @@ module EasyTalk
|
|
33
37
|
@schema[:subschemas] += subschemas
|
34
38
|
end
|
35
39
|
|
36
|
-
|
37
|
-
params(name: T.any(Symbol, String), type: T.untyped, constraints: T.untyped, blk: T.nilable(T.proc.void)).void
|
38
|
-
end
|
39
|
-
def property(name, type, constraints = {}, &blk)
|
40
|
+
def property(name, type, constraints = {}, &)
|
40
41
|
validate_property_name(name)
|
41
42
|
@schema[:properties] ||= {}
|
42
43
|
|
43
44
|
if block_given?
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
@schema[:properties][name] = {
|
48
|
-
type:,
|
49
|
-
constraints:,
|
50
|
-
properties: property_schema
|
51
|
-
}
|
52
|
-
else
|
53
|
-
@schema[:properties][name] = { type:, constraints: }
|
45
|
+
raise ArgumentError,
|
46
|
+
'Block-style sub-schemas are no longer supported. Use class references as types instead.'
|
54
47
|
end
|
48
|
+
|
49
|
+
@schema[:properties][name] = { type:, constraints: }
|
55
50
|
end
|
56
51
|
|
57
52
|
def validate_property_name(name)
|
58
53
|
return if name.to_s.match?(/^[A-Za-z_][A-Za-z0-9_]*$/)
|
59
54
|
|
60
|
-
|
61
|
-
|
55
|
+
message = "Invalid property name '#{name}'. Must start with letter/underscore " \
|
56
|
+
'and contain only letters, numbers, underscores'
|
57
|
+
raise InvalidPropertyNameError, message
|
62
58
|
end
|
63
59
|
|
64
60
|
def optional?
|
65
61
|
@schema[:optional]
|
66
62
|
end
|
63
|
+
|
64
|
+
# Helper method for nullable and optional properties
|
65
|
+
def nullable_optional_property(name, type, constraints = {})
|
66
|
+
# Ensure type is nilable
|
67
|
+
nilable_type = if type.respond_to?(:nilable?) && type.nilable?
|
68
|
+
type
|
69
|
+
else
|
70
|
+
T.nilable(type)
|
71
|
+
end
|
72
|
+
|
73
|
+
# Ensure constraints include optional: true
|
74
|
+
constraints = constraints.merge(optional: true)
|
75
|
+
|
76
|
+
# Call standard property method
|
77
|
+
property(name, nilable_type, constraints)
|
78
|
+
end
|
67
79
|
end
|
68
80
|
end
|
@@ -0,0 +1,89 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'base_composer'
|
4
|
+
|
5
|
+
module EasyTalk
|
6
|
+
module Types
|
7
|
+
# Base class for composition types
|
8
|
+
class Composer < BaseComposer
|
9
|
+
# Returns the name of the composition type.
|
10
|
+
def self.name
|
11
|
+
raise NotImplementedError, "#{self.class.name} must implement the name method"
|
12
|
+
end
|
13
|
+
|
14
|
+
# Returns the name of the composition type.
|
15
|
+
def name
|
16
|
+
self.class.name
|
17
|
+
end
|
18
|
+
|
19
|
+
# Represents a composition type that allows all of the specified types.
|
20
|
+
class AllOf < Composer
|
21
|
+
def self.name
|
22
|
+
:allOf
|
23
|
+
end
|
24
|
+
|
25
|
+
def name
|
26
|
+
:allOf
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
# Represents a composition type that allows any of the specified types.
|
31
|
+
class AnyOf < Composer
|
32
|
+
def self.name
|
33
|
+
:anyOf
|
34
|
+
end
|
35
|
+
|
36
|
+
def name
|
37
|
+
:anyOf
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
# Represents a composition type that allows one of the specified types.
|
42
|
+
class OneOf < Composer
|
43
|
+
def self.name
|
44
|
+
:oneOf
|
45
|
+
end
|
46
|
+
|
47
|
+
def name
|
48
|
+
:oneOf
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
# Shorthand module for accessing the AllOf composer
|
56
|
+
module T
|
57
|
+
# Provides composition logic for combining multiple schemas with AllOf semantics
|
58
|
+
module AllOf
|
59
|
+
# Creates a new instance of `EasyTalk::Types::Composer::AllOf` with the given arguments.
|
60
|
+
#
|
61
|
+
# @param args [Array] the list of arguments to be passed to the constructor
|
62
|
+
# @return [EasyTalk::Types::Composer::AllOf] a new instance
|
63
|
+
def self.[](*)
|
64
|
+
EasyTalk::Types::Composer::AllOf.new(*)
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
# Shorthand module for accessing the AnyOf composer
|
69
|
+
module AnyOf
|
70
|
+
# Creates a new instance of `EasyTalk::Types::Composer::AnyOf` with the given arguments.
|
71
|
+
#
|
72
|
+
# @param args [Array] the list of arguments to be passed to the constructor
|
73
|
+
# @return [EasyTalk::Types::Composer::AnyOf] a new instance
|
74
|
+
def self.[](*)
|
75
|
+
EasyTalk::Types::Composer::AnyOf.new(*)
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
# Shorthand module for accessing the OneOf composer
|
80
|
+
module OneOf
|
81
|
+
# Creates a new instance of `EasyTalk::Types::Composer::OneOf` with the given arguments.
|
82
|
+
#
|
83
|
+
# @param args [Array] the list of arguments to be passed to the constructor
|
84
|
+
# @return [EasyTalk::Types::Composer::OneOf] a new instance
|
85
|
+
def self.[](*)
|
86
|
+
EasyTalk::Types::Composer::OneOf.new(*)
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
@@ -0,0 +1,339 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'uri'
|
4
|
+
|
5
|
+
module EasyTalk
|
6
|
+
# The ValidationBuilder creates ActiveModel validations based on JSON Schema constraints
|
7
|
+
class ValidationBuilder
|
8
|
+
# Build validations for a property and apply them to the model class
|
9
|
+
#
|
10
|
+
# @param klass [Class] The model class to apply validations to
|
11
|
+
# @param property_name [Symbol, String] The name of the property
|
12
|
+
# @param type [Class, Object] The type of the property
|
13
|
+
# @param constraints [Hash] The JSON Schema constraints for the property
|
14
|
+
# @return [void]
|
15
|
+
def self.build_validations(klass, property_name, type, constraints)
|
16
|
+
builder = new(klass, property_name, type, constraints)
|
17
|
+
builder.apply_validations
|
18
|
+
end
|
19
|
+
|
20
|
+
# Initialize a new ValidationBuilder
|
21
|
+
#
|
22
|
+
# @param klass [Class] The model class to apply validations to
|
23
|
+
# @param property_name [Symbol, String] The name of the property
|
24
|
+
# @param type [Class, Object] The type of the property
|
25
|
+
# @param constraints [Hash] The JSON Schema constraints for the property
|
26
|
+
attr_reader :klass, :property_name, :type, :constraints
|
27
|
+
|
28
|
+
def initialize(klass, property_name, type, constraints)
|
29
|
+
@klass = klass
|
30
|
+
@property_name = property_name.to_sym
|
31
|
+
@type = type
|
32
|
+
@constraints = constraints || {}
|
33
|
+
end
|
34
|
+
|
35
|
+
# Apply validations based on property type and constraints
|
36
|
+
def apply_validations
|
37
|
+
# Determine if the type is boolean
|
38
|
+
type_class = get_type_class(@type)
|
39
|
+
is_boolean = type_class == [TrueClass, FalseClass] ||
|
40
|
+
type_class == TrueClass ||
|
41
|
+
type_class == FalseClass ||
|
42
|
+
@type.to_s.include?('T::Boolean')
|
43
|
+
|
44
|
+
# Skip presence validation for booleans and nilable types
|
45
|
+
apply_presence_validation unless optional? || is_boolean || nilable_type?
|
46
|
+
if nilable_type?
|
47
|
+
# For nilable types, get the inner type and apply validations to it
|
48
|
+
inner_type = extract_inner_type(@type)
|
49
|
+
apply_type_validations(inner_type)
|
50
|
+
else
|
51
|
+
apply_type_validations(@type)
|
52
|
+
end
|
53
|
+
|
54
|
+
# Common validations for most types
|
55
|
+
apply_enum_validation if @constraints[:enum]
|
56
|
+
apply_const_validation if @constraints[:const]
|
57
|
+
end
|
58
|
+
|
59
|
+
private
|
60
|
+
|
61
|
+
# Determine if a property is optional based on constraints and configuration
|
62
|
+
def optional?
|
63
|
+
@constraints[:optional] == true ||
|
64
|
+
(@type.respond_to?(:nilable?) && @type.nilable? && EasyTalk.configuration.nilable_is_optional)
|
65
|
+
end
|
66
|
+
|
67
|
+
# Check if the type is nilable (e.g., T.nilable(String))
|
68
|
+
def nilable_type?
|
69
|
+
@type.respond_to?(:nilable?) && @type.nilable?
|
70
|
+
end
|
71
|
+
|
72
|
+
# Extract the inner type from a complex type like T.nilable(String)
|
73
|
+
def extract_inner_type(type)
|
74
|
+
if type.respond_to?(:unwrap_nilable) && type.unwrap_nilable.respond_to?(:raw_type)
|
75
|
+
type.unwrap_nilable.raw_type
|
76
|
+
elsif type.respond_to?(:types)
|
77
|
+
# For union types like T.nilable(String), extract the non-nil type
|
78
|
+
type.types.find { |t| t.respond_to?(:raw_type) && t.raw_type != NilClass }
|
79
|
+
else
|
80
|
+
type
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
# Apply validations based on the type of the property
|
85
|
+
def apply_type_validations(type)
|
86
|
+
type_class = get_type_class(type)
|
87
|
+
|
88
|
+
if type_class == String
|
89
|
+
apply_string_validations
|
90
|
+
elsif type_class == Integer
|
91
|
+
apply_integer_validations
|
92
|
+
elsif [Float, BigDecimal].include?(type_class)
|
93
|
+
apply_number_validations
|
94
|
+
elsif type_class == Array
|
95
|
+
apply_array_validations(type)
|
96
|
+
elsif type_class == [TrueClass,
|
97
|
+
FalseClass] || [TrueClass,
|
98
|
+
FalseClass].include?(type_class) || type.to_s.include?('T::Boolean')
|
99
|
+
apply_boolean_validations
|
100
|
+
elsif type_class.is_a?(Object) && type_class.include?(EasyTalk::Model)
|
101
|
+
apply_object_validations
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
# Determine the actual class for a type, handling Sorbet types
|
106
|
+
def get_type_class(type)
|
107
|
+
if type.is_a?(Class)
|
108
|
+
type
|
109
|
+
elsif type.respond_to?(:raw_type)
|
110
|
+
type.raw_type
|
111
|
+
elsif type.is_a?(T::Types::TypedArray)
|
112
|
+
Array
|
113
|
+
elsif type.is_a?(Symbol) || type.is_a?(String)
|
114
|
+
begin
|
115
|
+
type.to_s.classify.constantize
|
116
|
+
rescue StandardError
|
117
|
+
String
|
118
|
+
end
|
119
|
+
elsif type.to_s.include?('T::Boolean')
|
120
|
+
[TrueClass, FalseClass] # Return both boolean classes
|
121
|
+
else
|
122
|
+
String # Default fallback
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
# Add presence validation for the property
|
127
|
+
def apply_presence_validation
|
128
|
+
@klass.validates @property_name, presence: true
|
129
|
+
end
|
130
|
+
|
131
|
+
# Validate string-specific constraints
|
132
|
+
def apply_string_validations
|
133
|
+
# Handle format constraints
|
134
|
+
apply_format_validation(@constraints[:format]) if @constraints[:format]
|
135
|
+
|
136
|
+
# Handle pattern (regex) constraints
|
137
|
+
@klass.validates @property_name, format: { with: Regexp.new(@constraints[:pattern]) } if @constraints[:pattern]
|
138
|
+
|
139
|
+
# Handle length constraints
|
140
|
+
length_options = {}
|
141
|
+
length_options[:minimum] = @constraints[:min_length] if @constraints[:min_length]
|
142
|
+
length_options[:maximum] = @constraints[:max_length] if @constraints[:max_length]
|
143
|
+
@klass.validates @property_name, length: length_options if length_options.any?
|
144
|
+
end
|
145
|
+
|
146
|
+
# Apply format-specific validations (email, url, etc.)
|
147
|
+
def apply_format_validation(format)
|
148
|
+
case format.to_s
|
149
|
+
when 'email'
|
150
|
+
@klass.validates @property_name, format: {
|
151
|
+
with: URI::MailTo::EMAIL_REGEXP,
|
152
|
+
message: 'must be a valid email address'
|
153
|
+
}
|
154
|
+
when 'uri', 'url'
|
155
|
+
@klass.validates @property_name, format: {
|
156
|
+
with: URI::DEFAULT_PARSER.make_regexp,
|
157
|
+
message: 'must be a valid URL'
|
158
|
+
}
|
159
|
+
when 'uuid'
|
160
|
+
uuid_regex = /\A[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\z/i
|
161
|
+
@klass.validates @property_name, format: {
|
162
|
+
with: uuid_regex,
|
163
|
+
message: 'must be a valid UUID'
|
164
|
+
}
|
165
|
+
when 'date'
|
166
|
+
@klass.validates @property_name, format: {
|
167
|
+
with: /\A\d{4}-\d{2}-\d{2}\z/,
|
168
|
+
message: 'must be a valid date in YYYY-MM-DD format'
|
169
|
+
}
|
170
|
+
when 'date-time'
|
171
|
+
# ISO 8601 date-time format
|
172
|
+
@klass.validates @property_name, format: {
|
173
|
+
with: /\A\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2})?\z/,
|
174
|
+
message: 'must be a valid ISO 8601 date-time'
|
175
|
+
}
|
176
|
+
when 'time'
|
177
|
+
@klass.validates @property_name, format: {
|
178
|
+
with: /\A\d{2}:\d{2}:\d{2}(?:\.\d+)?\z/,
|
179
|
+
message: 'must be a valid time in HH:MM:SS format'
|
180
|
+
}
|
181
|
+
end
|
182
|
+
end
|
183
|
+
|
184
|
+
# Validate integer-specific constraints
|
185
|
+
def apply_integer_validations
|
186
|
+
apply_numeric_validations(only_integer: true)
|
187
|
+
end
|
188
|
+
|
189
|
+
# Validate number-specific constraints
|
190
|
+
def apply_number_validations
|
191
|
+
apply_numeric_validations(only_integer: false)
|
192
|
+
end
|
193
|
+
|
194
|
+
# Apply numeric validations for integers and floats
|
195
|
+
def apply_numeric_validations(only_integer: false)
|
196
|
+
options = { only_integer: only_integer }
|
197
|
+
|
198
|
+
# Add range constraints
|
199
|
+
options[:greater_than_or_equal_to] = @constraints[:minimum] if @constraints[:minimum]
|
200
|
+
options[:less_than_or_equal_to] = @constraints[:maximum] if @constraints[:maximum]
|
201
|
+
options[:greater_than] = @constraints[:exclusive_minimum] if @constraints[:exclusive_minimum]
|
202
|
+
options[:less_than] = @constraints[:exclusive_maximum] if @constraints[:exclusive_maximum]
|
203
|
+
|
204
|
+
@klass.validates @property_name, numericality: options
|
205
|
+
|
206
|
+
# Add multiple_of validation
|
207
|
+
return unless @constraints[:multiple_of]
|
208
|
+
|
209
|
+
prop_name = @property_name
|
210
|
+
multiple_of_value = @constraints[:multiple_of]
|
211
|
+
@klass.validate do |record|
|
212
|
+
value = record.public_send(prop_name)
|
213
|
+
record.errors.add(prop_name, "must be a multiple of #{multiple_of_value}") if value && (value % multiple_of_value != 0)
|
214
|
+
end
|
215
|
+
end
|
216
|
+
|
217
|
+
# Validate array-specific constraints
|
218
|
+
def apply_array_validations(type)
|
219
|
+
# Validate array length
|
220
|
+
if @constraints[:min_items] || @constraints[:max_items]
|
221
|
+
length_options = {}
|
222
|
+
length_options[:minimum] = @constraints[:min_items] if @constraints[:min_items]
|
223
|
+
length_options[:maximum] = @constraints[:max_items] if @constraints[:max_items]
|
224
|
+
|
225
|
+
@klass.validates @property_name, length: length_options
|
226
|
+
end
|
227
|
+
|
228
|
+
# Validate uniqueness within the array
|
229
|
+
if @constraints[:unique_items]
|
230
|
+
prop_name = @property_name
|
231
|
+
@klass.validate do |record|
|
232
|
+
value = record.public_send(prop_name)
|
233
|
+
record.errors.add(prop_name, 'must contain unique items') if value && value.uniq.length != value.length
|
234
|
+
end
|
235
|
+
end
|
236
|
+
|
237
|
+
# Validate array item types if using T::Array[SomeType]
|
238
|
+
return unless type.respond_to?(:type_parameter)
|
239
|
+
|
240
|
+
inner_type = type.type_parameter
|
241
|
+
prop_name = @property_name
|
242
|
+
@klass.validate do |record|
|
243
|
+
value = record.public_send(prop_name)
|
244
|
+
if value.is_a?(Array)
|
245
|
+
value.each_with_index do |item, index|
|
246
|
+
record.errors.add(prop_name, "item at index #{index} must be a #{inner_type}") unless item.is_a?(inner_type)
|
247
|
+
end
|
248
|
+
end
|
249
|
+
end
|
250
|
+
end
|
251
|
+
|
252
|
+
# Validate boolean-specific constraints
|
253
|
+
def apply_boolean_validations
|
254
|
+
# For boolean values, validate inclusion in [true, false]
|
255
|
+
# If not optional, don't allow nil (equivalent to presence validation for booleans)
|
256
|
+
if optional?
|
257
|
+
@klass.validates @property_name, inclusion: { in: [true, false] }, allow_nil: true
|
258
|
+
else
|
259
|
+
@klass.validates @property_name, inclusion: { in: [true, false] }
|
260
|
+
# Add custom validation for nil values that provides the "can't be blank" message
|
261
|
+
prop_name = @property_name
|
262
|
+
@klass.validate do |record|
|
263
|
+
value = record.public_send(prop_name)
|
264
|
+
record.errors.add(prop_name, "can't be blank") if value.nil?
|
265
|
+
end
|
266
|
+
end
|
267
|
+
|
268
|
+
# Add type validation to ensure the value is actually a boolean
|
269
|
+
prop_name = @property_name
|
270
|
+
@klass.validate do |record|
|
271
|
+
value = record.public_send(prop_name)
|
272
|
+
record.errors.add(prop_name, 'must be a boolean') if value && ![true, false].include?(value)
|
273
|
+
end
|
274
|
+
end
|
275
|
+
|
276
|
+
# Validate object/hash-specific constraints
|
277
|
+
def apply_object_validations
|
278
|
+
# Capture necessary variables outside the validation block's scope
|
279
|
+
prop_name = @property_name
|
280
|
+
expected_type = get_type_class(@type) # Get the raw model class
|
281
|
+
|
282
|
+
@klass.validate do |record|
|
283
|
+
nested_object = record.public_send(prop_name)
|
284
|
+
|
285
|
+
# Only validate if the nested object is present
|
286
|
+
if nested_object
|
287
|
+
# Check if the object is of the expected type (e.g., an actual Email instance)
|
288
|
+
if nested_object.is_a?(expected_type)
|
289
|
+
# Check if this object appears to be empty (created from an empty hash)
|
290
|
+
# by checking if all defined properties are nil/blank
|
291
|
+
properties = expected_type.schema_definition.schema[:properties] || {}
|
292
|
+
all_properties_blank = properties.keys.all? do |property|
|
293
|
+
value = nested_object.public_send(property)
|
294
|
+
value.nil? || (value.respond_to?(:empty?) && value.empty?)
|
295
|
+
end
|
296
|
+
|
297
|
+
if all_properties_blank
|
298
|
+
# Treat as blank and add a presence error to the parent field
|
299
|
+
record.errors.add(prop_name, "can't be blank")
|
300
|
+
else
|
301
|
+
# If it's the correct type and not empty, validate it
|
302
|
+
unless nested_object.valid?
|
303
|
+
# Merge errors from the nested object into the parent
|
304
|
+
nested_object.errors.each do |error|
|
305
|
+
# Prefix the attribute name (e.g., 'email.address')
|
306
|
+
nested_key = "#{prop_name}.#{error.attribute}"
|
307
|
+
record.errors.add(nested_key.to_sym, error.message)
|
308
|
+
end
|
309
|
+
end
|
310
|
+
end
|
311
|
+
else
|
312
|
+
# If present but not the correct type, add a type error
|
313
|
+
record.errors.add(prop_name, "must be a valid #{expected_type.name}")
|
314
|
+
end
|
315
|
+
end
|
316
|
+
# NOTE: Presence validation (if nested_object is nil) is handled
|
317
|
+
# by apply_presence_validation based on the property definition.
|
318
|
+
end
|
319
|
+
end
|
320
|
+
|
321
|
+
# Apply enum validation for inclusion in a specific list
|
322
|
+
def apply_enum_validation
|
323
|
+
@klass.validates @property_name, inclusion: {
|
324
|
+
in: @constraints[:enum],
|
325
|
+
message: "must be one of: #{@constraints[:enum].join(', ')}"
|
326
|
+
}
|
327
|
+
end
|
328
|
+
|
329
|
+
# Apply const validation for equality with a specific value
|
330
|
+
def apply_const_validation
|
331
|
+
const_value = @constraints[:const]
|
332
|
+
prop_name = @property_name
|
333
|
+
@klass.validate do |record|
|
334
|
+
value = record.public_send(prop_name)
|
335
|
+
record.errors.add(prop_name, "must be equal to #{const_value}") if !value.nil? && value != const_value
|
336
|
+
end
|
337
|
+
end
|
338
|
+
end
|
339
|
+
end
|
data/lib/easy_talk/version.rb
CHANGED
data/lib/easy_talk.rb
CHANGED
@@ -7,9 +7,7 @@ module EasyTalk
|
|
7
7
|
require 'easy_talk/errors'
|
8
8
|
require 'easy_talk/errors_helper'
|
9
9
|
require 'easy_talk/configuration'
|
10
|
-
require 'easy_talk/types/
|
11
|
-
require 'easy_talk/types/all_of'
|
12
|
-
require 'easy_talk/types/one_of'
|
10
|
+
require 'easy_talk/types/composer'
|
13
11
|
require 'easy_talk/model'
|
14
12
|
require 'easy_talk/property'
|
15
13
|
require 'easy_talk/schema_definition'
|
@@ -24,4 +22,8 @@ module EasyTalk
|
|
24
22
|
ErrorHelper.raise_unknown_option_error(property_name: property_name, option: options, valid_options: valid_keys)
|
25
23
|
end
|
26
24
|
end
|
25
|
+
|
26
|
+
def self.configure_nilable_behavior(nilable_is_optional = false)
|
27
|
+
configuration.nilable_is_optional = nilable_is_optional
|
28
|
+
end
|
27
29
|
end
|