Class: Verquest::Properties::OneOf

Inherits:
Base
  • Object
show all
Defined in:
lib/verquest/properties/one_of.rb

Overview

OneOf property type for polymorphic schemas

Implements JSON Schema's oneOf keyword for defining polymorphic request structures where exactly one of multiple schemas must match. Supports optional discriminator-based schema selection using a property value to determine which schema applies.

According to JSON Schema specification, oneOf validates that the data is valid against exactly one of the subschemas. The discriminator is an OpenAPI extension that helps with efficient schema resolution but is not required for basic oneOf validation.

When used at the root level (without a name), it creates a "combination schema" where the entire request body can match one of the defined schemas.

Examples:

Root-level oneOf with discriminator

one_of = Verquest::Properties::OneOf.new(discriminator: "type")
one_of.add(Verquest::Properties::Reference.new(name: "dog", from: DogComponent))
one_of.add(Verquest::Properties::Reference.new(name: "cat", from: CatComponent))

Nested oneOf property

one_of = Verquest::Properties::OneOf.new(
  name: :payment,
  discriminator: "method",
  required: true
)

oneOf without discriminator (pure JSON Schema validation)

one_of = Verquest::Properties::OneOf.new(name: :value)
# Validates that exactly one schema matches

Constant Summary collapse

NULL_TYPE_SCHEMA =

JSON Schema for null type, used when nullable is true

{"type" => "null"}.freeze

Instance Attribute Summary collapse

Attributes inherited from Base

#map, #name, #nullable, #required

Instance Method Summary collapse

Methods inherited from Base

#mapping_value_key, #mapping_value_prefix

Methods included from HelperMethods::RequiredProperties

#dependent_required_properties, #required_properties

Constructor Details

#initialize(name: nil, discriminator: nil, required: false, nullable: false, map: nil) ⇒ OneOf

Initialize a new OneOf property

Parameters:

  • name (String, Symbol, nil) (defaults to: nil)

    The property name, or nil for root-level oneOf

  • discriminator (String, Symbol, nil) (defaults to: nil)

    The property name used to discriminate between schemas. When omitted, the transformer infers the variant by validating against each schema.

  • required (Boolean, Array<Symbol>) (defaults to: false)

    Whether this property is required, or array of dependency names

  • nullable (Boolean) (defaults to: false)

    Whether this property can be null

  • map (String, nil) (defaults to: nil)

    The mapping path for this property



48
49
50
51
52
53
54
55
# File 'lib/verquest/properties/one_of.rb', line 48

def initialize(name: nil, discriminator: nil, required: false, nullable: false, map: nil)
  @name = name&.to_s
  @required = required
  @nullable = nullable
  @map = map
  @discriminator = discriminator&.to_s
  @schemas = {}
end

Instance Attribute Details

#discriminatorString? (readonly)

Returns The discriminator property name for schema selection.

Returns:

  • (String, nil)

    The discriminator property name for schema selection



38
39
40
# File 'lib/verquest/properties/one_of.rb', line 38

def discriminator
  @discriminator
end

#schemasObject (readonly, private)

Returns the value of attribute schemas.



138
139
140
# File 'lib/verquest/properties/one_of.rb', line 138

def schemas
  @schemas
end

Instance Method Details

#absolute_path?(path) ⇒ Boolean (private)

Checks if a path is absolute (starts with /)

Parameters:

  • path (String, nil)

    The path to check

Returns:

  • (Boolean)

    true if the path is absolute



337
338
339
# File 'lib/verquest/properties/one_of.rb', line 337

def absolute_path?(path)
  path&.start_with?("/")
end

#add(schema) ⇒ Verquest::Properties::Base

Add a schema option to this oneOf

Both Reference and Object properties are allowed at any level. Object properties define inline schemas directly within the oneOf.

Parameters:

Returns:

Raises:

  • (ArgumentError)

    If schema is neither a Reference nor an Object



65
66
67
68
69
70
71
# File 'lib/verquest/properties/one_of.rb', line 65

def add(schema)
  unless schema.is_a?(Verquest::Properties::Reference) || schema.is_a?(Verquest::Properties::Object)
    raise ArgumentError, "Must be a Reference or Object property"
  end

  schemas[schema.name] = schema
end

#add_discriminator_to_schema(schema) ⇒ void (private)

This method returns an undefined value.

Adds discriminator information to the schema if present

Only Reference schemas are included in the discriminator mapping since Objects don't have $ref. This follows OpenAPI spec where discriminator mapping contains only $ref strings.

Parameters:

  • schema (Hash)

    The schema to modify



412
413
414
415
416
417
418
419
# File 'lib/verquest/properties/one_of.rb', line 412

def add_discriminator_to_schema(schema)
  return unless discriminator

  schema["discriminator"] = {
    "propertyName" => discriminator,
    "mapping" => build_discriminator_mapping
  }
end

#build_discriminator_mappingHash (private)

Builds the discriminator mapping with $ref values

Only Reference schemas are included since Objects don't have $ref. This follows OpenAPI spec where discriminator mapping contains only $ref strings.

Returns:

  • (Hash)

    The discriminator value to $ref mapping



427
428
429
430
431
432
433
434
# File 'lib/verquest/properties/one_of.rb', line 427

def build_discriminator_mapping
  schemas.each_with_object({}) do |(name, schema), mapping|
    # Only include References in discriminator mapping (Objects don't have $ref)
    next unless schema.is_a?(Verquest::Properties::Reference)

    mapping[name] = schema.to_schema[name]["$ref"]
  end
end

#build_object_variant_mapping(mapping, schema, source_prefix, target_prefix, version) ⇒ void (private)

This method returns an undefined value.

Builds mapping for an inline Object variant

Unlike nested objects, inline oneOf variants map their properties directly under the oneOf property path (not under oneOf/variant_name). This mirrors how Reference variants work.

Parameters:

  • mapping (Hash)

    The mapping hash to populate

  • schema (Verquest::Properties::Object)

    The object schema

  • source_prefix (Array<String>)

    Source path prefix

  • target_prefix (Array<String>)

    Target path prefix

  • version (String, nil)

    The version for schema resolution



247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
# File 'lib/verquest/properties/one_of.rb', line 247

def build_object_variant_mapping(mapping, schema, source_prefix, target_prefix, version)
  object_mapping = {}
  object_map = schema.send(:map)
  variant_target_prefix = compute_reference_target_prefix(object_map, target_prefix)

  # Build mapping from object's child properties
  # Properties are mapped directly under source_prefix (not source_prefix + object_name)
  # to match how Reference variants work
  schema.send(:properties).each_value do |property|
    property.mapping(
      key_prefix: source_prefix,
      value_prefix: variant_target_prefix,
      mapping: object_mapping,
      version: version
    )
  end

  mapping[schema.name] = object_mapping
end

#build_prefixed_mapping(base_mapping, source_prefix, target_prefix) ⇒ Hash (private)

Builds a mapping hash with prefixes applied to all keys and values

Parameters:

  • base_mapping (Hash)

    The source mapping from the referenced schema

  • source_prefix (Array<String>)

    Prefix for source keys

  • target_prefix (Array<String>)

    Prefix for target values

Returns:

  • (Hash)

    The mapping with prefixes applied



273
274
275
276
277
# File 'lib/verquest/properties/one_of.rb', line 273

def build_prefixed_mapping(base_mapping, source_prefix, target_prefix)
  base_mapping.each_with_object({}) do |(source_key, target_value), result|
    result[join_path(source_prefix, source_key)] = join_path(target_prefix, target_value)
  end
end

#build_reference_variant_mapping(mapping, schema, source_prefix, target_prefix, version) ⇒ void (private)

This method returns an undefined value.

Builds mapping for a Reference variant

Parameters:

  • mapping (Hash)

    The mapping hash to populate

  • schema (Verquest::Properties::Reference)

    The reference schema

  • source_prefix (Array<String>)

    Source path prefix

  • target_prefix (Array<String>)

    Target path prefix

  • version (String, nil)

    The version for schema resolution



223
224
225
226
227
228
229
230
231
232
233
# File 'lib/verquest/properties/one_of.rb', line 223

def build_reference_variant_mapping(mapping, schema, source_prefix, target_prefix, version)
  reference_mapping = schema.send(:from).mapping(version: version)
  reference_map = schema.send(:map)
  variant_target_prefix = compute_reference_target_prefix(reference_map, target_prefix)

  mapping[schema.name] = build_prefixed_mapping(
    reference_mapping,
    source_prefix,
    variant_target_prefix
  )
end

#build_schema_with_refsHash (private)

Builds the JSON schema structure with $ref references

Returns:

  • (Hash)

    Schema with oneOf array and optional discriminator



367
368
369
370
371
# File 'lib/verquest/properties/one_of.rb', line 367

def build_schema_with_refs
  schema = {schema_keyword => collect_schema_refs}
  add_discriminator_to_schema(schema)
  schema
end

#build_validation_schema(version:) ⇒ Hash (private)

Builds the validation schema structure with inline definitions

Unlike the documentation schema, the validation schema omits the discriminator since it's an OpenAPI extension that JSON Schema validators ignore. Validators validate against the oneOf array directly.

Parameters:

  • version (String, nil)

    The version to generate validation schema for

Returns:

  • (Hash)

    Validation schema with oneOf array (no discriminator)



381
382
383
# File 'lib/verquest/properties/one_of.rb', line 381

def build_validation_schema(version:)
  {schema_keyword => collect_inline_schemas(version)}
end

#build_variant_mappings(mapping, source_prefix, target_prefix, version) ⇒ void (private)

This method returns an undefined value.

Builds variant mappings for each schema option

Handles both Reference and Object schemas:

  • Reference: delegates to the referenced schema's mapping
  • Object: builds mapping from child properties directly

Parameters:

  • mapping (Hash)

    The mapping hash to populate

  • source_prefix (Array<String>)

    Source path prefix

  • target_prefix (Array<String>)

    Target path prefix

  • version (String, nil)

    The version for schema resolution



205
206
207
208
209
210
211
212
213
# File 'lib/verquest/properties/one_of.rb', line 205

def build_variant_mappings(mapping, source_prefix, target_prefix, version)
  schemas.each_value do |schema|
    if schema.is_a?(Verquest::Properties::Reference)
      build_reference_variant_mapping(mapping, schema, source_prefix, target_prefix, version)
    elsif schema.is_a?(Verquest::Properties::Object)
      build_object_variant_mapping(mapping, schema, source_prefix, target_prefix, version)
    end
  end
end

#collect_inline_schemas(version) ⇒ Array<Hash> (private)

Collects inline schema definitions for all variants

Parameters:

  • version (String, nil)

    The version for schema resolution

Returns:

  • (Array<Hash>)

    Array of inline schema definitions



398
399
400
401
402
# File 'lib/verquest/properties/one_of.rb', line 398

def collect_inline_schemas(version)
  inline_schemas = schemas.values.map { |schema| schema.to_validation_schema(version: version)[schema.name] }
  inline_schemas << NULL_TYPE_SCHEMA if nullable
  inline_schemas
end

#collect_schema_refsArray<Hash> (private)

Collects $ref schema references for all variants

Returns:

  • (Array<Hash>)

    Array of schema references



388
389
390
391
392
# File 'lib/verquest/properties/one_of.rb', line 388

def collect_schema_refs
  refs = schemas.values.map { |schema| schema.to_schema[schema.name] }
  refs << NULL_TYPE_SCHEMA if nullable
  refs
end

#compute_reference_target_prefix(reference_map, base_prefix) ⇒ Array<String> (private)

Computes the target prefix for a specific reference's mapping

Parameters:

  • reference_map (String, nil)

    The map parameter from the reference

  • base_prefix (Array<String>)

    The base value prefix

Returns:

  • (Array<String>)

    The target prefix as an array of path segments



187
188
189
190
191
192
# File 'lib/verquest/properties/one_of.rb', line 187

def compute_reference_target_prefix(reference_map, base_prefix)
  return base_prefix if reference_map.nil?
  return parse_absolute_path(reference_map) if absolute_path?(reference_map)

  base_prefix + parse_relative_path(reference_map)
end

#compute_source_prefix(key_prefix) ⇒ Array<String> (private)

Computes the source path prefix for mapping keys

Parameters:

  • key_prefix (Array<String>)

    The current key prefix

Returns:

  • (Array<String>)

    The effective key prefix including the property name if present



167
168
169
# File 'lib/verquest/properties/one_of.rb', line 167

def compute_source_prefix(key_prefix)
  root_level? ? key_prefix : key_prefix + [name]
end

#compute_target_prefix(value_prefix) ⇒ Array<String> (private)

Computes the target path prefix for mapping values

Parameters:

  • value_prefix (Array<String>)

    The current value prefix

Returns:

  • (Array<String>)

    The effective value prefix based on map or name



175
176
177
178
179
180
# File 'lib/verquest/properties/one_of.rb', line 175

def compute_target_prefix(value_prefix)
  return parse_absolute_path(@map) if absolute_path?(@map)
  return value_prefix + parse_relative_path(@map) if @map

  root_level? ? value_prefix : value_prefix + [name]
end

#freeze_schemasvoid (private)

This method returns an undefined value.

Freezes the schemas hash to prevent further modifications This is called on first read access to ensure immutability after setup



144
145
146
# File 'lib/verquest/properties/one_of.rb', line 144

def freeze_schemas
  schemas.freeze unless schemas.frozen?
end

#join_path(prefix, suffix) ⇒ String (private)

Joins path segments into a slash-separated path string

Parameters:

  • prefix (Array<String>)

    The path prefix segments

  • suffix (String)

    The path suffix

Returns:

  • (String)

    The combined path



329
330
331
# File 'lib/verquest/properties/one_of.rb', line 329

def join_path(prefix, suffix)
  prefix.empty? ? suffix : "#{prefix.join("/")}/#{suffix}"
end

#mapping(key_prefix:, value_prefix:, mapping:, version: nil) ⇒ void

This method returns an undefined value.

Create mapping for this oneOf property

For oneOf schemas, the mapping is keyed by discriminator value so the transformer can select the appropriate mapping based on the input. Each discriminator value maps to a hash of source => target path mappings.

For nested oneOf (with a name), the property name is included in the path prefixes. For root-level oneOf (name is nil), paths start from the root.

The map parameter on oneOf affects the target path prefix for all contained schemas.

When no discriminator is set, the transformer will infer the variant by validating the input against each schema and selecting the one that matches.

Parameters:

  • key_prefix (Array<String>)

    Prefix for the source key paths

  • value_prefix (Array<String>)

    Prefix for the target value paths

  • mapping (Hash)

    The mapping hash to be updated (discriminator value => path mappings)

  • version (String, nil) (defaults to: nil)

    The version to create mapping for



112
113
114
115
116
117
118
119
120
121
# File 'lib/verquest/properties/one_of.rb', line 112

def mapping(key_prefix:, value_prefix:, mapping:, version: nil)
  freeze_schemas
  source_prefix = compute_source_prefix(key_prefix)
  target_prefix = compute_target_prefix(value_prefix)

  build_variant_mappings(mapping, source_prefix, target_prefix, version)
  store_discriminator_path(mapping, source_prefix)
  store_variant_schemas(mapping, version) unless discriminator
  (mapping, target_prefix:) if nullable
end

#parse_absolute_path(path) ⇒ Array<String> (private)

Parses an absolute path into segments

Parameters:

  • path (String)

    The absolute path to parse

Returns:

  • (Array<String>)

    The path segments



345
346
347
# File 'lib/verquest/properties/one_of.rb', line 345

def parse_absolute_path(path)
  path.delete_prefix("/").split("/").reject(&:empty?)
end

#parse_relative_path(path) ⇒ Array<String> (private)

Parses a relative path into segments

Parameters:

  • path (String)

    The relative path to parse

Returns:

  • (Array<String>)

    The path segments



353
354
355
# File 'lib/verquest/properties/one_of.rb', line 353

def parse_relative_path(path)
  path.split("/")
end

#root_level?Boolean (private)

Check if this is a root-level oneOf (no property name)

Returns:

  • (Boolean)

    true if this oneOf is at the root level



151
152
153
# File 'lib/verquest/properties/one_of.rb', line 151

def root_level?
  name.nil?
end

#schema_keywordString (private)

Returns the JSON Schema keyword for this property type

Returns:

  • (String)

    Always returns "oneOf"



360
361
362
# File 'lib/verquest/properties/one_of.rb', line 360

def schema_keyword
  "oneOf"
end

#store_discriminator_path(mapping, source_prefix) ⇒ void (private)

This method returns an undefined value.

Stores the discriminator path in the mapping for nested oneOf

For nested oneOf (with a name) or oneOf inside a collection (source_prefix is not empty), stores the discriminator path so the transformer knows where to look for the value.

Parameters:

  • mapping (Hash)

    The mapping hash to update

  • source_prefix (Array<String>)

    The source path prefix



287
288
289
290
291
292
293
# File 'lib/verquest/properties/one_of.rb', line 287

def store_discriminator_path(mapping, source_prefix)
  return unless discriminator
  # Skip only for true root-level oneOf (no name AND no prefix from collection)
  return if root_level? && source_prefix.empty?

  mapping["_discriminator"] = join_path(source_prefix, discriminator)
end

#store_nullable_metadata(mapping, target_prefix:) ⇒ void (private)

This method returns an undefined value.

Stores nullable metadata in the mapping

When nullable is true, the transformer needs to know to allow null values without attempting variant resolution.

Parameters:

  • mapping (Hash)

    The mapping hash to update

  • target_prefix (Array<String>)

    The target path prefix for the oneOf property



316
317
318
319
320
321
322
# File 'lib/verquest/properties/one_of.rb', line 316

def (mapping, target_prefix:)
  mapping["_nullable"] = true
  return if root_level?

  mapping["_nullable_path"] = name
  mapping["_nullable_target_path"] = target_prefix.join("/")
end

#store_variant_schemas(mapping, version) ⇒ void (private)

This method returns an undefined value.

Stores variant schemas in the mapping for schema-based inference

When no discriminator is set, the transformer needs access to the validation schemas to determine which variant matches the input data.

Parameters:

  • mapping (Hash)

    The mapping hash to update

  • version (String, nil)

    The version for schema resolution



303
304
305
306
# File 'lib/verquest/properties/one_of.rb', line 303

def store_variant_schemas(mapping, version)
  mapping["_variant_schemas"] = variant_schemas(version: version)
  mapping["_variant_path"] = name unless root_level?
end

#to_schemaHash

Generate JSON schema definition for this oneOf property

Returns:

  • (Hash)

    The schema definition with oneOf array and optional discriminator



76
77
78
79
# File 'lib/verquest/properties/one_of.rb', line 76

def to_schema
  freeze_schemas
  wrap_schema(build_schema_with_refs)
end

#to_validation_schema(version: nil) ⇒ Hash

Generate validation schema for this oneOf property

Unlike to_schema which uses $ref, the validation schema includes the full inline schema definitions for each option.

Parameters:

  • version (String, nil) (defaults to: nil)

    The version to generate validation schema for

Returns:

  • (Hash)

    The validation schema with inline schema definitions



88
89
90
91
# File 'lib/verquest/properties/one_of.rb', line 88

def to_validation_schema(version: nil)
  freeze_schemas
  wrap_schema(build_validation_schema(version: version))
end

#variant_schemas(version: nil) ⇒ Hash<String, Hash>

Returns validation schemas for all variants

Used by the Transformer to infer which variant matches when no discriminator is set.

Parameters:

  • version (String, nil) (defaults to: nil)

    The version for schema resolution

Returns:

  • (Hash<String, Hash>)

    Variant name => validation schema mapping



129
130
131
132
133
134
# File 'lib/verquest/properties/one_of.rb', line 129

def variant_schemas(version: nil)
  freeze_schemas
  schemas.each_with_object({}) do |(name, schema), result|
    result[name] = schema.to_validation_schema(version: version)[schema.name]
  end
end

#wrap_schema(schema) ⇒ Hash (private)

Wraps the schema hash with the property name if present

Parameters:

  • schema (Hash)

    The schema to wrap

Returns:

  • (Hash)

    The schema, optionally wrapped with the property name



159
160
161
# File 'lib/verquest/properties/one_of.rb', line 159

def wrap_schema(schema)
  root_level? ? schema : {name => schema}
end