verquest 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,147 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Verquest
4
+ module Properties
5
+ # Collection property type for arrays of objects
6
+ #
7
+ # Represents an array of complex objects in the schema.
8
+ # Used for defining collections of structured data objects.
9
+ #
10
+ # @example Define a collection of items with inline properties
11
+ # products = Verquest::Properties::Collection.new(name: :products)
12
+ # products.add(Verquest::Properties::Field.new(name: :id, type: :string, required: true))
13
+ # products.add(Verquest::Properties::Field.new(name: :name, type: :string))
14
+ #
15
+ # @example Define a collection referencing an existing schema
16
+ # products = Verquest::Properties::Collection.new(
17
+ # name: :products,
18
+ # item: ProductRequest
19
+ # )
20
+ class Collection < Base
21
+ # Initialize a new Collection property
22
+ #
23
+ # @param name [Symbol] The name of the property
24
+ # @param item [Verquest::Base, nil] Optional reference to an external schema class
25
+ # @param required [Boolean] Whether this property is required
26
+ # @param map [String, nil] The mapping path for this property
27
+ # @param schema_options [Hash] Additional JSON schema options for this property
28
+ # @raise [ArgumentError] If attempting to map a collection to the root
29
+ def initialize(name:, item: nil, required: false, map: nil, **schema_options)
30
+ raise ArgumentError, "You can not map collection to the root" if map == "/"
31
+
32
+ @properties = {}
33
+
34
+ @name = name
35
+ @item = item
36
+ @required = required
37
+ @map = map
38
+ @schema_options = schema_options
39
+ end
40
+
41
+ # Add a child property to this collection's item definition
42
+ #
43
+ # @param property [Verquest::Properties::Base] The property to add to the collection items
44
+ # @return [Verquest::Properties::Base] The added property
45
+ def add(property)
46
+ properties[property.name] = property
47
+ end
48
+
49
+ # Check if this collection references an external item schema
50
+ #
51
+ # @return [Boolean] True if the collection uses an external reference
52
+ def has_item?
53
+ !item.nil?
54
+ end
55
+
56
+ # Generate JSON schema definition for this collection property
57
+ #
58
+ # @return [Hash] The schema definition for this collection property
59
+ def to_schema
60
+ if has_item?
61
+ {
62
+ name => {
63
+ type: :array,
64
+ items: {
65
+ "$ref": item.to_ref
66
+ }
67
+ }.merge(schema_options)
68
+ }
69
+ else
70
+ {
71
+ name => {
72
+ type: :array,
73
+ items: {
74
+ type: :object,
75
+ required: properties.values.select(&:required).map(&:name),
76
+ properties: properties.transform_values { |property| property.to_schema[property.name] }
77
+ }
78
+ }.merge(schema_options)
79
+ }
80
+ end
81
+ end
82
+
83
+ # Generate validation schema for this collection property
84
+ #
85
+ # @param version [String, nil] The version to generate validation schema for
86
+ # @return [Hash] The validation schema for this collection property
87
+ def to_validation_schema(version: nil)
88
+ if has_item?
89
+ {
90
+ name => {
91
+ type: :array,
92
+ items: item.to_validation_schema(version: version)
93
+ }.merge(schema_options)
94
+ }
95
+ else
96
+ {
97
+ name => {
98
+ type: :array,
99
+ items: {
100
+ type: :object,
101
+ required: properties.values.select(&:required).map(&:name),
102
+ properties: properties.transform_values { |property| property.to_validation_schema(version: version)[property.name] }
103
+ }
104
+ }.merge(schema_options)
105
+ }
106
+ end
107
+ end
108
+
109
+ # Create mapping for this collection property and all its children
110
+ #
111
+ # This method handles two different scenarios:
112
+ # 1. When the collection references an external item schema (`has_item?` returns true)
113
+ # - Creates mappings by transforming keys from the referenced item schema
114
+ # - Adds array notation ([]) to indicate this is a collection
115
+ # - Prefixes all keys and values with the appropriate paths
116
+ #
117
+ # 2. When the collection has inline item properties
118
+ # - Creates mappings for each property in the collection items
119
+ # - Each property gets mapped with array notation and appropriate prefixes
120
+ #
121
+ # @param key_prefix [Array<Symbol>] Prefix for the source key
122
+ # @param value_prefix [Array<String>] Prefix for the target value
123
+ # @param mapping [Hash] The mapping hash to be updated
124
+ # @param version [String, nil] The version to create mapping for
125
+ # @return [Hash] The updated mapping hash
126
+ def mapping(key_prefix:, value_prefix:, mapping:, version:)
127
+ if has_item?
128
+ value_key_prefix = mapping_value_key(value_prefix: value_prefix, collection: true)
129
+
130
+ reference_mapping = item.mapping(version:).dup
131
+ reference_mapping.transform_keys! { "#{(key_prefix + [name]).join(".")}[].#{_1}" }
132
+ reference_mapping.transform_values! { "#{value_key_prefix}.#{_1}" }
133
+
134
+ mapping.merge!(reference_mapping)
135
+ else
136
+ properties.values.each do |property|
137
+ property.mapping(key_prefix: key_prefix + ["#{name}[]"], value_prefix: mapping_value_prefix(value_prefix: value_prefix, collection: true), mapping:, version:)
138
+ end
139
+ end
140
+ end
141
+
142
+ private
143
+
144
+ attr_reader :item, :schema_options, :properties
145
+ end
146
+ end
147
+ end
@@ -0,0 +1,65 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Verquest
4
+ module Properties
5
+ # Field property type for basic scalar values
6
+ #
7
+ # Represents simple scalar types (string, number, integer, boolean) in the schema.
8
+ # Used for defining basic data fields without nesting.
9
+ #
10
+ # @example Define a required string field
11
+ # field = Verquest::Properties::Field.new(
12
+ # name: :email,
13
+ # type: :string,
14
+ # required: true,
15
+ # format: "email"
16
+ # )
17
+ class Field < Base
18
+ # List of allowed field types
19
+ # @return [Array<Symbol>]
20
+ ALLOWED_TYPES = %i[string number integer boolean].freeze
21
+
22
+ # Initialize a new Field property
23
+ #
24
+ # @param name [Symbol] The name of the property
25
+ # @param type [Symbol] The data type for this field, must be one of ALLOWED_TYPES
26
+ # @param required [Boolean] Whether this property is required
27
+ # @param map [String, nil] The mapping path for this property
28
+ # @param schema_options [Hash] Additional JSON schema options for this property
29
+ # @raise [ArgumentError] If type is not one of the allowed types
30
+ # @raise [ArgumentError] If attempting to map a field to root without a name
31
+ def initialize(name:, type:, required: false, map: nil, **schema_options)
32
+ raise ArgumentError, "Type must be one of #{ALLOWED_TYPES.join(", ")}" unless ALLOWED_TYPES.include?(type)
33
+ raise ArgumentError, "You can not map fields to the root without a name" if map == "/"
34
+
35
+ @name = name
36
+ @type = type
37
+ @required = required
38
+ @map = map
39
+ @schema_options = schema_options
40
+ end
41
+
42
+ # Generate JSON schema definition for this field
43
+ #
44
+ # @return [Hash] The schema definition for this field
45
+ def to_schema
46
+ {name => {type: type}.merge(schema_options)}
47
+ end
48
+
49
+ # Create mapping for this field property
50
+ #
51
+ # @param key_prefix [Array<Symbol>] Prefix for the source key
52
+ # @param value_prefix [Array<String>] Prefix for the target value
53
+ # @param mapping [Hash] The mapping hash to be updated
54
+ # @param version [String, nil] The version to create mapping for
55
+ # @return [Hash] The updated mapping hash
56
+ def mapping(key_prefix:, value_prefix:, mapping:, version: nil)
57
+ mapping[(key_prefix + [name]).join(".")] = mapping_value_key(value_prefix:)
58
+ end
59
+
60
+ private
61
+
62
+ attr_reader :type, :schema_options
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,83 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Verquest
4
+ module Properties
5
+ # Object property type for structured data
6
+ #
7
+ # Represents a complex object with nested properties in the schema.
8
+ # Used for defining structured data objects with multiple fields.
9
+ #
10
+ # @example Define an address object with nested properties
11
+ # address = Verquest::Properties::Object.new(name: :address)
12
+ # address.add(Verquest::Properties::Field.new(name: :street, type: :string))
13
+ # address.add(Verquest::Properties::Field.new(name: :city, type: :string, required: true))
14
+ class Object < Base
15
+ # Initialize a new Object property
16
+ #
17
+ # @param name [String] The name of the property
18
+ # @param required [Boolean] Whether this property is required
19
+ # @param map [String, nil] The mapping path for this property
20
+ # @param schema_options [Hash] Additional JSON schema options for this property
21
+ def initialize(name:, required: false, map: nil, **schema_options)
22
+ @properties = {}
23
+
24
+ @name = name
25
+ @required = required
26
+ @map = map
27
+ @schema_options = schema_options
28
+ end
29
+
30
+ # Add a child property to this object
31
+ #
32
+ # @param property [Verquest::Properties::Base] The property to add to this object
33
+ # @return [Verquest::Properties::Base] The added property
34
+ def add(property)
35
+ properties[property.name] = property
36
+ end
37
+
38
+ # Generate JSON schema definition for this object property
39
+ #
40
+ # @return [Hash] The schema definition for this object property
41
+ def to_schema
42
+ {
43
+ name => {
44
+ type: :object,
45
+ required: properties.values.select(&:required).map(&:name),
46
+ properties: properties.transform_values { |property| property.to_schema[property.name] }
47
+ }.merge(schema_options)
48
+ }
49
+ end
50
+
51
+ # Generate validation schema for this object property
52
+ #
53
+ # @param version [String, nil] The version to generate validation schema for
54
+ # @return [Hash] The validation schema for this object property
55
+ def to_validation_schema(version: nil)
56
+ {
57
+ name => {
58
+ type: :object,
59
+ required: properties.values.select(&:required).map(&:name),
60
+ properties: properties.transform_values { |property| property.to_validation_schema(version:)[property.name] }
61
+ }.merge(schema_options)
62
+ }
63
+ end
64
+
65
+ # Create mapping for this object property and all its children
66
+ #
67
+ # @param key_prefix [Array<Symbol>] Prefix for the source key
68
+ # @param value_prefix [Array<String>] Prefix for the target value
69
+ # @param mapping [Hash] The mapping hash to be updated
70
+ # @param version [String, nil] The version to create mapping for
71
+ # @return [Hash] The updated mapping hash
72
+ def mapping(key_prefix:, value_prefix:, mapping:, version: nil)
73
+ properties.values.each do |property|
74
+ property.mapping(key_prefix: key_prefix + [name], value_prefix: mapping_value_prefix(value_prefix:), mapping:, version:)
75
+ end
76
+ end
77
+
78
+ private
79
+
80
+ attr_reader :type, :schema_options, :properties
81
+ end
82
+ end
83
+ end
@@ -0,0 +1,92 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Verquest
4
+ module Properties
5
+ # Reference property type for schema reuse
6
+ #
7
+ # Allows referencing other schema definitions to promote reuse and DRY principles.
8
+ # Can reference either a complete schema or a specific property within a schema.
9
+ #
10
+ # @example Reference another schema
11
+ # reference = Verquest::Properties::Reference.new(
12
+ # name: :user,
13
+ # from: UserRequest,
14
+ # required: true
15
+ # )
16
+ #
17
+ # @example Reference a specific property from another schema
18
+ # reference = Verquest::Properties::Reference.new(
19
+ # name: :address,
20
+ # from: UserRequest,
21
+ # property: :address
22
+ # )
23
+ class Reference < Base
24
+ # Initialize a new Reference property
25
+ #
26
+ # @param name [String] The name of the property
27
+ # @param from [Class] The schema class to reference
28
+ # @param property [Symbol, nil] Optional specific property to reference
29
+ # @param map [String, nil] The mapping path for this property
30
+ # @param required [Boolean] Whether this property is required
31
+ def initialize(name:, from:, property: nil, map: nil, required: false)
32
+ @name = name
33
+ @from = from
34
+ @property = property
35
+ @map = map
36
+ @required = required
37
+ end
38
+
39
+ # Generate JSON schema definition for this reference property
40
+ #
41
+ # @return [Hash] The schema definition with a $ref pointer
42
+ def to_schema
43
+ {
44
+ name => {"$ref": from.to_ref(property:)}
45
+ }
46
+ end
47
+
48
+ # Generate validation schema for this reference property
49
+ #
50
+ # @param version [String, nil] The version to generate validation schema for
51
+ # @return [Hash] The validation schema for this reference
52
+ def to_validation_schema(version: nil)
53
+ {
54
+ name => from.to_validation_schema(version:, property: property)
55
+ }
56
+ end
57
+
58
+ # Create mapping for this reference property
59
+ # This delegates to the referenced schema's mapping with appropriate key prefixing
60
+ #
61
+ # @param key_prefix [Array<Symbol>] Prefix for the source key
62
+ # @param value_prefix [Array<String>] Prefix for the target value
63
+ # @param mapping [Hash] The mapping hash to be updated
64
+ # @param version [String, nil] The version to create mapping for
65
+ # @return [Hash] The updated mapping hash
66
+ def mapping(key_prefix:, value_prefix:, mapping:, version:)
67
+ reference_mapping = from.mapping(version:, property:).dup
68
+ value_key_prefix = mapping_value_key(value_prefix:)
69
+
70
+ # Single field mapping
71
+ if property && reference_mapping.size == 1 && !reference_mapping.keys.first.include?(".")
72
+ reference_mapping = {
73
+ (key_prefix + [name]).join(".") => value_key_prefix
74
+ }
75
+ else
76
+ if value_key_prefix != "" && !value_key_prefix.end_with?(".")
77
+ value_key_prefix = "#{value_key_prefix}."
78
+ end
79
+
80
+ reference_mapping.transform_keys! { "#{(key_prefix + [name]).join(".")}.#{_1}" }
81
+ reference_mapping.transform_values! { "#{value_key_prefix}#{_1}" }
82
+ end
83
+
84
+ mapping.merge!(reference_mapping)
85
+ end
86
+
87
+ private
88
+
89
+ attr_reader :from, :property
90
+ end
91
+ end
92
+ end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Verquest
4
+ # Property types for defining versioned API request schemas
5
+ #
6
+ # The Properties module contains classes representing different types of
7
+ # properties that can be used when defining API request schemas. Each property
8
+ # type knows how to generate its own schema representation and handles mapping
9
+ # between external and internal parameter structures.
10
+ #
11
+ # @example Using properties in a schema definition
12
+ # class UserRequest < Verquest::Base
13
+ # version "2023-01" do
14
+ # # Field - Basic scalar properties
15
+ # field :email, type: :string, required: true
16
+ #
17
+ # # Object - Nested structure with properties
18
+ # object :address do
19
+ # field :street, type: :string
20
+ # field :city, type: :string, required: true
21
+ # end
22
+ #
23
+ # # Collection - Array of objects
24
+ # collection :orders do
25
+ # field :id, type: :string, required: true
26
+ # field :amount, type: :number
27
+ # end
28
+ #
29
+ # # Array - Simple array of scalar values
30
+ # array :tags, type: :string
31
+ #
32
+ # # Reference - Reference to another schema
33
+ # reference :payment, from: PaymentRequest
34
+ # end
35
+ # end
36
+ #
37
+ # @see Verquest::Properties::Base Base class for all property types
38
+ # @see Verquest::Properties::Field For scalar values like strings and numbers
39
+ # @see Verquest::Properties::Object For nested objects with their own properties
40
+ # @see Verquest::Properties::Collection For arrays of structured objects
41
+ # @see Verquest::Properties::Array For arrays of scalar values
42
+ # @see Verquest::Properties::Reference For references to other schemas
43
+ module Properties
44
+ # This module is a namespace for property type classes
45
+ # Each property type is defined in its own file under lib/verquest/properties/
46
+ end
47
+ end
@@ -0,0 +1,75 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Verquest
4
+ # A result object for operation outcomes
5
+ #
6
+ # Result represents the outcome of an operation in the Verquest gem,
7
+ # particularly parameter mapping and validation operations. It follows
8
+ # the Result pattern, providing a consistent interface for both successful
9
+ # and failed operations, avoiding exceptions for control flow.
10
+ #
11
+ # @example Handling a successful result
12
+ # result = Verquest::Result.success(transformed_params)
13
+ # if result.success?
14
+ # process_params(result.value)
15
+ # end
16
+ #
17
+ # @example Handling a failed result
18
+ # result = Verquest::Result.failure(["Invalid email format"])
19
+ # if result.failure?
20
+ # display_errors(result.errors)
21
+ # end
22
+ class Result
23
+ # @!attribute [r] success
24
+ # @return [Boolean] Whether the operation was successful
25
+ #
26
+ # @!attribute [r] value
27
+ # @return [Object, nil] The result value if successful, nil otherwise
28
+ #
29
+ # @!attribute [r] errors
30
+ # @return [Array] List of errors if failed, empty array otherwise
31
+ attr_reader :success, :value, :errors
32
+
33
+ # Initialize a new Result instance
34
+ #
35
+ # @param success [Boolean] Whether the operation was successful
36
+ # @param value [Object, nil] The result value for successful operations
37
+ # @param errors [Array, nil] List of errors for failed operations
38
+ # @return [Result] A new Result instance
39
+ def initialize(success:, value: nil, errors: nil)
40
+ @success = success
41
+ @value = value
42
+ @errors = errors
43
+ end
44
+
45
+ # Create a successful result with a value
46
+ #
47
+ # @param value [Object] The successful operation's result value
48
+ # @return [Result] A successful Result instance containing the value
49
+ def self.success(value)
50
+ new(success: true, value: value)
51
+ end
52
+
53
+ # Create a failed result with errors
54
+ #
55
+ # @param errors [Array, String] Error message(s) describing the failure
56
+ # @return [Result] A failed Result instance containing the errors
57
+ def self.failure(errors)
58
+ new(success: false, errors: errors)
59
+ end
60
+
61
+ # Check if the result represents a successful operation
62
+ #
63
+ # @return [Boolean] true if the operation was successful, false otherwise
64
+ def success?
65
+ success
66
+ end
67
+
68
+ # Check if the result represents a failed operation
69
+ #
70
+ # @return [Boolean] true if the operation failed, false otherwise
71
+ def failure?
72
+ !success
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,179 @@
1
+ module Verquest
2
+ # Transforms parameters based on path mappings
3
+ #
4
+ # The Transformer class handles the conversion of parameter structures based on
5
+ # a mapping of source paths to target paths. It supports deep nested structures,
6
+ # array notations, and complex path expressions using dot notation.
7
+ #
8
+ # @example Basic transformation
9
+ # mapping = {
10
+ # "user.firstName" => "user.first_name",
11
+ # "user.lastName" => "user.last_name",
12
+ # "addresses[].zip" => "addresses[].postal_code"
13
+ # }
14
+ #
15
+ # transformer = Verquest::Transformer.new(mapping: mapping)
16
+ # result = transformer.call({
17
+ # user: {
18
+ # firstName: "John",
19
+ # lastName: "Doe"
20
+ # },
21
+ # addresses: [
22
+ # { zip: "12345" },
23
+ # { zip: "67890" }
24
+ # ]
25
+ # })
26
+ #
27
+ # # Result will be:
28
+ # # {
29
+ # # user: {
30
+ # # first_name: "John",
31
+ # # last_name: "Doe"
32
+ # # },
33
+ # # addresses: [
34
+ # # { postal_code: "12345" },
35
+ # # { postal_code: "67890" }
36
+ # # ]
37
+ # # }
38
+ class Transformer
39
+ # Creates a new Transformer with the specified mapping
40
+ #
41
+ # @param mapping [Hash] A hash where keys are source paths and values are target paths
42
+ # @return [Transformer] A new transformer instance
43
+ def initialize(mapping:)
44
+ @mapping = mapping
45
+ @path_cache = {} # Cache for parsed paths to improve performance
46
+ precompile_paths # Prepare cache during initialization
47
+ end
48
+
49
+ # Transforms input parameters according to the provided mapping
50
+ #
51
+ # @param params [Hash] The input parameters to transform
52
+ # @return [Hash] The transformed parameters with symbol keys
53
+ def call(params)
54
+ result = {}
55
+
56
+ mapping.each do |source_path, target_path|
57
+ # Extract value using the source path
58
+ value = extract_value(params, parse_path(source_path.to_s))
59
+ next if value.nil?
60
+
61
+ # Set the extracted value at the target path
62
+ set_value(result, parse_path(target_path.to_s), value)
63
+ end
64
+
65
+ result
66
+ end
67
+
68
+ private
69
+
70
+ # @!attribute [r] mapping
71
+ # @return [Hash] The source-to-target path mapping
72
+ # @!attribute [r] path_cache
73
+ # @return [Hash] Cache for parsed paths
74
+ attr_reader :mapping, :path_cache
75
+
76
+ # Precompiles all paths from the mapping to improve performance
77
+ # This is called during initialization to prepare the cache
78
+ #
79
+ # @return [void]
80
+ def precompile_paths
81
+ mapping.each do |source_path, target_path|
82
+ parse_path(source_path.to_s)
83
+ parse_path(target_path.to_s)
84
+ end
85
+ end
86
+
87
+ # Parses a dot-notation path into structured path parts
88
+ # Uses memoization for performance optimization
89
+ #
90
+ # @param path [String] The dot-notation path (e.g., "user.address.street")
91
+ # @return [Array<Hash>] Array of path parts with :key and :array attributes
92
+ def parse_path(path)
93
+ path_cache[path] ||= path.split(".").map do |part|
94
+ if part.end_with?("[]")
95
+ {key: part[0...-2], array: true}
96
+ else
97
+ {key: part, array: false}
98
+ end
99
+ end
100
+ end
101
+
102
+ # Extracts a value from nested data structure using the parsed path parts
103
+ #
104
+ # @param data [Hash, Array, Object] The data to extract value from
105
+ # @param path_parts [Array<Hash>] The parsed path parts
106
+ # @return [Object, nil] The extracted value or nil if not found
107
+ def extract_value(data, path_parts)
108
+ return data if path_parts.empty?
109
+
110
+ current_part = path_parts.first
111
+ remaining_path = path_parts[1..]
112
+ key = current_part[:key]
113
+
114
+ case data
115
+ when Hash
116
+ # Check if the key exists (as string or symbol)
117
+ return nil unless data.key?(key.to_s) || data.key?(key.to_sym)
118
+
119
+ # Determine the actual key type used in the hash
120
+ actual_key = data.key?(key.to_s) ? key.to_s : key.to_sym
121
+ value = data[actual_key]
122
+
123
+ if current_part[:array] && value.is_a?(Array)
124
+ # Process array elements and filter out nil values
125
+ value.map { |item| extract_value(item, remaining_path) }.compact
126
+ else
127
+ # Continue traversing the path
128
+ extract_value(value, remaining_path)
129
+ end
130
+ when Array
131
+ if current_part[:array]
132
+ # Map through array elements with remaining path
133
+ data.map { |item| extract_value(item, remaining_path) }.compact
134
+ else
135
+ # Try to extract from each array element with the full path
136
+ result = data.map { |item| extract_value(item, path_parts) }.compact
137
+ result.empty? ? nil : result
138
+ end
139
+ else
140
+ # For scalar values, return only if we're at the end of the path
141
+ remaining_path.empty? ? data : nil
142
+ end
143
+ end
144
+
145
+ # Sets a value in a result hash at the specified path
146
+ #
147
+ # @param result [Hash] The result hash to modify
148
+ # @param path_parts [Array<Hash>] The parsed path parts
149
+ # @param value [Object] The value to set
150
+ # @return [Hash] The modified result hash with symbol keys
151
+ def set_value(result, path_parts, value)
152
+ return result if path_parts.empty?
153
+
154
+ current_part = path_parts.first
155
+ remaining_path = path_parts[1..]
156
+ key = current_part[:key].to_sym # Convert key to symbol for consistent symbol keys
157
+
158
+ if remaining_path.empty?
159
+ # End of path, set the value directly
160
+ result[key] = value
161
+ elsif current_part[:array] && value.is_a?(Array)
162
+ # Handle array notation in target path
163
+ result[key] ||= []
164
+
165
+ # Process each value in the array
166
+ value.each_with_index do |v, i|
167
+ result[key][i] ||= {}
168
+ set_value(result[key][i], remaining_path, v)
169
+ end
170
+ else
171
+ # Continue building nested structure
172
+ result[key] ||= {}
173
+ set_value(result[key], remaining_path, value)
174
+ end
175
+
176
+ result
177
+ end
178
+ end
179
+ end