verquest 0.4.0 → 0.6.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 725838444df6f3861ab842c88b430aa7bbc81023637595b4a0a8a909df5a2fbe
4
- data.tar.gz: 88041a5f8bc888f90894975b5fb371ac8184877bb75b74e2e099ce57997b331a
3
+ metadata.gz: 92e6047d9812ae82cf55b5d0512c459a61306c7b2cc78c9cd224e2b52b4b716c
4
+ data.tar.gz: db8943d346e30972f3b6b516d9365d7ef37dedf89fa7f4a09c5db4cb1adbe545
5
5
  SHA512:
6
- metadata.gz: 3991e8ff96b436e82a6edce43d591f35f669cbab9ef99b01bc26192bef94e3f48afe59595d8ae5d3d605bd96c0150bfdb557e555f1d20e79d74cbe4343d99c49
7
- data.tar.gz: dcc10eeadec0f32073569db402893bb3f1ab5629da79eee32d605c476bbcf05f637f5b348f83478863bf7d28fc1d0b3d2cd9b54c38f356f3bfa53d51d3ac50bb
6
+ metadata.gz: 0163416a3ea0f4d5cb5619b74e31ca7f2999ddccba55091d8fa8f10244a36eeb59dadd1b6d9eae6bd22d1f49e5ea7213aeb42218731f248be69d54c2127bb216
7
+ data.tar.gz: ee7adc256954aafa194a3175f50649479a8f45292d8bed51cf076f17c1d0693fbe7099e64675d249448c1e66cb9b1dce4749da71e76f36fc2d976b7387f23382
data/CHANGELOG.md CHANGED
@@ -1,5 +1,27 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [0.6.0] - 2025-07-18
4
+
5
+ ### Breaking Changes
6
+ - **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/CiTroNaK/verquest/pull/12), [@CiTroNaK](https://github.com/CiTroNaK))
7
+
8
+ ### New Features
9
+ - Add support for `enum` properties. ([#13](https://github.com/CiTroNaK/verquest/pull/13), [@CiTroNaK](https://github.com/CiTroNaK))
10
+ - Add support for inverted mapping, that can be used to map internal structure to external representation (e.g., for errors). ([#10](https://github.com/CiTroNaK/verquest/pull/10), [@CiTroNaK](https://github.com/CiTroNaK))
11
+ - Add support for constants in the schema, allowing to define fixed values for properties. ([#11](https://github.com/CiTroNaK/verquest/pull/11), [@CiTroNaK](https://github.com/CiTroNaK))
12
+
13
+ ## [0.5.0] - 2025-07-01
14
+
15
+ ### Fixed
16
+ - Handling `with_options` defaults like required and nullable.
17
+
18
+ ### New Features
19
+ - Add `default_additional_properties` option to configuration.
20
+ - Add support for nullable properties (`nullable: true`) based on the latest JSON Schema specification, which is also used in OpenAPI 3.1.
21
+ - Add support for `dependentRequired` (see https://json-schema.org/understanding-json-schema/reference/conditionals#dependentRequired).
22
+
23
+ ## [0.4.0] - 2025-06-28
24
+
3
25
  ### Breaking Changes
4
26
  - **BREAKING:** Renaming validation method from `validate_schema` to `valid_schema?` to better reflect its purpose.
5
27
  - **BREAKING:** The `validate_schema` now returns an array of errors instead of a boolean value, allowing for more detailed error reporting.
data/README.md CHANGED
@@ -8,18 +8,18 @@ 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
+ > 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/CiTroNaK/verquest/issues?q=sort:updated-desc%20is:issue%20is:open%20label:enhancement).
16
16
 
17
17
  ## Installation
18
18
 
19
19
  Add this line to your application's Gemfile:
20
20
 
21
21
  ```ruby
22
- gem "verquest", "~> 0.3"
22
+ gem "verquest", "~> 0.6"
23
23
  ```
24
24
 
25
25
  And then execute:
@@ -62,7 +62,7 @@ class UserCreateRequest < Verquest::Base
62
62
  field :email, format: "email", description: "The email address of the user"
63
63
  end
64
64
 
65
- field :birth_date, type: :string, format: "date", description: "The birth date of the user"
65
+ field :birth_date, type: :string, nullable: true, format: "date", description: "The birth date of the user"
66
66
 
67
67
  reference :address, from: AddressCreateRequest, required: true
68
68
 
@@ -75,7 +75,7 @@ class UserCreateRequest < Verquest::Base
75
75
  end
76
76
  end
77
77
 
78
- field :role, type: :string, description: "Role of the user", enum: %w[member manager], default: "member"
78
+ enum :role, values: %w[member manager], default: "member", description: "Role of the user", required: true
79
79
 
80
80
  object :profile_details do
81
81
  field :bio, type: :string, description: "Short biography of the user"
@@ -89,6 +89,8 @@ class UserCreateRequest < Verquest::Base
89
89
  end
90
90
  end
91
91
  end
92
+
93
+ const :company, value: "Awesome Inc."
92
94
  end
93
95
  end
94
96
  ```
@@ -130,12 +132,12 @@ Output:
130
132
  {
131
133
  "type" => "object",
132
134
  "description" => "User Create Request",
133
- "required" => ["first_name", "last_name", "email", "address"],
135
+ "required" => ["first_name", "last_name", "email", "address", "role"],
134
136
  "properties" => {
135
137
  "first_name" => {"type" => "string", "description" => "The first name of the user", "maxLength" => 50},
136
138
  "last_name" => {"type" => "string", "description" => "The last name of the user", "maxLength" => 50},
137
139
  "email" => {"type" => "string", "format" => "email", "description" => "The email address of the user"},
138
- "birth_date" => {"type" => "string", "format" => "date", "description" => "The birth date of the user"},
140
+ "birth_date" => {"type" => ["string", "null"], "format" => "date", "description" => "The birth date of the user"},
139
141
  "address" => {"$ref" => "#/components/schemas/AddressCreateRequest"},
140
142
  "permissions" => {
141
143
  "type" => "array",
@@ -151,10 +153,9 @@ Output:
151
153
  "description" => "Permissions associated with the user"
152
154
  },
153
155
  "role" => {
154
- "type" => "string",
155
- "description" => "Role of the user",
156
- "enum" => ["member", "manager"],
157
- "default" => "member"
156
+ "enum" => ["member", "manager"],
157
+ "default" => "member",
158
+ "description" => "Role of the user"
158
159
  },
159
160
  "profile_details" => {
160
161
  "type" => "object",
@@ -172,7 +173,8 @@ Output:
172
173
  "description" => "Some social networks"
173
174
  }
174
175
  }
175
- }
176
+ },
177
+ "company" => {"const" => "Awesome Inc."}
176
178
  },
177
179
  "additionalProperties" => false
178
180
  }
@@ -191,7 +193,7 @@ Output:
191
193
  {
192
194
  "type" => "object",
193
195
  "description" => "User Create Request",
194
- "required" => ["first_name", "last_name", "email", "address"],
196
+ "required" => ["first_name", "last_name", "email", "address", "role"],
195
197
  "properties" => {
196
198
  "first_name" => {"type" => "string", "description" => "The first name of the user", "maxLength" => 50},
197
199
  "last_name" => {"type" => "string", "description" => "The last name of the user", "maxLength" => 50},
@@ -222,10 +224,9 @@ Output:
222
224
  "description" => "Permissions associated with the user"
223
225
  },
224
226
  "role" => {
225
- "type" => "string",
226
- "description" => "Role of the user",
227
227
  "enum" => ["member", "manager"],
228
- "default" => "member"
228
+ "default" => "member",
229
+ "description" => "Role of the user"
229
230
  },
230
231
  "profile_details" => {"type" => "object",
231
232
  "required" => [],
@@ -240,7 +241,8 @@ Output:
240
241
  "mastodon" => {"type" => "string", "format" => "uri", "description" => "Mastodon profile URL"}
241
242
  },
242
243
  "description" => "Some social networks"}
243
- }
244
+ },
245
+ "company" => {"const" => "Awesome Inc."}
244
246
  }
245
247
  },
246
248
  "additionalProperties" => false
@@ -265,10 +267,12 @@ The JSON schema can be used for both validation of incoming parameters and for g
265
267
  #### Component types
266
268
 
267
269
  - `field`: Represents a scalar value (string, integer, boolean, etc.).
270
+ - `enum`: Represents a property with a limited set of values (enumeration).
268
271
  - `object`: Represents a JSON object with properties.
269
272
  - `array`: Represents a JSON array with scalar items.
270
273
  - `collection`: Represents a array of objects defined manually or by a reference to another request.
271
274
  - `reference`: Represents a reference to another request, allowing you to reuse existing request structures.
275
+ - `const`: Represents a [constant](https://json-schema.org/understanding-json-schema/reference/const#constant-values) value that is always present in the request.
272
276
 
273
277
  #### Helper methods
274
278
 
@@ -276,6 +280,101 @@ The JSON schema can be used for both validation of incoming parameters and for g
276
280
  - `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
281
  - `with_options`: Allows you to define multiple fields with the same options, reducing repetition.
278
282
 
283
+ #### Required properties
284
+
285
+ 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.
286
+
287
+ ```ruby
288
+ class DependentRequiredRequest < Verquest::Base
289
+ description "This is a simple request with nullable properties for testing purposes."
290
+
291
+ version "2025-06" do
292
+ field :name, type: :string, required: true
293
+ field :credit_card, type: :number, required: %i[billing_address]
294
+ field :billing_address, type: :string
295
+ end
296
+ end
297
+ ```
298
+
299
+ Will produce this validation schema:
300
+
301
+ ```ruby
302
+ {
303
+ "type" => "object",
304
+ "description" => "This is a simple request with nullable properties for testing purposes.",
305
+ "required" => ["name"],
306
+ "dependentRequired" => {"credit_card" => ["billing_address"]},
307
+ "properties" => {
308
+ "name" => {"type" => "string"},
309
+ "credit_card" => {"type" => "number"},
310
+ "billing_address" => {"type" => "string"}
311
+ },
312
+ "additionalProperties" => false
313
+ }
314
+ ```
315
+
316
+ #### Nullable properties
317
+
318
+ You can define nullable properties in your request schema by setting the `nullable` option to `true`. This feature is based on the latest JSON Schema specification, which is also used in OpenAPI 3.1.
319
+
320
+ ```ruby
321
+ class NullableRequest < Verquest::Base
322
+ description "This is a simple request with nullable properties for testing purposes."
323
+
324
+ version "2025-06" do
325
+ with_options nullable: true do
326
+ array :array, type: :string
327
+ collection :collection_with_item, item: ReferencedRequest
328
+ collection :collection_with_object do
329
+ field :field, type: :string, nullable: false
330
+ end
331
+
332
+ field :field, type: :string
333
+
334
+ object :object do
335
+ field :field, type: :string, nullable: false
336
+ end
337
+
338
+ reference :referenced_object, from: ReferencedRequest
339
+ reference :referenced_field, from: ReferencedRequest, property: :simple_field
340
+ end
341
+ end
342
+ end
343
+ ```
344
+
345
+ Will produce this validation schema:
346
+
347
+ ```ruby
348
+ {
349
+ "type" => "object",
350
+ "description" => "This is a simple request with nullable properties for testing purposes.",
351
+ "required" => [],
352
+ "properties" => {
353
+ "array" => {"type" => %w[array null], "items" => {"type" => "string"}},
354
+ "collection_with_item" => {"type" => %w[array null], "items" => {"type" => "object", "description" => "This is an another example for testing purposes.", "required" => %w[simple_field nested], "properties" => {"simple_field" => {"type" => "string", "description" => "The simple field"}, "nested" => {"type" => "object", "required" => %w[nested_field_1 nested_field_2], "properties" => {"nested_field_1" => {"type" => "string", "description" => "This is a nested field"}, "nested_field_2" => {"type" => "string", "description" => "This is another nested field"}}, "additionalProperties" => false}}, "additionalProperties" => false}},
355
+ "collection_with_object" => {"type" => %w[array null], "items" => {"type" => "object", "required" => [], "properties" => {"field" => {"type" => "string"}}, "additionalProperties" => false}},
356
+ "field" => {"type" => %w[string null]},
357
+ "object" => {
358
+ "type" => %w[object null],
359
+ "required" => [],
360
+ "properties" => {
361
+ "field" => {"type" => "string"}
362
+ },
363
+ "additionalProperties" => false
364
+ },
365
+ "referenced_object" => {
366
+ "type" => %w[object null],
367
+ "description" => "This is an another example for testing purposes.",
368
+ "required" => %w[simple_field nested],
369
+ "properties" => {"simple_field" => {"type" => "string", "description" => "The simple field"}, "nested" => {"type" => "object", "required" => %w[nested_field_1 nested_field_2], "properties" => {"nested_field_1" => {"type" => "string", "description" => "This is a nested field"}, "nested_field_2" => {"type" => "string", "description" => "This is another nested field"}}, "additionalProperties" => false}},
370
+ "additionalProperties" => false
371
+ },
372
+ "referenced_field" => {"type" => %w[string null], "description" => "The simple field"}
373
+ },
374
+ "additionalProperties" => false
375
+ }
376
+ ```
377
+
279
378
  #### Custom Field Types
280
379
 
281
380
  You can define custom field types that can be used in `field` and `array` in the configuration.
@@ -443,9 +542,28 @@ Will be transformed to:
443
542
 
444
543
  What you can use:
445
544
  - `/` to reference the root of the request structure
446
- - `nested.structure` use dot notation to reference nested structures
545
+ - `nested/structure` use slash notation to reference nested structures
447
546
  - if the `map` is not set, the field name will be used as the key in the internal structure
448
547
 
548
+ To get the mapping to map the request structure back to the external API structure, you can use the `external_mapping` method:
549
+
550
+ ```ruby
551
+ UserCreateRequest.external_mapping(version: "2025-06")
552
+ ```
553
+
554
+ Will produce the following mapping:
555
+
556
+ ```ruby
557
+ {
558
+ "name" => "full_name",
559
+ "email" => "email",
560
+ "phone" => "phone",
561
+ "address_street" => "address/street",
562
+ "address_city" => "address/city",
563
+ "address_zip" => "address/postal_code"
564
+ }
565
+ ```
566
+
449
567
  There are some limitations and the implementation can be improved, but it should works for most common use cases.
450
568
 
451
569
  See the mapping test (in `test/verquest/base_test.rb`) for more examples of mapping.
@@ -483,12 +601,15 @@ Verquest.configure do |config|
483
601
 
484
602
  # Set custom version resolver
485
603
  config.version_resolver = CustomeVersionResolver # default is `Verquest::VersionResolver`
604
+
605
+ # Set default value for additional properties
606
+ config.default_additional_properties = false # default
486
607
  end
487
608
  ```
488
609
 
489
610
  ## Documentation
490
611
 
491
- For detailed documentation, please visit the [YARD documentation](https://www.rubydoc.info/gems/verquest/0.4.0/).
612
+ For detailed documentation, please visit the [YARD documentation](https://www.rubydoc.info/gems/verquest/0.6.0/).
492
613
 
493
614
  ## Development
494
615
 
@@ -92,9 +92,9 @@ module Verquest
92
92
  camelize(schema_options)
93
93
 
94
94
  if current_scope.nil?
95
- versions.schema_options = schema_options
95
+ versions.schema_options.merge!(schema_options)
96
96
  elsif current_scope.is_a?(Version)
97
- current_scope.schema_options = schema_options
97
+ current_scope.schema_options.merge!(schema_options)
98
98
  else
99
99
  raise "Additional properties can only be set within a version scope or globally"
100
100
  end
@@ -122,38 +122,78 @@ module Verquest
122
122
  # @param name [Symbol] The name of the field
123
123
  # @param type [Symbol] The data type of the field
124
124
  # @param map [String, nil] An optional mapping to another field
125
- # @param required [Boolean] Whether the field is required
125
+ # @param required [Boolean, Array<Symbol>] Whether the field is required
126
+ # @param nullable [Boolean] Whether the field can be null
126
127
  # @param schema_options [Hash] Additional schema options for the field
127
128
  # @return [void]
128
- def field(name, type: nil, map: nil, required: false, **schema_options)
129
+ def field(name, type: nil, map: nil, required: nil, nullable: nil, **schema_options)
129
130
  camelize(schema_options)
130
131
 
131
132
  type = default_options.fetch(:type, type)
132
- required = default_options.fetch(:required, required)
133
- schema_options = default_options.except(:type, :required).merge(schema_options)
133
+ required = default_options.fetch(:required, false) if required.nil?
134
+ nullable = default_options.fetch(:nullable, false) if nullable.nil?
135
+ schema_options = default_options.except(:type, :required, :nullable).merge(schema_options)
134
136
 
135
- field = Properties::Field.new(name:, type:, map:, required:, **schema_options)
137
+ field = Properties::Field.new(name:, type:, map:, required:, nullable:, **schema_options)
136
138
  current_scope.add(field)
137
139
  end
138
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
+
139
177
  # Defines a new object for the current version scope
140
178
  #
141
179
  # @param name [Symbol] The name of the object
142
180
  # @param map [String, nil] An optional mapping to another object
143
- # @param required [Boolean] Whether the object is required
181
+ # @param required [Boolean, Array<Symbol>] Whether the object is required
182
+ # @param nullable [Boolean] Whether the object can be null
144
183
  # @param schema_options [Hash] Additional schema options for the object
145
184
  # @yield Block executed in the context of the new object definition
146
185
  # @return [void]
147
- def object(name, map: nil, required: false, **schema_options, &block)
186
+ def object(name, map: nil, required: nil, nullable: nil, **schema_options, &block)
148
187
  unless block_given?
149
188
  raise ArgumentError, "a block must be given to define the object"
150
189
  end
151
190
 
152
191
  camelize(schema_options)
153
- required = default_options.fetch(:required, required)
154
- schema_options = default_options.except(:type, :required).merge(schema_options)
192
+ required = default_options.fetch(:required, false) if required.nil?
193
+ nullable = default_options.fetch(:nullable, false) if nullable.nil?
194
+ schema_options = default_options.except(:type, :required, :nullable).merge(schema_options)
155
195
 
156
- object = Properties::Object.new(name:, map:, required:, **schema_options)
196
+ object = Properties::Object.new(name:, map:, required:, nullable:, **schema_options)
157
197
  current_scope.add(object)
158
198
 
159
199
  if block_given?
@@ -170,12 +210,13 @@ module Verquest
170
210
  #
171
211
  # @param name [Symbol] The name of the collection
172
212
  # @param item [Class, nil] The item type in the collection
173
- # @param required [Boolean] Whether the collection is required
213
+ # @param required [Boolean, Array<Symbol>] Whether the collection is required
214
+ # @param nullable [Boolean] Whether the collection can be null
174
215
  # @param map [String, nil] An optional mapping to another collection
175
216
  # @param schema_options [Hash] Additional schema options for the collection
176
217
  # @yield Block executed in the context of the new collection definition
177
218
  # @return [void]
178
- def collection(name, item: nil, required: false, map: nil, **schema_options, &block)
219
+ def collection(name, item: nil, required: nil, nullable: nil, map: nil, **schema_options, &block)
179
220
  if item.nil? && !block_given?
180
221
  raise ArgumentError, "item must be provided or a block must be given to define the collection"
181
222
  elsif !item.nil? && !block_given? && !(item <= Verquest::Base)
@@ -183,10 +224,11 @@ module Verquest
183
224
  end
184
225
 
185
226
  camelize(schema_options)
186
- required = default_options.fetch(:required, required)
187
- schema_options = default_options.except(:required).merge(schema_options)
227
+ required = default_options.fetch(:required, false) if required.nil?
228
+ nullable = default_options.fetch(:nullable, false) if nullable.nil?
229
+ schema_options = default_options.except(:required, :nullable).merge(schema_options)
188
230
 
189
- collection = Properties::Collection.new(name:, item:, required:, map:, **schema_options)
231
+ collection = Properties::Collection.new(name:, item:, required:, nullable:, map:, **schema_options)
190
232
  current_scope.add(collection)
191
233
 
192
234
  if block_given?
@@ -205,31 +247,35 @@ module Verquest
205
247
  # @param from [Verquest::Base] The source of the reference
206
248
  # @param property [Symbol, nil] An optional specific property to reference
207
249
  # @param map [String, nil] An optional mapping to another reference
208
- # @param required [Boolean] Whether the reference is required
250
+ # @param required [Boolean, Array<Symbol>] Whether the reference is required
251
+ # @param nullable [Boolean] Whether this reference can be null
209
252
  # @return [void]
210
- def reference(name, from:, property: nil, map: nil, required: false)
211
- required = default_options.fetch(:required, required)
253
+ def reference(name, from:, property: nil, map: nil, required: nil, nullable: nil)
254
+ required = default_options.fetch(:required, false) if required.nil?
255
+ nullable = default_options.fetch(:nullable, false) if nullable.nil?
212
256
 
213
- reference = Properties::Reference.new(name:, from:, property:, map:, required:)
257
+ reference = Properties::Reference.new(name:, from:, property:, map:, required:, nullable:)
214
258
  current_scope.add(reference)
215
259
  end
216
260
 
217
261
  # Defines a new array property for the current version scope
218
262
  #
219
263
  # @param name [Symbol] The name of the array property
220
- # @param type [String] The data type of the array elements
221
- # @param required [Boolean] Whether the array property is required
264
+ # @param type [Symbol] The data type of the array elements
265
+ # @param required [Boolean, Array<Symbol>] Whether the array property is required
266
+ # @param nullable [Boolean] Whether this array can be null
222
267
  # @param map [String, nil] An optional mapping to another array property
223
268
  # @param schema_options [Hash] Additional schema options for the array property
224
269
  # @return [void]
225
- def array(name, type:, required: false, map: nil, **schema_options)
270
+ def array(name, type:, required: nil, nullable: nil, map: nil, **schema_options)
226
271
  camelize(schema_options)
227
272
 
228
273
  type = default_options.fetch(:type, type)
229
- required = default_options.fetch(:required, required)
230
- schema_options = default_options.except(:type, :required).merge(schema_options)
274
+ required = default_options.fetch(:required, false) if required.nil?
275
+ nullable = default_options.fetch(:nullable, false) if nullable.nil?
276
+ schema_options = default_options.except(:type, :required, :nullable).merge(schema_options)
231
277
 
232
- array = Properties::Array.new(name:, type:, required:, map:, **schema_options)
278
+ array = Properties::Array.new(name:, type:, required:, nullable:, map:, **schema_options)
233
279
  current_scope.add(array)
234
280
  end
235
281
 
@@ -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
@@ -52,7 +52,12 @@ module Verquest
52
52
  # @!attribute [rw] insert_property_defaults
53
53
  # Controls whether default values defined in property schemas should be inserted when not provided during validation
54
54
  # @return [Boolean] true if default values should be inserted, false otherwise
55
- attr_accessor :validate_params, :json_schema_version, :validation_error_handling, :remove_extra_root_keys, :insert_property_defaults
55
+ #
56
+ # @!attribute [rw] default_additional_properties
57
+ # Controls the default behavior for handling properties not defined in the schema
58
+ # @return [Boolean] false to disallow additional properties (default), true to allow them
59
+ attr_accessor :validate_params, :json_schema_version, :validation_error_handling,
60
+ :remove_extra_root_keys, :insert_property_defaults, :default_additional_properties
56
61
 
57
62
  # @!attribute [r] current_version
58
63
  # A callable object that returns the current API version to use when not explicitly specified
@@ -73,11 +78,12 @@ module Verquest
73
78
  def initialize
74
79
  @validate_params = true
75
80
  @json_schema_version = :draft2020_12
76
- @validation_error_handling = :raise # or :result
81
+ @validation_error_handling = :raise
77
82
  @remove_extra_root_keys = true
78
83
  @version_resolver = VersionResolver
79
84
  @insert_property_defaults = true
80
85
  @custom_field_types = {}
86
+ @default_additional_properties = false
81
87
  end
82
88
 
83
89
  # Sets the current version strategy using a callable object
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Verquest
4
- GEM_VERSION = "0.4.0"
4
+ GEM_VERSION = "0.6.0"
5
5
  end
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Verquest
4
+ # HelperMethods module provides utility methods for Verquest
5
+ module HelperMethods
6
+ # Module that provides methods for working with required properties in schemas
7
+ #
8
+ # This module offers functionality to identify and categorize properties based on
9
+ # their required status within schema definitions. It distinguishes between
10
+ # unconditionally required properties (those marked with `required: true`) and
11
+ # conditionally required properties (those with dependencies expressed as arrays).
12
+ #
13
+ # When included in classes that manage properties (like Version or Base property classes),
14
+ # it provides methods to extract both types of required properties which can be used
15
+ # for schema validation, documentation generation, or UI rendering.
16
+ module RequiredProperties
17
+ # Returns all properties that are unconditionally required
18
+ #
19
+ # This method identifies properties that must always be present in valid data,
20
+ # by selecting those with their required attribute set to true (boolean).
21
+ # Results are memoized to avoid recalculating on subsequent calls.
22
+ #
23
+ # @return [Array<Symbol>] Names of properties marked as unconditionally required (required == true)
24
+ def required_properties
25
+ @_required_properties ||= properties.values.select { _1.required == true }.map(&:name)
26
+ end
27
+
28
+ # Returns properties that are conditionally required based on other properties
29
+ #
30
+ # This method identifies properties that are required only when certain other
31
+ # properties are present. These are properties where the required attribute
32
+ # is an array of dependency names rather than a boolean.
33
+ # Results are memoized to avoid recalculating on subsequent calls.
34
+ #
35
+ # @return [Hash<String, Array<String>>] Hash mapping property names to their dependency arrays
36
+ # @example Return value format:
37
+ # {
38
+ # "property_name1": ["dependency1", "dependency2"],
39
+ # "property_name2": ["dependency3"]
40
+ # }
41
+ def dependent_required_properties
42
+ @_dependent_required_properties ||= properties.values.select { _1.required.is_a?(Array) }.each_with_object({}) do |property, hash|
43
+ hash[property.name] = property.required.map(&:to_s)
44
+ end
45
+ end
46
+ end
47
+ end
48
+ end