verquest 0.6.1 → 0.6.3

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.
@@ -113,11 +113,19 @@ module Verquest
113
113
 
114
114
  schema_options.delete_if { |_, v| v.nil? }
115
115
 
116
- prepare_schema
117
- prepare_validation_schema
118
- prepare_mapping
119
- prepare_external_mapping
120
- @transformer = Transformer.new(mapping: mapping)
116
+ if combination?
117
+ prepare_combination_schema
118
+ prepare_combination_validation_schema
119
+ prepare_combination_mapping
120
+ prepare_combination_external_mapping
121
+ @transformer = Transformer.new(mapping: mapping, discriminator: combination_discriminator)
122
+ else
123
+ prepare_schema
124
+ prepare_validation_schema
125
+ prepare_mapping
126
+ prepare_external_mapping
127
+ @transformer = Transformer.new(mapping: mapping)
128
+ end
121
129
 
122
130
  freeze
123
131
  end
@@ -196,6 +204,45 @@ module Verquest
196
204
  transformer.call(params)
197
205
  end
198
206
 
207
+ # Check if this version is a combination schema (root-level oneOf)
208
+ #
209
+ # A combination schema has a single root-level oneOf property (name is nil)
210
+ # where the entire request body matches one of the defined schemas.
211
+ #
212
+ # @return [Boolean] true if this is a combination schema
213
+ def combination?
214
+ return @_combination if defined?(@_combination)
215
+
216
+ @_combination = properties.values.count == 1 &&
217
+ properties.values.first&.is_a?(Verquest::Properties::OneOf) &&
218
+ properties.values.first.name.nil?
219
+ end
220
+
221
+ # Check if this version has a nested oneOf property (oneOf with a name)
222
+ #
223
+ # @return [Boolean] true if there's a nested oneOf property
224
+ def has_nested_one_of?
225
+ nested_one_of_count > 0
226
+ end
227
+
228
+ # Check if this version has multiple nested oneOf properties
229
+ #
230
+ # @return [Boolean] true if there are multiple nested oneOf properties
231
+ def has_multiple_nested_one_of?
232
+ nested_one_of_count > 1
233
+ end
234
+
235
+ # Returns the count of nested oneOf properties (computed once and cached)
236
+ #
237
+ # @return [Integer] Number of nested oneOf properties
238
+ def nested_one_of_count
239
+ return @_nested_one_of_count if defined?(@_nested_one_of_count)
240
+
241
+ @_nested_one_of_count = properties.values.count do |p|
242
+ p.is_a?(Verquest::Properties::OneOf) && !p.name.nil?
243
+ end
244
+ end
245
+
199
246
  private
200
247
 
201
248
  # Generates the JSON schema for this version
@@ -208,14 +255,24 @@ module Verquest
208
255
  def prepare_schema
209
256
  @schema = {
210
257
  "type" => "object",
211
- "description" => description,
212
258
  "required" => required_properties,
213
259
  "properties" => properties.transform_values { |property| property.to_schema[property.name] }
214
260
  }.merge(schema_options).tap do |schema|
215
261
  schema["dependentRequired"] = dependent_required_properties if dependent_required_properties.any?
262
+ schema["description"] = description if description
216
263
  end.freeze
217
264
  end
218
265
 
266
+ # Generates the JSON schema for combination schemas (oneOf at root level)
267
+ #
268
+ # For combination schemas, the schema is delegated directly to the oneOf
269
+ # property since it represents the entire request structure.
270
+ #
271
+ # @return [Hash] The schema from the oneOf property
272
+ def prepare_combination_schema
273
+ @schema = properties.values.first.to_schema
274
+ end
275
+
219
276
  # Generates the validation schema for this version
220
277
  #
221
278
  # Similar to prepare_schema but specifically for validation purposes.
@@ -225,28 +282,114 @@ module Verquest
225
282
  def prepare_validation_schema
226
283
  @validation_schema = {
227
284
  "type" => "object",
228
- "description" => description,
229
285
  "required" => required_properties,
230
286
  "properties" => properties.transform_values { |property| property.to_validation_schema(version: name)[property.name] }
231
287
  }.merge(schema_options).tap do |schema|
232
288
  schema["dependentRequired"] = dependent_required_properties if dependent_required_properties.any?
289
+ schema["description"] = description if description
233
290
  end.freeze
234
291
  end
235
292
 
293
+ # Generates the validation schema for combination schemas (oneOf at root level)
294
+ #
295
+ # For combination schemas, the validation schema is delegated directly to
296
+ # the oneOf property, which includes inline schema definitions for each option.
297
+ #
298
+ # @return [Hash] The validation schema from the oneOf property
299
+ def prepare_combination_validation_schema
300
+ @validation_schema = properties.values.first.to_validation_schema(version: name)
301
+ end
302
+
236
303
  # Prepares the parameter mapping for this version
237
304
  #
238
305
  # Collects mappings from all properties in this version and checks for
239
306
  # duplicate mappings, which would cause conflicts during transformation.
240
307
  #
308
+ # When nested oneOf properties are present, the mapping includes a _oneOfs array
309
+ # containing each oneOf's metadata and variant mappings, plus base properties.
310
+ #
241
311
  # @return [Hash] The mapping from schema property paths to internal paths
242
312
  # @raise [MappingError] If duplicate mappings are detected
243
313
  def prepare_mapping
244
- @mapping = properties.values.each_with_object({}) do |property, mapping|
314
+ # Separate oneOf properties from regular properties
315
+ one_of_properties = properties.values.select { |p| p.is_a?(Verquest::Properties::OneOf) }
316
+ regular_properties = properties.values.reject { |p| p.is_a?(Verquest::Properties::OneOf) }
317
+
318
+ if one_of_properties.size == 1
319
+ prepare_single_nested_one_of_mapping(one_of_properties.first, regular_properties)
320
+ elsif one_of_properties.size > 1
321
+ prepare_multiple_nested_one_of_mapping(one_of_properties, regular_properties)
322
+ else
323
+ prepare_flat_mapping(regular_properties)
324
+ end
325
+ end
326
+
327
+ # Prepares mapping for versions with a single nested oneOf property (legacy format)
328
+ #
329
+ # @param one_of_property [Verquest::Properties::OneOf] The nested oneOf property
330
+ # @param regular_properties [Array<Verquest::Properties::Base>] Non-oneOf properties
331
+ # @return [void]
332
+ def prepare_single_nested_one_of_mapping(one_of_property, regular_properties)
333
+ # Collect regular property mappings
334
+ regular_mapping = {}
335
+ regular_properties.each do |property|
336
+ property.mapping(key_prefix: [], value_prefix: [], mapping: regular_mapping, version: name)
337
+ end
338
+
339
+ # Collect oneOf property mappings
340
+ one_of_mapping = {}
341
+ one_of_property.mapping(key_prefix: [], value_prefix: [], mapping: one_of_mapping, version: name)
342
+
343
+ # Merge regular mappings into each oneOf variant
344
+ @mapping = {}
345
+
346
+ # Preserve metadata keys
347
+ %w[_discriminator _variant_schemas _variant_path _nullable _nullable_path _nullable_target_path].each do |metadata_key|
348
+ @mapping[metadata_key] = one_of_mapping[metadata_key] if one_of_mapping.key?(metadata_key)
349
+ end
350
+
351
+ one_of_mapping.each do |discriminator_value, variant_mapping|
352
+ next if discriminator_value.start_with?("_") # Skip metadata keys
353
+
354
+ @mapping[discriminator_value] = regular_mapping.merge(variant_mapping)
355
+ end
356
+ end
357
+
358
+ # Prepares mapping for versions with multiple nested oneOf properties
359
+ #
360
+ # @param one_of_properties [Array<Verquest::Properties::OneOf>] The nested oneOf properties
361
+ # @param regular_properties [Array<Verquest::Properties::Base>] Non-oneOf properties
362
+ # @return [void]
363
+ def prepare_multiple_nested_one_of_mapping(one_of_properties, regular_properties)
364
+ @mapping = {}
365
+
366
+ # Collect regular property mappings at root level
367
+ regular_properties.each do |property|
368
+ property.mapping(key_prefix: [], value_prefix: [], mapping: @mapping, version: name)
369
+ end
370
+
371
+ # Collect each oneOf's mapping into _oneOfs array
372
+ @mapping["_oneOfs"] = one_of_properties.map do |one_of_property|
373
+ one_of_mapping = {}
374
+ one_of_property.mapping(key_prefix: [], value_prefix: [], mapping: one_of_mapping, version: name)
375
+ one_of_mapping
376
+ end
377
+ end
378
+
379
+ # Prepares flat mapping for versions without nested oneOf
380
+ #
381
+ # @param properties_list [Array<Verquest::Properties::Base>] Properties to map
382
+ # @return [void]
383
+ # @raise [MappingError] If duplicate mappings are detected
384
+ def prepare_flat_mapping(properties_list)
385
+ @mapping = properties_list.each_with_object({}) do |property, mapping|
245
386
  property.mapping(key_prefix: [], value_prefix: [], mapping: mapping, version: name)
246
387
  end
247
388
 
248
- if (duplicates = mapping.keys.select { |k| mapping.values.count(k) > 1 }).any?
249
- raise MappingError.new("Mapping must be unique. Found duplicates in version '#{name}': #{duplicates.join(", ")}")
389
+ seen = Set.new
390
+ duplicates = mapping.values.select { |v| !seen.add?(v) }
391
+ if duplicates.any?
392
+ raise MappingError.new("Mapping must be unique. Found duplicates in version '#{name}': #{duplicates.uniq.join(", ")}")
250
393
  end
251
394
  end
252
395
 
@@ -256,11 +399,97 @@ module Verquest
256
399
  # attribute names back to external parameter names. This is useful when
257
400
  # transforming internal data back to the external API representation.
258
401
  #
402
+ # For nested oneOf schemas, inverts each discriminator value's mapping.
403
+ # Skips metadata keys that are not variant mappings.
404
+ #
259
405
  # @return [Hash] The frozen inverted mapping where keys are internal attribute
260
406
  # paths and values are the corresponding external schema paths
261
407
  # @see #prepare_mapping
262
408
  def prepare_external_mapping
263
- @external_mapping = mapping.invert.freeze
409
+ @external_mapping = if has_multiple_nested_one_of?
410
+ invert_multiple_one_of_mapping
411
+ elsif has_nested_one_of?
412
+ invert_single_one_of_mapping
413
+ else
414
+ mapping.invert.freeze
415
+ end
416
+ end
417
+
418
+ # Inverts mapping for single nested oneOf
419
+ #
420
+ # @return [Hash] The inverted mapping
421
+ def invert_single_one_of_mapping
422
+ mapping.each_with_object({}) do |(key, value), result|
423
+ result[key] = if key.start_with?("_")
424
+ value
425
+ else
426
+ value.invert
427
+ end
428
+ end.freeze
429
+ end
430
+
431
+ # Inverts mapping for multiple nested oneOf
432
+ #
433
+ # @return [Hash] The inverted mapping
434
+ def invert_multiple_one_of_mapping
435
+ result = {}
436
+
437
+ mapping.each do |key, value|
438
+ if key == "_oneOfs"
439
+ result["_oneOfs"] = value.map do |one_of_mapping|
440
+ one_of_mapping.each_with_object({}) do |(k, v), inverted|
441
+ inverted[k] = if k.start_with?("_")
442
+ v
443
+ else
444
+ v.invert
445
+ end
446
+ end
447
+ end
448
+ elsif key.start_with?("_")
449
+ result[key] = value
450
+ else
451
+ result[value] = key # Invert base properties
452
+ end
453
+ end
454
+
455
+ result.freeze
456
+ end
457
+
458
+ # Prepares the parameter mapping for combination schemas (oneOf)
459
+ #
460
+ # For combination schemas, the mapping is keyed by the discriminator value
461
+ # so the transformer can select the appropriate mapping based on the input.
462
+ #
463
+ # @return [Hash] A hash where keys are discriminator values and values are mapping hashes
464
+ def prepare_combination_mapping
465
+ @mapping = {}
466
+ properties.values.first.mapping(key_prefix: [], value_prefix: [], mapping: @mapping, version: name)
467
+ end
468
+
469
+ # Prepares the inverted parameter mapping for combination schemas
470
+ #
471
+ # For combination schemas, inverts each discriminator value's mapping.
472
+ # Skips metadata keys that are not variant mappings.
473
+ #
474
+ # @return [Hash] The frozen inverted mapping for each discriminator value
475
+ def prepare_combination_external_mapping
476
+ @external_mapping = mapping.each_with_object({}) do |(key, value), result|
477
+ # Skip metadata keys, only invert variant mapping hashes
478
+ result[key] = if key.start_with?("_")
479
+ value
480
+ else
481
+ value.invert
482
+ end
483
+ end.freeze
484
+ end
485
+
486
+ # Returns the discriminator property name for combination schemas
487
+ #
488
+ # @return [String, nil] The discriminator property name
489
+ def combination_discriminator
490
+ return nil unless combination?
491
+
492
+ properties.values.first.send(:discriminator)
264
493
  end
265
494
  end
266
495
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: verquest
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.6.1
4
+ version: 0.6.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Petr Hlavicka
@@ -70,6 +70,7 @@ files:
70
70
  - lib/verquest/properties/enum.rb
71
71
  - lib/verquest/properties/field.rb
72
72
  - lib/verquest/properties/object.rb
73
+ - lib/verquest/properties/one_of.rb
73
74
  - lib/verquest/properties/reference.rb
74
75
  - lib/verquest/result.rb
75
76
  - lib/verquest/transformer.rb