verquest 0.5.0 → 0.6.1

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b9349b2773c4a03024805d78257b1e763ffb87b87f27726620e343395c683aa0
4
- data.tar.gz: ec2b70a4a10c3314ca27283776138f85b53b245d88869de39e450ca2eb2064de
3
+ metadata.gz: 48e1067531cf426f709ebe2dfffe4548cfd161b19f9481ca87599a0152e1fcab
4
+ data.tar.gz: d4387ad9de93bceebb05e94b69560f43f7b374cad28588a3ff0599d9f8d17667
5
5
  SHA512:
6
- metadata.gz: 0d0042f959285f4253c7802166ab64d53b7013d765bfe954fd68a8ab46bb59f162512a285aca11c77e86a8e6b5ce36445f47e4e9530fcaa0010e8c6cadcefdf2
7
- data.tar.gz: 06bbfceb1c989e1e3eabb20e495790ac35934e1cb4687e71960315c3e64d3a30b42b345c03d19d32be921b75550a6fef002d8ba44018ded8f7c9a3af73d920f0
6
+ metadata.gz: bc95249593570b003e7dc49881fe4a3a34a60efc535be6f1803c17db7cd6475317eaacb0c9ff5c1f11ba24bb18ff5065d86d070be92e24f7ff4d1e1fa88f14e9
7
+ data.tar.gz: 8d915f5cbd2f3fdfea67a1d94f13803d9a1125db6a8a21fe3cec81233693017391e40f69feee0fd9a60a8bb6926764cc0d7de1c77a3746c7ef87e2672acdd978
data/CHANGELOG.md CHANGED
@@ -1,5 +1,20 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [0.6.1] - 2025-10-13
4
+
5
+ ### Fixed
6
+ - Mapping for collections (array of objects). ([#15](https://github.com/verquest/verquest/pull/15), [@CiTroNaK](https://github.com/CiTroNaK))
7
+
8
+ ## [0.6.0] - 2025-07-18
9
+
10
+ ### Breaking Changes
11
+ - **BREAKING:** Switching to slash notation for mapping to improve consistency with how properties are referenced in JSON Schema. Before: `example.nested.property`, now: `example/nested/property`. ([#12](https://github.com/verquest/verquest/pull/12), [@CiTroNaK](https://github.com/CiTroNaK))
12
+
13
+ ### New Features
14
+ - Add support for `enum` properties. ([#13](https://github.com/verquest/verquest/pull/13), [@CiTroNaK](https://github.com/CiTroNaK))
15
+ - Add support for inverted mapping, that can be used to map internal structure to external representation (e.g., for errors). ([#10](https://github.com/verquest/verquest/pull/10), [@CiTroNaK](https://github.com/CiTroNaK))
16
+ - Add support for constants in the schema, allowing to define fixed values for properties. ([#11](https://github.com/verquest/verquest/pull/11), [@CiTroNaK](https://github.com/CiTroNaK))
17
+
3
18
  ## [0.5.0] - 2025-07-01
4
19
 
5
20
  ### Fixed
data/README.md CHANGED
@@ -8,18 +8,20 @@ Verquest is a Ruby gem that offers an elegant solution for versioning API reques
8
8
  - Defining versioned request structures
9
9
  - Gracefully handling API versioning
10
10
  - Mapping between external and internal parameter structures
11
- - Validating parameters against [JSON Schema](https://json-schema.org/learn)
11
+ - Validating parameters against [JSON Schema](https://json-schema.org/)
12
12
  - Generating components for OpenAPI documentation
13
- - Mapping error keys back to the external API structure (planned feature)
13
+ - Mapping error keys back to the external API structure
14
14
 
15
- > The gem is still in development. Until version 1.0, the API may change. There are some features like `oneOf`, `anyOf`, `allOf` that are not implemented yet.
15
+ Sponsored by [Stora](https://stora.co)
16
+
17
+ > The gem is still in development. Until version 1.0, the API may change. There are some features like `oneOf`, `anyOf`, `allOf` that are not implemented yet. See open [issues](https://github.com/verquest/verquest/issues?q=sort:updated-desc%20is:issue%20is:open%20label:enhancement).
16
18
 
17
19
  ## Installation
18
20
 
19
21
  Add this line to your application's Gemfile:
20
22
 
21
23
  ```ruby
22
- gem "verquest", "~> 0.5"
24
+ gem "verquest", "~> 0.6"
23
25
  ```
24
26
 
25
27
  And then execute:
@@ -75,7 +77,7 @@ class UserCreateRequest < Verquest::Base
75
77
  end
76
78
  end
77
79
 
78
- field :role, type: :string, description: "Role of the user", enum: %w[member manager], default: "member"
80
+ enum :role, values: %w[member manager], default: "member", description: "Role of the user", required: true
79
81
 
80
82
  object :profile_details do
81
83
  field :bio, type: :string, description: "Short biography of the user"
@@ -89,6 +91,8 @@ class UserCreateRequest < Verquest::Base
89
91
  end
90
92
  end
91
93
  end
94
+
95
+ const :company, value: "Awesome Inc."
92
96
  end
93
97
  end
94
98
  ```
@@ -130,7 +134,7 @@ Output:
130
134
  {
131
135
  "type" => "object",
132
136
  "description" => "User Create Request",
133
- "required" => ["first_name", "last_name", "email", "address"],
137
+ "required" => ["first_name", "last_name", "email", "address", "role"],
134
138
  "properties" => {
135
139
  "first_name" => {"type" => "string", "description" => "The first name of the user", "maxLength" => 50},
136
140
  "last_name" => {"type" => "string", "description" => "The last name of the user", "maxLength" => 50},
@@ -151,10 +155,9 @@ Output:
151
155
  "description" => "Permissions associated with the user"
152
156
  },
153
157
  "role" => {
154
- "type" => "string",
155
- "description" => "Role of the user",
156
- "enum" => ["member", "manager"],
157
- "default" => "member"
158
+ "enum" => ["member", "manager"],
159
+ "default" => "member",
160
+ "description" => "Role of the user"
158
161
  },
159
162
  "profile_details" => {
160
163
  "type" => "object",
@@ -172,7 +175,8 @@ Output:
172
175
  "description" => "Some social networks"
173
176
  }
174
177
  }
175
- }
178
+ },
179
+ "company" => {"const" => "Awesome Inc."}
176
180
  },
177
181
  "additionalProperties" => false
178
182
  }
@@ -191,7 +195,7 @@ Output:
191
195
  {
192
196
  "type" => "object",
193
197
  "description" => "User Create Request",
194
- "required" => ["first_name", "last_name", "email", "address"],
198
+ "required" => ["first_name", "last_name", "email", "address", "role"],
195
199
  "properties" => {
196
200
  "first_name" => {"type" => "string", "description" => "The first name of the user", "maxLength" => 50},
197
201
  "last_name" => {"type" => "string", "description" => "The last name of the user", "maxLength" => 50},
@@ -222,10 +226,9 @@ Output:
222
226
  "description" => "Permissions associated with the user"
223
227
  },
224
228
  "role" => {
225
- "type" => "string",
226
- "description" => "Role of the user",
227
229
  "enum" => ["member", "manager"],
228
- "default" => "member"
230
+ "default" => "member",
231
+ "description" => "Role of the user"
229
232
  },
230
233
  "profile_details" => {"type" => "object",
231
234
  "required" => [],
@@ -240,7 +243,8 @@ Output:
240
243
  "mastodon" => {"type" => "string", "format" => "uri", "description" => "Mastodon profile URL"}
241
244
  },
242
245
  "description" => "Some social networks"}
243
- }
246
+ },
247
+ "company" => {"const" => "Awesome Inc."}
244
248
  }
245
249
  },
246
250
  "additionalProperties" => false
@@ -265,10 +269,12 @@ The JSON schema can be used for both validation of incoming parameters and for g
265
269
  #### Component types
266
270
 
267
271
  - `field`: Represents a scalar value (string, integer, boolean, etc.).
272
+ - `enum`: Represents a property with a limited set of values (enumeration).
268
273
  - `object`: Represents a JSON object with properties.
269
274
  - `array`: Represents a JSON array with scalar items.
270
275
  - `collection`: Represents a array of objects defined manually or by a reference to another request.
271
276
  - `reference`: Represents a reference to another request, allowing you to reuse existing request structures.
277
+ - `const`: Represents a [constant](https://json-schema.org/understanding-json-schema/reference/const#constant-values) value that is always present in the request.
272
278
 
273
279
  #### Helper methods
274
280
 
@@ -276,7 +282,7 @@ The JSON schema can be used for both validation of incoming parameters and for g
276
282
  - `schema_options`: Allows you to set additional options for the JSON Schema, such as `additional_properties` for request or per version. All fields (except `reference`) can be defined with options like `required`, `format`, `min_lenght`, `max_length`, etc. all in snake case.
277
283
  - `with_options`: Allows you to define multiple fields with the same options, reducing repetition.
278
284
 
279
- ### Required properties
285
+ #### Required properties
280
286
 
281
287
  You can define required properties in your request schema by setting the `required` option to `true`, or provide a list of dependent required properties. This feature is based on the latest [JSON Schema specification](https://json-schema.org/understanding-json-schema/reference/conditionals#dependentRequired), which is also used in OpenAPI 3.1.
282
288
 
@@ -538,9 +544,28 @@ Will be transformed to:
538
544
 
539
545
  What you can use:
540
546
  - `/` to reference the root of the request structure
541
- - `nested.structure` use dot notation to reference nested structures
547
+ - `nested/structure` use slash notation to reference nested structures
542
548
  - if the `map` is not set, the field name will be used as the key in the internal structure
543
549
 
550
+ To get the mapping to map the request structure back to the external API structure, you can use the `external_mapping` method:
551
+
552
+ ```ruby
553
+ UserCreateRequest.external_mapping(version: "2025-06")
554
+ ```
555
+
556
+ Will produce the following mapping:
557
+
558
+ ```ruby
559
+ {
560
+ "name" => "full_name",
561
+ "email" => "email",
562
+ "phone" => "phone",
563
+ "address_street" => "address/street",
564
+ "address_city" => "address/city",
565
+ "address_zip" => "address/postal_code"
566
+ }
567
+ ```
568
+
544
569
  There are some limitations and the implementation can be improved, but it should works for most common use cases.
545
570
 
546
571
  See the mapping test (in `test/verquest/base_test.rb`) for more examples of mapping.
@@ -586,7 +611,7 @@ end
586
611
 
587
612
  ## Documentation
588
613
 
589
- For detailed documentation, please visit the [YARD documentation](https://www.rubydoc.info/gems/verquest/0.5.0/).
614
+ For detailed documentation, please visit the [YARD documentation](https://www.rubydoc.info/gems/verquest/0.6.0/).
590
615
 
591
616
  ## Development
592
617
 
@@ -596,7 +621,7 @@ To install this gem onto your local machine, run `bundle exec rake install`. To
596
621
 
597
622
  ## Contributing
598
623
 
599
- Bug reports and pull requests are welcome on GitHub at https://github.com/CiTroNaK/verquest. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [code of conduct](https://github.com/CiTroNaK/verquest/blob/main/CODE_OF_CONDUCT.md).
624
+ Bug reports and pull requests are welcome on GitHub at https://github.com/verquest/verquest. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [code of conduct](https://github.com/verquest/verquest/blob/main/CODE_OF_CONDUCT.md).
600
625
 
601
626
  ## License
602
627
 
@@ -604,4 +629,4 @@ The gem is available as open source under the terms of the [MIT License](https:/
604
629
 
605
630
  ## Code of Conduct
606
631
 
607
- Everyone interacting in the Verquest project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/CiTroNaK/verquest/blob/main/CODE_OF_CONDUCT.md).
632
+ Everyone interacting in the Verquest project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/verquest/verquest/blob/main/CODE_OF_CONDUCT.md).
@@ -138,6 +138,42 @@ module Verquest
138
138
  current_scope.add(field)
139
139
  end
140
140
 
141
+ # Defines a new enum property for the current version scope
142
+ #
143
+ # @param name [Symbol] The name of the enum
144
+ # @param values [Array] The possible values for the enum
145
+ # @param map [String, nil] An optional mapping to another property
146
+ # @param required [Boolean, Array<Symbol>] Whether the enum is required
147
+ # @param nullable [Boolean] Whether the enum can be null
148
+ # @param schema_options [Hash] Additional schema options for the enum
149
+ # @return [void]
150
+ def enum(name, values:, map: nil, required: nil, nullable: nil, **schema_options)
151
+ camelize(schema_options)
152
+
153
+ required = default_options.fetch(:required, false) if required.nil?
154
+ nullable = default_options.fetch(:nullable, false) if nullable.nil?
155
+ schema_options = default_options.except(:required, :nullable).merge(schema_options)
156
+
157
+ enum_property = Properties::Enum.new(name:, values:, map:, required:, nullable:, **schema_options)
158
+ current_scope.add(enum_property)
159
+ end
160
+
161
+ # Defines a new constant property for the current version scope
162
+ #
163
+ # @param name [Symbol] The name of the constant
164
+ # @param value [Object] The value of the constant
165
+ # @param map [String, nil] An optional mapping to another constant
166
+ # @param required [Boolean, Array<Symbol>] Whether the constant is required
167
+ # @param schema_options [Hash] Additional schema options for the constant
168
+ # @return [void]
169
+ def const(name, value:, map: nil, required: nil, **schema_options)
170
+ camelize(schema_options)
171
+ required = default_options.fetch(:required, false) if required.nil?
172
+
173
+ const = Properties::Const.new(name:, value:, map:, required:, **schema_options)
174
+ current_scope.add(const)
175
+ end
176
+
141
177
  # Defines a new object for the current version scope
142
178
  #
143
179
  # @param name [Symbol] The name of the object
@@ -102,6 +102,18 @@ module Verquest
102
102
  end
103
103
  end
104
104
 
105
+ # Returns the external mapping for a specific version
106
+ #
107
+ # This method returns a mapping hash that translates from internal attribute names back to external parameter names.
108
+ #
109
+ # @param version [String, nil] Specific version to use, defaults to configuration setting
110
+ # @return [Hash] The inverted mapping configuration where keys are internal names and values are external names
111
+ # @see #mapping
112
+ def external_mapping(version: nil)
113
+ version = resolve(version)
114
+ version.external_mapping
115
+ end
116
+
105
117
  # Returns the JSON reference for the request or a specific property
106
118
  #
107
119
  # @param property [String, Symbol, nil] Specific property to retrieve reference for
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Verquest
4
- GEM_VERSION = "0.5.0"
4
+ GEM_VERSION = "0.6.1"
5
5
  end
@@ -75,7 +75,7 @@ module Verquest
75
75
  # @param version [String, nil] The version to create mapping for, defaults to configuration setting
76
76
  # @return [Hash] The updated mapping hash
77
77
  def mapping(key_prefix:, value_prefix:, mapping:, version: nil)
78
- mapping[(key_prefix + [name]).join(".")] = mapping_value_key(value_prefix:)
78
+ mapping[(key_prefix + [name]).join("/")] = mapping_value_key(value_prefix:)
79
79
  end
80
80
 
81
81
  private
@@ -67,13 +67,13 @@ module Verquest
67
67
  # @return [String] The target mapping key
68
68
  def mapping_value_key(value_prefix:, collection: false)
69
69
  value_key = if map.nil?
70
- (value_prefix + [name]).join(".")
70
+ (value_prefix + [name]).join("/")
71
71
  elsif map == "/"
72
72
  ""
73
73
  elsif map.start_with?("/")
74
74
  map.gsub(%r{^/}, "")
75
75
  else
76
- (value_prefix + map.split(".")).join(".")
76
+ (value_prefix + map.split("/")).join("/")
77
77
  end
78
78
 
79
79
  if collection
@@ -93,9 +93,9 @@ module Verquest
93
93
  elsif map == "/"
94
94
  []
95
95
  elsif map.start_with?("/")
96
- map.gsub(%r{^/}, "").split(".")
96
+ map.gsub(%r{^/}, "").split("/")
97
97
  else
98
- value_prefix + map.split(".")
98
+ value_prefix + map.split("/")
99
99
  end
100
100
 
101
101
  if collection && value_prefix.any?
@@ -142,8 +142,8 @@ module Verquest
142
142
  value_key_prefix = mapping_value_key(value_prefix: value_prefix, collection: true)
143
143
 
144
144
  reference_mapping = item.mapping(version:).dup
145
- reference_mapping.transform_keys! { "#{(key_prefix + [name]).join(".")}[].#{_1}" }
146
- reference_mapping.transform_values! { "#{value_key_prefix}.#{_1}" }
145
+ reference_mapping.transform_keys! { "#{(key_prefix + [name]).join("/")}[]/#{_1}" }
146
+ reference_mapping.transform_values! { "#{value_key_prefix}/#{_1}" }
147
147
 
148
148
  mapping.merge!(reference_mapping)
149
149
  else
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Verquest
4
+ module Properties
5
+ # The Const class represents a constant property with a fixed value in a JSON schema.
6
+ # It's used for properties that must have a specific, immutable value.
7
+ #
8
+ # @example
9
+ # const = Const.new(name: "type", value: "user")
10
+ class Const < Base
11
+ # Initialize a new constant property
12
+ #
13
+ # @param name [String, Symbol] The name of the constant property
14
+ # @param value [Object] The fixed value of the constant (can be any scalar value)
15
+ # @param map [Object, nil] Optional mapping information
16
+ # @param required [Boolean, Array<Symbol>] Whether this property is required, or array of dependency names (can be overridden by custom type)
17
+ # @param schema_options [Hash] Additional JSON schema options for this property
18
+ def initialize(name:, value:, map: nil, required: false, **schema_options)
19
+ @name = name.to_s
20
+ @value = value
21
+ @map = map
22
+ @required = required
23
+ @schema_options = schema_options&.transform_keys(&:to_s)
24
+ end
25
+
26
+ # Generate JSON schema definition for this constant
27
+ #
28
+ # @return [Hash] The schema definition for this constant
29
+ def to_schema
30
+ {
31
+ name => {
32
+ "const" => value
33
+ }.merge(schema_options)
34
+ }
35
+ end
36
+
37
+ # Create mapping for this const property
38
+ #
39
+ # @param key_prefix [Array<Symbol>] Prefix for the source key
40
+ # @param value_prefix [Array<String>] Prefix for the target value
41
+ # @param mapping [Hash] The mapping hash to be updated
42
+ # @param version [String, nil] The version to create mapping for
43
+ # @return [Hash] The updated mapping hash
44
+ def mapping(key_prefix:, value_prefix:, mapping:, version: nil)
45
+ mapping[(key_prefix + [name]).join("/")] = mapping_value_key(value_prefix:)
46
+ end
47
+
48
+ private
49
+
50
+ attr_reader :value, :schema_options
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,65 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Verquest
4
+ module Properties
5
+ # The Enum class represents a enum property with a list of possible values in a JSON schema.
6
+ #
7
+ # @example
8
+ # enum = Enum.new(name: "type", values: ["member", "admin"])
9
+ class Enum < Base
10
+ # Initialize a new Enum property
11
+ #
12
+ # @param name [String, Symbol] The name of the property
13
+ # @param values [Array] The enum values for this property
14
+ # @param required [Boolean, Array<Symbol>] Whether this property is required, or array of dependency names
15
+ # @param nullable [Boolean] Whether this property can be null
16
+ # @param map [String, nil] The mapping path for this property
17
+ # @param schema_options [Hash] Additional JSON schema options for this property
18
+ # @raise [ArgumentError] If attempting to map an enum to root without a name
19
+ # @raise [ArgumentError] If values is empty
20
+ # @raise [ArgumentError] If values are not unique
21
+ # @raise [ArgumentError] If only one value is provided (should use const instead)
22
+ def initialize(name:, values:, required: false, nullable: false, map: nil, **schema_options)
23
+ raise ArgumentError, "You can not map enums to the root without a name" if map == "/"
24
+ raise ArgumentError, "Values must not be empty" if values.empty?
25
+ raise ArgumentError, "Values must be unique" if values.uniq.length != values.length
26
+ raise ArgumentError, "Use const for a single value" if values.length == 1
27
+
28
+ @name = name.to_s
29
+ @values = values
30
+ @required = required
31
+ @nullable = nullable
32
+ @map = map
33
+ @schema_options = schema_options&.transform_keys(&:to_s)
34
+
35
+ if nullable && !values.include?("null")
36
+ values << "null"
37
+ end
38
+ end
39
+
40
+ # Generate JSON schema definition for this enum
41
+ #
42
+ # @return [Hash] The schema definition for this enum
43
+ def to_schema
44
+ {
45
+ name => {"enum" => values}.merge(schema_options)
46
+ }
47
+ end
48
+
49
+ # Create mapping for this enum 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 :values, :schema_options
63
+ end
64
+ end
65
+ end
@@ -74,7 +74,7 @@ module Verquest
74
74
  # @param version [String, nil] The version to create mapping for
75
75
  # @return [Hash] The updated mapping hash
76
76
  def mapping(key_prefix:, value_prefix:, mapping:, version: nil)
77
- mapping[(key_prefix + [name]).join(".")] = mapping_value_key(value_prefix:)
77
+ mapping[(key_prefix + [name]).join("/")] = mapping_value_key(value_prefix:)
78
78
  end
79
79
 
80
80
  private
@@ -87,16 +87,16 @@ module Verquest
87
87
  value_key_prefix = mapping_value_key(value_prefix:)
88
88
 
89
89
  # Single field mapping
90
- if property && reference_mapping.size == 1 && !reference_mapping.keys.first.include?(".")
90
+ if property && reference_mapping.size == 1 && !reference_mapping.keys.first.include?("/")
91
91
  reference_mapping = {
92
- (key_prefix + [name]).join(".") => value_key_prefix
92
+ (key_prefix + [name]).join("/") => value_key_prefix
93
93
  }
94
94
  else
95
- if value_key_prefix != "" && !value_key_prefix.end_with?(".")
96
- value_key_prefix = "#{value_key_prefix}."
95
+ if value_key_prefix != "" && !value_key_prefix.end_with?("/")
96
+ value_key_prefix = "#{value_key_prefix}/"
97
97
  end
98
98
 
99
- reference_mapping.transform_keys! { "#{(key_prefix + [name]).join(".")}.#{_1}" }
99
+ reference_mapping.transform_keys! { "#{(key_prefix + [name]).join("/")}/#{_1}" }
100
100
  reference_mapping.transform_values! { "#{value_key_prefix}#{_1}" }
101
101
  end
102
102
 
@@ -3,13 +3,13 @@ module Verquest
3
3
  #
4
4
  # The Transformer class handles the conversion of parameter structures based on
5
5
  # a mapping of source paths to target paths. It supports deep nested structures,
6
- # array notations, and complex path expressions using dot notation.
6
+ # array notations, and complex path expressions using slash notation.
7
7
  #
8
8
  # @example Basic transformation
9
9
  # mapping = {
10
- # "user.firstName" => "user.first_name",
11
- # "user.lastName" => "user.last_name",
12
- # "addresses[].zip" => "addresses[].postal_code"
10
+ # "user/firstName" => "user/first_name",
11
+ # "user/lastName" => "user/last_name",
12
+ # "addresses[]/zip" => "addresses[]/postal_code"
13
13
  # }
14
14
  #
15
15
  # transformer = Verquest::Transformer.new(mapping: mapping)
@@ -84,13 +84,13 @@ module Verquest
84
84
  end
85
85
  end
86
86
 
87
- # Parses a dot-notation path into structured path parts
87
+ # Parses a slash-notation path into structured path parts
88
88
  # Uses memoization for performance optimization
89
89
  #
90
- # @param path [String] The dot-notation path (e.g., "user.address.street")
90
+ # @param path [String] The slash-notation path (e.g., "user/address/street")
91
91
  # @return [Array<Hash>] Array of path parts with :key and :array attributes
92
92
  def parse_path(path)
93
- path_cache[path] ||= path.split(".").map do |part|
93
+ path_cache[path] ||= path.split("/").map do |part|
94
94
  if part.end_with?("[]")
95
95
  {key: part[0...-2], array: true}
96
96
  else
@@ -113,30 +113,23 @@ module Verquest
113
113
 
114
114
  case data
115
115
  when Hash
116
- # Only check for string keys
117
116
  return nil unless data.key?(key.to_s)
118
-
119
- # Always use string keys
120
117
  value = data[key.to_s]
121
-
122
118
  if current_part[:array] && value.is_a?(Array)
123
- # Process array elements and filter out nil values
124
- value.map { |item| extract_value(item, remaining_path) }.compact
119
+ # Process each object in the array separately
120
+ value.map { |item| extract_value(item, remaining_path) }
125
121
  else
126
- # Continue traversing the path
127
122
  extract_value(value, remaining_path)
128
123
  end
129
124
  when Array
130
125
  if current_part[:array]
131
126
  # Map through array elements with remaining path
132
- data.map { |item| extract_value(item, remaining_path) }.compact
127
+ data.map { |item| extract_value(item, remaining_path) }
133
128
  else
134
129
  # Try to extract from each array element with the full path
135
- result = data.map { |item| extract_value(item, path_parts) }.compact
136
- result.empty? ? nil : result
130
+ data.map { |item| extract_value(item, path_parts) }
137
131
  end
138
132
  else
139
- # For scalar values, return only if we're at the end of the path
140
133
  remaining_path.empty? ? data : nil
141
134
  end
142
135
  end
@@ -152,26 +145,32 @@ module Verquest
152
145
 
153
146
  current_part = path_parts.first
154
147
  remaining_path = path_parts[1..]
155
- key = current_part[:key].to_s # Ensure key is a string for consistency
148
+ key = current_part[:key].to_s
149
+
150
+ if value.nil?
151
+ # Skip setting nil values
152
+ return result
153
+ end
156
154
 
157
155
  if remaining_path.empty?
158
- # End of path, set the value directly
159
156
  result[key] = value
160
157
  elsif current_part[:array] && value.is_a?(Array)
161
- # Handle array notation in target path
162
158
  result[key] ||= []
163
-
164
- # Process each value in the array
165
159
  value.each_with_index do |v, i|
160
+ next if v.nil? # Skip nil items in array
166
161
  result[key][i] ||= {}
167
162
  set_value(result[key][i], remaining_path, v)
163
+ # Remove keys with nil values from each object
164
+ result[key][i].delete_if { |_, val| val.nil? }
168
165
  end
166
+ # Remove nils and compact the array
167
+ result[key] = result[key].compact
169
168
  else
170
- # Continue building nested structure
171
169
  result[key] ||= {}
172
170
  set_value(result[key], remaining_path, value)
171
+ # Remove keys with nil values from nested object
172
+ result[key].delete_if { |_, val| val.nil? }
173
173
  end
174
-
175
174
  result
176
175
  end
177
176
  end
@@ -38,7 +38,10 @@ module Verquest
38
38
  #
39
39
  # @!attribute [r] transformer
40
40
  # @return [Verquest::Transformer] The transformer that applies the mapping
41
- attr_reader :name, :properties, :schema, :validation_schema, :mapping, :transformer
41
+ #
42
+ # @!attribute [r] external_mapping
43
+ # @return [Hash] The mapping from internal attribute paths back to external paths
44
+ attr_reader :name, :properties, :schema, :validation_schema, :mapping, :transformer, :external_mapping
42
45
 
43
46
  # @!attribute [rw] schema_options
44
47
  # @return [Hash] Additional JSON schema options for this version
@@ -113,6 +116,7 @@ module Verquest
113
116
  prepare_schema
114
117
  prepare_validation_schema
115
118
  prepare_mapping
119
+ prepare_external_mapping
116
120
  @transformer = Transformer.new(mapping: mapping)
117
121
 
118
122
  freeze
@@ -245,5 +249,18 @@ module Verquest
245
249
  raise MappingError.new("Mapping must be unique. Found duplicates in version '#{name}': #{duplicates.join(", ")}")
246
250
  end
247
251
  end
252
+
253
+ # Prepares the inverted parameter mapping for this version
254
+ #
255
+ # Inverts the standard mapping to create a reverse lookup from internal
256
+ # attribute names back to external parameter names. This is useful when
257
+ # transforming internal data back to the external API representation.
258
+ #
259
+ # @return [Hash] The frozen inverted mapping where keys are internal attribute
260
+ # paths and values are the corresponding external schema paths
261
+ # @see #prepare_mapping
262
+ def prepare_external_mapping
263
+ @external_mapping = mapping.invert.freeze
264
+ end
248
265
  end
249
266
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: verquest
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.5.0
4
+ version: 0.6.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Petr Hlavicka
@@ -66,6 +66,8 @@ files:
66
66
  - lib/verquest/properties/array.rb
67
67
  - lib/verquest/properties/base.rb
68
68
  - lib/verquest/properties/collection.rb
69
+ - lib/verquest/properties/const.rb
70
+ - lib/verquest/properties/enum.rb
69
71
  - lib/verquest/properties/field.rb
70
72
  - lib/verquest/properties/object.rb
71
73
  - lib/verquest/properties/reference.rb
@@ -74,13 +76,13 @@ files:
74
76
  - lib/verquest/version.rb
75
77
  - lib/verquest/version_resolver.rb
76
78
  - lib/verquest/versions.rb
77
- homepage: https://github.com/CiTroNaK/verquest
79
+ homepage: https://github.com/verquest/verquest
78
80
  licenses:
79
81
  - MIT
80
82
  metadata:
81
- homepage_uri: https://github.com/CiTroNaK/verquest
82
- source_code_uri: https://github.com/CiTroNaK/verquest
83
- changelog_uri: https://github.com/CiTroNaK/verquest/blob/main/CHANGELOG.md
83
+ homepage_uri: https://github.com/verquest/verquest
84
+ source_code_uri: https://github.com/verquest/verquest
85
+ changelog_uri: https://github.com/verquest/verquest/blob/main/CHANGELOG.md
84
86
  rdoc_options: []
85
87
  require_paths:
86
88
  - lib
@@ -95,7 +97,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
95
97
  - !ruby/object:Gem::Version
96
98
  version: '0'
97
99
  requirements: []
98
- rubygems_version: 3.6.9
100
+ rubygems_version: 3.6.7
99
101
  specification_version: 4
100
102
  summary: Verquest is a Ruby gem that offers an elegant solution for versioning API
101
103
  requests