Class: Verquest::Transformer

Inherits:
Object
  • Object
show all
Defined in:
lib/verquest/transformer.rb

Overview

Transforms parameters based on path mappings

The Transformer class handles the conversion of parameter structures based on a mapping of source paths to target paths. It supports deep nested structures, array notations, and complex path expressions using slash notation.

Examples:

Basic transformation

mapping = {
  "user/firstName" => "user/first_name",
  "user/lastName" => "user/last_name",
  "addresses[]/zip" => "addresses[]/postal_code"
}

transformer = Verquest::Transformer.new(mapping: mapping)
result = transformer.call({
  user: {
    firstName: "John",
    lastName: "Doe"
  },
  addresses: [
    { zip: "12345" },
    { zip: "67890" }
  ]
})

# Result will be:
# {
#   user: {
#     first_name: "John",
#     last_name: "Doe"
#   },
#   addresses: [
#     { postal_code: "12345" },
#     { postal_code: "67890" }
#   ]
# }

Discriminator-based transformation (oneOf)

# For oneOf schemas with a discriminator, the mapping is keyed by discriminator value
mapping = {
  "dog" => { "name" => "name", "bark" => "bark" },
  "cat" => { "name" => "name", "meow" => "meow" }
}

transformer = Verquest::Transformer.new(mapping: mapping, discriminator: "type")
result = transformer.call({ "type" => "dog", "name" => "Rex", "bark" => true })
# Uses the "dog" mapping

Schema-based variant inference (oneOf without discriminator)

# When no discriminator is present, the transformer infers the variant by validating
# against each schema and selecting the one that matches
mapping = {
  "_variant_schemas" => {
    "with_id" => { "type" => "object", "required" => ["id"], ... },
    "without_id" => { "type" => "object", ... }
  },
  "with_id" => { "id" => "id", "name" => "name" },
  "without_id" => { "name" => "name" }
}

transformer = Verquest::Transformer.new(mapping: mapping)
result = transformer.call({ "id" => "123", "name" => "Test" })
# Infers "with_id" variant and uses its mapping

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(mapping:, discriminator: nil) ⇒ Transformer

Creates a new Transformer with the specified mapping

Parameters:

  • mapping (Hash)

    A hash where keys are source paths and values are target paths, or for discriminator-based schemas, keys are discriminator values and values are mapping hashes

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

    The property name used to discriminate between schemas (for oneOf)



77
78
79
80
81
82
83
84
# File 'lib/verquest/transformer.rb', line 77

def initialize(mapping:, discriminator: nil)
  @mapping = mapping
  @discriminator = discriminator
  @path_cache = {} # Cache for parsed paths to improve performance
  @schemer_cache = {} # Cache for JSONSchemer instances
  precompile_paths # Prepare cache during initialization
  precompile_schemers # Prepare schemer cache during initialization
end

Instance Attribute Details

#discriminatorString? (readonly, private)

Returns The discriminator property name for oneOf schemas.

Returns:

  • (String, nil)

    The discriminator property name for oneOf schemas



115
# File 'lib/verquest/transformer.rb', line 115

attr_reader :mapping, :path_cache, :discriminator, :schemer_cache

#mappingHash (readonly, private)

Returns The source-to-target path mapping.

Returns:

  • (Hash)

    The source-to-target path mapping



115
116
117
# File 'lib/verquest/transformer.rb', line 115

def mapping
  @mapping
end

#path_cacheHash (readonly, private)

Returns Cache for parsed paths.

Returns:

  • (Hash)

    Cache for parsed paths



115
# File 'lib/verquest/transformer.rb', line 115

attr_reader :mapping, :path_cache, :discriminator, :schemer_cache

#schemer_cacheObject (readonly, private)

Returns the value of attribute schemer_cache.



115
# File 'lib/verquest/transformer.rb', line 115

attr_reader :mapping, :path_cache, :discriminator, :schemer_cache

Instance Method Details

#apply_mapping_with_null_propagation(active_mapping, params) ⇒ Hash (private)

Applies a mapping to params, extracting values and preserving null parents

When a source path’s parent is explicitly null, the corresponding target path is set to null in the result instead of being omitted.

Parameters:

  • active_mapping (Hash)

    Source-to-target path mapping

  • params (Hash)

    The input parameters

Returns:

  • (Hash)

    The transformed result



153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
# File 'lib/verquest/transformer.rb', line 153

def apply_mapping_with_null_propagation(active_mapping, params)
  result = {}
  null_parent_targets = {}

  active_mapping.each do |source_path, target_path|
    source_parts = parse_path(source_path.to_s)
    target_parts = parse_path(target_path.to_s)

    value = extract_value(params, source_parts)

    if value.equal?(NOT_FOUND)
      track_null_parent(params, source_parts, target_parts, null_parent_targets)
      next
    end

    set_value(result, target_parts, value)
  end

  flush_null_parents(result, null_parent_targets)

  result
end

#call(params) ⇒ Hash

Transforms input parameters according to the provided mapping

Parameters:

  • params (Hash)

    The input parameters to transform

Returns:

  • (Hash)

    The transformed parameters with string keys



90
91
92
93
94
95
96
97
98
99
100
101
102
103
# File 'lib/verquest/transformer.rb', line 90

def call(params)
  # Handle collection with oneOf (per-item variant inference)
  if collection_with_one_of?
    return transform_collection_with_one_of(params)
  end

  active_mapping = resolve_mapping(params)
  return {} if active_mapping.nil?

  # Handle nullable oneOf with null value
  return transform_null_value(params) if active_mapping == :null_value

  apply_mapping_with_null_propagation(active_mapping, params)
end

#collection_with_one_of?Boolean (private)

Checks if this is a collection with oneOf (requires per-item variant resolution)

Returns:

  • (Boolean)

    True if mapping contains array paths with variant mappings



483
484
485
486
487
488
489
490
491
492
493
# File 'lib/verquest/transformer.rb', line 483

def collection_with_one_of?
  # Check for discriminator-less oneOf with variant schemas
  has_schema_based = variant_schemas &&
    variant_mappings.any? { |_, m| m.keys.any? { |path| path.include?("[]") } }

  # Check for discriminator-based oneOf in collection (discriminator path contains [])
  has_discriminator_based = effective_discriminator&.include?("[]") &&
    variant_mappings.any? { |_, m| m.keys.any? { |path| path.include?("[]") } }

  has_schema_based || has_discriminator_based
end

#discriminator_in_collection?Boolean (private)

Checks if this uses a discriminator for collection items

Returns:

  • (Boolean)

    True if discriminator is inside a collection



498
499
500
# File 'lib/verquest/transformer.rb', line 498

def discriminator_in_collection?
  effective_discriminator&.include?("[]")
end

#effective_discriminatorString? (private)

Returns the effective discriminator path

Returns:

  • (String, nil)

    The discriminator path from constructor or mapping



763
764
765
# File 'lib/verquest/transformer.rb', line 763

def effective_discriminator
  discriminator || mapping["_discriminator"]
end

#extract_base_mapping_without_one_of(one_of_property) ⇒ Hash (private)

Extracts base mapping for non-oneOf properties when oneOf is absent

When the oneOf property is optional and not provided, we still need to transform the non-oneOf properties. This method extracts those mappings from any variant (they should all have the same non-oneOf properties).

Parameters:

  • one_of_property (String)

    The oneOf property name to exclude

Returns:

  • (Hash)

    Mapping containing only non-oneOf properties



405
406
407
408
409
410
# File 'lib/verquest/transformer.rb', line 405

def extract_base_mapping_without_one_of(one_of_property)
  sample_variant = first_variant_mapping
  return {} unless sample_variant

  sample_variant[1].reject { |k, _| k.start_with?("#{one_of_property}/") }
end

#extract_item_mapping(variant_name, collection_path) ⇒ Hash (private)

Extracts the item-level mapping from a variant mapping

Converts paths like “items[]/id” => “items[]/id” to “id” => “id” Also includes non-variant collection properties (e.g., fields alongside oneOf).

Parameters:

  • variant_name (String)

    The variant name

  • collection_path (String)

    The collection path prefix

Returns:

  • (Hash)

    The item-level mapping



655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
# File 'lib/verquest/transformer.rb', line 655

def extract_item_mapping(variant_name, collection_path)
  variant_mapping = mapping[variant_name]
  prefix = "#{collection_path}[]/"

  item_map = {}

  # Include non-variant properties from the collection (e.g., entry_id alongside oneOf)
  mapping.each do |source, target|
    next if source.start_with?("_")
    next if target.is_a?(Hash) # Skip variant mappings

    if source.start_with?(prefix)
      item_source = source.delete_prefix(prefix)
      item_target = target.start_with?(prefix) ? target.delete_prefix(prefix) : target
      item_map[item_source] = item_target
    end
  end

  # Include variant-specific properties
  variant_mapping.each do |source, target|
    if source.start_with?(prefix)
      item_source = source.delete_prefix(prefix)
      item_target = target.start_with?(prefix) ? target.delete_prefix(prefix) : target
      item_map[item_source] = item_target
    end
  end

  item_map
end

#extract_item_variant_data(item) ⇒ Hash (private)

Extracts the variant data from a collection item for schema validation

Parameters:

  • item (Hash)

    The collection item

Returns:

  • (Hash)

    The data to validate against variant schemas



639
640
641
642
643
644
645
# File 'lib/verquest/transformer.rb', line 639

def extract_item_variant_data(item)
  variant_path = mapping["_variant_path"]
  return item unless variant_path

  result = extract_value(item, parse_path(variant_path))
  result.equal?(NOT_FOUND) ? {} : result
end

#extract_non_one_of_mapping(nullable_path) ⇒ Hash (private)

Extracts non-oneOf property mappings from a variant for nullable oneOf handling

Parameters:

  • nullable_path (String)

    The oneOf property path to exclude

Returns:

  • (Hash)

    Mapping containing only non-oneOf properties



814
815
816
817
818
819
820
# File 'lib/verquest/transformer.rb', line 814

def extract_non_one_of_mapping(nullable_path)
  sample_variant = first_variant_mapping
  return {} unless sample_variant

  prefix = "#{nullable_path}/"
  sample_variant[1].reject { |source, _| source.start_with?(prefix) }
end

#extract_value(data, path_parts, index = 0) ⇒ Object, NOT_FOUND (private)

Extracts a value from nested data structure using the parsed path parts

Parameters:

  • data (Hash, Array, Object)

    The data to extract value from

  • path_parts (Array<Hash>)

    The parsed path parts

  • index (Integer) (defaults to: 0)

    Current position in path_parts (avoids array slicing)

Returns:

  • (Object, NOT_FOUND)

    The extracted value or NOT_FOUND if key doesn’t exist



875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
# File 'lib/verquest/transformer.rb', line 875

def extract_value(data, path_parts, index = 0)
  return data if index >= path_parts.length

  current_part = path_parts[index]
  key = current_part[:key]

  case data
  when Hash
    return NOT_FOUND unless data.key?(key.to_s)
    value = data[key.to_s]
    if current_part[:array] && value.is_a?(Array)
      # Process each object in the array separately
      value.map { |item| extract_value(item, path_parts, index + 1) }
    else
      extract_value(value, path_parts, index + 1)
    end
  when Array
    if current_part[:array]
      # Map through array elements with remaining path
      data.map { |item| extract_value(item, path_parts, index + 1) }
    else
      # Try to extract from each array element with the full path
      data.map { |item| extract_value(item, path_parts, index) }
    end
  else
    # If data is not a Hash or Array (e.g., nil, string, number), we cannot
    # traverse further to extract nested values. Return NOT_FOUND.
    NOT_FOUND
  end
end

#extract_variant_data(params) ⇒ Hash (private)

Extracts the data portion to validate for nested oneOf

Parameters:

  • params (Hash)

    The full input parameters

Returns:

  • (Hash)

    The data to validate against variant schemas



449
450
451
452
453
454
455
# File 'lib/verquest/transformer.rb', line 449

def extract_variant_data(params)
  variant_path = mapping["_variant_path"]
  return params unless variant_path

  result = extract_value(params, parse_path(variant_path))
  result.equal?(NOT_FOUND) ? {} : result
end

#find_matching_variants(data) ⇒ Array<String> (private)

Finds all variants whose schema validates the input

Uses cached JSONSchemer instances for performance and exits early if more than one match is found (ambiguous case).

Parameters:

  • data (Hash)

    The data to validate

Returns:

  • (Array<String>)

    Names of matching variants



464
465
466
467
468
469
470
471
# File 'lib/verquest/transformer.rb', line 464

def find_matching_variants(data)
  matches = []
  schemer_cache.each do |name, schemer|
    matches << name if schemer.valid?(data)
    break if matches.size > 1 # Early exit on ambiguity
  end
  matches
end

#find_matching_variants_for(data, variant_schemas) ⇒ Array<String> (private)

Finds matching variants for given data and schemas

Uses cached JSONSchemer instances for performance and exits early if more than one match is found (ambiguous case).

Parameters:

  • data (Hash)

    The data to validate

  • variant_schemas (Hash)

    The variant schemas to validate against

Returns:

  • (Array<String>)

    Names of matching variants



353
354
355
356
357
358
359
360
361
# File 'lib/verquest/transformer.rb', line 353

def find_matching_variants_for(data, variant_schemas)
  schemers = schemers_for(variant_schemas)
  matches = []
  schemers.each do |name, schemer|
    matches << name if schemer.valid?(data)
    break if matches.size > 1 # Early exit on ambiguity
  end
  matches
end

#find_null_parent_depth(params, path_parts) ⇒ Integer? (private)

Finds the depth of the first null parent in the path

Traverses the path parts and checks if any intermediate value is explicitly null (the key exists but the value is nil). Returns the depth (1-indexed) of the first null parent found, or nil if no null parent exists.

Parameters:

  • params (Hash)

    The input parameters

  • path_parts (Array<Hash>)

    The parsed path parts

Returns:

  • (Integer, nil)

    The depth of the null parent, or nil if none found



126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
# File 'lib/verquest/transformer.rb', line 126

def find_null_parent_depth(params, path_parts)
  current = params

  path_parts.each_with_index do |part, index|
    return nil unless current.is_a?(Hash)

    key = part[:key]
    return nil unless current.key?(key)

    value = current[key]
    # Found an explicit null - return depth (1-indexed, so index + 1)
    return index + 1 if value.nil?

    current = value
  end

  nil
end

#first_variant_mappingArray? (private)

Returns the first non-metadata variant mapping entry

Returns:

  • (Array, nil)

    The [key, value] pair of the first variant, or nil



825
826
827
# File 'lib/verquest/transformer.rb', line 825

def first_variant_mapping
  mapping.find { |k, v| !k.start_with?("_") && v.is_a?(Hash) }
end

#flush_null_parents(result, null_parent_targets) ⇒ void (private)

This method returns an undefined value.

Sets null values in result for tracked null parent targets

Parameters:

  • result (Hash)

    The result hash to update

  • null_parent_targets (Hash)

    Target paths to set null



204
205
206
207
208
209
210
# File 'lib/verquest/transformer.rb', line 204

def flush_null_parents(result, null_parent_targets)
  null_parent_targets.each_key do |target_prefix|
    target_parts = parse_path(target_prefix)
    existing = extract_value(result, target_parts)
    set_value(result, target_parts, nil) if existing.equal?(NOT_FOUND)
  end
end

#multiple_one_of?Boolean (private)

Checks if this mapping has multiple oneOf definitions

Returns:

  • (Boolean)

    True if _oneOfs array is present



254
255
256
# File 'lib/verquest/transformer.rb', line 254

def multiple_one_of?
  mapping.key?("_oneOfs")
end

#nullable_one_of_with_null_value?(params) ⇒ Boolean (private)

Checks if this is a nullable oneOf and the value is null

Parameters:

  • params (Hash)

    The input parameters

Returns:

  • (Boolean)

    True if nullable oneOf with null value



771
772
773
774
775
776
777
778
779
780
781
782
783
# File 'lib/verquest/transformer.rb', line 771

def nullable_one_of_with_null_value?(params)
  return false unless mapping["_nullable"]

  nullable_path = mapping["_nullable_path"]

  if nullable_path
    # Nested oneOf - check if the property exists and is null
    params.key?(nullable_path) && params[nullable_path].nil?
  else
    # Root-level oneOf - check if params itself is null
    params.nil?
  end
end

#one_of_property_absent?(params, one_of_property) ⇒ Boolean (private)

Checks if the oneOf property is absent from params

Parameters:

  • params (Hash)

    The input parameters

  • one_of_property (String)

    The oneOf property name

Returns:

  • (Boolean)

    True if the oneOf property is not present in params



393
394
395
# File 'lib/verquest/transformer.rb', line 393

def one_of_property_absent?(params, one_of_property)
  !params.key?(one_of_property)
end

#parse_path(path) ⇒ Array<Hash> (private)

Parses a slash-notation path into structured path parts Uses memoization for performance optimization

Parameters:

  • path (String)

    The slash-notation path (e.g., “user/address/street”)

Returns:

  • (Array<Hash>)

    Array of frozen path parts with :key and :array attributes



859
860
861
862
863
864
865
866
867
# File 'lib/verquest/transformer.rb', line 859

def parse_path(path)
  path_cache[path] ||= path.split("/").map do |part|
    if part.end_with?("[]")
      {key: part[0...-2], array: true}.freeze
    else
      {key: part, array: false}.freeze
    end
  end.freeze
end

#precompile_flat_mappingvoid (private)

This method returns an undefined value.

Precompiles paths for flat (non-discriminator) mappings



847
848
849
850
851
852
# File 'lib/verquest/transformer.rb', line 847

def precompile_flat_mapping
  mapping.each do |source_path, target_path|
    parse_path(source_path.to_s)
    parse_path(target_path.to_s)
  end
end

#precompile_multiple_one_of_pathsvoid (private)

This method returns an undefined value.

Precompiles paths for multiple oneOf mappings



713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
# File 'lib/verquest/transformer.rb', line 713

def precompile_multiple_one_of_paths
  # Precompile base property paths
  mapping.each do |key, value|
    next if key == "_oneOfs"
    next if key.start_with?("_")

    parse_path(key.to_s)
    parse_path(value.to_s)
  end

  # Precompile each oneOf's paths
  mapping["_oneOfs"].each do |one_of_mapping|
    parse_path(one_of_mapping["_discriminator"]) if one_of_mapping["_discriminator"]
    parse_path(one_of_mapping["_variant_path"]) if one_of_mapping["_variant_path"]

    one_of_mapping.each do |key, variant_mapping|
      next if key.start_with?("_")
      next unless variant_mapping.is_a?(Hash)

      variant_mapping.each do |source_path, target_path|
        parse_path(source_path.to_s)
        parse_path(target_path.to_s)
      end
    end
  end
end

#precompile_pathsvoid (private)

This method returns an undefined value.

Precompiles all paths from the mapping to improve performance This is called during initialization to prepare the cache



698
699
700
701
702
703
704
705
706
707
708
# File 'lib/verquest/transformer.rb', line 698

def precompile_paths
  if multiple_one_of?
    precompile_multiple_one_of_paths
  elsif effective_discriminator || variant_schemas
    parse_path(effective_discriminator) if effective_discriminator
    parse_path(mapping["_variant_path"]) if mapping["_variant_path"]
    precompile_variant_mappings
  else
    precompile_flat_mapping
  end
end

#precompile_schemersvoid (private)

This method returns an undefined value.

Precompiles JSONSchemer instances for variant schemas This is called during initialization to prepare the schemer cache



744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
# File 'lib/verquest/transformer.rb', line 744

def precompile_schemers
  # Precompile for single oneOf
  variant_schemas&.each do |name, schema|
    schemer_cache[name] = JSONSchemer.schema(schema)
  end

  # Precompile for multiple oneOf
  return unless multiple_one_of?

  mapping["_oneOfs"].each do |one_of_mapping|
    next unless one_of_mapping["_variant_schemas"]

    schemers_for(one_of_mapping["_variant_schemas"])
  end
end

#precompile_variant_mappingsvoid (private)

This method returns an undefined value.

Precompiles paths for discriminator-based variant mappings



832
833
834
835
836
837
838
839
840
841
842
# File 'lib/verquest/transformer.rb', line 832

def precompile_variant_mappings
  mapping.each do |key, variant_mapping|
    next if key.start_with?("_") # Skip metadata keys like _discriminator, _variant_schemas, _variant_path
    next unless variant_mapping.is_a?(Hash)

    variant_mapping.each do |source_path, target_path|
      parse_path(source_path.to_s)
      parse_path(target_path.to_s)
    end
  end
end

#resolve_by_discriminator(params, disc_path) ⇒ Hash? (private)

Resolves variant mapping using discriminator value

Parameters:

  • params (Hash)

    The input parameters

  • disc_path (String)

    The discriminator path

Returns:

  • (Hash, nil)

    The resolved mapping



381
382
383
384
385
386
# File 'lib/verquest/transformer.rb', line 381

def resolve_by_discriminator(params, disc_path)
  discriminator_value = extract_value(params, parse_path(disc_path))
  return nil if discriminator_value.equal?(NOT_FOUND) || discriminator_value.nil?

  mapping[discriminator_value.to_s] || mapping[discriminator_value]
end

#resolve_by_schema_inference(params) ⇒ Hash? (private)

Resolves variant mapping by validating against each schema

Parameters:

  • params (Hash)

    The input parameters

Returns:

  • (Hash, nil)

    The resolved mapping

Raises:



417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
# File 'lib/verquest/transformer.rb', line 417

def resolve_by_schema_inference(params)
  # Check if oneOf property is absent or null (optional/nullable oneOf)
  variant_path = mapping["_variant_path"]
  if variant_path
    return extract_base_mapping_without_one_of(variant_path) if one_of_property_absent?(params, variant_path)

    if params.key?(variant_path) && params[variant_path].nil?
      base = extract_base_mapping_without_one_of(variant_path)
      return base.merge(variant_path => variant_path)
    end
  end

  data_to_validate = extract_variant_data(params)
  matching_variants = find_matching_variants(data_to_validate)

  case matching_variants.size
  when 0
    raise Verquest::MappingError, "No matching schema found for oneOf. " \
      "Input does not match any of the defined schemas."
  when 1
    mapping[matching_variants.first]
  else
    raise Verquest::MappingError, "Ambiguous oneOf match. " \
      "Input matches multiple schemas: #{matching_variants.join(", ")}. " \
      "Consider adding a discriminator or making schemas mutually exclusive."
  end
end

#resolve_mapping(params) ⇒ Hash? (private)

Resolves which mapping to use based on the discriminator value or schema inference

For nested oneOf, the discriminator can be a path (e.g., “payment/method”) and the mapping contains a “_discriminator” key with the path.

When no discriminator is present but “_variant_schemas” exists, the variant is inferred by validating the input against each schema.

For multiple oneOf, the mapping contains a “_oneOfs” array and base properties.

Parameters:

  • params (Hash)

    The input parameters

Returns:

  • (Hash, nil)

    The resolved mapping to use for transformation



224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
# File 'lib/verquest/transformer.rb', line 224

def resolve_mapping(params)
  # Handle multiple oneOf
  return resolve_multiple_one_of(params) if multiple_one_of?

  # Handle nullable oneOf - if value is null, skip variant resolution
  return :null_value if nullable_one_of_with_null_value?(params)

  disc_path = effective_discriminator
  return mapping unless disc_path || variant_schemas

  if disc_path
    result = resolve_by_discriminator(params, disc_path)
    return result if result

    # If discriminator not found, check if oneOf property is absent
    # In that case, return base mapping without oneOf properties
    one_of_property = disc_path.split("/").first
    return extract_base_mapping_without_one_of(one_of_property) if one_of_property_absent?(params, one_of_property)

    disc_value = extract_value(params, parse_path(disc_path))
    raise Verquest::MappingError, "Unknown discriminator value \"#{disc_value}\" for oneOf. " \
      "No matching variant found."
  else
    resolve_by_schema_inference(params)
  end
end

#resolve_multiple_one_of(params) ⇒ Hash (private)

Resolves mapping for multiple oneOf by resolving each independently and combining

Parameters:

  • params (Hash)

    The input parameters

Returns:

  • (Hash)

    Combined mapping from base properties + all resolved variants



262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
# File 'lib/verquest/transformer.rb', line 262

def resolve_multiple_one_of(params)
  result = {}

  # Add base (non-oneOf) properties
  mapping.each do |key, value|
    next if key == "_oneOfs"
    next if key.start_with?("_")

    result[key] = value
  end

  # Resolve each oneOf and add its variant mapping
  mapping["_oneOfs"].each do |one_of_mapping|
    variant_mapping = resolve_single_one_of(params, one_of_mapping)
    result.merge!(variant_mapping) if variant_mapping
  end

  result
end

#resolve_one_of_by_discriminator(params, one_of_mapping, disc_path) ⇒ Hash? (private)

Resolves a oneOf variant using discriminator

Parameters:

  • params (Hash)

    The input parameters

  • one_of_mapping (Hash)

    The mapping for this oneOf

  • disc_path (String)

    The discriminator path

Returns:

  • (Hash, nil)

    The resolved variant mapping



303
304
305
306
307
308
# File 'lib/verquest/transformer.rb', line 303

def resolve_one_of_by_discriminator(params, one_of_mapping, disc_path)
  discriminator_value = extract_value(params, parse_path(disc_path))
  return nil if discriminator_value.equal?(NOT_FOUND) || discriminator_value.nil?

  one_of_mapping[discriminator_value.to_s] || one_of_mapping[discriminator_value]
end

#resolve_one_of_by_schema_inference(params, one_of_mapping) ⇒ Hash? (private)

Resolves a oneOf variant by schema inference

Parameters:

  • params (Hash)

    The input parameters

  • one_of_mapping (Hash)

    The mapping for this oneOf

Returns:

  • (Hash, nil)

    The resolved variant mapping

Raises:



316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
# File 'lib/verquest/transformer.rb', line 316

def resolve_one_of_by_schema_inference(params, one_of_mapping)
  variant_path = one_of_mapping["_variant_path"]
  variant_schemas = one_of_mapping["_variant_schemas"]

  data_to_validate = if variant_path
    extracted = extract_value(params, parse_path(variant_path))
    # If the oneOf field is not present, skip it (optional field)
    return nil if extracted.equal?(NOT_FOUND)

    extracted
  else
    params
  end

  matching_variants = find_matching_variants_for(data_to_validate, variant_schemas)

  case matching_variants.size
  when 0
    raise Verquest::MappingError, "No matching schema found for oneOf. " \
      "Input does not match any of the defined schemas."
  when 1
    one_of_mapping[matching_variants.first]
  else
    raise Verquest::MappingError, "Ambiguous oneOf match. " \
      "Input matches multiple schemas: #{matching_variants.join(", ")}. " \
      "Consider adding a discriminator or making schemas mutually exclusive."
  end
end

#resolve_single_one_of(params, one_of_mapping) ⇒ Hash? (private)

Resolves a single oneOf from the _oneOfs array

Parameters:

  • params (Hash)

    The input parameters

  • one_of_mapping (Hash)

    The mapping for this oneOf

Returns:

  • (Hash, nil)

    The resolved variant mapping



287
288
289
290
291
292
293
294
295
# File 'lib/verquest/transformer.rb', line 287

def resolve_single_one_of(params, one_of_mapping)
  disc_path = one_of_mapping["_discriminator"]

  if disc_path
    resolve_one_of_by_discriminator(params, one_of_mapping, disc_path)
  elsif one_of_mapping["_variant_schemas"]
    resolve_one_of_by_schema_inference(params, one_of_mapping)
  end
end

#schemers_for(variant_schemas) ⇒ Hash (private)

Returns cached schemers for a given variant_schemas hash

Parameters:

  • variant_schemas (Hash)

    The variant schemas

Returns:

  • (Hash)

    Cached schemer instances keyed by variant name



367
368
369
370
371
372
373
374
# File 'lib/verquest/transformer.rb', line 367

def schemers_for(variant_schemas)
  # Use object_id as cache key since variant_schemas is a frozen hash
  cache_key = variant_schemas.object_id
  @one_of_schemer_caches ||= {}
  @one_of_schemer_caches[cache_key] ||= variant_schemas.transform_values do |schema|
    JSONSchemer.schema(schema)
  end
end

#set_value(result, path_parts, value, index = 0) ⇒ Hash (private)

Sets a value in a result hash at the specified path

Parameters:

  • result (Hash)

    The result hash to modify

  • path_parts (Array<Hash>)

    The parsed path parts

  • value (Object)

    The value to set

  • index (Integer) (defaults to: 0)

    Current position in path_parts (avoids array slicing)

Returns:

  • (Hash)

    The modified result hash with string keys



913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
# File 'lib/verquest/transformer.rb', line 913

def set_value(result, path_parts, value, index = 0)
  return result if index >= path_parts.length

  current_part = path_parts[index]
  key = current_part[:key].to_s
  last_part = index == path_parts.length - 1

  if last_part
    result[key] = value
  elsif current_part[:array] && value.is_a?(Array)
    result[key] ||= []
    value.each_with_index do |v, i|
      next if v.equal?(NOT_FOUND) # Skip NOT_FOUND items in array
      result[key][i] ||= {}
      set_value(result[key][i], path_parts, v, index + 1)
      # Remove keys with NOT_FOUND values from each object
      result[key][i].delete_if { |_, val| val.equal?(NOT_FOUND) }
    end
    # Remove NOT_FOUND entries and compact the array
    result[key] = result[key].reject { |item| item.equal?(NOT_FOUND) }
  else
    result[key] ||= {}
    set_value(result[key], path_parts, value, index + 1)
    # Remove keys with NOT_FOUND values from nested object
    result[key].delete_if { |_, val| val.equal?(NOT_FOUND) }
  end
  result
end

#target_collection_path(source_path) ⇒ String (private)

Returns the target collection path, accounting for any mapping

Parameters:

  • source_path (String)

    The source collection path

Returns:

  • (String)

    The target collection path



565
566
567
568
569
570
571
572
573
# File 'lib/verquest/transformer.rb', line 565

def target_collection_path(source_path)
  # Check if there's a custom mapping for the collection path
  # by looking at the target paths in any variant
  sample_variant = first_variant_mapping
  return source_path unless sample_variant

  sample_target = sample_variant[1].values.first
  sample_target.split("[]").first
end

#track_null_parent(params, source_parts, target_parts, null_parent_targets) ⇒ void (private)

This method returns an undefined value.

Checks if a source path has a null parent and records the target path to nullify

Parameters:

  • params (Hash)

    The input parameters

  • source_parts (Array<Hash>)

    Parsed source path parts

  • target_parts (Array<Hash>)

    Parsed target path parts

  • null_parent_targets (Hash)

    Accumulator for target paths to set null



183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
# File 'lib/verquest/transformer.rb', line 183

def track_null_parent(params, source_parts, target_parts, null_parent_targets)
  null_depth = find_null_parent_depth(params, source_parts)
  return unless null_depth

  # Number of path segments after the null position in the source
  parts_after_null = source_parts.length - null_depth

  # The target depth at which to set null. When target is shallower than
  # the remaining source segments, clamp to the full target path.
  target_null_depth = [target_parts.length - parts_after_null, target_parts.length].min
  target_null_depth = [target_null_depth, 1].max

  target_prefix = target_parts[0...target_null_depth].map { |p| p[:key] }.join("/")
  null_parent_targets[target_prefix] = true
end

#transform_collection_item(item, collection_path) ⇒ Hash (private)

Transforms a single item from a collection using variant resolution

Uses discriminator if present, otherwise infers variant by schema validation.

Parameters:

  • item (Hash)

    The item to transform

  • collection_path (String)

    The path to the collection

Returns:

  • (Hash)

    The transformed item



582
583
584
585
586
587
588
# File 'lib/verquest/transformer.rb', line 582

def transform_collection_item(item, collection_path)
  if discriminator_in_collection?
    transform_item_with_discriminator(item, collection_path)
  else
    transform_item_with_schema_inference(item, collection_path)
  end
end

#transform_collection_with_one_of(params) ⇒ Hash (private)

Transforms a collection where each item may match different oneOf variants

Parameters:

  • params (Hash)

    The input parameters to transform

Returns:

  • (Hash)

    The transformed parameters



513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
# File 'lib/verquest/transformer.rb', line 513

def transform_collection_with_one_of(params)
  # Find the collection path from the variant mappings
  sample_variant = first_variant_mapping
  return {} unless sample_variant

  sample_path = sample_variant[1].keys.first
  collection_path = sample_path.split("[]").first

  result = {}

  # Transform non-collection properties first (root-level properties outside the oneOf)
  transform_non_collection_properties(params, result)

  # Extract the collection
  collection = extract_value(params, parse_path(collection_path))
  return result unless collection.is_a?(Array)

  # Transform each item in the collection
  transformed_items = collection.map do |item|
    transform_collection_item(item, collection_path)
  end

  # Set the transformed collection in the result
  set_value(result, parse_path(target_collection_path(collection_path)), transformed_items)

  result
end

#transform_item_with_discriminator(item, collection_path) ⇒ Hash (private)

Transforms an item using discriminator-based variant resolution

Parameters:

  • item (Hash)

    The item to transform

  • collection_path (String)

    The path to the collection

Returns:

  • (Hash)

    The transformed item



595
596
597
598
599
600
601
602
603
604
605
606
607
608
# File 'lib/verquest/transformer.rb', line 595

def transform_item_with_discriminator(item, collection_path)
  # Extract discriminator field name from the path (e.g., "pets[]/type" -> "type")
  disc_field = effective_discriminator.split("/").last
  disc_value = item[disc_field]

  return {} if disc_value.nil?

  variant_name = disc_value.to_s
  variant_mapping = mapping[variant_name]
  return {} unless variant_mapping

  item_mapping = extract_item_mapping(variant_name, collection_path)
  transform_item_with_mapping(item, item_mapping)
end

#transform_item_with_mapping(item, item_mapping) ⇒ Hash (private)

Transforms an item using a specific mapping

Parameters:

  • item (Hash)

    The item to transform

  • item_mapping (Hash)

    The mapping to use

Returns:

  • (Hash)

    The transformed item



690
691
692
# File 'lib/verquest/transformer.rb', line 690

def transform_item_with_mapping(item, item_mapping)
  apply_mapping_with_null_propagation(item_mapping, item)
end

#transform_item_with_schema_inference(item, collection_path) ⇒ Hash (private)

Transforms an item using schema-based variant inference

Parameters:

  • item (Hash)

    The item to transform

  • collection_path (String)

    The path to the collection

Returns:

  • (Hash)

    The transformed item



615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
# File 'lib/verquest/transformer.rb', line 615

def transform_item_with_schema_inference(item, collection_path)
  # When there's a variant_path, validate just that nested portion
  data_to_validate = extract_item_variant_data(item)
  matching_variants = find_matching_variants(data_to_validate)

  case matching_variants.size
  when 0
    raise Verquest::MappingError, "No matching schema found for oneOf. " \
      "Input does not match any of the defined schemas."
  when 1
    variant_name = matching_variants.first
    item_mapping = extract_item_mapping(variant_name, collection_path)
    transform_item_with_mapping(item, item_mapping)
  else
    raise Verquest::MappingError, "Ambiguous oneOf match. " \
      "Input matches multiple schemas: #{matching_variants.join(", ")}. " \
      "Consider adding a discriminator or making schemas mutually exclusive."
  end
end

#transform_non_collection_properties(params, result) ⇒ void (private)

This method returns an undefined value.

Transforms non-collection properties from the mapping

These are root-level properties that exist outside the variant-keyed mappings.

Parameters:

  • params (Hash)

    The input parameters

  • result (Hash)

    The result hash to update



548
549
550
551
552
553
554
555
556
557
558
559
# File 'lib/verquest/transformer.rb', line 548

def transform_non_collection_properties(params, result)
  non_collection_mapping = {}
  mapping.each do |key, value|
    next if key.start_with?("_")
    next if value.is_a?(Hash)

    non_collection_mapping[key] = value
  end

  merged = apply_mapping_with_null_propagation(non_collection_mapping, params)
  result.merge!(merged)
end

#transform_null_value(params) ⇒ Hash (private)

Transforms a null value for nullable oneOf

For nested oneOf, we need to transform non-oneOf properties as well, then add the null value for the oneOf property.

Parameters:

  • params (Hash)

    The input parameters

Returns:

  • (Hash)

    The result with null value at the appropriate path



792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
# File 'lib/verquest/transformer.rb', line 792

def transform_null_value(params)
  nullable_path = mapping["_nullable_path"]

  if nullable_path
    # Nested oneOf - transform non-oneOf properties plus null for the oneOf property
    non_one_of_mapping = extract_non_one_of_mapping(nullable_path)
    result = apply_mapping_with_null_propagation(non_one_of_mapping, params)

    # Add the null value for the oneOf property using target path (respects map: option)
    nullable_target_path = mapping["_nullable_target_path"] || nullable_path
    result[nullable_target_path] = nil
    result
  else
    # Root-level oneOf - return empty hash (null at root)
    {}
  end
end

#variant_mappingsHash (private)

Returns only the variant mapping entries (excludes metadata keys)

Returns:

  • (Hash)

    Hash of variant name => mapping pairs



505
506
507
# File 'lib/verquest/transformer.rb', line 505

def variant_mappings
  mapping.select { |key, value| !key.start_with?("_") && value.is_a?(Hash) }
end

#variant_schemasHash? (private)

Returns the variant schemas for schema-based inference

Returns:

  • (Hash, nil)

    The variant schemas hash or nil if not present



476
477
478
# File 'lib/verquest/transformer.rb', line 476

def variant_schemas
  mapping["_variant_schemas"]
end