spikard 0.3.5 → 0.3.6
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/LICENSE +1 -1
- data/README.md +659 -659
- data/ext/spikard_rb/Cargo.toml +17 -17
- data/ext/spikard_rb/extconf.rb +10 -10
- data/ext/spikard_rb/src/lib.rs +6 -6
- data/lib/spikard/app.rb +386 -386
- data/lib/spikard/background.rb +27 -27
- data/lib/spikard/config.rb +396 -396
- data/lib/spikard/converters.rb +13 -13
- data/lib/spikard/handler_wrapper.rb +113 -113
- data/lib/spikard/provide.rb +214 -214
- data/lib/spikard/response.rb +173 -173
- data/lib/spikard/schema.rb +243 -243
- data/lib/spikard/sse.rb +111 -111
- data/lib/spikard/streaming_response.rb +44 -44
- data/lib/spikard/testing.rb +221 -221
- data/lib/spikard/upload_file.rb +131 -131
- data/lib/spikard/version.rb +5 -5
- data/lib/spikard/websocket.rb +59 -59
- data/lib/spikard.rb +43 -43
- data/sig/spikard.rbs +366 -360
- data/vendor/crates/spikard-core/Cargo.toml +40 -40
- data/vendor/crates/spikard-core/src/bindings/mod.rs +3 -3
- data/vendor/crates/spikard-core/src/bindings/response.rs +133 -133
- data/vendor/crates/spikard-core/src/debug.rs +63 -63
- data/vendor/crates/spikard-core/src/di/container.rs +726 -726
- data/vendor/crates/spikard-core/src/di/dependency.rs +273 -273
- data/vendor/crates/spikard-core/src/di/error.rs +118 -118
- data/vendor/crates/spikard-core/src/di/factory.rs +538 -538
- data/vendor/crates/spikard-core/src/di/graph.rs +545 -545
- data/vendor/crates/spikard-core/src/di/mod.rs +192 -192
- data/vendor/crates/spikard-core/src/di/resolved.rs +411 -411
- data/vendor/crates/spikard-core/src/di/value.rs +283 -283
- data/vendor/crates/spikard-core/src/errors.rs +39 -39
- data/vendor/crates/spikard-core/src/http.rs +153 -153
- data/vendor/crates/spikard-core/src/lib.rs +29 -29
- data/vendor/crates/spikard-core/src/lifecycle.rs +422 -422
- data/vendor/crates/spikard-core/src/parameters.rs +722 -722
- data/vendor/crates/spikard-core/src/problem.rs +310 -310
- data/vendor/crates/spikard-core/src/request_data.rs +189 -189
- data/vendor/crates/spikard-core/src/router.rs +249 -249
- data/vendor/crates/spikard-core/src/schema_registry.rs +183 -183
- data/vendor/crates/spikard-core/src/type_hints.rs +304 -304
- data/vendor/crates/spikard-core/src/validation.rs +699 -699
- data/vendor/crates/spikard-http/Cargo.toml +68 -68
- data/vendor/crates/spikard-http/src/auth.rs +247 -247
- data/vendor/crates/spikard-http/src/background.rs +249 -249
- data/vendor/crates/spikard-http/src/bindings/mod.rs +3 -3
- data/vendor/crates/spikard-http/src/bindings/response.rs +1 -1
- data/vendor/crates/spikard-http/src/body_metadata.rs +8 -8
- data/vendor/crates/spikard-http/src/cors.rs +490 -490
- data/vendor/crates/spikard-http/src/debug.rs +63 -63
- data/vendor/crates/spikard-http/src/di_handler.rs +423 -423
- data/vendor/crates/spikard-http/src/handler_response.rs +190 -190
- data/vendor/crates/spikard-http/src/handler_trait.rs +228 -228
- data/vendor/crates/spikard-http/src/handler_trait_tests.rs +284 -284
- data/vendor/crates/spikard-http/src/lib.rs +529 -529
- data/vendor/crates/spikard-http/src/lifecycle/adapter.rs +149 -149
- data/vendor/crates/spikard-http/src/lifecycle.rs +428 -428
- data/vendor/crates/spikard-http/src/middleware/mod.rs +285 -285
- data/vendor/crates/spikard-http/src/middleware/multipart.rs +86 -86
- data/vendor/crates/spikard-http/src/middleware/urlencoded.rs +147 -147
- data/vendor/crates/spikard-http/src/middleware/validation.rs +287 -287
- data/vendor/crates/spikard-http/src/openapi/mod.rs +309 -309
- data/vendor/crates/spikard-http/src/openapi/parameter_extraction.rs +190 -190
- data/vendor/crates/spikard-http/src/openapi/schema_conversion.rs +308 -308
- data/vendor/crates/spikard-http/src/openapi/spec_generation.rs +195 -195
- data/vendor/crates/spikard-http/src/parameters.rs +1 -1
- data/vendor/crates/spikard-http/src/problem.rs +1 -1
- data/vendor/crates/spikard-http/src/query_parser.rs +369 -369
- data/vendor/crates/spikard-http/src/response.rs +399 -399
- data/vendor/crates/spikard-http/src/router.rs +1 -1
- data/vendor/crates/spikard-http/src/schema_registry.rs +1 -1
- data/vendor/crates/spikard-http/src/server/handler.rs +87 -87
- data/vendor/crates/spikard-http/src/server/lifecycle_execution.rs +98 -98
- data/vendor/crates/spikard-http/src/server/mod.rs +805 -805
- data/vendor/crates/spikard-http/src/server/request_extraction.rs +119 -119
- data/vendor/crates/spikard-http/src/sse.rs +447 -447
- data/vendor/crates/spikard-http/src/testing/form.rs +14 -14
- data/vendor/crates/spikard-http/src/testing/multipart.rs +60 -60
- data/vendor/crates/spikard-http/src/testing/test_client.rs +285 -285
- data/vendor/crates/spikard-http/src/testing.rs +377 -377
- data/vendor/crates/spikard-http/src/type_hints.rs +1 -1
- data/vendor/crates/spikard-http/src/validation.rs +1 -1
- data/vendor/crates/spikard-http/src/websocket.rs +324 -324
- data/vendor/crates/spikard-rb/Cargo.toml +42 -42
- data/vendor/crates/spikard-rb/build.rs +8 -8
- data/vendor/crates/spikard-rb/src/background.rs +63 -63
- data/vendor/crates/spikard-rb/src/config.rs +294 -294
- data/vendor/crates/spikard-rb/src/conversion.rs +453 -453
- data/vendor/crates/spikard-rb/src/di.rs +409 -409
- data/vendor/crates/spikard-rb/src/handler.rs +625 -625
- data/vendor/crates/spikard-rb/src/lib.rs +2771 -2771
- data/vendor/crates/spikard-rb/src/lifecycle.rs +274 -274
- data/vendor/crates/spikard-rb/src/server.rs +283 -283
- data/vendor/crates/spikard-rb/src/sse.rs +231 -231
- data/vendor/crates/spikard-rb/src/test_client.rs +404 -404
- data/vendor/crates/spikard-rb/src/test_sse.rs +143 -143
- data/vendor/crates/spikard-rb/src/test_websocket.rs +221 -221
- data/vendor/crates/spikard-rb/src/websocket.rs +233 -233
- metadata +1 -1
data/lib/spikard/schema.rb
CHANGED
|
@@ -1,243 +1,243 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
# rubocop:disable Metrics/ModuleLength
|
|
4
|
-
module Spikard
|
|
5
|
-
# Schema extraction helpers for Ruby type systems
|
|
6
|
-
#
|
|
7
|
-
# Supports:
|
|
8
|
-
# - Plain JSON Schema (Hash)
|
|
9
|
-
# - Dry::Schema with :json_schema extension
|
|
10
|
-
# - Dry::Struct (Dry-Types)
|
|
11
|
-
#
|
|
12
|
-
# @example With Dry::Schema
|
|
13
|
-
# require 'dry-schema'
|
|
14
|
-
# Dry::Schema.load_extensions(:json_schema)
|
|
15
|
-
#
|
|
16
|
-
# UserSchema = Dry::Schema.JSON do
|
|
17
|
-
# required(:email).filled(:str?)
|
|
18
|
-
# required(:age).filled(:int?)
|
|
19
|
-
# end
|
|
20
|
-
#
|
|
21
|
-
# schema = Spikard::Schema.extract_json_schema(UserSchema)
|
|
22
|
-
#
|
|
23
|
-
# @example With Dry::Struct
|
|
24
|
-
# require 'dry-struct'
|
|
25
|
-
#
|
|
26
|
-
# class User < Dry::Struct
|
|
27
|
-
# attribute :email, Types::String
|
|
28
|
-
# attribute :age, Types::Integer
|
|
29
|
-
# end
|
|
30
|
-
#
|
|
31
|
-
# schema = Spikard::Schema.extract_json_schema(User)
|
|
32
|
-
#
|
|
33
|
-
# @example With plain JSON Schema
|
|
34
|
-
# schema_hash = {
|
|
35
|
-
# "type" => "object",
|
|
36
|
-
# "properties" => {
|
|
37
|
-
# "email" => { "type" => "string" },
|
|
38
|
-
# "age" => { "type" => "integer" }
|
|
39
|
-
# },
|
|
40
|
-
# "required" => ["email", "age"]
|
|
41
|
-
# }
|
|
42
|
-
#
|
|
43
|
-
# schema = Spikard::Schema.extract_json_schema(schema_hash)
|
|
44
|
-
module Schema
|
|
45
|
-
# rubocop:disable Metrics/ClassLength
|
|
46
|
-
class << self
|
|
47
|
-
# Extract JSON Schema from various Ruby schema sources
|
|
48
|
-
#
|
|
49
|
-
# @param schema_source [Object] The schema source (Hash, Dry::Schema, Dry::Struct class)
|
|
50
|
-
# @return [Hash, nil] JSON Schema hash or nil if extraction fails
|
|
51
|
-
def extract_json_schema(schema_source)
|
|
52
|
-
return nil if schema_source.nil?
|
|
53
|
-
|
|
54
|
-
# 1. Check if plain JSON Schema hash
|
|
55
|
-
return schema_source if schema_source.is_a?(Hash) && json_schema_hash?(schema_source)
|
|
56
|
-
|
|
57
|
-
# 2. Check for Dry::Schema with json_schema extension
|
|
58
|
-
return extract_from_dry_schema(schema_source) if dry_schema?(schema_source)
|
|
59
|
-
|
|
60
|
-
# 3. Check for Dry::Struct (Dry-Types)
|
|
61
|
-
return extract_from_dry_struct(schema_source) if dry_struct_class?(schema_source)
|
|
62
|
-
|
|
63
|
-
# 4. Unknown type
|
|
64
|
-
warn "Spikard: Unable to extract JSON Schema from #{schema_source.class}. " \
|
|
65
|
-
'Supported types: Hash, Dry::Schema, Dry::Struct'
|
|
66
|
-
nil
|
|
67
|
-
end
|
|
68
|
-
|
|
69
|
-
private
|
|
70
|
-
|
|
71
|
-
# Check if object is a plain JSON Schema hash
|
|
72
|
-
def json_schema_hash?(obj)
|
|
73
|
-
return false unless obj.is_a?(Hash)
|
|
74
|
-
|
|
75
|
-
# Must have 'type' key or '$schema' key
|
|
76
|
-
obj.key?('type') || obj.key?('$schema') || obj.key?(:type) || obj.key?(:$schema)
|
|
77
|
-
end
|
|
78
|
-
|
|
79
|
-
# Check if object is a Dry::Schema
|
|
80
|
-
def dry_schema?(obj)
|
|
81
|
-
defined?(Dry::Schema::Processor) && obj.is_a?(Dry::Schema::Processor)
|
|
82
|
-
end
|
|
83
|
-
|
|
84
|
-
# Check if object is a Dry::Struct class
|
|
85
|
-
def dry_struct_class?(obj)
|
|
86
|
-
return false unless obj.is_a?(Class)
|
|
87
|
-
|
|
88
|
-
defined?(Dry::Struct) && obj < Dry::Struct
|
|
89
|
-
end
|
|
90
|
-
|
|
91
|
-
# Extract JSON Schema from Dry::Schema
|
|
92
|
-
def extract_from_dry_schema(schema)
|
|
93
|
-
unless schema.respond_to?(:json_schema)
|
|
94
|
-
warn 'Spikard: Dry::Schema instance does not have json_schema method. ' \
|
|
95
|
-
'Did you load the :json_schema extension? ' \
|
|
96
|
-
'Add: Dry::Schema.load_extensions(:json_schema)'
|
|
97
|
-
return nil
|
|
98
|
-
end
|
|
99
|
-
|
|
100
|
-
begin
|
|
101
|
-
schema.json_schema
|
|
102
|
-
rescue StandardError => e
|
|
103
|
-
warn "Spikard: Failed to extract JSON Schema from Dry::Schema: #{e.message}"
|
|
104
|
-
nil
|
|
105
|
-
end
|
|
106
|
-
end
|
|
107
|
-
|
|
108
|
-
# Extract JSON Schema from Dry::Struct class
|
|
109
|
-
# rubocop:disable Metrics/MethodLength
|
|
110
|
-
def extract_from_dry_struct(struct_class)
|
|
111
|
-
# Dry::Struct doesn't have built-in JSON Schema export
|
|
112
|
-
# We need to manually build it from the attribute schema
|
|
113
|
-
|
|
114
|
-
properties = {}
|
|
115
|
-
required = []
|
|
116
|
-
|
|
117
|
-
struct_class.schema.each do |key, type_definition|
|
|
118
|
-
# Extract attribute name
|
|
119
|
-
attr_name = key.to_s
|
|
120
|
-
|
|
121
|
-
# Determine if required (non-optional)
|
|
122
|
-
is_required = !type_definition.optional?
|
|
123
|
-
required << attr_name if is_required
|
|
124
|
-
|
|
125
|
-
# Convert Dry::Types to JSON Schema type
|
|
126
|
-
json_type = dry_type_to_json_schema(type_definition)
|
|
127
|
-
properties[attr_name] = json_type if json_type
|
|
128
|
-
end
|
|
129
|
-
|
|
130
|
-
{
|
|
131
|
-
'type' => 'object',
|
|
132
|
-
'properties' => properties,
|
|
133
|
-
'required' => required
|
|
134
|
-
}
|
|
135
|
-
rescue StandardError => e
|
|
136
|
-
warn "Spikard: Failed to extract JSON Schema from Dry::Struct: #{e.message}"
|
|
137
|
-
nil
|
|
138
|
-
end
|
|
139
|
-
# rubocop:enable Metrics/MethodLength
|
|
140
|
-
|
|
141
|
-
# Convert Dry::Types type to JSON Schema type
|
|
142
|
-
def dry_type_to_json_schema(type_def)
|
|
143
|
-
schema = base_schema_for(type_def)
|
|
144
|
-
apply_metadata_constraints(schema, type_def)
|
|
145
|
-
rescue StandardError
|
|
146
|
-
{ 'type' => 'object' }
|
|
147
|
-
end
|
|
148
|
-
|
|
149
|
-
# rubocop:disable Metrics/MethodLength
|
|
150
|
-
def base_schema_for(type_def)
|
|
151
|
-
type_class = type_def.primitive.to_s
|
|
152
|
-
case type_class
|
|
153
|
-
when 'String' then { 'type' => 'string' }
|
|
154
|
-
when 'Integer' then { 'type' => 'integer' }
|
|
155
|
-
when 'Float', 'BigDecimal' then { 'type' => 'number' }
|
|
156
|
-
when 'TrueClass', 'FalseClass' then { 'type' => 'boolean' }
|
|
157
|
-
when 'Array'
|
|
158
|
-
{
|
|
159
|
-
'type' => 'array',
|
|
160
|
-
'items' => infer_array_items_schema(type_def)
|
|
161
|
-
}
|
|
162
|
-
when 'Hash'
|
|
163
|
-
{ 'type' => 'object', 'additionalProperties' => true }
|
|
164
|
-
when 'NilClass' then { 'type' => 'null' }
|
|
165
|
-
else
|
|
166
|
-
{ 'type' => 'object' }
|
|
167
|
-
end
|
|
168
|
-
end
|
|
169
|
-
# rubocop:enable Metrics/MethodLength
|
|
170
|
-
|
|
171
|
-
def infer_array_items_schema(type_def)
|
|
172
|
-
if type_def.respond_to?(:member) && type_def.member
|
|
173
|
-
dry_type_to_json_schema(type_def.member)
|
|
174
|
-
else
|
|
175
|
-
{}
|
|
176
|
-
end
|
|
177
|
-
rescue StandardError
|
|
178
|
-
{}
|
|
179
|
-
end
|
|
180
|
-
|
|
181
|
-
def apply_metadata_constraints(schema, type_def)
|
|
182
|
-
metadata = extract_metadata(type_def)
|
|
183
|
-
return schema if metadata.empty?
|
|
184
|
-
|
|
185
|
-
schema = apply_enum_and_format(schema, metadata)
|
|
186
|
-
apply_numeric_constraints(schema, metadata)
|
|
187
|
-
end
|
|
188
|
-
|
|
189
|
-
def apply_enum_and_format(schema, metadata)
|
|
190
|
-
enum_values = metadata[:enum] || metadata['enum']
|
|
191
|
-
schema['enum'] = Array(enum_values) if enum_values
|
|
192
|
-
|
|
193
|
-
format_value = metadata[:format] || metadata['format']
|
|
194
|
-
schema['format'] = format_value.to_s if format_value
|
|
195
|
-
|
|
196
|
-
description = metadata[:description] || metadata['description']
|
|
197
|
-
schema['description'] = description.to_s if description
|
|
198
|
-
schema
|
|
199
|
-
end
|
|
200
|
-
|
|
201
|
-
# rubocop:disable Metrics/MethodLength
|
|
202
|
-
def apply_numeric_constraints(schema, metadata)
|
|
203
|
-
mapping = {
|
|
204
|
-
min_size: 'minLength',
|
|
205
|
-
max_size: 'maxLength',
|
|
206
|
-
min_items: 'minItems',
|
|
207
|
-
max_items: 'maxItems',
|
|
208
|
-
min: 'minimum',
|
|
209
|
-
max: 'maximum',
|
|
210
|
-
gte: 'minimum',
|
|
211
|
-
lte: 'maximum',
|
|
212
|
-
gt: 'exclusiveMinimum',
|
|
213
|
-
lt: 'exclusiveMaximum'
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
mapping.each do |meta_key, json_key|
|
|
217
|
-
value = metadata[meta_key] || metadata[meta_key.to_s]
|
|
218
|
-
next unless value
|
|
219
|
-
|
|
220
|
-
schema[json_key] = value
|
|
221
|
-
end
|
|
222
|
-
schema
|
|
223
|
-
end
|
|
224
|
-
# rubocop:enable Metrics/MethodLength
|
|
225
|
-
|
|
226
|
-
def extract_metadata(type_def)
|
|
227
|
-
return {} unless type_def.respond_to?(:meta) || type_def.respond_to?(:options)
|
|
228
|
-
|
|
229
|
-
if type_def.respond_to?(:meta) && type_def.meta
|
|
230
|
-
type_def.meta
|
|
231
|
-
elsif type_def.respond_to?(:options) && type_def.options.is_a?(Hash)
|
|
232
|
-
type_def.options
|
|
233
|
-
else
|
|
234
|
-
{}
|
|
235
|
-
end
|
|
236
|
-
rescue StandardError
|
|
237
|
-
{}
|
|
238
|
-
end
|
|
239
|
-
end
|
|
240
|
-
# rubocop:enable Metrics/ClassLength
|
|
241
|
-
end
|
|
242
|
-
end
|
|
243
|
-
# rubocop:enable Metrics/ModuleLength
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# rubocop:disable Metrics/ModuleLength
|
|
4
|
+
module Spikard
|
|
5
|
+
# Schema extraction helpers for Ruby type systems
|
|
6
|
+
#
|
|
7
|
+
# Supports:
|
|
8
|
+
# - Plain JSON Schema (Hash)
|
|
9
|
+
# - Dry::Schema with :json_schema extension
|
|
10
|
+
# - Dry::Struct (Dry-Types)
|
|
11
|
+
#
|
|
12
|
+
# @example With Dry::Schema
|
|
13
|
+
# require 'dry-schema'
|
|
14
|
+
# Dry::Schema.load_extensions(:json_schema)
|
|
15
|
+
#
|
|
16
|
+
# UserSchema = Dry::Schema.JSON do
|
|
17
|
+
# required(:email).filled(:str?)
|
|
18
|
+
# required(:age).filled(:int?)
|
|
19
|
+
# end
|
|
20
|
+
#
|
|
21
|
+
# schema = Spikard::Schema.extract_json_schema(UserSchema)
|
|
22
|
+
#
|
|
23
|
+
# @example With Dry::Struct
|
|
24
|
+
# require 'dry-struct'
|
|
25
|
+
#
|
|
26
|
+
# class User < Dry::Struct
|
|
27
|
+
# attribute :email, Types::String
|
|
28
|
+
# attribute :age, Types::Integer
|
|
29
|
+
# end
|
|
30
|
+
#
|
|
31
|
+
# schema = Spikard::Schema.extract_json_schema(User)
|
|
32
|
+
#
|
|
33
|
+
# @example With plain JSON Schema
|
|
34
|
+
# schema_hash = {
|
|
35
|
+
# "type" => "object",
|
|
36
|
+
# "properties" => {
|
|
37
|
+
# "email" => { "type" => "string" },
|
|
38
|
+
# "age" => { "type" => "integer" }
|
|
39
|
+
# },
|
|
40
|
+
# "required" => ["email", "age"]
|
|
41
|
+
# }
|
|
42
|
+
#
|
|
43
|
+
# schema = Spikard::Schema.extract_json_schema(schema_hash)
|
|
44
|
+
module Schema
|
|
45
|
+
# rubocop:disable Metrics/ClassLength
|
|
46
|
+
class << self
|
|
47
|
+
# Extract JSON Schema from various Ruby schema sources
|
|
48
|
+
#
|
|
49
|
+
# @param schema_source [Object] The schema source (Hash, Dry::Schema, Dry::Struct class)
|
|
50
|
+
# @return [Hash, nil] JSON Schema hash or nil if extraction fails
|
|
51
|
+
def extract_json_schema(schema_source)
|
|
52
|
+
return nil if schema_source.nil?
|
|
53
|
+
|
|
54
|
+
# 1. Check if plain JSON Schema hash
|
|
55
|
+
return schema_source if schema_source.is_a?(Hash) && json_schema_hash?(schema_source)
|
|
56
|
+
|
|
57
|
+
# 2. Check for Dry::Schema with json_schema extension
|
|
58
|
+
return extract_from_dry_schema(schema_source) if dry_schema?(schema_source)
|
|
59
|
+
|
|
60
|
+
# 3. Check for Dry::Struct (Dry-Types)
|
|
61
|
+
return extract_from_dry_struct(schema_source) if dry_struct_class?(schema_source)
|
|
62
|
+
|
|
63
|
+
# 4. Unknown type
|
|
64
|
+
warn "Spikard: Unable to extract JSON Schema from #{schema_source.class}. " \
|
|
65
|
+
'Supported types: Hash, Dry::Schema, Dry::Struct'
|
|
66
|
+
nil
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
private
|
|
70
|
+
|
|
71
|
+
# Check if object is a plain JSON Schema hash
|
|
72
|
+
def json_schema_hash?(obj)
|
|
73
|
+
return false unless obj.is_a?(Hash)
|
|
74
|
+
|
|
75
|
+
# Must have 'type' key or '$schema' key
|
|
76
|
+
obj.key?('type') || obj.key?('$schema') || obj.key?(:type) || obj.key?(:$schema)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# Check if object is a Dry::Schema
|
|
80
|
+
def dry_schema?(obj)
|
|
81
|
+
defined?(Dry::Schema::Processor) && obj.is_a?(Dry::Schema::Processor)
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# Check if object is a Dry::Struct class
|
|
85
|
+
def dry_struct_class?(obj)
|
|
86
|
+
return false unless obj.is_a?(Class)
|
|
87
|
+
|
|
88
|
+
defined?(Dry::Struct) && obj < Dry::Struct
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# Extract JSON Schema from Dry::Schema
|
|
92
|
+
def extract_from_dry_schema(schema)
|
|
93
|
+
unless schema.respond_to?(:json_schema)
|
|
94
|
+
warn 'Spikard: Dry::Schema instance does not have json_schema method. ' \
|
|
95
|
+
'Did you load the :json_schema extension? ' \
|
|
96
|
+
'Add: Dry::Schema.load_extensions(:json_schema)'
|
|
97
|
+
return nil
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
begin
|
|
101
|
+
schema.json_schema
|
|
102
|
+
rescue StandardError => e
|
|
103
|
+
warn "Spikard: Failed to extract JSON Schema from Dry::Schema: #{e.message}"
|
|
104
|
+
nil
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# Extract JSON Schema from Dry::Struct class
|
|
109
|
+
# rubocop:disable Metrics/MethodLength
|
|
110
|
+
def extract_from_dry_struct(struct_class)
|
|
111
|
+
# Dry::Struct doesn't have built-in JSON Schema export
|
|
112
|
+
# We need to manually build it from the attribute schema
|
|
113
|
+
|
|
114
|
+
properties = {}
|
|
115
|
+
required = []
|
|
116
|
+
|
|
117
|
+
struct_class.schema.each do |key, type_definition|
|
|
118
|
+
# Extract attribute name
|
|
119
|
+
attr_name = key.to_s
|
|
120
|
+
|
|
121
|
+
# Determine if required (non-optional)
|
|
122
|
+
is_required = !type_definition.optional?
|
|
123
|
+
required << attr_name if is_required
|
|
124
|
+
|
|
125
|
+
# Convert Dry::Types to JSON Schema type
|
|
126
|
+
json_type = dry_type_to_json_schema(type_definition)
|
|
127
|
+
properties[attr_name] = json_type if json_type
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
{
|
|
131
|
+
'type' => 'object',
|
|
132
|
+
'properties' => properties,
|
|
133
|
+
'required' => required
|
|
134
|
+
}
|
|
135
|
+
rescue StandardError => e
|
|
136
|
+
warn "Spikard: Failed to extract JSON Schema from Dry::Struct: #{e.message}"
|
|
137
|
+
nil
|
|
138
|
+
end
|
|
139
|
+
# rubocop:enable Metrics/MethodLength
|
|
140
|
+
|
|
141
|
+
# Convert Dry::Types type to JSON Schema type
|
|
142
|
+
def dry_type_to_json_schema(type_def)
|
|
143
|
+
schema = base_schema_for(type_def)
|
|
144
|
+
apply_metadata_constraints(schema, type_def)
|
|
145
|
+
rescue StandardError
|
|
146
|
+
{ 'type' => 'object' }
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
# rubocop:disable Metrics/MethodLength
|
|
150
|
+
def base_schema_for(type_def)
|
|
151
|
+
type_class = type_def.primitive.to_s
|
|
152
|
+
case type_class
|
|
153
|
+
when 'String' then { 'type' => 'string' }
|
|
154
|
+
when 'Integer' then { 'type' => 'integer' }
|
|
155
|
+
when 'Float', 'BigDecimal' then { 'type' => 'number' }
|
|
156
|
+
when 'TrueClass', 'FalseClass' then { 'type' => 'boolean' }
|
|
157
|
+
when 'Array'
|
|
158
|
+
{
|
|
159
|
+
'type' => 'array',
|
|
160
|
+
'items' => infer_array_items_schema(type_def)
|
|
161
|
+
}
|
|
162
|
+
when 'Hash'
|
|
163
|
+
{ 'type' => 'object', 'additionalProperties' => true }
|
|
164
|
+
when 'NilClass' then { 'type' => 'null' }
|
|
165
|
+
else
|
|
166
|
+
{ 'type' => 'object' }
|
|
167
|
+
end
|
|
168
|
+
end
|
|
169
|
+
# rubocop:enable Metrics/MethodLength
|
|
170
|
+
|
|
171
|
+
def infer_array_items_schema(type_def)
|
|
172
|
+
if type_def.respond_to?(:member) && type_def.member
|
|
173
|
+
dry_type_to_json_schema(type_def.member)
|
|
174
|
+
else
|
|
175
|
+
{}
|
|
176
|
+
end
|
|
177
|
+
rescue StandardError
|
|
178
|
+
{}
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
def apply_metadata_constraints(schema, type_def)
|
|
182
|
+
metadata = extract_metadata(type_def)
|
|
183
|
+
return schema if metadata.empty?
|
|
184
|
+
|
|
185
|
+
schema = apply_enum_and_format(schema, metadata)
|
|
186
|
+
apply_numeric_constraints(schema, metadata)
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
def apply_enum_and_format(schema, metadata)
|
|
190
|
+
enum_values = metadata[:enum] || metadata['enum']
|
|
191
|
+
schema['enum'] = Array(enum_values) if enum_values
|
|
192
|
+
|
|
193
|
+
format_value = metadata[:format] || metadata['format']
|
|
194
|
+
schema['format'] = format_value.to_s if format_value
|
|
195
|
+
|
|
196
|
+
description = metadata[:description] || metadata['description']
|
|
197
|
+
schema['description'] = description.to_s if description
|
|
198
|
+
schema
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
# rubocop:disable Metrics/MethodLength
|
|
202
|
+
def apply_numeric_constraints(schema, metadata)
|
|
203
|
+
mapping = {
|
|
204
|
+
min_size: 'minLength',
|
|
205
|
+
max_size: 'maxLength',
|
|
206
|
+
min_items: 'minItems',
|
|
207
|
+
max_items: 'maxItems',
|
|
208
|
+
min: 'minimum',
|
|
209
|
+
max: 'maximum',
|
|
210
|
+
gte: 'minimum',
|
|
211
|
+
lte: 'maximum',
|
|
212
|
+
gt: 'exclusiveMinimum',
|
|
213
|
+
lt: 'exclusiveMaximum'
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
mapping.each do |meta_key, json_key|
|
|
217
|
+
value = metadata[meta_key] || metadata[meta_key.to_s]
|
|
218
|
+
next unless value
|
|
219
|
+
|
|
220
|
+
schema[json_key] = value
|
|
221
|
+
end
|
|
222
|
+
schema
|
|
223
|
+
end
|
|
224
|
+
# rubocop:enable Metrics/MethodLength
|
|
225
|
+
|
|
226
|
+
def extract_metadata(type_def)
|
|
227
|
+
return {} unless type_def.respond_to?(:meta) || type_def.respond_to?(:options)
|
|
228
|
+
|
|
229
|
+
if type_def.respond_to?(:meta) && type_def.meta
|
|
230
|
+
type_def.meta
|
|
231
|
+
elsif type_def.respond_to?(:options) && type_def.options.is_a?(Hash)
|
|
232
|
+
type_def.options
|
|
233
|
+
else
|
|
234
|
+
{}
|
|
235
|
+
end
|
|
236
|
+
rescue StandardError
|
|
237
|
+
{}
|
|
238
|
+
end
|
|
239
|
+
end
|
|
240
|
+
# rubocop:enable Metrics/ClassLength
|
|
241
|
+
end
|
|
242
|
+
end
|
|
243
|
+
# rubocop:enable Metrics/ModuleLength
|