easy_talk 3.1.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 -39
- data/.yardopts +13 -0
- data/CHANGELOG.md +164 -0
- data/README.md +442 -1529
- 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 +169 -0
- data/docs/nested-models.markdown +216 -0
- data/docs/primitive-schema-rfc.md +894 -0
- data/docs/property-types.markdown +212 -0
- data/docs/schema-definition.markdown +180 -0
- data/lib/easy_talk/builders/base_builder.rb +6 -3
- 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 +16 -13
- 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 +109 -33
- data/lib/easy_talk/builders/registry.rb +182 -0
- 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 +19 -6
- data/lib/easy_talk/builders/union_builder.rb +5 -1
- data/lib/easy_talk/configuration.rb +47 -2
- 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 +3 -0
- data/lib/easy_talk/errors_helper.rb +66 -34
- data/lib/easy_talk/json_schema_equality.rb +46 -0
- data/lib/easy_talk/keywords.rb +0 -1
- data/lib/easy_talk/model.rb +148 -89
- data/lib/easy_talk/model_helper.rb +17 -0
- data/lib/easy_talk/naming_strategies.rb +24 -0
- data/lib/easy_talk/property.rb +23 -94
- data/lib/easy_talk/ref_helper.rb +33 -0
- data/lib/easy_talk/schema.rb +199 -0
- data/lib/easy_talk/schema_definition.rb +57 -5
- data/lib/easy_talk/schema_methods.rb +111 -0
- data/lib/easy_talk/sorbet_extension.rb +1 -0
- data/lib/easy_talk/tools/function_builder.rb +1 -1
- data/lib/easy_talk/type_introspection.rb +222 -0
- data/lib/easy_talk/types/base_composer.rb +2 -1
- data/lib/easy_talk/types/composer.rb +4 -0
- data/lib/easy_talk/types/tuple.rb +77 -0
- data/lib/easy_talk/validation_adapters/active_model_adapter.rb +617 -0
- data/lib/easy_talk/validation_adapters/active_model_schema_validation.rb +106 -0
- data/lib/easy_talk/validation_adapters/base.rb +156 -0
- data/lib/easy_talk/validation_adapters/none_adapter.rb +45 -0
- data/lib/easy_talk/validation_adapters/registry.rb +87 -0
- data/lib/easy_talk/validation_builder.rb +29 -309
- data/lib/easy_talk/version.rb +1 -1
- data/lib/easy_talk.rb +42 -0
- metadata +38 -7
- data/docs/404.html +0 -25
- data/docs/_posts/2024-05-07-welcome-to-jekyll.markdown +0 -29
- data/easy_talk.gemspec +0 -39
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
|
+
# typed: true
|
|
2
3
|
|
|
3
4
|
require_relative 'base_builder'
|
|
5
|
+
require_relative '../model_helper'
|
|
4
6
|
|
|
5
7
|
module EasyTalk
|
|
6
8
|
module Builders
|
|
@@ -19,7 +21,12 @@ module EasyTalk
|
|
|
19
21
|
# Required by BaseBuilder: recognized schema options for "object" types
|
|
20
22
|
VALID_OPTIONS = {
|
|
21
23
|
properties: { type: T::Hash[T.any(Symbol, String), T.untyped], key: :properties },
|
|
22
|
-
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 },
|
|
23
30
|
subschemas: { type: T::Array[T.untyped], key: :subschemas },
|
|
24
31
|
required: { type: T::Array[T.any(Symbol, String)], key: :required },
|
|
25
32
|
defs: { type: T.untyped, key: :$defs },
|
|
@@ -33,8 +40,8 @@ module EasyTalk
|
|
|
33
40
|
def initialize(schema_definition)
|
|
34
41
|
# Keep a reference to the original schema definition
|
|
35
42
|
@schema_definition = schema_definition
|
|
36
|
-
#
|
|
37
|
-
@original_schema = schema_definition.schema
|
|
43
|
+
# Deep duplicate the raw schema hash so we can mutate it safely
|
|
44
|
+
@original_schema = deep_dup(schema_definition.schema)
|
|
38
45
|
|
|
39
46
|
# We'll collect required property names in this Set
|
|
40
47
|
@required_properties = Set.new
|
|
@@ -54,8 +61,34 @@ module EasyTalk
|
|
|
54
61
|
)
|
|
55
62
|
end
|
|
56
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
|
+
|
|
57
72
|
private
|
|
58
73
|
|
|
74
|
+
##
|
|
75
|
+
# Deep duplicates a hash, including nested hashes.
|
|
76
|
+
# This prevents mutations from leaking back to the original schema.
|
|
77
|
+
#
|
|
78
|
+
def deep_dup(obj)
|
|
79
|
+
case obj
|
|
80
|
+
when Hash
|
|
81
|
+
obj.transform_values { |v| deep_dup(v) }
|
|
82
|
+
when Array
|
|
83
|
+
obj.map { |v| deep_dup(v) }
|
|
84
|
+
when Class, Module
|
|
85
|
+
# Don't duplicate Class or Module objects - they represent types
|
|
86
|
+
obj
|
|
87
|
+
else
|
|
88
|
+
obj.duplicable? ? obj.dup : obj
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
|
|
59
92
|
##
|
|
60
93
|
# Main aggregator: merges the top-level schema keys (like :properties, :subschemas)
|
|
61
94
|
# into a single hash that we'll feed to BaseBuilder.
|
|
@@ -80,8 +113,9 @@ module EasyTalk
|
|
|
80
113
|
# Populate the final "required" array from @required_properties
|
|
81
114
|
merged[:required] = @required_properties.to_a if @required_properties.any?
|
|
82
115
|
|
|
83
|
-
#
|
|
84
|
-
|
|
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)
|
|
85
119
|
|
|
86
120
|
# Prune empty or nil values so we don't produce stuff like "properties": {} unnecessarily
|
|
87
121
|
merged.reject! { |_k, v| v.nil? || v == {} || v == [] }
|
|
@@ -89,6 +123,52 @@ module EasyTalk
|
|
|
89
123
|
merged
|
|
90
124
|
end
|
|
91
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
|
+
|
|
92
172
|
##
|
|
93
173
|
# Given the property definitions hash, produce a new hash of
|
|
94
174
|
# { property_name => [Property or nested schema builder result] }.
|
|
@@ -99,16 +179,18 @@ module EasyTalk
|
|
|
99
179
|
# Cache with a key based on property name and its full configuration
|
|
100
180
|
@properties_cache ||= {}
|
|
101
181
|
|
|
102
|
-
properties_hash.each_with_object({}) do |(
|
|
103
|
-
|
|
182
|
+
properties_hash.each_with_object({}) do |(original_name, prop_options), result|
|
|
183
|
+
# Use :as constraint for property name without mutating original constraints
|
|
184
|
+
property_name = (prop_options[:constraints][:as] || original_name).to_sym
|
|
185
|
+
cache_key = [property_name, prop_options].hash
|
|
104
186
|
|
|
105
187
|
# Use cache if the exact property and configuration have been processed before
|
|
106
188
|
@properties_cache[cache_key] ||= begin
|
|
107
|
-
mark_required_unless_optional(
|
|
108
|
-
build_property(
|
|
189
|
+
mark_required_unless_optional(property_name, prop_options)
|
|
190
|
+
build_property(property_name, prop_options)
|
|
109
191
|
end
|
|
110
192
|
|
|
111
|
-
result[
|
|
193
|
+
result[property_name] = @properties_cache[cache_key]
|
|
112
194
|
end
|
|
113
195
|
end
|
|
114
196
|
|
|
@@ -145,8 +227,8 @@ module EasyTalk
|
|
|
145
227
|
|
|
146
228
|
# Memoize so we only build each property once
|
|
147
229
|
@property_cache[prop_name] ||= begin
|
|
148
|
-
# Remove
|
|
149
|
-
constraints = prop_options[:constraints].except(:optional)
|
|
230
|
+
# Remove internal constraints that shouldn't be passed to Property
|
|
231
|
+
constraints = prop_options[:constraints].except(:optional, :as)
|
|
150
232
|
prop_type = prop_options[:type]
|
|
151
233
|
|
|
152
234
|
# Track models that will use $ref for later $defs generation
|
|
@@ -165,10 +247,11 @@ module EasyTalk
|
|
|
165
247
|
# Check if this type should use $ref
|
|
166
248
|
if should_collect_ref?(prop_type, constraints)
|
|
167
249
|
@ref_models.add(prop_type)
|
|
250
|
+
elsif prop_type.is_a?(EasyTalk::Types::Composer)
|
|
251
|
+
collect_ref_models(prop_type.items, constraints)
|
|
168
252
|
# Handle typed arrays with EasyTalk model items
|
|
169
|
-
elsif
|
|
170
|
-
inner_type
|
|
171
|
-
@ref_models.add(inner_type) if should_collect_ref?(inner_type, constraints)
|
|
253
|
+
elsif typed_array?(prop_type)
|
|
254
|
+
extract_inner_types(prop_type).each { |inner_type| collect_ref_models(inner_type, constraints) }
|
|
172
255
|
# Handle nilable types
|
|
173
256
|
elsif nilable_with_model?(prop_type)
|
|
174
257
|
actual_type = T::Utils::Nilable.get_underlying_type(prop_type)
|
|
@@ -180,7 +263,7 @@ module EasyTalk
|
|
|
180
263
|
# Determines if a type should be collected for $ref based on config and constraints.
|
|
181
264
|
#
|
|
182
265
|
def should_collect_ref?(check_type, constraints)
|
|
183
|
-
return false unless easytalk_model?(check_type)
|
|
266
|
+
return false unless ModelHelper.easytalk_model?(check_type)
|
|
184
267
|
|
|
185
268
|
# Per-property constraint takes precedence
|
|
186
269
|
return constraints[:ref] if constraints.key?(:ref)
|
|
@@ -189,25 +272,18 @@ module EasyTalk
|
|
|
189
272
|
EasyTalk.configuration.use_refs
|
|
190
273
|
end
|
|
191
274
|
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
#
|
|
195
|
-
def easytalk_model?(check_type)
|
|
196
|
-
check_type.is_a?(Class) &&
|
|
197
|
-
check_type.respond_to?(:schema) &&
|
|
198
|
-
check_type.respond_to?(:ref_template) &&
|
|
199
|
-
defined?(EasyTalk::Model) &&
|
|
200
|
-
check_type.include?(EasyTalk::Model)
|
|
275
|
+
def typed_array?(prop_type)
|
|
276
|
+
prop_type.is_a?(T::Types::TypedArray)
|
|
201
277
|
end
|
|
202
278
|
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
#
|
|
206
|
-
def typed_array_with_model?(prop_type)
|
|
207
|
-
return false unless prop_type.is_a?(T::Types::TypedArray)
|
|
279
|
+
def extract_inner_types(prop_type)
|
|
280
|
+
return [] unless typed_array?(prop_type)
|
|
208
281
|
|
|
209
|
-
|
|
210
|
-
|
|
282
|
+
if prop_type.type.is_a?(EasyTalk::Types::Composer)
|
|
283
|
+
prop_type.type.items
|
|
284
|
+
else
|
|
285
|
+
[prop_type.type.raw_type]
|
|
286
|
+
end
|
|
211
287
|
end
|
|
212
288
|
|
|
213
289
|
##
|
|
@@ -219,7 +295,7 @@ module EasyTalk
|
|
|
219
295
|
return false unless prop_type.types.any? { |t| t.raw_type == NilClass }
|
|
220
296
|
|
|
221
297
|
actual_type = T::Utils::Nilable.get_underlying_type(prop_type)
|
|
222
|
-
easytalk_model?(actual_type)
|
|
298
|
+
ModelHelper.easytalk_model?(actual_type)
|
|
223
299
|
end
|
|
224
300
|
|
|
225
301
|
##
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
# typed: true
|
|
3
|
+
|
|
4
|
+
module EasyTalk
|
|
5
|
+
module Builders
|
|
6
|
+
# Registry for type-to-builder mappings.
|
|
7
|
+
#
|
|
8
|
+
# The registry allows custom types to be registered with their corresponding
|
|
9
|
+
# schema builder classes at runtime, without modifying the gem's source code.
|
|
10
|
+
#
|
|
11
|
+
# Custom registrations take priority over built-in types, allowing users to
|
|
12
|
+
# override default behavior when needed.
|
|
13
|
+
#
|
|
14
|
+
# @example Registering a custom type
|
|
15
|
+
# EasyTalk::Builders::Registry.register(Money, MoneySchemaBuilder)
|
|
16
|
+
#
|
|
17
|
+
# @example Registering a collection type
|
|
18
|
+
# EasyTalk::Builders::Registry.register(CustomArray, CustomArrayBuilder, collection: true)
|
|
19
|
+
#
|
|
20
|
+
# @example Resolving a builder for a type
|
|
21
|
+
# builder_class, is_collection = EasyTalk::Builders::Registry.resolve(Money)
|
|
22
|
+
# builder_class.new(name, constraints).build
|
|
23
|
+
#
|
|
24
|
+
class Registry
|
|
25
|
+
class << self
|
|
26
|
+
extend T::Sig
|
|
27
|
+
|
|
28
|
+
# Get the hash of registered type builders.
|
|
29
|
+
#
|
|
30
|
+
# @return [Hash{String => Hash}] The registered builders with metadata
|
|
31
|
+
sig { returns(T::Hash[String, T::Hash[Symbol, T.untyped]]) }
|
|
32
|
+
def registry
|
|
33
|
+
@registry ||= {}
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Register a type with its corresponding builder class.
|
|
37
|
+
#
|
|
38
|
+
# @param type_key [Class, String, Symbol] The type identifier
|
|
39
|
+
# @param builder_class [Class] The builder class (must respond to .new)
|
|
40
|
+
# @param collection [Boolean] Whether this is a collection type builder
|
|
41
|
+
# Collection builders receive (name, type, constraints) instead of (name, constraints)
|
|
42
|
+
# @raise [ArgumentError] if the builder does not respond to .new
|
|
43
|
+
# @return [void]
|
|
44
|
+
#
|
|
45
|
+
# @example Register a simple type
|
|
46
|
+
# Registry.register(Money, MoneySchemaBuilder)
|
|
47
|
+
#
|
|
48
|
+
# @example Register a collection type
|
|
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 }
|
|
51
|
+
def register(type_key, builder_class, collection: false)
|
|
52
|
+
raise ArgumentError, 'Builder must respond to .new' unless builder_class.respond_to?(:new)
|
|
53
|
+
|
|
54
|
+
key = normalize_key(type_key)
|
|
55
|
+
registry[key] = { builder: builder_class, collection: collection }
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Resolve a builder for the given type.
|
|
59
|
+
#
|
|
60
|
+
# Resolution order:
|
|
61
|
+
# 1. Check type.class.name (e.g., "T::Types::TypedArray")
|
|
62
|
+
# 2. Check type.name if type responds to :name (e.g., "String")
|
|
63
|
+
# 3. Check type itself if it's a Class (e.g., String class)
|
|
64
|
+
#
|
|
65
|
+
# @param type [Object] The type to find a builder for
|
|
66
|
+
# @return [Array(Class, Boolean), nil] A tuple of [builder_class, is_collection] or nil if not found
|
|
67
|
+
#
|
|
68
|
+
# @example
|
|
69
|
+
# builder_class, is_collection = Registry.resolve(String)
|
|
70
|
+
# # => [StringBuilder, false]
|
|
71
|
+
sig { params(type: T.untyped).returns(T.nilable(T::Array[T.untyped])) }
|
|
72
|
+
def resolve(type)
|
|
73
|
+
entry = find_registration(type)
|
|
74
|
+
return nil unless entry
|
|
75
|
+
|
|
76
|
+
[entry[:builder], entry[:collection]]
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# Check if a type is registered.
|
|
80
|
+
#
|
|
81
|
+
# @param type_key [Class, String, Symbol] The type to check
|
|
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) }
|
|
84
|
+
def registered?(type_key)
|
|
85
|
+
registry.key?(normalize_key(type_key))
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# Unregister a type.
|
|
89
|
+
#
|
|
90
|
+
# @param type_key [Class, String, Symbol] The type to unregister
|
|
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])) }
|
|
93
|
+
def unregister(type_key)
|
|
94
|
+
registry.delete(normalize_key(type_key))
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# Get a list of all registered type keys.
|
|
98
|
+
#
|
|
99
|
+
# @return [Array<String>] The registered type keys
|
|
100
|
+
sig { returns(T::Array[String]) }
|
|
101
|
+
def registered_types
|
|
102
|
+
registry.keys
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# Reset the registry to empty state and re-register built-in types.
|
|
106
|
+
#
|
|
107
|
+
# @return [void]
|
|
108
|
+
sig { void }
|
|
109
|
+
def reset!
|
|
110
|
+
@registry = nil
|
|
111
|
+
register_built_in_types
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# Register all built-in type builders.
|
|
115
|
+
# This is called during gem initialization and after reset!
|
|
116
|
+
#
|
|
117
|
+
# @return [void]
|
|
118
|
+
sig { void }
|
|
119
|
+
def register_built_in_types
|
|
120
|
+
register(String, Builders::StringBuilder)
|
|
121
|
+
register(Integer, Builders::IntegerBuilder)
|
|
122
|
+
register(Float, Builders::NumberBuilder)
|
|
123
|
+
register(BigDecimal, Builders::NumberBuilder)
|
|
124
|
+
register('T::Boolean', Builders::BooleanBuilder)
|
|
125
|
+
register(TrueClass, Builders::BooleanBuilder)
|
|
126
|
+
register(NilClass, Builders::NullBuilder)
|
|
127
|
+
register(Date, Builders::TemporalBuilder::DateBuilder)
|
|
128
|
+
register(DateTime, Builders::TemporalBuilder::DatetimeBuilder)
|
|
129
|
+
register(Time, Builders::TemporalBuilder::TimeBuilder)
|
|
130
|
+
register('allOf', Builders::CompositionBuilder::AllOfBuilder, collection: true)
|
|
131
|
+
register('anyOf', Builders::CompositionBuilder::AnyOfBuilder, collection: true)
|
|
132
|
+
register('oneOf', Builders::CompositionBuilder::OneOfBuilder, collection: true)
|
|
133
|
+
register('EasyTalk::Types::Tuple', Builders::TupleBuilder, collection: true)
|
|
134
|
+
register('T::Types::TypedArray', Builders::TypedArrayBuilder, collection: true)
|
|
135
|
+
register('T::Types::Union', Builders::UnionBuilder, collection: true)
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
private
|
|
139
|
+
|
|
140
|
+
# Normalize a type key to a canonical string form.
|
|
141
|
+
#
|
|
142
|
+
# @param type_key [Class, String, Symbol] The type key to normalize
|
|
143
|
+
# @return [String] The normalized key
|
|
144
|
+
sig { params(type_key: T.any(T::Class[T.anything], String, Symbol)).returns(String) }
|
|
145
|
+
def normalize_key(type_key)
|
|
146
|
+
case type_key
|
|
147
|
+
when Class
|
|
148
|
+
type_key.name.to_s
|
|
149
|
+
when Symbol
|
|
150
|
+
type_key.to_s
|
|
151
|
+
else
|
|
152
|
+
type_key.to_s
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
# Find a registration for the given type.
|
|
157
|
+
#
|
|
158
|
+
# Tries multiple resolution strategies in order.
|
|
159
|
+
#
|
|
160
|
+
# @param type [Object] The type to find
|
|
161
|
+
# @return [Hash, nil] The registration entry or nil
|
|
162
|
+
sig { params(type: T.untyped).returns(T.nilable(T::Hash[Symbol, T.untyped])) }
|
|
163
|
+
def find_registration(type)
|
|
164
|
+
# Strategy 1: Check type.class.name (for Sorbet types like T::Types::TypedArray)
|
|
165
|
+
class_name = type.class.name.to_s
|
|
166
|
+
return registry[class_name] if registry.key?(class_name)
|
|
167
|
+
|
|
168
|
+
# Strategy 2: Check type.name (for types that respond to :name, like "String")
|
|
169
|
+
if type.respond_to?(:name)
|
|
170
|
+
type_name = type.name.to_s
|
|
171
|
+
return registry[type_name] if registry.key?(type_name)
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
# Strategy 3: Check the type itself if it's a Class
|
|
175
|
+
return registry[type.name.to_s] if type.is_a?(Class) && registry.key?(type.name.to_s)
|
|
176
|
+
|
|
177
|
+
nil
|
|
178
|
+
end
|
|
179
|
+
end
|
|
180
|
+
end
|
|
181
|
+
end
|
|
182
|
+
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 '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,16 +21,23 @@ 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
|
|
28
|
+
@valid_options = deep_dup_options(VALID_OPTIONS)
|
|
27
29
|
update_option_types
|
|
28
|
-
super(name, { type: 'array' }, constraints,
|
|
30
|
+
super(name, { type: 'array' }, constraints, @valid_options)
|
|
29
31
|
end
|
|
30
32
|
|
|
31
33
|
private
|
|
32
34
|
|
|
35
|
+
# Creates a copy of options with duped nested hashes to avoid mutating the constant.
|
|
36
|
+
sig { params(options: T::Hash[Symbol, T.untyped]).returns(T::Hash[Symbol, T.untyped]) }
|
|
37
|
+
def deep_dup_options(options)
|
|
38
|
+
options.transform_values(&:dup)
|
|
39
|
+
end
|
|
40
|
+
|
|
33
41
|
# Modifies the schema to include the `items` property.
|
|
34
42
|
#
|
|
35
43
|
# @return [Hash] The built schema.
|
|
@@ -42,17 +50,22 @@ module EasyTalk
|
|
|
42
50
|
end
|
|
43
51
|
end
|
|
44
52
|
|
|
53
|
+
sig { returns(T.untyped) }
|
|
45
54
|
def inner_type
|
|
46
55
|
return unless type.is_a?(T::Types::TypedArray)
|
|
47
56
|
|
|
48
|
-
type.type.
|
|
57
|
+
if type.type.is_a?(EasyTalk::Types::Composer)
|
|
58
|
+
type.type
|
|
59
|
+
else
|
|
60
|
+
type.type.raw_type
|
|
61
|
+
end
|
|
49
62
|
end
|
|
50
63
|
|
|
51
64
|
sig { void }
|
|
52
65
|
# Updates the option types for the array builder.
|
|
53
66
|
def update_option_types
|
|
54
|
-
|
|
55
|
-
|
|
67
|
+
@valid_options[:enum][:type] = T::Array[inner_type]
|
|
68
|
+
@valid_options[:const][:type] = T::Array[inner_type]
|
|
56
69
|
end
|
|
57
70
|
end
|
|
58
71
|
end
|
|
@@ -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
|