easy_talk_two 1.1.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 +7 -0
- data/.rubocop.yml +36 -0
- data/CHANGELOG.md +127 -0
- data/CONSTRAINTS.md +70 -0
- data/LICENSE.txt +21 -0
- data/README.md +1060 -0
- data/Rakefile +11 -0
- data/docs/.gitignore +5 -0
- data/docs/404.html +25 -0
- data/docs/Gemfile +38 -0
- data/docs/_config.yml +53 -0
- data/docs/_posts/2024-05-07-welcome-to-jekyll.markdown +29 -0
- data/docs/about.markdown +18 -0
- data/docs/index.markdown +7 -0
- data/lib/easy_talk/active_record_schema_builder.rb +299 -0
- data/lib/easy_talk/builders/base_builder.rb +65 -0
- data/lib/easy_talk/builders/boolean_builder.rb +23 -0
- data/lib/easy_talk/builders/collection_helpers.rb +12 -0
- data/lib/easy_talk/builders/composition_builder.rb +77 -0
- data/lib/easy_talk/builders/integer_builder.rb +28 -0
- data/lib/easy_talk/builders/null_builder.rb +16 -0
- data/lib/easy_talk/builders/number_builder.rb +27 -0
- data/lib/easy_talk/builders/object_builder.rb +180 -0
- data/lib/easy_talk/builders/string_builder.rb +27 -0
- data/lib/easy_talk/builders/temporal_builder.rb +49 -0
- data/lib/easy_talk/builders/typed_array_builder.rb +56 -0
- data/lib/easy_talk/builders/union_builder.rb +37 -0
- data/lib/easy_talk/configuration.rb +29 -0
- data/lib/easy_talk/errors.rb +8 -0
- data/lib/easy_talk/errors_helper.rb +147 -0
- data/lib/easy_talk/keywords.rb +37 -0
- data/lib/easy_talk/model.rb +197 -0
- data/lib/easy_talk/property.rb +130 -0
- data/lib/easy_talk/schema_definition.rb +78 -0
- data/lib/easy_talk/sorbet_extension.rb +15 -0
- data/lib/easy_talk/tools/function_builder.rb +40 -0
- data/lib/easy_talk/types/base_composer.rb +23 -0
- data/lib/easy_talk/types/composer.rb +88 -0
- data/lib/easy_talk/version.rb +5 -0
- data/lib/easy_talk.rb +29 -0
- metadata +265 -0
@@ -0,0 +1,28 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'base_builder'
|
4
|
+
|
5
|
+
module EasyTalk
|
6
|
+
module Builders
|
7
|
+
# Builder class for integer properties.
|
8
|
+
class IntegerBuilder < BaseBuilder
|
9
|
+
extend T::Sig
|
10
|
+
VALID_OPTIONS = {
|
11
|
+
minimum: { type: Integer, key: :minimum },
|
12
|
+
maximum: { type: Integer, key: :maximum },
|
13
|
+
exclusive_minimum: { type: Integer, key: :exclusiveMinimum },
|
14
|
+
exclusive_maximum: { type: Integer, key: :exclusiveMaximum },
|
15
|
+
multiple_of: { type: Integer, key: :multipleOf },
|
16
|
+
enum: { type: T::Array[Integer], key: :enum },
|
17
|
+
const: { type: Integer, key: :const },
|
18
|
+
default: { type: Integer, key: :default }
|
19
|
+
}.freeze
|
20
|
+
|
21
|
+
# Initializes a new instance of the IntegerBuilder class.
|
22
|
+
sig { params(name: Symbol, constraints: Hash).void }
|
23
|
+
def initialize(name, constraints = {})
|
24
|
+
super(name, { type: 'integer' }, constraints, VALID_OPTIONS)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'base_builder'
|
4
|
+
|
5
|
+
module EasyTalk
|
6
|
+
module Builders
|
7
|
+
# builder class for Null properties.
|
8
|
+
class NullBuilder < BaseBuilder
|
9
|
+
# Initializes a new instance of the NullBuilder class.
|
10
|
+
sig { params(name: Symbol, _constraints: Hash).void }
|
11
|
+
def initialize(name, _constraints = {})
|
12
|
+
super(name, { type: 'null' }, {}, {})
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'base_builder'
|
4
|
+
|
5
|
+
module EasyTalk
|
6
|
+
module Builders
|
7
|
+
# Builder class for number properties.
|
8
|
+
class NumberBuilder < BaseBuilder
|
9
|
+
VALID_OPTIONS = {
|
10
|
+
multiple_of: { type: T.any(Integer, Float), key: :multipleOf },
|
11
|
+
minimum: { type: T.any(Integer, Float), key: :minimum },
|
12
|
+
maximum: { type: T.any(Integer, Float), key: :maximum },
|
13
|
+
exclusive_minimum: { type: T.any(Integer, Float), key: :exclusiveMinimum },
|
14
|
+
exclusive_maximum: { type: T.any(Integer, Float), key: :exclusiveMaximum },
|
15
|
+
enum: { type: T::Array[T.any(Integer, Float)], key: :enum },
|
16
|
+
const: { type: T.any(Integer, Float), key: :const },
|
17
|
+
default: { type: T.any(Integer, Float), key: :default }
|
18
|
+
}.freeze
|
19
|
+
|
20
|
+
# Initializes a new instance of the NumberBuilder class.
|
21
|
+
sig { params(name: Symbol, constraints: Hash).void }
|
22
|
+
def initialize(name, constraints = {})
|
23
|
+
super(name, { type: 'number' }, constraints, VALID_OPTIONS)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,180 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'base_builder'
|
4
|
+
|
5
|
+
module EasyTalk
|
6
|
+
module Builders
|
7
|
+
#
|
8
|
+
# ObjectBuilder is responsible for turning a SchemaDefinition of an "object" type
|
9
|
+
# into a validated JSON Schema hash. It:
|
10
|
+
#
|
11
|
+
# 1) Recursively processes the schema's :properties,
|
12
|
+
# 2) Determines which properties are required (unless optional),
|
13
|
+
# 3) Handles sub-schema composition (allOf, anyOf, oneOf, not),
|
14
|
+
# 4) Produces the final object-level schema hash.
|
15
|
+
#
|
16
|
+
class ObjectBuilder < BaseBuilder
|
17
|
+
extend T::Sig
|
18
|
+
|
19
|
+
# Required by BaseBuilder: recognized schema options for "object" types
|
20
|
+
VALID_OPTIONS = {
|
21
|
+
properties: { type: T::Hash[T.any(Symbol, String), T.untyped], key: :properties },
|
22
|
+
additional_properties: { type: T::Boolean, key: :additionalProperties },
|
23
|
+
subschemas: { type: T::Array[T.untyped], key: :subschemas },
|
24
|
+
required: { type: T::Array[T.any(Symbol, String)], key: :required },
|
25
|
+
defs: { type: T.untyped, key: :$defs },
|
26
|
+
allOf: { type: T.untyped, key: :allOf },
|
27
|
+
anyOf: { type: T.untyped, key: :anyOf },
|
28
|
+
oneOf: { type: T.untyped, key: :oneOf },
|
29
|
+
not: { type: T.untyped, key: :not }
|
30
|
+
}.freeze
|
31
|
+
|
32
|
+
sig { params(schema_definition: EasyTalk::SchemaDefinition).void }
|
33
|
+
def initialize(schema_definition)
|
34
|
+
# Keep a reference to the original schema definition
|
35
|
+
@schema_definition = schema_definition
|
36
|
+
# Duplicate the raw schema hash so we can mutate it safely
|
37
|
+
@original_schema = schema_definition.schema.dup
|
38
|
+
|
39
|
+
# We'll collect required property names in this Set
|
40
|
+
@required_properties = Set.new
|
41
|
+
|
42
|
+
# Usually the name is a string (class name). Fallback to :klass if nil.
|
43
|
+
name_for_builder = schema_definition.name ? schema_definition.name.to_sym : :klass
|
44
|
+
|
45
|
+
# Build the base structure: { type: 'object' } plus any top-level options
|
46
|
+
super(
|
47
|
+
name_for_builder,
|
48
|
+
{ type: 'object' }, # minimal "object" structure
|
49
|
+
build_options_hash, # method below merges & cleans final top-level keys
|
50
|
+
VALID_OPTIONS
|
51
|
+
)
|
52
|
+
end
|
53
|
+
|
54
|
+
private
|
55
|
+
|
56
|
+
##
|
57
|
+
# Main aggregator: merges the top-level schema keys (like :properties, :subschemas)
|
58
|
+
# into a single hash that we'll feed to BaseBuilder.
|
59
|
+
def build_options_hash
|
60
|
+
# Start with a copy of the raw schema
|
61
|
+
merged = @original_schema.dup
|
62
|
+
|
63
|
+
# Extract and build sub-schemas first (handles allOf/anyOf/oneOf references, etc.)
|
64
|
+
process_subschemas(merged)
|
65
|
+
|
66
|
+
# Build :properties into a final form (and find "required" props)
|
67
|
+
merged[:properties] = build_properties(merged.delete(:properties))
|
68
|
+
|
69
|
+
# Populate the final "required" array from @required_properties
|
70
|
+
merged[:required] = @required_properties.to_a if @required_properties.any?
|
71
|
+
|
72
|
+
# Add additionalProperties: false by default if not explicitly set
|
73
|
+
merged[:additional_properties] = false unless merged.key?(:additional_properties)
|
74
|
+
|
75
|
+
# Prune empty or nil values so we don't produce stuff like "properties": {} unnecessarily
|
76
|
+
merged.reject! { |_k, v| v.nil? || v == {} || v == [] }
|
77
|
+
|
78
|
+
merged
|
79
|
+
end
|
80
|
+
|
81
|
+
##
|
82
|
+
# Given the property definitions hash, produce a new hash of
|
83
|
+
# { property_name => [Property or nested schema builder result] }.
|
84
|
+
#
|
85
|
+
def build_properties(properties_hash)
|
86
|
+
return {} unless properties_hash.is_a?(Hash)
|
87
|
+
|
88
|
+
# Cache with a key based on property name and its full configuration
|
89
|
+
@properties_cache ||= {}
|
90
|
+
|
91
|
+
properties_hash.each_with_object({}) do |(prop_name, prop_options), result|
|
92
|
+
cache_key = [prop_name, prop_options].hash
|
93
|
+
|
94
|
+
# Use cache if the exact property and configuration have been processed before
|
95
|
+
@properties_cache[cache_key] ||= begin
|
96
|
+
mark_required_unless_optional(prop_name, prop_options)
|
97
|
+
build_property(prop_name, prop_options)
|
98
|
+
end
|
99
|
+
|
100
|
+
result[prop_name] = @properties_cache[cache_key]
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
##
|
105
|
+
# Decide if a property should be required. If it's optional or nilable,
|
106
|
+
# we won't include it in the "required" array.
|
107
|
+
#
|
108
|
+
def mark_required_unless_optional(prop_name, prop_options)
|
109
|
+
return if property_optional?(prop_options)
|
110
|
+
|
111
|
+
@required_properties.add(prop_name)
|
112
|
+
end
|
113
|
+
|
114
|
+
##
|
115
|
+
# Returns true if the property is declared optional.
|
116
|
+
#
|
117
|
+
def property_optional?(prop_options)
|
118
|
+
# Check constraints[:optional]
|
119
|
+
return true if prop_options.dig(:constraints, :optional)
|
120
|
+
|
121
|
+
# Check for nil_optional config to determine if nilable should also mean optional
|
122
|
+
if prop_options[:type].respond_to?(:nilable?) && prop_options[:type].nilable?
|
123
|
+
return EasyTalk.configuration.nilable_is_optional
|
124
|
+
end
|
125
|
+
|
126
|
+
false
|
127
|
+
end
|
128
|
+
|
129
|
+
##
|
130
|
+
# Builds a single property. Could be a nested schema if it has sub-properties,
|
131
|
+
# or a standard scalar property (String, Integer, etc.).
|
132
|
+
#
|
133
|
+
def build_property(prop_name, prop_options)
|
134
|
+
@property_cache ||= {}
|
135
|
+
|
136
|
+
# Memoize so we only build each property once
|
137
|
+
@property_cache[prop_name] ||= begin
|
138
|
+
# Remove optional constraints from the property
|
139
|
+
constraints = prop_options[:constraints].except(:optional)
|
140
|
+
# Normal property: e.g. { type: String, constraints: {...} }
|
141
|
+
Property.new(prop_name, prop_options[:type], constraints)
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
145
|
+
##
|
146
|
+
# Process top-level composition keywords (e.g. allOf, anyOf, oneOf),
|
147
|
+
# converting them to definitions + references if appropriate.
|
148
|
+
#
|
149
|
+
def process_subschemas(schema_hash)
|
150
|
+
subschemas = schema_hash.delete(:subschemas) || []
|
151
|
+
subschemas.each do |subschema|
|
152
|
+
add_defs_from_subschema(schema_hash, subschema)
|
153
|
+
add_refs_from_subschema(schema_hash, subschema)
|
154
|
+
end
|
155
|
+
end
|
156
|
+
|
157
|
+
##
|
158
|
+
# For each item in the composer, add it to :defs so that we can reference it later.
|
159
|
+
#
|
160
|
+
def add_defs_from_subschema(schema_hash, subschema)
|
161
|
+
# Build up a hash of class_name => schema for each sub-item
|
162
|
+
definitions = subschema.items.each_with_object({}) do |item, acc|
|
163
|
+
acc[item.name] = item.schema
|
164
|
+
end
|
165
|
+
# Merge or create :defs
|
166
|
+
existing_defs = schema_hash[:defs] || {}
|
167
|
+
schema_hash[:defs] = existing_defs.merge(definitions)
|
168
|
+
end
|
169
|
+
|
170
|
+
##
|
171
|
+
# Add references to the schema for each sub-item in the composer
|
172
|
+
# e.g. { "$ref": "#/$defs/SomeClass" }
|
173
|
+
#
|
174
|
+
def add_refs_from_subschema(schema_hash, subschema)
|
175
|
+
references = subschema.items.map { |item| { '$ref': item.ref_template } }
|
176
|
+
schema_hash[subschema.name] = references
|
177
|
+
end
|
178
|
+
end
|
179
|
+
end
|
180
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'base_builder'
|
4
|
+
require 'sorbet-runtime' # Add the import statement for the T module
|
5
|
+
|
6
|
+
module EasyTalk
|
7
|
+
module Builders
|
8
|
+
# Builder class for string properties.
|
9
|
+
class StringBuilder < BaseBuilder
|
10
|
+
extend T::Sig
|
11
|
+
VALID_OPTIONS = {
|
12
|
+
format: { type: String, key: :format },
|
13
|
+
pattern: { type: String, key: :pattern },
|
14
|
+
min_length: { type: Integer, key: :minLength },
|
15
|
+
max_length: { type: Integer, key: :maxLength },
|
16
|
+
enum: { type: T::Array[String], key: :enum },
|
17
|
+
const: { type: String, key: :const },
|
18
|
+
default: { type: String, key: :default }
|
19
|
+
}.freeze
|
20
|
+
|
21
|
+
sig { params(name: Symbol, constraints: Hash).void }
|
22
|
+
def initialize(name, constraints = {})
|
23
|
+
super(name, { type: 'string' }, constraints, VALID_OPTIONS)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'string_builder'
|
4
|
+
|
5
|
+
module EasyTalk
|
6
|
+
module Builders
|
7
|
+
# Builder class for temporal properties (date, datetime, time).
|
8
|
+
class TemporalBuilder < StringBuilder
|
9
|
+
# Initializes a new instance of the TemporalBuilder class.
|
10
|
+
#
|
11
|
+
# @param property_name [Symbol] The name of the property.
|
12
|
+
# @param options [Hash] The options for the builder.
|
13
|
+
# @param format [String] The format of the temporal property (date, date-time, time).
|
14
|
+
def initialize(property_name, options = {}, format = nil)
|
15
|
+
super(property_name, options)
|
16
|
+
@format = format
|
17
|
+
end
|
18
|
+
|
19
|
+
# Modifies the schema to include the format constraint for a temporal property.
|
20
|
+
sig { returns(T::Hash[Symbol, T.untyped]) }
|
21
|
+
def schema
|
22
|
+
super.tap do |schema|
|
23
|
+
schema[:format] = @format if @format
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
# Builder class for date properties.
|
28
|
+
class DateBuilder < TemporalBuilder
|
29
|
+
def initialize(property_name, options = {})
|
30
|
+
super(property_name, options, 'date')
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
# Builder class for datetime properties.
|
35
|
+
class DatetimeBuilder < TemporalBuilder
|
36
|
+
def initialize(property_name, options = {})
|
37
|
+
super(property_name, options, 'date-time')
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
# Builder class for time properties.
|
42
|
+
class TimeBuilder < TemporalBuilder
|
43
|
+
def initialize(property_name, options = {})
|
44
|
+
super(property_name, options, 'time')
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'collection_helpers'
|
4
|
+
|
5
|
+
module EasyTalk
|
6
|
+
module Builders
|
7
|
+
# Builder class for array properties.
|
8
|
+
class TypedArrayBuilder < BaseBuilder
|
9
|
+
extend CollectionHelpers
|
10
|
+
extend T::Sig
|
11
|
+
|
12
|
+
VALID_OPTIONS = {
|
13
|
+
min_items: { type: Integer, key: :minItems },
|
14
|
+
max_items: { type: Integer, key: :maxItems },
|
15
|
+
unique_items: { type: T::Boolean, key: :uniqueItems },
|
16
|
+
enum: { type: T::Array[T.untyped], key: :enum },
|
17
|
+
const: { type: T::Array[T.untyped], key: :const }
|
18
|
+
}.freeze
|
19
|
+
|
20
|
+
attr_reader :type
|
21
|
+
|
22
|
+
sig { params(name: Symbol, type: T.untyped, constraints: Hash).void }
|
23
|
+
def initialize(name, type, constraints = {})
|
24
|
+
@name = name
|
25
|
+
@type = type
|
26
|
+
update_option_types
|
27
|
+
super(name, { type: 'array' }, constraints, VALID_OPTIONS)
|
28
|
+
end
|
29
|
+
|
30
|
+
private
|
31
|
+
|
32
|
+
# Modifies the schema to include the `items` property.
|
33
|
+
#
|
34
|
+
# @return [Hash] The built schema.
|
35
|
+
sig { returns(T::Hash[Symbol, T.untyped]) }
|
36
|
+
def schema
|
37
|
+
super.tap do |schema|
|
38
|
+
schema[:items] = Property.new(@name, inner_type, {}).build
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def inner_type
|
43
|
+
return unless type.is_a?(T::Types::TypedArray)
|
44
|
+
|
45
|
+
type.type.raw_type
|
46
|
+
end
|
47
|
+
|
48
|
+
sig { void }
|
49
|
+
# Updates the option types for the array builder.
|
50
|
+
def update_option_types
|
51
|
+
VALID_OPTIONS[:enum][:type] = T::Array[inner_type]
|
52
|
+
VALID_OPTIONS[:const][:type] = T::Array[inner_type]
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'collection_helpers'
|
4
|
+
|
5
|
+
module EasyTalk
|
6
|
+
module Builders
|
7
|
+
# Base builder class for array-type properties.
|
8
|
+
class UnionBuilder
|
9
|
+
extend CollectionHelpers
|
10
|
+
extend T::Sig
|
11
|
+
|
12
|
+
sig { params(name: Symbol, type: T.untyped, constraints: T.untyped).void }
|
13
|
+
def initialize(name, type, constraints)
|
14
|
+
@name = name
|
15
|
+
@type = type
|
16
|
+
@constraints = constraints
|
17
|
+
@context = {}
|
18
|
+
end
|
19
|
+
|
20
|
+
def build
|
21
|
+
@context[@name] = {
|
22
|
+
'anyOf' => schemas
|
23
|
+
}
|
24
|
+
end
|
25
|
+
|
26
|
+
def schemas
|
27
|
+
types.map do |type|
|
28
|
+
Property.new(@name, type, @constraints).build
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def types
|
33
|
+
@type.types
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module EasyTalk
|
4
|
+
class Configuration
|
5
|
+
attr_accessor :exclude_foreign_keys, :exclude_associations, :excluded_columns,
|
6
|
+
:exclude_primary_key, :exclude_timestamps, :default_additional_properties,
|
7
|
+
:nilable_is_optional
|
8
|
+
|
9
|
+
def initialize
|
10
|
+
@exclude_foreign_keys = true
|
11
|
+
@exclude_associations = true
|
12
|
+
@excluded_columns = []
|
13
|
+
@exclude_primary_key = true # New option, defaulting to true
|
14
|
+
@exclude_timestamps = true # New option, defaulting to true
|
15
|
+
@default_additional_properties = false
|
16
|
+
@nilable_is_optional = false
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
class << self
|
21
|
+
def configuration
|
22
|
+
@configuration ||= Configuration.new
|
23
|
+
end
|
24
|
+
|
25
|
+
def configure
|
26
|
+
yield(configuration)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,147 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module EasyTalk
|
4
|
+
module ErrorHelper
|
5
|
+
def self.raise_constraint_error(property_name:, constraint_name:, expected:, got:)
|
6
|
+
message = "Error in property '#{property_name}': Constraint '#{constraint_name}' expects #{expected}, " \
|
7
|
+
"but received #{got.inspect} (#{got.class})."
|
8
|
+
raise ConstraintError, message
|
9
|
+
end
|
10
|
+
|
11
|
+
def self.raise_array_constraint_error(property_name:, constraint_name:, index:, expected:, got:)
|
12
|
+
message = "Error in property '#{property_name}': Constraint '#{constraint_name}' at index #{index} " \
|
13
|
+
"expects #{expected}, but received #{got.inspect} (#{got.class})."
|
14
|
+
raise ConstraintError, message
|
15
|
+
end
|
16
|
+
|
17
|
+
def self.raise_unknown_option_error(property_name:, option:, valid_options:)
|
18
|
+
option = option.keys.first if option.is_a?(Hash)
|
19
|
+
message = "Unknown option '#{option}' for property '#{property_name}'. " \
|
20
|
+
"Valid options are: #{valid_options.join(', ')}."
|
21
|
+
raise UnknownOptionError, message
|
22
|
+
end
|
23
|
+
|
24
|
+
def self.extract_inner_type(type_info)
|
25
|
+
# No change needed here
|
26
|
+
if type_info.respond_to?(:type) && type_info.type.respond_to?(:raw_type)
|
27
|
+
type_info.type.raw_type
|
28
|
+
# special boolean handling
|
29
|
+
elsif type_info.try(:type).try(:name) == 'T::Boolean'
|
30
|
+
T::Boolean
|
31
|
+
elsif type_info.respond_to?(:type_parameter)
|
32
|
+
type_info.type_parameter
|
33
|
+
elsif type_info.respond_to?(:raw_a) && type_info.respond_to?(:raw_b)
|
34
|
+
# Handle SimplePairUnion types
|
35
|
+
[type_info.raw_a, type_info.raw_b]
|
36
|
+
elsif type_info.respond_to?(:types)
|
37
|
+
# Handle complex union types
|
38
|
+
type_info.types.map { |t| t.respond_to?(:raw_type) ? t.raw_type : t }
|
39
|
+
else
|
40
|
+
# Fallback to something sensible
|
41
|
+
Object
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def self.validate_typed_array_values(property_name:, constraint_name:, type_info:, array_value:)
|
46
|
+
# Skip validation if it's not actually an array
|
47
|
+
return unless array_value.is_a?(Array)
|
48
|
+
|
49
|
+
# Extract the inner type from the array type definition
|
50
|
+
inner_type = extract_inner_type(type_info)
|
51
|
+
|
52
|
+
# Check each element of the array
|
53
|
+
array_value.each_with_index do |element, index|
|
54
|
+
if inner_type.is_a?(Array)
|
55
|
+
# For union types, check if the element matches any of the allowed types
|
56
|
+
unless inner_type.any? { |t| element.is_a?(t) }
|
57
|
+
expected = inner_type.join(' or ')
|
58
|
+
raise_array_constraint_error(
|
59
|
+
property_name: property_name,
|
60
|
+
constraint_name: constraint_name,
|
61
|
+
index: index,
|
62
|
+
expected: expected,
|
63
|
+
got: element
|
64
|
+
)
|
65
|
+
end
|
66
|
+
else
|
67
|
+
# For single types, just check against that type
|
68
|
+
next if [true, false].include?(element)
|
69
|
+
|
70
|
+
unless element.is_a?(inner_type)
|
71
|
+
raise_array_constraint_error(
|
72
|
+
property_name: property_name,
|
73
|
+
constraint_name: constraint_name,
|
74
|
+
index: index,
|
75
|
+
expected: inner_type,
|
76
|
+
got: element
|
77
|
+
)
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
def self.validate_constraint_value(property_name:, constraint_name:, value_type:, value:)
|
84
|
+
return if value.nil?
|
85
|
+
|
86
|
+
if value_type.to_s.include?('Boolean')
|
87
|
+
return if value.is_a?(Array) && value.all? { |v| [true, false].include?(v) }
|
88
|
+
|
89
|
+
unless [true, false].include?(value)
|
90
|
+
raise_constraint_error(
|
91
|
+
property_name: property_name,
|
92
|
+
constraint_name: constraint_name,
|
93
|
+
expected: 'Boolean (true or false)',
|
94
|
+
got: value
|
95
|
+
)
|
96
|
+
end
|
97
|
+
return
|
98
|
+
end
|
99
|
+
|
100
|
+
# Handle simple scalar types (String, Integer, etc.)
|
101
|
+
if value_type.is_a?(Class)
|
102
|
+
unless value.is_a?(value_type)
|
103
|
+
raise_constraint_error(
|
104
|
+
property_name: property_name,
|
105
|
+
constraint_name: constraint_name,
|
106
|
+
expected: value_type,
|
107
|
+
got: value
|
108
|
+
)
|
109
|
+
end
|
110
|
+
# Handle array types specifically
|
111
|
+
elsif value_type.class.name.include?('TypedArray') ||
|
112
|
+
(value_type.respond_to?(:to_s) && value_type.to_s.include?('T::Array'))
|
113
|
+
# This is an array type, validate it
|
114
|
+
validate_typed_array_values(
|
115
|
+
property_name: property_name,
|
116
|
+
constraint_name: constraint_name,
|
117
|
+
type_info: value_type,
|
118
|
+
array_value: value
|
119
|
+
)
|
120
|
+
# Handle Sorbet type objects
|
121
|
+
elsif value_type.class.ancestors.include?(T::Types::Base)
|
122
|
+
# Extract the inner type
|
123
|
+
inner_type = extract_inner_type(value_type)
|
124
|
+
|
125
|
+
if inner_type.is_a?(Array)
|
126
|
+
# For union types, check if the value matches any of the allowed types
|
127
|
+
unless inner_type.any? { |t| value.is_a?(t) }
|
128
|
+
expected = inner_type.join(' or ')
|
129
|
+
raise_constraint_error(
|
130
|
+
property_name: property_name,
|
131
|
+
constraint_name: constraint_name,
|
132
|
+
expected: expected,
|
133
|
+
got: value
|
134
|
+
)
|
135
|
+
end
|
136
|
+
elsif !value.is_a?(inner_type)
|
137
|
+
raise_constraint_error(
|
138
|
+
property_name: property_name,
|
139
|
+
constraint_name: constraint_name,
|
140
|
+
expected: inner_type,
|
141
|
+
got: value
|
142
|
+
)
|
143
|
+
end
|
144
|
+
end
|
145
|
+
end
|
146
|
+
end
|
147
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module EasyTalk
|
4
|
+
KEYWORDS = %i[
|
5
|
+
description
|
6
|
+
type
|
7
|
+
title
|
8
|
+
property
|
9
|
+
required
|
10
|
+
items
|
11
|
+
additional_items
|
12
|
+
pattern_properties
|
13
|
+
additional_properties
|
14
|
+
dependencies
|
15
|
+
dependent_required
|
16
|
+
format
|
17
|
+
content_media_type
|
18
|
+
content_encoding
|
19
|
+
enum
|
20
|
+
const
|
21
|
+
default
|
22
|
+
examples
|
23
|
+
max_length
|
24
|
+
min_length
|
25
|
+
pattern
|
26
|
+
maximum
|
27
|
+
exclusive_maximum
|
28
|
+
minimum
|
29
|
+
exclusive_minimum
|
30
|
+
multiple_of
|
31
|
+
max_items
|
32
|
+
min_items
|
33
|
+
unique_items
|
34
|
+
max_properties
|
35
|
+
min_properties
|
36
|
+
].freeze
|
37
|
+
end
|