Class: Verquest::Version

Inherits:
Object
  • Object
show all
Includes:
HelperMethods::RequiredProperties
Defined in:
lib/verquest/version.rb

Overview

Represents a specific version of an API request schema

The Version class manages the properties, schema generation, and mapping for a specific version of an API request. It holds the collection of properties that define the request structure and handles transforming between different property naming conventions.

Examples:

version = Verquest::Version.new(name: "2023-01")
version.add(Verquest::Properties::Field.new(name: :email, type: :string))
version.prepare

# Generate schema
schema = version.schema

# Get mapping
mapping = version.mapping

Instance Attribute Summary collapse

Instance Method Summary collapse

Methods included from HelperMethods::RequiredProperties

#dependent_required_properties, #required_properties

Constructor Details

#initialize(name:) ⇒ Version

Initialize a new Version instance

Parameters:

  • name (String)

    The name/identifier of the version



57
58
59
60
61
# File 'lib/verquest/version.rb', line 57

def initialize(name:)
  @name = name.to_s
  @schema_options = {}
  @properties = {}
end

Instance Attribute Details

#descriptionString

Returns Description of this version.

Returns:

  • (String)

    Description of this version



51
# File 'lib/verquest/version.rb', line 51

attr_accessor :schema_options, :description

#external_mappingObject (readonly)

Returns the value of attribute external_mapping.



44
# File 'lib/verquest/version.rb', line 44

attr_reader :name, :properties, :schema, :validation_schema, :mapping, :transformer, :external_mapping

#mappingHash (readonly)

Returns The mapping from schema property paths to internal paths.

Returns:

  • (Hash)

    The mapping from schema property paths to internal paths



44
# File 'lib/verquest/version.rb', line 44

attr_reader :name, :properties, :schema, :validation_schema, :mapping, :transformer, :external_mapping

#nameString (readonly)

Returns The name/identifier of the version (e.g., “2023-01”).

Returns:

  • (String)

    The name/identifier of the version (e.g., “2023-01”)



44
45
46
# File 'lib/verquest/version.rb', line 44

def name
  @name
end

#propertiesHash<Symbol, Verquest::Properties::Base> (readonly)

Returns The properties that define the version’s schema.

Returns:



44
# File 'lib/verquest/version.rb', line 44

attr_reader :name, :properties, :schema, :validation_schema, :mapping, :transformer, :external_mapping

#schemaHash (readonly)

Returns The generated JSON schema for this version.

Returns:

  • (Hash)

    The generated JSON schema for this version



44
# File 'lib/verquest/version.rb', line 44

attr_reader :name, :properties, :schema, :validation_schema, :mapping, :transformer, :external_mapping

#schema_optionsHash

Returns Additional JSON schema options for this version.

Returns:

  • (Hash)

    Additional JSON schema options for this version



51
52
53
# File 'lib/verquest/version.rb', line 51

def schema_options
  @schema_options
end

#transformerVerquest::Transformer (readonly)

Returns The transformer that applies the mapping.

Returns:



44
# File 'lib/verquest/version.rb', line 44

attr_reader :name, :properties, :schema, :validation_schema, :mapping, :transformer, :external_mapping

#validation_schemaHash (readonly)

Returns The schema used for request validation.

Returns:

  • (Hash)

    The schema used for request validation



44
# File 'lib/verquest/version.rb', line 44

attr_reader :name, :properties, :schema, :validation_schema, :mapping, :transformer, :external_mapping

Instance Method Details

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

Add a property to this version

Parameters:

Returns:



67
68
69
# File 'lib/verquest/version.rb', line 67

def add(property)
  properties[property.name] = property
end

#combination?Boolean

Check if this version is a combination schema (root-level oneOf)

A combination schema has a single root-level oneOf property (name is nil) where the entire request body matches one of the defined schemas.

Returns:

  • (Boolean)

    true if this is a combination schema



213
214
215
216
217
218
219
# File 'lib/verquest/version.rb', line 213

def combination?
  return @_combination if defined?(@_combination)

  @_combination = properties.values.count == 1 &&
    properties.values.first&.is_a?(Verquest::Properties::OneOf) &&
    properties.values.first.name.nil?
end

#combination_discriminatorString? (private)

Returns the discriminator property name for combination schemas

Returns:

  • (String, nil)

    The discriminator property name



489
490
491
492
493
# File 'lib/verquest/version.rb', line 489

def combination_discriminator
  return nil unless combination?

  properties.values.first.send(:discriminator)
end

#copy_from(version, exclude_properties: []) ⇒ void

This method returns an undefined value.

Copy properties from another version

Parameters:

  • version (Verquest::Version)

    The version to copy properties from

  • exclude_properties (Array<Symbol>) (defaults to: [])

    Names of properties to not copy

Raises:

  • (ArgumentError)

    If version is not a Verquest::Version instance



94
95
96
97
98
99
100
101
102
# File 'lib/verquest/version.rb', line 94

def copy_from(version, exclude_properties: [])
  raise ArgumentError, "Expected a Verquest::Version instance" unless version.is_a?(Version)

  version.properties.values.each do |property|
    next if exclude_properties.include?(property.name.to_sym)

    add(property)
  end
end

#has?(property_name) ⇒ Boolean

Check if this version has a property with the given name

Parameters:

  • property_name (Symbol, String)

    The name of the property to check

Returns:

  • (Boolean)

    true if the property exists, false otherwise



84
85
86
# File 'lib/verquest/version.rb', line 84

def has?(property_name)
  properties.key?(property_name.to_s)
end

#has_multiple_nested_one_of?Boolean

Check if this version has multiple nested oneOf properties

Returns:

  • (Boolean)

    true if there are multiple nested oneOf properties



231
232
233
# File 'lib/verquest/version.rb', line 231

def has_multiple_nested_one_of?
  nested_one_of_count > 1
end

#has_nested_one_of?Boolean

Check if this version has a nested oneOf property (oneOf with a name)

Returns:

  • (Boolean)

    true if there’s a nested oneOf property



224
225
226
# File 'lib/verquest/version.rb', line 224

def has_nested_one_of?
  nested_one_of_count > 0
end

#invert_multiple_one_of_mappingHash (private)

Inverts mapping for multiple nested oneOf

Returns:

  • (Hash)

    The inverted mapping



434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
# File 'lib/verquest/version.rb', line 434

def invert_multiple_one_of_mapping
  result = {}

  mapping.each do |key, value|
    if key == "_oneOfs"
      result["_oneOfs"] = value.map do |one_of_mapping|
        one_of_mapping.each_with_object({}) do |(k, v), inverted|
          inverted[k] = if k.start_with?("_")
            v
          else
            v.invert
          end
        end
      end
    elsif key.start_with?("_")
      result[key] = value
    else
      result[value] = key # Invert base properties
    end
  end

  result.freeze
end

#invert_single_one_of_mappingHash (private)

Inverts mapping for single nested oneOf

Returns:

  • (Hash)

    The inverted mapping



421
422
423
424
425
426
427
428
429
# File 'lib/verquest/version.rb', line 421

def invert_single_one_of_mapping
  mapping.each_with_object({}) do |(key, value), result|
    result[key] = if key.start_with?("_")
      value
    else
      value.invert
    end
  end.freeze
end

#map_params(params) ⇒ Hash

Map request parameters to internal representation using the transformer

Parameters:

  • params (Hash)

    The request parameters to map

Returns:

  • (Hash)

    The mapped parameters



203
204
205
# File 'lib/verquest/version.rb', line 203

def map_params(params)
  transformer.call(params)
end

#mapping_for(property) ⇒ Hash

Get the mapping for a specific property

Parameters:

  • property (Symbol, String)

    The property name to get the mapping for

Returns:

  • (Hash)

    The mapping for the property

Raises:



191
192
193
194
195
196
197
# File 'lib/verquest/version.rb', line 191

def mapping_for(property)
  raise PropertyNotFoundError.new("Property '#{property}' is not defined on '#{name}'") unless has?(property)

  {}.tap do |mapping|
    properties[property.to_s].mapping(key_prefix: [], value_prefix: [], mapping: mapping, version: name)
  end
end

#nested_one_of_countInteger

Returns the count of nested oneOf properties (computed once and cached)

Returns:

  • (Integer)

    Number of nested oneOf properties



238
239
240
241
242
243
244
# File 'lib/verquest/version.rb', line 238

def nested_one_of_count
  return @_nested_one_of_count if defined?(@_nested_one_of_count)

  @_nested_one_of_count = properties.values.count do |p|
    p.is_a?(Verquest::Properties::OneOf) && !p.name.nil?
  end
end

#preparevoid

This method returns an undefined value.

Prepare this version by generating schema and creating transformer



107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
# File 'lib/verquest/version.rb', line 107

def prepare
  return if frozen?

  unless schema_options.key?("additionalProperties")
    schema_options["additionalProperties"] = Verquest.configuration.default_additional_properties
  end

  schema_options.delete_if { |_, v| v.nil? }

  if combination?
    prepare_combination_schema
    prepare_combination_validation_schema
    prepare_combination_mapping
    prepare_combination_external_mapping
    @transformer = Transformer.new(mapping: mapping, discriminator: combination_discriminator)
  else
    prepare_schema
    prepare_validation_schema
    prepare_mapping
    prepare_external_mapping
    @transformer = Transformer.new(mapping: mapping)
  end

  freeze
end

#prepare_combination_external_mappingHash (private)

Prepares the inverted parameter mapping for combination schemas

For combination schemas, inverts each discriminator value’s mapping. Skips metadata keys that are not variant mappings.

Returns:

  • (Hash)

    The frozen inverted mapping for each discriminator value



475
476
477
478
479
480
481
482
483
484
# File 'lib/verquest/version.rb', line 475

def prepare_combination_external_mapping
  @external_mapping = mapping.each_with_object({}) do |(key, value), result|
    # Skip metadata keys, only invert variant mapping hashes
    result[key] = if key.start_with?("_")
      value
    else
      value.invert
    end
  end.freeze
end

#prepare_combination_mappingHash (private)

Prepares the parameter mapping for combination schemas (oneOf)

For combination schemas, the mapping is keyed by the discriminator value so the transformer can select the appropriate mapping based on the input.

Returns:

  • (Hash)

    A hash where keys are discriminator values and values are mapping hashes



464
465
466
467
# File 'lib/verquest/version.rb', line 464

def prepare_combination_mapping
  @mapping = {}
  properties.values.first.mapping(key_prefix: [], value_prefix: [], mapping: @mapping, version: name)
end

#prepare_combination_schemaHash (private)

Generates the JSON schema for combination schemas (oneOf at root level)

For combination schemas, the schema is delegated directly to the oneOf property since it represents the entire request structure.

Returns:

  • (Hash)

    The schema from the oneOf property



272
273
274
# File 'lib/verquest/version.rb', line 272

def prepare_combination_schema
  @schema = properties.values.first.to_schema
end

#prepare_combination_validation_schemaHash (private)

Generates the validation schema for combination schemas (oneOf at root level)

For combination schemas, the validation schema is delegated directly to the oneOf property, which includes inline schema definitions for each option.

Returns:

  • (Hash)

    The validation schema from the oneOf property



299
300
301
# File 'lib/verquest/version.rb', line 299

def prepare_combination_validation_schema
  @validation_schema = properties.values.first.to_validation_schema(version: name)
end

#prepare_external_mappingHash (private)

Prepares the inverted parameter mapping for this version

Inverts the standard mapping to create a reverse lookup from internal attribute names back to external parameter names. This is useful when transforming internal data back to the external API representation.

For nested oneOf schemas, inverts each discriminator value’s mapping. Skips metadata keys that are not variant mappings.

Returns:

  • (Hash)

    The frozen inverted mapping where keys are internal attribute paths and values are the corresponding external schema paths

See Also:



408
409
410
411
412
413
414
415
416
# File 'lib/verquest/version.rb', line 408

def prepare_external_mapping
  @external_mapping = if has_multiple_nested_one_of?
    invert_multiple_one_of_mapping
  elsif has_nested_one_of?
    invert_single_one_of_mapping
  else
    mapping.invert.freeze
  end
end

#prepare_flat_mapping(properties_list) ⇒ void (private)

This method returns an undefined value.

Prepares flat mapping for versions without nested oneOf

Parameters:

Raises:



384
385
386
387
388
389
390
391
392
393
394
# File 'lib/verquest/version.rb', line 384

def prepare_flat_mapping(properties_list)
  @mapping = properties_list.each_with_object({}) do |property, mapping|
    property.mapping(key_prefix: [], value_prefix: [], mapping: mapping, version: name)
  end

  seen = Set.new
  duplicates = mapping.values.select { |v| !seen.add?(v) }
  if duplicates.any?
    raise MappingError.new("Mapping must be unique. Found duplicates in version '#{name}': #{duplicates.uniq.join(", ")}")
  end
end

#prepare_mappingHash (private)

Prepares the parameter mapping for this version

Collects mappings from all properties in this version and checks for duplicate mappings, which would cause conflicts during transformation.

When nested oneOf properties are present, the mapping includes a _oneOfs array containing each oneOf’s metadata and variant mappings, plus base properties.

Returns:

  • (Hash)

    The mapping from schema property paths to internal paths

Raises:



313
314
315
316
317
318
319
320
321
322
323
324
325
# File 'lib/verquest/version.rb', line 313

def prepare_mapping
  # Separate oneOf properties from regular properties
  one_of_properties = properties.values.select { |p| p.is_a?(Verquest::Properties::OneOf) }
  regular_properties = properties.values.reject { |p| p.is_a?(Verquest::Properties::OneOf) }

  if one_of_properties.size == 1
    prepare_single_nested_one_of_mapping(one_of_properties.first, regular_properties)
  elsif one_of_properties.size > 1
    prepare_multiple_nested_one_of_mapping(one_of_properties, regular_properties)
  else
    prepare_flat_mapping(regular_properties)
  end
end

#prepare_multiple_nested_one_of_mapping(one_of_properties, regular_properties) ⇒ void (private)

This method returns an undefined value.

Prepares mapping for versions with multiple nested oneOf properties

Parameters:



363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
# File 'lib/verquest/version.rb', line 363

def prepare_multiple_nested_one_of_mapping(one_of_properties, regular_properties)
  @mapping = {}

  # Collect regular property mappings at root level
  regular_properties.each do |property|
    property.mapping(key_prefix: [], value_prefix: [], mapping: @mapping, version: name)
  end

  # Collect each oneOf's mapping into _oneOfs array
  @mapping["_oneOfs"] = one_of_properties.map do |one_of_property|
    one_of_mapping = {}
    one_of_property.mapping(key_prefix: [], value_prefix: [], mapping: one_of_mapping, version: name)
    one_of_mapping
  end
end

#prepare_schemaHash (private)

Generates the JSON schema for this version

Creates a schema object with type, description, required properties, and property definitions based on the properties in this version. The schema is frozen to prevent modification after preparation.

Returns:

  • (Hash)

    The frozen schema hash



255
256
257
258
259
260
261
262
263
264
# File 'lib/verquest/version.rb', line 255

def prepare_schema
  @schema = {
    "type" => "object",
    "required" => required_properties,
    "properties" => properties.transform_values { |property| property.to_schema[property.name] }
  }.merge(schema_options).tap do |schema|
    schema["dependentRequired"] = dependent_required_properties if dependent_required_properties.any?
    schema["description"] = description if description
  end.freeze
end

#prepare_single_nested_one_of_mapping(one_of_property, regular_properties) ⇒ void (private)

This method returns an undefined value.

Prepares mapping for versions with a single nested oneOf property (legacy format)

Parameters:



332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
# File 'lib/verquest/version.rb', line 332

def prepare_single_nested_one_of_mapping(one_of_property, regular_properties)
  # Collect regular property mappings
  regular_mapping = {}
  regular_properties.each do |property|
    property.mapping(key_prefix: [], value_prefix: [], mapping: regular_mapping, version: name)
  end

  # Collect oneOf property mappings
  one_of_mapping = {}
  one_of_property.mapping(key_prefix: [], value_prefix: [], mapping: one_of_mapping, version: name)

  # Merge regular mappings into each oneOf variant
  @mapping = {}

  # Preserve metadata keys
  %w[_discriminator _variant_schemas _variant_path _nullable _nullable_path _nullable_target_path].each do ||
    @mapping[] = one_of_mapping[] if one_of_mapping.key?()
  end

  one_of_mapping.each do |discriminator_value, variant_mapping|
    next if discriminator_value.start_with?("_") # Skip metadata keys

    @mapping[discriminator_value] = regular_mapping.merge(variant_mapping)
  end
end

#prepare_validation_schemaHash (private)

Generates the validation schema for this version

Similar to prepare_schema but specifically for validation purposes. The validation schema will include all referenced components and properties.

Returns:

  • (Hash)

    The frozen validation schema hash



282
283
284
285
286
287
288
289
290
291
# File 'lib/verquest/version.rb', line 282

def prepare_validation_schema
  @validation_schema = {
    "type" => "object",
    "required" => required_properties,
    "properties" => properties.transform_values { |property| property.to_validation_schema(version: name)[property.name] }
  }.merge(schema_options).tap do |schema|
    schema["dependentRequired"] = dependent_required_properties if dependent_required_properties.any?
    schema["description"] = description if description
  end.freeze
end

#remove(property_name) ⇒ Verquest::Properties::Base

Remove a property from this version by name

Parameters:

  • property_name (Symbol, String)

    The name of the property to remove

Returns:

Raises:



76
77
78
# File 'lib/verquest/version.rb', line 76

def remove(property_name)
  properties.delete(property_name.to_s) || raise(PropertyNotFoundError.new("Property '#{property_name}' is not defined on '#{name}'"))
end

#valid_schema?Boolean

Validate the schema against the metaschema

Returns:

  • (Boolean)

    true if the schema is valid, false otherwise



136
137
138
139
140
141
# File 'lib/verquest/version.rb', line 136

def valid_schema?
  JSONSchemer.valid_schema?(
    validation_schema,
    meta_schema: Verquest.configuration.json_schema_uri
  )
end

#validate_params(params:) ⇒ Array<Hash>

Validate request parameters against the version’s validation schema

Parameters:

  • params (Hash)

    The request parameters to validate

Returns:

  • (Array<Hash>)

    An array of validation error details, or empty if valid



169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
# File 'lib/verquest/version.rb', line 169

def validate_params(params:)
  schemer = JSONSchemer.schema(
    validation_schema,
    meta_schema: Verquest.configuration.json_schema_uri,
    insert_property_defaults: Verquest.configuration.insert_property_defaults
  )

  schemer.validate(params).map do |error|
    {
      pointer: error["data_pointer"],
      type: error["type"],
      message: error["error"],
      details: error["details"]
    }
  end
end

#validate_schemaArray<Hash>

Validate the schema against the metaschema and return detailed errors

This method validates the schema against the configured JSON Schema metaschema and returns detailed validation errors if any are found. It uses the JSONSchemer library with the schema version specified in the configuration.

Returns:

  • (Array<Hash>)

    An array of validation error details, empty if schema is valid

See Also:



151
152
153
154
155
156
157
158
159
160
161
162
163
# File 'lib/verquest/version.rb', line 151

def validate_schema
  JSONSchemer.validate_schema(
    validation_schema,
    meta_schema: Verquest.configuration.json_schema_uri
  ).map do |error|
    {
      pointer: error["data_pointer"],
      type: error["type"],
      message: error["error"],
      details: error["details"]
    }
  end
end