Class: Verquest::Properties::OneOf
- 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.
Constant Summary collapse
- NULL_TYPE_SCHEMA =
JSON Schema for null type, used when nullable is true
{"type" => "null"}.freeze
Instance Attribute Summary collapse
-
#discriminator ⇒ String?
readonly
The discriminator property name for schema selection.
-
#schemas ⇒ Object
readonly
private
Returns the value of attribute schemas.
Attributes inherited from Base
#map, #name, #nullable, #required
Instance Method Summary collapse
-
#absolute_path?(path) ⇒ Boolean
private
Checks if a path is absolute (starts with /).
-
#add(schema) ⇒ Verquest::Properties::Base
Add a schema option to this oneOf.
-
#add_discriminator_to_schema(schema) ⇒ void
private
Adds discriminator information to the schema if present.
-
#build_discriminator_mapping ⇒ Hash
private
Builds the discriminator mapping with $ref values.
-
#build_object_variant_mapping(mapping, schema, source_prefix, target_prefix, version) ⇒ void
private
Builds mapping for an inline Object variant.
-
#build_prefixed_mapping(base_mapping, source_prefix, target_prefix) ⇒ Hash
private
Builds a mapping hash with prefixes applied to all keys and values.
-
#build_reference_variant_mapping(mapping, schema, source_prefix, target_prefix, version) ⇒ void
private
Builds mapping for a Reference variant.
-
#build_schema_with_refs ⇒ Hash
private
Builds the JSON schema structure with $ref references.
-
#build_validation_schema(version:) ⇒ Hash
private
Builds the validation schema structure with inline definitions.
-
#build_variant_mappings(mapping, source_prefix, target_prefix, version) ⇒ void
private
Builds variant mappings for each schema option.
-
#collect_inline_schemas(version) ⇒ Array<Hash>
private
Collects inline schema definitions for all variants.
-
#collect_schema_refs ⇒ Array<Hash>
private
Collects $ref schema references for all variants.
-
#compute_reference_target_prefix(reference_map, base_prefix) ⇒ Array<String>
private
Computes the target prefix for a specific reference's mapping.
-
#compute_source_prefix(key_prefix) ⇒ Array<String>
private
Computes the source path prefix for mapping keys.
-
#compute_target_prefix(value_prefix) ⇒ Array<String>
private
Computes the target path prefix for mapping values.
-
#freeze_schemas ⇒ void
private
Freezes the schemas hash to prevent further modifications This is called on first read access to ensure immutability after setup.
-
#initialize(name: nil, discriminator: nil, required: false, nullable: false, map: nil) ⇒ OneOf
constructor
Initialize a new OneOf property.
-
#join_path(prefix, suffix) ⇒ String
private
Joins path segments into a slash-separated path string.
-
#mapping(key_prefix:, value_prefix:, mapping:, version: nil) ⇒ void
Create mapping for this oneOf property.
-
#parse_absolute_path(path) ⇒ Array<String>
private
Parses an absolute path into segments.
-
#parse_relative_path(path) ⇒ Array<String>
private
Parses a relative path into segments.
-
#root_level? ⇒ Boolean
private
Check if this is a root-level oneOf (no property name).
-
#schema_keyword ⇒ String
private
Returns the JSON Schema keyword for this property type.
-
#store_discriminator_path(mapping, source_prefix) ⇒ void
private
Stores the discriminator path in the mapping for nested oneOf.
-
#store_nullable_metadata(mapping, target_prefix:) ⇒ void
private
Stores nullable metadata in the mapping.
-
#store_variant_schemas(mapping, version) ⇒ void
private
Stores variant schemas in the mapping for schema-based inference.
-
#to_schema ⇒ Hash
Generate JSON schema definition for this oneOf property.
-
#to_validation_schema(version: nil) ⇒ Hash
Generate validation schema for this oneOf property.
-
#variant_schemas(version: nil) ⇒ Hash<String, Hash>
Returns validation schemas for all variants.
-
#wrap_schema(schema) ⇒ Hash
private
Wraps the schema hash with the property name if present.
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
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
#discriminator ⇒ String? (readonly)
Returns The discriminator property name for schema selection.
38 39 40 |
# File 'lib/verquest/properties/one_of.rb', line 38 def discriminator @discriminator end |
#schemas ⇒ Object (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 /)
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.
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.
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_mapping ⇒ Hash (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.
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.
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
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
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_refs ⇒ Hash (private)
Builds the JSON schema structure with $ref references
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.
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
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
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_refs ⇒ Array<Hash> (private)
Collects $ref schema references for all variants
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
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
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
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_schemas ⇒ void (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
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.
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
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
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)
151 152 153 |
# File 'lib/verquest/properties/one_of.rb', line 151 def root_level? name.nil? end |
#schema_keyword ⇒ String (private)
Returns the JSON Schema keyword for this property type
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.
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.
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.
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_schema ⇒ Hash
Generate JSON schema definition for this oneOf property
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.
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.
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
159 160 161 |
# File 'lib/verquest/properties/one_of.rb', line 159 def wrap_schema(schema) root_level? ? schema : {name => schema} end |