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



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

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



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

def mapping
  @mapping
end

#path_cacheHash (readonly, private)

Returns Cache for parsed paths.

Returns:

  • (Hash)

    Cache for parsed paths



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

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

#schemer_cacheObject (readonly, private)

Returns the value of attribute schemer_cache.



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

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

Instance Method Details

#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 symbol keys



90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
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
132
133
134
135
136
137
138
139
# 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

  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)

    # Extract value using the source path
    value = extract_value(params, source_parts)

    if value.equal?(NOT_FOUND)
      # Check if a parent of the source path is explicitly null
      null_depth = find_null_parent_depth(params, source_parts)
      if null_depth
        # Calculate the corresponding target depth by preserving the same
        # number of trailing path parts that come after the null position
        parts_after_null = source_parts.length - null_depth
        target_null_depth = target_parts.length - parts_after_null
        target_prefix = target_parts[0...target_null_depth].map { |p| p[:key] }.join("/")
        null_parent_targets[target_prefix] = true
      end
      next
    end

    # Set the extracted value at the target path
    set_value(result, target_parts, value)
  end

  # Preserve null parents in the result
  null_parent_targets.each_key do |target_prefix|
    target_parts = parse_path(target_prefix)
    # Only set null if the path doesn't already exist with a value
    existing = extract_value(result, target_parts)
    set_value(result, target_parts, nil) if existing.equal?(NOT_FOUND)
  end

  result
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



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

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



460
461
462
# File 'lib/verquest/transformer.rb', line 460

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



761
762
763
# File 'lib/verquest/transformer.rb', line 761

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



372
373
374
375
376
377
# File 'lib/verquest/transformer.rb', line 372

def extract_base_mapping_without_one_of(one_of_property)
  sample_variant = mapping.find { |k, v| !k.start_with?("_") && v.is_a?(Hash) }
  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



644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
# File 'lib/verquest/transformer.rb', line 644

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



628
629
630
631
632
633
634
# File 'lib/verquest/transformer.rb', line 628

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_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



891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
# File 'lib/verquest/transformer.rb', line 891

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



411
412
413
414
415
416
417
# File 'lib/verquest/transformer.rb', line 411

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



426
427
428
429
430
431
432
433
# File 'lib/verquest/transformer.rb', line 426

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



320
321
322
323
324
325
326
327
328
# File 'lib/verquest/transformer.rb', line 320

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



162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
# File 'lib/verquest/transformer.rb', line 162

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

#multiple_one_of?Boolean (private)

Checks if this mapping has multiple oneOf definitions

Returns:

  • (Boolean)

    True if _oneOfs array is present



221
222
223
# File 'lib/verquest/transformer.rb', line 221

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



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

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



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

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



875
876
877
878
879
880
881
882
883
# File 'lib/verquest/transformer.rb', line 875

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



863
864
865
866
867
868
# File 'lib/verquest/transformer.rb', line 863

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



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

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



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

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



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

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



848
849
850
851
852
853
854
855
856
857
858
# File 'lib/verquest/transformer.rb', line 848

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



348
349
350
351
352
353
# File 'lib/verquest/transformer.rb', line 348

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:



384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
# File 'lib/verquest/transformer.rb', line 384

def resolve_by_schema_inference(params)
  # Check if oneOf property is absent (optional oneOf)
  variant_path = mapping["_variant_path"]
  if variant_path && one_of_property_absent?(params, variant_path)
    return extract_base_mapping_without_one_of(variant_path)
  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



193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
# File 'lib/verquest/transformer.rb', line 193

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)

    nil
  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



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

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



270
271
272
273
274
275
# File 'lib/verquest/transformer.rb', line 270

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:



283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
# File 'lib/verquest/transformer.rb', line 283

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



254
255
256
257
258
259
260
261
262
# File 'lib/verquest/transformer.rb', line 254

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



334
335
336
337
338
339
340
341
# File 'lib/verquest/transformer.rb', line 334

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



929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
# File 'lib/verquest/transformer.rb', line 929

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



554
555
556
557
558
559
560
561
562
# File 'lib/verquest/transformer.rb', line 554

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 = mapping.find { |k, v| !k.start_with?("_") && v.is_a?(Hash) }
  return source_path unless sample_variant

  sample_target = sample_variant[1].values.first
  sample_target.split("[]").first
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



571
572
573
574
575
576
577
# File 'lib/verquest/transformer.rb', line 571

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



475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
# File 'lib/verquest/transformer.rb', line 475

def transform_collection_with_one_of(params)
  # Find the collection path from the variant mappings
  sample_variant = mapping.find { |k, v| !k.start_with?("_") && v.is_a?(Hash) }
  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



584
585
586
587
588
589
590
591
592
593
594
595
596
597
# File 'lib/verquest/transformer.rb', line 584

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



679
680
681
682
683
684
685
686
687
688
689
690
# File 'lib/verquest/transformer.rb', line 679

def transform_item_with_mapping(item, item_mapping)
  result = {}

  item_mapping.each do |source_path, target_path|
    value = extract_value(item, parse_path(source_path))
    next if value.equal?(NOT_FOUND)

    set_value(result, parse_path(target_path), value)
  end

  result
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



604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
# File 'lib/verquest/transformer.rb', line 604

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



510
511
512
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
540
541
542
543
544
545
546
547
548
# File 'lib/verquest/transformer.rb', line 510

def transform_non_collection_properties(params, result)
  null_parent_targets = {}

  mapping.each do |key, value|
    # Skip metadata keys and variant mappings (which are Hashes)
    next if key.start_with?("_")
    next if value.is_a?(Hash)

    source_parts = parse_path(key.to_s)
    target_parts = parse_path(value.to_s)

    # This is a simple key => value mapping for a root-level property
    extracted = extract_value(params, source_parts)

    if extracted.equal?(NOT_FOUND)
      # Check if a parent of the source path is explicitly null
      null_depth = find_null_parent_depth(params, source_parts)
      if null_depth
        # Calculate the corresponding target depth by preserving the same
        # number of trailing path parts that come after the null position
        parts_after_null = source_parts.length - null_depth
        target_null_depth = target_parts.length - parts_after_null
        target_prefix = target_parts[0...target_null_depth].map { |p| p[:key] }.join("/")
        null_parent_targets[target_prefix] = true
      end
      next
    end

    set_value(result, target_parts, extracted)
  end

  # Preserve null parents in the result
  null_parent_targets.each_key do |target_prefix|
    target_parts = parse_path(target_prefix)
    # Only set null if the path doesn't already exist with a value
    existing = extract_value(result, target_parts)
    set_value(result, target_parts, nil) if existing.equal?(NOT_FOUND)
  end
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



790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
# File 'lib/verquest/transformer.rb', line 790

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
    result = {}
    null_parent_targets = {}

    # Get any variant to extract the non-oneOf property mappings
    sample_variant = mapping.find { |k, v| !k.start_with?("_") && v.is_a?(Hash) }
    if sample_variant
      variant_mapping = sample_variant[1]
      variant_mapping.each do |source_path, target_path|
        # Skip paths that belong to the oneOf property (start with nullable_path/)
        next if source_path.start_with?("#{nullable_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)
          # Check if a parent of the source path is explicitly null
          null_depth = find_null_parent_depth(params, source_parts)
          if null_depth
            # Calculate the corresponding target depth by preserving the same
            # number of trailing path parts that come after the null position
            parts_after_null = source_parts.length - null_depth
            target_null_depth = target_parts.length - parts_after_null
            target_prefix = target_parts[0...target_null_depth].map { |p| p[:key] }.join("/")
            null_parent_targets[target_prefix] = true
          end
          next
        end

        set_value(result, target_parts, value)
      end
    end

    # Preserve null parents in the result
    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

    # 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



467
468
469
# File 'lib/verquest/transformer.rb', line 467

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



438
439
440
# File 'lib/verquest/transformer.rb', line 438

def variant_schemas
  mapping["_variant_schemas"]
end