easy_talk 3.2.0 → 3.3.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 +15 -43
- data/CHANGELOG.md +89 -0
- data/README.md +447 -2115
- data/docs/json_schema_compliance.md +140 -26
- data/docs/primitive-schema-rfc.md +894 -0
- data/lib/easy_talk/builders/base_builder.rb +2 -1
- data/lib/easy_talk/builders/boolean_builder.rb +2 -1
- data/lib/easy_talk/builders/collection_helpers.rb +4 -0
- data/lib/easy_talk/builders/composition_builder.rb +7 -2
- data/lib/easy_talk/builders/integer_builder.rb +2 -1
- data/lib/easy_talk/builders/null_builder.rb +4 -1
- data/lib/easy_talk/builders/number_builder.rb +4 -1
- data/lib/easy_talk/builders/object_builder.rb +64 -3
- data/lib/easy_talk/builders/registry.rb +15 -1
- data/lib/easy_talk/builders/string_builder.rb +3 -1
- data/lib/easy_talk/builders/temporal_builder.rb +7 -0
- data/lib/easy_talk/builders/tuple_builder.rb +89 -0
- data/lib/easy_talk/builders/typed_array_builder.rb +4 -2
- data/lib/easy_talk/builders/union_builder.rb +5 -1
- data/lib/easy_talk/configuration.rb +17 -2
- data/lib/easy_talk/errors.rb +1 -0
- data/lib/easy_talk/errors_helper.rb +3 -0
- data/lib/easy_talk/json_schema_equality.rb +46 -0
- data/lib/easy_talk/keywords.rb +0 -1
- data/lib/easy_talk/model.rb +27 -1
- data/lib/easy_talk/model_helper.rb +4 -0
- data/lib/easy_talk/naming_strategies.rb +4 -0
- data/lib/easy_talk/property.rb +7 -0
- data/lib/easy_talk/ref_helper.rb +6 -0
- data/lib/easy_talk/schema.rb +1 -0
- data/lib/easy_talk/schema_definition.rb +52 -6
- data/lib/easy_talk/schema_methods.rb +36 -5
- data/lib/easy_talk/sorbet_extension.rb +1 -0
- data/lib/easy_talk/type_introspection.rb +45 -1
- data/lib/easy_talk/types/tuple.rb +77 -0
- data/lib/easy_talk/validation_adapters/active_model_adapter.rb +350 -62
- data/lib/easy_talk/validation_adapters/active_model_schema_validation.rb +106 -0
- data/lib/easy_talk/validation_adapters/base.rb +12 -0
- data/lib/easy_talk/validation_adapters/none_adapter.rb +9 -0
- data/lib/easy_talk/validation_builder.rb +1 -0
- data/lib/easy_talk/version.rb +1 -1
- data/lib/easy_talk.rb +1 -0
- metadata +13 -4
|
@@ -23,7 +23,7 @@ module EasyTalk
|
|
|
23
23
|
params(
|
|
24
24
|
property_name: Symbol,
|
|
25
25
|
schema: T::Hash[Symbol, T.untyped],
|
|
26
|
-
options: T::Hash[Symbol,
|
|
26
|
+
options: T::Hash[Symbol, T.untyped],
|
|
27
27
|
valid_options: T::Hash[Symbol, T.untyped]
|
|
28
28
|
).void
|
|
29
29
|
end
|
|
@@ -59,6 +59,7 @@ module EasyTalk
|
|
|
59
59
|
end
|
|
60
60
|
end
|
|
61
61
|
|
|
62
|
+
sig { returns(T::Boolean) }
|
|
62
63
|
def self.collection_type?
|
|
63
64
|
false
|
|
64
65
|
end
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
|
+
# typed: true
|
|
2
3
|
|
|
3
4
|
require_relative 'base_builder'
|
|
4
5
|
|
|
@@ -14,7 +15,7 @@ module EasyTalk
|
|
|
14
15
|
default: { type: T::Boolean, key: :default }
|
|
15
16
|
}.freeze
|
|
16
17
|
|
|
17
|
-
sig { params(name: Symbol, constraints: Hash).void }
|
|
18
|
+
sig { params(name: Symbol, constraints: T::Hash[Symbol, T.untyped]).void }
|
|
18
19
|
def initialize(name, constraints = {})
|
|
19
20
|
super(name, { type: 'boolean' }, constraints, VALID_OPTIONS)
|
|
20
21
|
end
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
|
+
# typed: true
|
|
2
3
|
|
|
3
4
|
require_relative 'collection_helpers'
|
|
4
5
|
require_relative '../ref_helper'
|
|
@@ -16,7 +17,7 @@ module EasyTalk
|
|
|
16
17
|
'OneOfBuilder' => 'oneOf'
|
|
17
18
|
}.freeze
|
|
18
19
|
|
|
19
|
-
sig { params(name: Symbol, type: T.untyped, constraints: Hash).void }
|
|
20
|
+
sig { params(name: Symbol, type: T.untyped, constraints: T::Hash[Symbol, T.untyped]).void }
|
|
20
21
|
# Initializes a new instance of the CompositionBuilder class.
|
|
21
22
|
#
|
|
22
23
|
# @param name [Symbol] The name of the composition.
|
|
@@ -32,7 +33,8 @@ module EasyTalk
|
|
|
32
33
|
|
|
33
34
|
# Builds the composed JSON schema.
|
|
34
35
|
#
|
|
35
|
-
# @return [
|
|
36
|
+
# @return [Hash] The composed JSON schema.
|
|
37
|
+
sig { returns(T::Hash[Symbol, T.untyped]) }
|
|
36
38
|
def build
|
|
37
39
|
@context[@name.to_sym] = {
|
|
38
40
|
type: 'object',
|
|
@@ -43,6 +45,7 @@ module EasyTalk
|
|
|
43
45
|
# Returns the composer keyword based on the composer type.
|
|
44
46
|
#
|
|
45
47
|
# @return [String] The composer keyword.
|
|
48
|
+
sig { returns(T.nilable(String)) }
|
|
46
49
|
def composer_keyword
|
|
47
50
|
COMPOSER_TO_KEYWORD[@composer_type]
|
|
48
51
|
end
|
|
@@ -50,6 +53,7 @@ module EasyTalk
|
|
|
50
53
|
# Returns an array of schemas for the composed JSON schema.
|
|
51
54
|
#
|
|
52
55
|
# @return [Array<Hash>] The array of schemas.
|
|
56
|
+
sig { returns(T::Array[T.untyped]) }
|
|
53
57
|
def schemas
|
|
54
58
|
items.map do |type|
|
|
55
59
|
if EasyTalk::RefHelper.should_use_ref?(type, @constraints)
|
|
@@ -66,6 +70,7 @@ module EasyTalk
|
|
|
66
70
|
# Returns the items of the type.
|
|
67
71
|
#
|
|
68
72
|
# @return [T.untyped] The items of the type.
|
|
73
|
+
sig { returns(T.untyped) }
|
|
69
74
|
def items
|
|
70
75
|
@type.items
|
|
71
76
|
end
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
|
+
# typed: true
|
|
2
3
|
|
|
3
4
|
require_relative 'base_builder'
|
|
4
5
|
|
|
@@ -20,7 +21,7 @@ module EasyTalk
|
|
|
20
21
|
}.freeze
|
|
21
22
|
|
|
22
23
|
# Initializes a new instance of the IntegerBuilder class.
|
|
23
|
-
sig { params(name: Symbol, constraints: Hash).void }
|
|
24
|
+
sig { params(name: Symbol, constraints: T::Hash[Symbol, T.untyped]).void }
|
|
24
25
|
def initialize(name, constraints = {})
|
|
25
26
|
super(name, { type: 'integer' }, constraints, VALID_OPTIONS)
|
|
26
27
|
end
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
|
+
# typed: true
|
|
2
3
|
|
|
3
4
|
require_relative 'base_builder'
|
|
4
5
|
|
|
@@ -6,8 +7,10 @@ module EasyTalk
|
|
|
6
7
|
module Builders
|
|
7
8
|
# builder class for Null properties.
|
|
8
9
|
class NullBuilder < BaseBuilder
|
|
10
|
+
extend T::Sig
|
|
11
|
+
|
|
9
12
|
# Initializes a new instance of the NullBuilder class.
|
|
10
|
-
sig { params(name: Symbol, _constraints: Hash).void }
|
|
13
|
+
sig { params(name: Symbol, _constraints: T::Hash[Symbol, T.untyped]).void }
|
|
11
14
|
def initialize(name, _constraints = {})
|
|
12
15
|
super(name, { type: 'null' }, {}, {})
|
|
13
16
|
end
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
|
+
# typed: true
|
|
2
3
|
|
|
3
4
|
require_relative 'base_builder'
|
|
4
5
|
|
|
@@ -6,6 +7,8 @@ module EasyTalk
|
|
|
6
7
|
module Builders
|
|
7
8
|
# Builder class for number properties.
|
|
8
9
|
class NumberBuilder < BaseBuilder
|
|
10
|
+
extend T::Sig
|
|
11
|
+
|
|
9
12
|
VALID_OPTIONS = {
|
|
10
13
|
multiple_of: { type: T.any(Integer, Float), key: :multipleOf },
|
|
11
14
|
minimum: { type: T.any(Integer, Float), key: :minimum },
|
|
@@ -18,7 +21,7 @@ module EasyTalk
|
|
|
18
21
|
}.freeze
|
|
19
22
|
|
|
20
23
|
# Initializes a new instance of the NumberBuilder class.
|
|
21
|
-
sig { params(name: Symbol, constraints: Hash).void }
|
|
24
|
+
sig { params(name: Symbol, constraints: T::Hash[Symbol, T.untyped]).void }
|
|
22
25
|
def initialize(name, constraints = {})
|
|
23
26
|
super(name, { type: 'number' }, constraints, VALID_OPTIONS)
|
|
24
27
|
end
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
|
+
# typed: true
|
|
2
3
|
|
|
3
4
|
require_relative 'base_builder'
|
|
4
5
|
require_relative '../model_helper'
|
|
@@ -20,7 +21,12 @@ module EasyTalk
|
|
|
20
21
|
# Required by BaseBuilder: recognized schema options for "object" types
|
|
21
22
|
VALID_OPTIONS = {
|
|
22
23
|
properties: { type: T::Hash[T.any(Symbol, String), T.untyped], key: :properties },
|
|
23
|
-
additional_properties: { type: T::Boolean, key: :additionalProperties },
|
|
24
|
+
additional_properties: { type: T.any(T::Boolean, Class, T::Hash[Symbol, T.untyped]), key: :additionalProperties },
|
|
25
|
+
pattern_properties: { type: T::Hash[String, T.untyped], key: :patternProperties },
|
|
26
|
+
min_properties: { type: Integer, key: :minProperties },
|
|
27
|
+
max_properties: { type: Integer, key: :maxProperties },
|
|
28
|
+
dependencies: { type: T::Hash[String, T.any(T::Array[String], T::Hash[String, T.untyped])], key: :dependencies },
|
|
29
|
+
dependent_required: { type: T::Hash[String, T::Array[String]], key: :dependentRequired },
|
|
24
30
|
subschemas: { type: T::Array[T.untyped], key: :subschemas },
|
|
25
31
|
required: { type: T::Array[T.any(Symbol, String)], key: :required },
|
|
26
32
|
defs: { type: T.untyped, key: :$defs },
|
|
@@ -55,6 +61,14 @@ module EasyTalk
|
|
|
55
61
|
)
|
|
56
62
|
end
|
|
57
63
|
|
|
64
|
+
# Override build to add additionalProperties after BaseBuilder validation
|
|
65
|
+
sig { override.returns(T::Hash[Symbol, T.untyped]) }
|
|
66
|
+
def build
|
|
67
|
+
result = super
|
|
68
|
+
process_additional_properties(result)
|
|
69
|
+
result
|
|
70
|
+
end
|
|
71
|
+
|
|
58
72
|
private
|
|
59
73
|
|
|
60
74
|
##
|
|
@@ -99,8 +113,9 @@ module EasyTalk
|
|
|
99
113
|
# Populate the final "required" array from @required_properties
|
|
100
114
|
merged[:required] = @required_properties.to_a if @required_properties.any?
|
|
101
115
|
|
|
102
|
-
#
|
|
103
|
-
|
|
116
|
+
# Process additionalProperties separately (don't let BaseBuilder validate it)
|
|
117
|
+
# Extract the value, process it, and we'll add it back after BaseBuilder runs
|
|
118
|
+
@additional_properties_value = merged.delete(:additional_properties)
|
|
104
119
|
|
|
105
120
|
# Prune empty or nil values so we don't produce stuff like "properties": {} unnecessarily
|
|
106
121
|
merged.reject! { |_k, v| v.nil? || v == {} || v == [] }
|
|
@@ -108,6 +123,52 @@ module EasyTalk
|
|
|
108
123
|
merged
|
|
109
124
|
end
|
|
110
125
|
|
|
126
|
+
##
|
|
127
|
+
# Process additionalProperties to handle schema objects.
|
|
128
|
+
# Converts type classes or constraint hashes into proper JSON Schema.
|
|
129
|
+
# Called from build() method with the final schema hash.
|
|
130
|
+
#
|
|
131
|
+
def process_additional_properties(schema_hash)
|
|
132
|
+
value = @additional_properties_value
|
|
133
|
+
|
|
134
|
+
# If not set, use config default
|
|
135
|
+
if value.nil?
|
|
136
|
+
schema_hash[:additionalProperties] = EasyTalk.configuration.default_additional_properties
|
|
137
|
+
return
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
# Boolean: pass through as-is
|
|
141
|
+
if value.is_a?(TrueClass) || value.is_a?(FalseClass)
|
|
142
|
+
schema_hash[:additionalProperties] = value
|
|
143
|
+
return
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
# Class type: build schema
|
|
147
|
+
if value.is_a?(Class)
|
|
148
|
+
schema_hash[:additionalProperties] = build_additional_properties_schema(value, {})
|
|
149
|
+
return
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
# Hash with type + constraints: build schema with constraints
|
|
153
|
+
return unless value.is_a?(Hash)
|
|
154
|
+
|
|
155
|
+
type = value[:type] || value['type']
|
|
156
|
+
constraints = value.except(:type, 'type')
|
|
157
|
+
schema_hash[:additionalProperties] = build_additional_properties_schema(type, constraints)
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
##
|
|
161
|
+
# Builds a JSON Schema for additionalProperties from a type and constraints.
|
|
162
|
+
# Uses the Property builder to generate the schema.
|
|
163
|
+
#
|
|
164
|
+
def build_additional_properties_schema(type, constraints)
|
|
165
|
+
return {} unless type
|
|
166
|
+
|
|
167
|
+
# Use Property builder to generate schema for the type
|
|
168
|
+
property = EasyTalk::Property.new(:_additional, type, constraints)
|
|
169
|
+
property.as_json
|
|
170
|
+
end
|
|
171
|
+
|
|
111
172
|
##
|
|
112
173
|
# Given the property definitions hash, produce a new hash of
|
|
113
174
|
# { property_name => [Property or nested schema builder result] }.
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
|
+
# typed: true
|
|
2
3
|
|
|
3
4
|
module EasyTalk
|
|
4
5
|
module Builders
|
|
@@ -22,9 +23,12 @@ module EasyTalk
|
|
|
22
23
|
#
|
|
23
24
|
class Registry
|
|
24
25
|
class << self
|
|
26
|
+
extend T::Sig
|
|
27
|
+
|
|
25
28
|
# Get the hash of registered type builders.
|
|
26
29
|
#
|
|
27
30
|
# @return [Hash{String => Hash}] The registered builders with metadata
|
|
31
|
+
sig { returns(T::Hash[String, T::Hash[Symbol, T.untyped]]) }
|
|
28
32
|
def registry
|
|
29
33
|
@registry ||= {}
|
|
30
34
|
end
|
|
@@ -43,6 +47,7 @@ module EasyTalk
|
|
|
43
47
|
#
|
|
44
48
|
# @example Register a collection type
|
|
45
49
|
# Registry.register(CustomArray, CustomArrayBuilder, collection: true)
|
|
50
|
+
sig { params(type_key: T.any(T::Class[T.anything], String, Symbol), builder_class: T.untyped, collection: T::Boolean).void }
|
|
46
51
|
def register(type_key, builder_class, collection: false)
|
|
47
52
|
raise ArgumentError, 'Builder must respond to .new' unless builder_class.respond_to?(:new)
|
|
48
53
|
|
|
@@ -63,6 +68,7 @@ module EasyTalk
|
|
|
63
68
|
# @example
|
|
64
69
|
# builder_class, is_collection = Registry.resolve(String)
|
|
65
70
|
# # => [StringBuilder, false]
|
|
71
|
+
sig { params(type: T.untyped).returns(T.nilable(T::Array[T.untyped])) }
|
|
66
72
|
def resolve(type)
|
|
67
73
|
entry = find_registration(type)
|
|
68
74
|
return nil unless entry
|
|
@@ -74,6 +80,7 @@ module EasyTalk
|
|
|
74
80
|
#
|
|
75
81
|
# @param type_key [Class, String, Symbol] The type to check
|
|
76
82
|
# @return [Boolean] true if the type is registered
|
|
83
|
+
sig { params(type_key: T.any(T::Class[T.anything], String, Symbol)).returns(T::Boolean) }
|
|
77
84
|
def registered?(type_key)
|
|
78
85
|
registry.key?(normalize_key(type_key))
|
|
79
86
|
end
|
|
@@ -82,6 +89,7 @@ module EasyTalk
|
|
|
82
89
|
#
|
|
83
90
|
# @param type_key [Class, String, Symbol] The type to unregister
|
|
84
91
|
# @return [Hash, nil] The removed registration or nil if not found
|
|
92
|
+
sig { params(type_key: T.any(T::Class[T.anything], String, Symbol)).returns(T.nilable(T::Hash[Symbol, T.untyped])) }
|
|
85
93
|
def unregister(type_key)
|
|
86
94
|
registry.delete(normalize_key(type_key))
|
|
87
95
|
end
|
|
@@ -89,6 +97,7 @@ module EasyTalk
|
|
|
89
97
|
# Get a list of all registered type keys.
|
|
90
98
|
#
|
|
91
99
|
# @return [Array<String>] The registered type keys
|
|
100
|
+
sig { returns(T::Array[String]) }
|
|
92
101
|
def registered_types
|
|
93
102
|
registry.keys
|
|
94
103
|
end
|
|
@@ -96,6 +105,7 @@ module EasyTalk
|
|
|
96
105
|
# Reset the registry to empty state and re-register built-in types.
|
|
97
106
|
#
|
|
98
107
|
# @return [void]
|
|
108
|
+
sig { void }
|
|
99
109
|
def reset!
|
|
100
110
|
@registry = nil
|
|
101
111
|
register_built_in_types
|
|
@@ -105,6 +115,7 @@ module EasyTalk
|
|
|
105
115
|
# This is called during gem initialization and after reset!
|
|
106
116
|
#
|
|
107
117
|
# @return [void]
|
|
118
|
+
sig { void }
|
|
108
119
|
def register_built_in_types
|
|
109
120
|
register(String, Builders::StringBuilder)
|
|
110
121
|
register(Integer, Builders::IntegerBuilder)
|
|
@@ -116,9 +127,10 @@ module EasyTalk
|
|
|
116
127
|
register(Date, Builders::TemporalBuilder::DateBuilder)
|
|
117
128
|
register(DateTime, Builders::TemporalBuilder::DatetimeBuilder)
|
|
118
129
|
register(Time, Builders::TemporalBuilder::TimeBuilder)
|
|
119
|
-
register('anyOf', Builders::CompositionBuilder::AnyOfBuilder, collection: true)
|
|
120
130
|
register('allOf', Builders::CompositionBuilder::AllOfBuilder, collection: true)
|
|
131
|
+
register('anyOf', Builders::CompositionBuilder::AnyOfBuilder, collection: true)
|
|
121
132
|
register('oneOf', Builders::CompositionBuilder::OneOfBuilder, collection: true)
|
|
133
|
+
register('EasyTalk::Types::Tuple', Builders::TupleBuilder, collection: true)
|
|
122
134
|
register('T::Types::TypedArray', Builders::TypedArrayBuilder, collection: true)
|
|
123
135
|
register('T::Types::Union', Builders::UnionBuilder, collection: true)
|
|
124
136
|
end
|
|
@@ -129,6 +141,7 @@ module EasyTalk
|
|
|
129
141
|
#
|
|
130
142
|
# @param type_key [Class, String, Symbol] The type key to normalize
|
|
131
143
|
# @return [String] The normalized key
|
|
144
|
+
sig { params(type_key: T.any(T::Class[T.anything], String, Symbol)).returns(String) }
|
|
132
145
|
def normalize_key(type_key)
|
|
133
146
|
case type_key
|
|
134
147
|
when Class
|
|
@@ -146,6 +159,7 @@ module EasyTalk
|
|
|
146
159
|
#
|
|
147
160
|
# @param type [Object] The type to find
|
|
148
161
|
# @return [Hash, nil] The registration entry or nil
|
|
162
|
+
sig { params(type: T.untyped).returns(T.nilable(T::Hash[Symbol, T.untyped])) }
|
|
149
163
|
def find_registration(type)
|
|
150
164
|
# Strategy 1: Check type.class.name (for Sorbet types like T::Types::TypedArray)
|
|
151
165
|
class_name = type.class.name.to_s
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
|
+
# typed: true
|
|
2
3
|
|
|
3
4
|
require_relative 'base_builder'
|
|
4
5
|
require 'js_regex' # Compile the ruby regex to JS regex
|
|
@@ -20,11 +21,12 @@ module EasyTalk
|
|
|
20
21
|
default: { type: String, key: :default }
|
|
21
22
|
}.freeze
|
|
22
23
|
|
|
23
|
-
sig { params(name: Symbol, constraints: Hash).void }
|
|
24
|
+
sig { params(name: Symbol, constraints: T::Hash[Symbol, T.untyped]).void }
|
|
24
25
|
def initialize(name, constraints = {})
|
|
25
26
|
super(name, { type: 'string' }, constraints, VALID_OPTIONS)
|
|
26
27
|
end
|
|
27
28
|
|
|
29
|
+
sig { returns(T::Hash[Symbol, T.untyped]) }
|
|
28
30
|
def build
|
|
29
31
|
super.tap do |schema|
|
|
30
32
|
pattern = schema[:pattern]
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
|
+
# typed: true
|
|
2
3
|
|
|
3
4
|
require_relative 'string_builder'
|
|
4
5
|
|
|
@@ -6,11 +7,14 @@ module EasyTalk
|
|
|
6
7
|
module Builders
|
|
7
8
|
# Builder class for temporal properties (date, datetime, time).
|
|
8
9
|
class TemporalBuilder < StringBuilder
|
|
10
|
+
extend T::Sig
|
|
11
|
+
|
|
9
12
|
# Initializes a new instance of the TemporalBuilder class.
|
|
10
13
|
#
|
|
11
14
|
# @param property_name [Symbol] The name of the property.
|
|
12
15
|
# @param options [Hash] The options for the builder.
|
|
13
16
|
# @param format [String] The format of the temporal property (date, date-time, time).
|
|
17
|
+
sig { params(property_name: Symbol, options: T::Hash[Symbol, T.untyped], format: T.nilable(String)).void }
|
|
14
18
|
def initialize(property_name, options = {}, format = nil)
|
|
15
19
|
super(property_name, options)
|
|
16
20
|
@format = format
|
|
@@ -26,6 +30,7 @@ module EasyTalk
|
|
|
26
30
|
|
|
27
31
|
# Builder class for date properties.
|
|
28
32
|
class DateBuilder < TemporalBuilder
|
|
33
|
+
sig { params(property_name: Symbol, options: T::Hash[Symbol, T.untyped]).void }
|
|
29
34
|
def initialize(property_name, options = {})
|
|
30
35
|
super(property_name, options, 'date')
|
|
31
36
|
end
|
|
@@ -33,6 +38,7 @@ module EasyTalk
|
|
|
33
38
|
|
|
34
39
|
# Builder class for datetime properties.
|
|
35
40
|
class DatetimeBuilder < TemporalBuilder
|
|
41
|
+
sig { params(property_name: Symbol, options: T::Hash[Symbol, T.untyped]).void }
|
|
36
42
|
def initialize(property_name, options = {})
|
|
37
43
|
super(property_name, options, 'date-time')
|
|
38
44
|
end
|
|
@@ -40,6 +46,7 @@ module EasyTalk
|
|
|
40
46
|
|
|
41
47
|
# Builder class for time properties.
|
|
42
48
|
class TimeBuilder < TemporalBuilder
|
|
49
|
+
sig { params(property_name: Symbol, options: T::Hash[Symbol, T.untyped]).void }
|
|
43
50
|
def initialize(property_name, options = {})
|
|
44
51
|
super(property_name, options, 'time')
|
|
45
52
|
end
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
# typed: true
|
|
3
|
+
|
|
4
|
+
module EasyTalk
|
|
5
|
+
module Builders
|
|
6
|
+
# Builder class for tuple array properties (T::Tuple[Type1, Type2, ...]).
|
|
7
|
+
#
|
|
8
|
+
# Tuples are arrays with positional type validation where each index
|
|
9
|
+
# has a specific expected type.
|
|
10
|
+
#
|
|
11
|
+
# @example Basic tuple
|
|
12
|
+
# property :coordinates, T::Tuple[Float, Float]
|
|
13
|
+
#
|
|
14
|
+
# @example Tuple with additional items constraint
|
|
15
|
+
# property :record, T::Tuple[String, Integer], additional_items: false
|
|
16
|
+
#
|
|
17
|
+
class TupleBuilder < BaseBuilder
|
|
18
|
+
extend T::Sig
|
|
19
|
+
|
|
20
|
+
# NOTE: additional_items is handled separately in build() since it can be a type
|
|
21
|
+
VALID_OPTIONS = {
|
|
22
|
+
min_items: { type: Integer, key: :minItems },
|
|
23
|
+
max_items: { type: Integer, key: :maxItems },
|
|
24
|
+
unique_items: { type: T::Boolean, key: :uniqueItems }
|
|
25
|
+
}.freeze
|
|
26
|
+
|
|
27
|
+
attr_reader :type
|
|
28
|
+
|
|
29
|
+
sig { params(name: Symbol, type: Types::Tuple, constraints: T::Hash[Symbol, T.untyped]).void }
|
|
30
|
+
def initialize(name, type, constraints = {})
|
|
31
|
+
@name = name
|
|
32
|
+
@type = type
|
|
33
|
+
# Work on a copy to avoid mutating the original constraints hash
|
|
34
|
+
local_constraints = constraints.dup
|
|
35
|
+
# Extract additional_items before passing to super (it's handled separately in build)
|
|
36
|
+
@additional_items_constraint = local_constraints.delete(:additional_items)
|
|
37
|
+
super(name, { type: 'array' }, local_constraints, VALID_OPTIONS)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
sig { returns(T::Boolean) }
|
|
41
|
+
def self.collection_type?
|
|
42
|
+
true
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Builds the tuple schema with positional items.
|
|
46
|
+
#
|
|
47
|
+
# @return [Hash] The built schema.
|
|
48
|
+
sig { returns(T::Hash[Symbol, T.untyped]) }
|
|
49
|
+
def build
|
|
50
|
+
schema = super
|
|
51
|
+
|
|
52
|
+
# Build items array from tuple types
|
|
53
|
+
schema[:items] = build_items
|
|
54
|
+
|
|
55
|
+
# Handle additional_items constraint
|
|
56
|
+
schema[:additionalItems] = build_additional_items_schema(@additional_items_constraint) unless @additional_items_constraint.nil?
|
|
57
|
+
|
|
58
|
+
schema
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
private
|
|
62
|
+
|
|
63
|
+
# Builds the items array from tuple types.
|
|
64
|
+
#
|
|
65
|
+
# @return [Array<Hash>] Array of schemas for each position
|
|
66
|
+
sig { returns(T::Array[T::Hash[Symbol, T.untyped]]) }
|
|
67
|
+
def build_items
|
|
68
|
+
type.types.map.with_index do |item_type, index|
|
|
69
|
+
Property.new(:"#{@name}_item_#{index}", item_type, {}).build
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# Builds the additionalItems schema value.
|
|
74
|
+
#
|
|
75
|
+
# @param value [Boolean, Class] The additional_items constraint
|
|
76
|
+
# @return [Boolean, Hash] The schema value for additionalItems
|
|
77
|
+
sig { params(value: T.untyped).returns(T.any(T::Boolean, T::Hash[Symbol, T.untyped])) }
|
|
78
|
+
def build_additional_items_schema(value)
|
|
79
|
+
case value
|
|
80
|
+
when true, false
|
|
81
|
+
value
|
|
82
|
+
else
|
|
83
|
+
# It's a type - build a schema for it
|
|
84
|
+
Property.new(:"#{@name}_additional", value, {}).build
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
|
+
# typed: true
|
|
2
3
|
|
|
3
4
|
require_relative 'collection_helpers'
|
|
4
5
|
|
|
5
6
|
module EasyTalk
|
|
6
7
|
module Builders
|
|
7
|
-
# Builder class for array properties.
|
|
8
|
+
# Builder class for homogeneous array properties (T::Array[Type]).
|
|
8
9
|
class TypedArrayBuilder < BaseBuilder
|
|
9
10
|
extend CollectionHelpers
|
|
10
11
|
extend T::Sig
|
|
@@ -20,7 +21,7 @@ module EasyTalk
|
|
|
20
21
|
|
|
21
22
|
attr_reader :type
|
|
22
23
|
|
|
23
|
-
sig { params(name: Symbol, type: T.untyped, constraints: Hash).void }
|
|
24
|
+
sig { params(name: Symbol, type: T.untyped, constraints: T::Hash[Symbol, T.untyped]).void }
|
|
24
25
|
def initialize(name, type, constraints = {})
|
|
25
26
|
@name = name
|
|
26
27
|
@type = type
|
|
@@ -49,6 +50,7 @@ module EasyTalk
|
|
|
49
50
|
end
|
|
50
51
|
end
|
|
51
52
|
|
|
53
|
+
sig { returns(T.untyped) }
|
|
52
54
|
def inner_type
|
|
53
55
|
return unless type.is_a?(T::Types::TypedArray)
|
|
54
56
|
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
|
+
# typed: true
|
|
2
3
|
|
|
3
4
|
require_relative 'collection_helpers'
|
|
4
5
|
|
|
@@ -9,7 +10,7 @@ module EasyTalk
|
|
|
9
10
|
extend CollectionHelpers
|
|
10
11
|
extend T::Sig
|
|
11
12
|
|
|
12
|
-
sig { params(name: Symbol, type: T.untyped, constraints: T.untyped).void }
|
|
13
|
+
sig { params(name: Symbol, type: T.untyped, constraints: T::Hash[Symbol, T.untyped]).void }
|
|
13
14
|
def initialize(name, type, constraints)
|
|
14
15
|
@name = name
|
|
15
16
|
@type = type
|
|
@@ -17,18 +18,21 @@ module EasyTalk
|
|
|
17
18
|
@context = {}
|
|
18
19
|
end
|
|
19
20
|
|
|
21
|
+
sig { returns(T::Hash[Symbol, T.untyped]) }
|
|
20
22
|
def build
|
|
21
23
|
@context[@name] = {
|
|
22
24
|
'anyOf' => schemas
|
|
23
25
|
}
|
|
24
26
|
end
|
|
25
27
|
|
|
28
|
+
sig { returns(T::Array[T.untyped]) }
|
|
26
29
|
def schemas
|
|
27
30
|
types.map do |type|
|
|
28
31
|
Property.new(@name, type, @constraints).build
|
|
29
32
|
end
|
|
30
33
|
end
|
|
31
34
|
|
|
35
|
+
sig { returns(T.untyped) }
|
|
32
36
|
def types
|
|
33
37
|
@type.types
|
|
34
38
|
end
|
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
|
+
# typed: true
|
|
2
3
|
|
|
3
4
|
require_relative 'naming_strategies'
|
|
4
5
|
|
|
5
6
|
module EasyTalk
|
|
6
7
|
class Configuration
|
|
8
|
+
extend T::Sig
|
|
9
|
+
|
|
7
10
|
# JSON Schema draft version URIs
|
|
8
11
|
SCHEMA_VERSIONS = {
|
|
9
12
|
draft202012: 'https://json-schema.org/draft/2020-12/schema',
|
|
@@ -14,9 +17,11 @@ module EasyTalk
|
|
|
14
17
|
}.freeze
|
|
15
18
|
|
|
16
19
|
attr_accessor :default_additional_properties, :nilable_is_optional, :auto_validations, :schema_version, :schema_id,
|
|
17
|
-
:use_refs, :validation_adapter, :default_error_format, :error_type_base_uri, :include_error_codes
|
|
20
|
+
:use_refs, :validation_adapter, :default_error_format, :error_type_base_uri, :include_error_codes,
|
|
21
|
+
:base_schema_uri, :auto_generate_ids, :prefer_external_refs
|
|
18
22
|
attr_reader :property_naming_strategy
|
|
19
23
|
|
|
24
|
+
sig { void }
|
|
20
25
|
def initialize
|
|
21
26
|
@default_additional_properties = false
|
|
22
27
|
@nilable_is_optional = false
|
|
@@ -28,16 +33,21 @@ module EasyTalk
|
|
|
28
33
|
@default_error_format = :flat
|
|
29
34
|
@error_type_base_uri = 'about:blank'
|
|
30
35
|
@include_error_codes = true
|
|
36
|
+
@base_schema_uri = nil
|
|
37
|
+
@auto_generate_ids = false
|
|
38
|
+
@prefer_external_refs = false
|
|
31
39
|
self.property_naming_strategy = :identity
|
|
32
40
|
end
|
|
33
41
|
|
|
34
42
|
# Returns the URI for the configured schema version, or nil if :none
|
|
43
|
+
sig { returns(T.nilable(String)) }
|
|
35
44
|
def schema_uri
|
|
36
45
|
return nil if @schema_version == :none
|
|
37
46
|
|
|
38
47
|
SCHEMA_VERSIONS[@schema_version] || @schema_version.to_s
|
|
39
48
|
end
|
|
40
49
|
|
|
50
|
+
sig { params(strategy: T.any(Symbol, T.proc.params(arg0: T.untyped).returns(Symbol))).void }
|
|
41
51
|
def property_naming_strategy=(strategy)
|
|
42
52
|
@property_naming_strategy = EasyTalk::NamingStrategies.derive_strategy(strategy)
|
|
43
53
|
end
|
|
@@ -56,17 +66,22 @@ module EasyTalk
|
|
|
56
66
|
# EasyTalk.configure do |config|
|
|
57
67
|
# config.register_type Money, MoneySchemaBuilder
|
|
58
68
|
# end
|
|
69
|
+
sig { params(type_key: T.any(T::Class[T.anything], String, Symbol), builder_class: T.untyped, collection: T::Boolean).void }
|
|
59
70
|
def register_type(type_key, builder_class, collection: false)
|
|
60
71
|
EasyTalk::Builders::Registry.register(type_key, builder_class, collection: collection)
|
|
61
72
|
end
|
|
62
73
|
end
|
|
63
74
|
|
|
64
75
|
class << self
|
|
76
|
+
extend T::Sig
|
|
77
|
+
|
|
78
|
+
sig { returns(Configuration) }
|
|
65
79
|
def configuration
|
|
66
80
|
@configuration ||= Configuration.new
|
|
67
81
|
end
|
|
68
82
|
|
|
69
|
-
|
|
83
|
+
sig { params(block: T.proc.params(config: Configuration).void).void }
|
|
84
|
+
def configure(&block)
|
|
70
85
|
yield(configuration)
|
|
71
86
|
end
|
|
72
87
|
end
|
data/lib/easy_talk/errors.rb
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
|
+
# typed: true
|
|
2
3
|
|
|
3
4
|
module EasyTalk
|
|
4
5
|
# Helper module for generating consistent error messages
|
|
5
6
|
module ErrorHelper
|
|
7
|
+
extend T::Sig
|
|
8
|
+
|
|
6
9
|
def self.raise_constraint_error(property_name:, constraint_name:, expected:, got:)
|
|
7
10
|
message = "Error in property '#{property_name}': Constraint '#{constraint_name}' expects #{expected}, " \
|
|
8
11
|
"but received #{got.inspect} (#{got.class})."
|