verquest 0.4.0 → 0.5.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: b9349b2773c4a03024805d78257b1e763ffb87b87f27726620e343395c683aa0
4
+ data.tar.gz: ec2b70a4a10c3314ca27283776138f85b53b245d88869de39e450ca2eb2064de
5
5
  SHA512:
6
- metadata.gz: 3991e8ff96b436e82a6edce43d591f35f669cbab9ef99b01bc26192bef94e3f48afe59595d8ae5d3d605bd96c0150bfdb557e555f1d20e79d74cbe4343d99c49
7
- data.tar.gz: dcc10eeadec0f32073569db402893bb3f1ab5629da79eee32d605c476bbcf05f637f5b348f83478863bf7d28fc1d0b3d2cd9b54c38f356f3bfa53d51d3ac50bb
6
+ metadata.gz: 0d0042f959285f4253c7802166ab64d53b7013d765bfe954fd68a8ab46bb59f162512a285aca11c77e86a8e6b5ce36445f47e4e9530fcaa0010e8c6cadcefdf2
7
+ data.tar.gz: 06bbfceb1c989e1e3eabb20e495790ac35934e1cb4687e71960315c3e64d3a30b42b345c03d19d32be921b75550a6fef002d8ba44018ded8f7c9a3af73d920f0
data/CHANGELOG.md CHANGED
@@ -1,5 +1,17 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [0.5.0] - 2025-07-01
4
+
5
+ ### Fixed
6
+ - Handling `with_options` defaults like required and nullable.
7
+
8
+ ### New Features
9
+ - Add `default_additional_properties` option to configuration.
10
+ - Add support for nullable properties (`nullable: true`) based on the latest JSON Schema specification, which is also used in OpenAPI 3.1.
11
+ - Add support for `dependentRequired` (see https://json-schema.org/understanding-json-schema/reference/conditionals#dependentRequired).
12
+
13
+ ## [0.4.0] - 2025-06-28
14
+
3
15
  ### Breaking Changes
4
16
  - **BREAKING:** Renaming validation method from `validate_schema` to `valid_schema?` to better reflect its purpose.
5
17
  - **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
@@ -19,7 +19,7 @@ Verquest is a Ruby gem that offers an elegant solution for versioning API reques
19
19
  Add this line to your application's Gemfile:
20
20
 
21
21
  ```ruby
22
- gem "verquest", "~> 0.3"
22
+ gem "verquest", "~> 0.5"
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
 
@@ -135,7 +135,7 @@ Output:
135
135
  "first_name" => {"type" => "string", "description" => "The first name of the user", "maxLength" => 50},
136
136
  "last_name" => {"type" => "string", "description" => "The last name of the user", "maxLength" => 50},
137
137
  "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"},
138
+ "birth_date" => {"type" => ["string", "null"], "format" => "date", "description" => "The birth date of the user"},
139
139
  "address" => {"$ref" => "#/components/schemas/AddressCreateRequest"},
140
140
  "permissions" => {
141
141
  "type" => "array",
@@ -276,6 +276,101 @@ The JSON schema can be used for both validation of incoming parameters and for g
276
276
  - `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
277
  - `with_options`: Allows you to define multiple fields with the same options, reducing repetition.
278
278
 
279
+ ### Required properties
280
+
281
+ 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
+
283
+ ```ruby
284
+ class DependentRequiredRequest < Verquest::Base
285
+ description "This is a simple request with nullable properties for testing purposes."
286
+
287
+ version "2025-06" do
288
+ field :name, type: :string, required: true
289
+ field :credit_card, type: :number, required: %i[billing_address]
290
+ field :billing_address, type: :string
291
+ end
292
+ end
293
+ ```
294
+
295
+ Will produce this validation schema:
296
+
297
+ ```ruby
298
+ {
299
+ "type" => "object",
300
+ "description" => "This is a simple request with nullable properties for testing purposes.",
301
+ "required" => ["name"],
302
+ "dependentRequired" => {"credit_card" => ["billing_address"]},
303
+ "properties" => {
304
+ "name" => {"type" => "string"},
305
+ "credit_card" => {"type" => "number"},
306
+ "billing_address" => {"type" => "string"}
307
+ },
308
+ "additionalProperties" => false
309
+ }
310
+ ```
311
+
312
+ #### Nullable properties
313
+
314
+ 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.
315
+
316
+ ```ruby
317
+ class NullableRequest < Verquest::Base
318
+ description "This is a simple request with nullable properties for testing purposes."
319
+
320
+ version "2025-06" do
321
+ with_options nullable: true do
322
+ array :array, type: :string
323
+ collection :collection_with_item, item: ReferencedRequest
324
+ collection :collection_with_object do
325
+ field :field, type: :string, nullable: false
326
+ end
327
+
328
+ field :field, type: :string
329
+
330
+ object :object do
331
+ field :field, type: :string, nullable: false
332
+ end
333
+
334
+ reference :referenced_object, from: ReferencedRequest
335
+ reference :referenced_field, from: ReferencedRequest, property: :simple_field
336
+ end
337
+ end
338
+ end
339
+ ```
340
+
341
+ Will produce this validation schema:
342
+
343
+ ```ruby
344
+ {
345
+ "type" => "object",
346
+ "description" => "This is a simple request with nullable properties for testing purposes.",
347
+ "required" => [],
348
+ "properties" => {
349
+ "array" => {"type" => %w[array null], "items" => {"type" => "string"}},
350
+ "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}},
351
+ "collection_with_object" => {"type" => %w[array null], "items" => {"type" => "object", "required" => [], "properties" => {"field" => {"type" => "string"}}, "additionalProperties" => false}},
352
+ "field" => {"type" => %w[string null]},
353
+ "object" => {
354
+ "type" => %w[object null],
355
+ "required" => [],
356
+ "properties" => {
357
+ "field" => {"type" => "string"}
358
+ },
359
+ "additionalProperties" => false
360
+ },
361
+ "referenced_object" => {
362
+ "type" => %w[object null],
363
+ "description" => "This is an another example for testing purposes.",
364
+ "required" => %w[simple_field nested],
365
+ "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}},
366
+ "additionalProperties" => false
367
+ },
368
+ "referenced_field" => {"type" => %w[string null], "description" => "The simple field"}
369
+ },
370
+ "additionalProperties" => false
371
+ }
372
+ ```
373
+
279
374
  #### Custom Field Types
280
375
 
281
376
  You can define custom field types that can be used in `field` and `array` in the configuration.
@@ -483,12 +578,15 @@ Verquest.configure do |config|
483
578
 
484
579
  # Set custom version resolver
485
580
  config.version_resolver = CustomeVersionResolver # default is `Verquest::VersionResolver`
581
+
582
+ # Set default value for additional properties
583
+ config.default_additional_properties = false # default
486
584
  end
487
585
  ```
488
586
 
489
587
  ## Documentation
490
588
 
491
- For detailed documentation, please visit the [YARD documentation](https://www.rubydoc.info/gems/verquest/0.4.0/).
589
+ For detailed documentation, please visit the [YARD documentation](https://www.rubydoc.info/gems/verquest/0.5.0/).
492
590
 
493
591
  ## Development
494
592
 
@@ -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,17 +122,19 @@ 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
 
@@ -140,20 +142,22 @@ module Verquest
140
142
  #
141
143
  # @param name [Symbol] The name of the object
142
144
  # @param map [String, nil] An optional mapping to another object
143
- # @param required [Boolean] Whether the object is required
145
+ # @param required [Boolean, Array<Symbol>] Whether the object is required
146
+ # @param nullable [Boolean] Whether the object can be null
144
147
  # @param schema_options [Hash] Additional schema options for the object
145
148
  # @yield Block executed in the context of the new object definition
146
149
  # @return [void]
147
- def object(name, map: nil, required: false, **schema_options, &block)
150
+ def object(name, map: nil, required: nil, nullable: nil, **schema_options, &block)
148
151
  unless block_given?
149
152
  raise ArgumentError, "a block must be given to define the object"
150
153
  end
151
154
 
152
155
  camelize(schema_options)
153
- required = default_options.fetch(:required, required)
154
- schema_options = default_options.except(:type, :required).merge(schema_options)
156
+ required = default_options.fetch(:required, false) if required.nil?
157
+ nullable = default_options.fetch(:nullable, false) if nullable.nil?
158
+ schema_options = default_options.except(:type, :required, :nullable).merge(schema_options)
155
159
 
156
- object = Properties::Object.new(name:, map:, required:, **schema_options)
160
+ object = Properties::Object.new(name:, map:, required:, nullable:, **schema_options)
157
161
  current_scope.add(object)
158
162
 
159
163
  if block_given?
@@ -170,12 +174,13 @@ module Verquest
170
174
  #
171
175
  # @param name [Symbol] The name of the collection
172
176
  # @param item [Class, nil] The item type in the collection
173
- # @param required [Boolean] Whether the collection is required
177
+ # @param required [Boolean, Array<Symbol>] Whether the collection is required
178
+ # @param nullable [Boolean] Whether the collection can be null
174
179
  # @param map [String, nil] An optional mapping to another collection
175
180
  # @param schema_options [Hash] Additional schema options for the collection
176
181
  # @yield Block executed in the context of the new collection definition
177
182
  # @return [void]
178
- def collection(name, item: nil, required: false, map: nil, **schema_options, &block)
183
+ def collection(name, item: nil, required: nil, nullable: nil, map: nil, **schema_options, &block)
179
184
  if item.nil? && !block_given?
180
185
  raise ArgumentError, "item must be provided or a block must be given to define the collection"
181
186
  elsif !item.nil? && !block_given? && !(item <= Verquest::Base)
@@ -183,10 +188,11 @@ module Verquest
183
188
  end
184
189
 
185
190
  camelize(schema_options)
186
- required = default_options.fetch(:required, required)
187
- schema_options = default_options.except(:required).merge(schema_options)
191
+ required = default_options.fetch(:required, false) if required.nil?
192
+ nullable = default_options.fetch(:nullable, false) if nullable.nil?
193
+ schema_options = default_options.except(:required, :nullable).merge(schema_options)
188
194
 
189
- collection = Properties::Collection.new(name:, item:, required:, map:, **schema_options)
195
+ collection = Properties::Collection.new(name:, item:, required:, nullable:, map:, **schema_options)
190
196
  current_scope.add(collection)
191
197
 
192
198
  if block_given?
@@ -205,31 +211,35 @@ module Verquest
205
211
  # @param from [Verquest::Base] The source of the reference
206
212
  # @param property [Symbol, nil] An optional specific property to reference
207
213
  # @param map [String, nil] An optional mapping to another reference
208
- # @param required [Boolean] Whether the reference is required
214
+ # @param required [Boolean, Array<Symbol>] Whether the reference is required
215
+ # @param nullable [Boolean] Whether this reference can be null
209
216
  # @return [void]
210
- def reference(name, from:, property: nil, map: nil, required: false)
211
- required = default_options.fetch(:required, required)
217
+ def reference(name, from:, property: nil, map: nil, required: nil, nullable: nil)
218
+ required = default_options.fetch(:required, false) if required.nil?
219
+ nullable = default_options.fetch(:nullable, false) if nullable.nil?
212
220
 
213
- reference = Properties::Reference.new(name:, from:, property:, map:, required:)
221
+ reference = Properties::Reference.new(name:, from:, property:, map:, required:, nullable:)
214
222
  current_scope.add(reference)
215
223
  end
216
224
 
217
225
  # Defines a new array property for the current version scope
218
226
  #
219
227
  # @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
228
+ # @param type [Symbol] The data type of the array elements
229
+ # @param required [Boolean, Array<Symbol>] Whether the array property is required
230
+ # @param nullable [Boolean] Whether this array can be null
222
231
  # @param map [String, nil] An optional mapping to another array property
223
232
  # @param schema_options [Hash] Additional schema options for the array property
224
233
  # @return [void]
225
- def array(name, type:, required: false, map: nil, **schema_options)
234
+ def array(name, type:, required: nil, nullable: nil, map: nil, **schema_options)
226
235
  camelize(schema_options)
227
236
 
228
237
  type = default_options.fetch(:type, type)
229
- required = default_options.fetch(:required, required)
230
- schema_options = default_options.except(:type, :required).merge(schema_options)
238
+ required = default_options.fetch(:required, false) if required.nil?
239
+ nullable = default_options.fetch(:nullable, false) if nullable.nil?
240
+ schema_options = default_options.except(:type, :required, :nullable).merge(schema_options)
231
241
 
232
- array = Properties::Array.new(name:, type:, required:, map:, **schema_options)
242
+ array = Properties::Array.new(name:, type:, required:, nullable:, map:, **schema_options)
233
243
  current_scope.add(array)
234
244
  end
235
245
 
@@ -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.5.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
@@ -20,31 +20,39 @@ module Verquest
20
20
  # @param name [String, Symbol] The name of the property
21
21
  # @param type [String, Symbol] The type of items in the array, can be a default type or a custom field type
22
22
  # @param map [String, nil] The mapping path for this property (nil for no explicit mapping)
23
- # @param required [Boolean] Whether this property is required
23
+ # @param required [Boolean, Array<Symbol>] Whether this property is required, or array of dependency names
24
+ # @param nullable [Boolean] Whether this property can be null
24
25
  # @param item_schema_options [Hash] Additional JSON schema options for the array items (merged with custom type options)
25
26
  # @param schema_options [Hash] Additional JSON schema options for the array property itself
26
27
  # @raise [ArgumentError] If type is not one of the allowed types (default or custom)
27
28
  # @raise [ArgumentError] If attempting to map an array to the root
28
- def initialize(name:, type:, map: nil, required: false, item_schema_options: {}, **schema_options)
29
+ def initialize(name:, type:, map: nil, required: false, nullable: false, item_schema_options: {}, **schema_options)
29
30
  raise ArgumentError, "Type must be one of #{allowed_types.join(", ")}" unless allowed_types.include?(type.to_s)
30
31
  raise ArgumentError, "You can not map array to the root" if map == "/"
31
32
 
32
33
  if (custom_type = Verquest.configuration.custom_field_types[type.to_sym])
33
- @type = custom_type[:type].to_s
34
+ @item_type = custom_type[:type].to_s
34
35
  @item_schema_options = if custom_type.key?(:schema_options)
35
36
  custom_type[:schema_options].merge(item_schema_options).transform_keys(&:to_s)
36
37
  else
37
38
  item_schema_options.transform_keys(&:to_s)
38
39
  end
39
40
  else
40
- @type = type.to_s
41
+ @item_type = type.to_s
41
42
  @item_schema_options = item_schema_options.transform_keys(&:to_s)
42
43
  end
43
44
 
44
45
  @name = name.to_s
45
46
  @map = map
46
47
  @required = required
48
+ @nullable = nullable
47
49
  @schema_options = schema_options&.transform_keys(&:to_s)
50
+
51
+ @type = if nullable
52
+ %w[array null]
53
+ else
54
+ "array"
55
+ end
48
56
  end
49
57
 
50
58
  # Generate JSON schema definition for this array property
@@ -53,8 +61,8 @@ module Verquest
53
61
  def to_schema
54
62
  {
55
63
  name => {
56
- "type" => "array",
57
- "items" => {"type" => type}.merge(item_schema_options)
64
+ "type" => type,
65
+ "items" => {"type" => item_type}.merge(item_schema_options)
58
66
  }.merge(schema_options)
59
67
  }
60
68
  end
@@ -72,7 +80,7 @@ module Verquest
72
80
 
73
81
  private
74
82
 
75
- attr_reader :type, :schema_options, :item_schema_options
83
+ attr_reader :type, :item_type, :schema_options, :item_schema_options
76
84
 
77
85
  # Gets the list of allowed item types, including both default and custom types
78
86
  #
@@ -10,6 +10,8 @@ module Verquest
10
10
  #
11
11
  # @abstract Subclass and override {#to_schema}, {#mapping} to implement
12
12
  class Base
13
+ include HelperMethods::RequiredProperties
14
+
13
15
  # @!attribute [rw] name
14
16
  # @return [String] The name of the property
15
17
  # @!attribute [rw] required
@@ -55,6 +57,10 @@ module Verquest
55
57
 
56
58
  private
57
59
 
60
+ # @!attribute [r] nullable
61
+ # @return [Boolean] Whether this property can be null
62
+ attr_reader :nullable
63
+
58
64
  # Determines the mapping target key based on mapping configuration
59
65
  # @param value_prefix [Array<String>] Prefix for the target value
60
66
  # @param collection [Boolean] Whether this is a collection mapping
@@ -22,11 +22,12 @@ module Verquest
22
22
  #
23
23
  # @param name [String, Symbol] The name of the property
24
24
  # @param item [Verquest::Base, nil] Optional reference to an external schema class
25
- # @param required [Boolean] Whether this property is required
25
+ # @param required [Boolean, Array<Symbol>] Whether this property is required, or array of dependency names
26
+ # @param nullable [Boolean] Whether this property can be null
26
27
  # @param map [String, nil] The mapping path for this property
27
28
  # @param schema_options [Hash] Additional JSON schema options for this property
28
29
  # @raise [ArgumentError] If attempting to map a collection to the root
29
- def initialize(name:, item: nil, required: false, map: nil, **schema_options)
30
+ def initialize(name:, item: nil, required: false, nullable: false, map: nil, **schema_options)
30
31
  raise ArgumentError, "You can not map collection to the root" if map == "/"
31
32
 
32
33
  @properties = {}
@@ -34,8 +35,15 @@ module Verquest
34
35
  @name = name.to_s
35
36
  @item = item
36
37
  @required = required
38
+ @nullable = nullable
37
39
  @map = map
38
40
  @schema_options = schema_options&.transform_keys(&:to_s)
41
+
42
+ @type = if nullable
43
+ %w[array null]
44
+ else
45
+ "array"
46
+ end
39
47
  end
40
48
 
41
49
  # Add a child property to this collection's item definition
@@ -60,7 +68,7 @@ module Verquest
60
68
  if has_item?
61
69
  {
62
70
  name => {
63
- "type" => "array",
71
+ "type" => type,
64
72
  "items" => {
65
73
  "$ref" => item.to_ref
66
74
  }
@@ -69,12 +77,15 @@ module Verquest
69
77
  else
70
78
  {
71
79
  name => {
72
- "type" => "array",
80
+ "type" => type,
73
81
  "items" => {
74
82
  "type" => "object",
75
- "required" => properties.values.select(&:required).map(&:name),
76
- "properties" => properties.transform_values { |property| property.to_schema[property.name] }
77
- }
83
+ "required" => required_properties,
84
+ "properties" => properties.transform_values { |property| property.to_schema[property.name] },
85
+ "additionalProperties" => Verquest.configuration.default_additional_properties
86
+ }.tap do |schema|
87
+ schema["dependentRequired"] = dependent_required_properties if dependent_required_properties.any?
88
+ end
78
89
  }.merge(schema_options)
79
90
  }
80
91
  end
@@ -88,19 +99,22 @@ module Verquest
88
99
  if has_item?
89
100
  {
90
101
  name => {
91
- "type" => "array",
102
+ "type" => type,
92
103
  "items" => item.to_validation_schema(version: version)
93
104
  }.merge(schema_options)
94
105
  }
95
106
  else
96
107
  {
97
108
  name => {
98
- "type" => "array",
109
+ "type" => type,
99
110
  "items" => {
100
111
  "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
- }
112
+ "required" => required_properties,
113
+ "properties" => properties.transform_values { |property| property.to_validation_schema(version: version)[property.name] },
114
+ "additionalProperties" => Verquest.configuration.default_additional_properties
115
+ }.tap do |schema|
116
+ schema["dependentRequired"] = dependent_required_properties if dependent_required_properties.any?
117
+ end
104
118
  }.merge(schema_options)
105
119
  }
106
120
  end
@@ -141,7 +155,7 @@ module Verquest
141
155
 
142
156
  private
143
157
 
144
- attr_reader :item, :schema_options, :properties
158
+ attr_reader :item, :schema_options, :properties, :type
145
159
  end
146
160
  end
147
161
  end
@@ -24,12 +24,13 @@ module Verquest
24
24
  #
25
25
  # @param name [String, Symbol] The name of the property
26
26
  # @param type [String, Symbol] The data type for this field, can be a default type or a custom field type
27
- # @param required [Boolean] Whether this property is required (overridden by custom type if it defines required)
27
+ # @param required [Boolean, Array<Symbol>] Whether this property is required, or array of dependency names (can be overridden by custom type)
28
+ # @param nullable [Boolean] Whether this property can be null
28
29
  # @param map [String, nil] The mapping path for this property
29
30
  # @param schema_options [Hash] Additional JSON schema options for this property (merged with custom type options)
30
31
  # @raise [ArgumentError] If type is not one of the allowed types (default or custom)
31
32
  # @raise [ArgumentError] If attempting to map a field to root without a name
32
- def initialize(name:, type:, required: false, map: nil, **schema_options)
33
+ def initialize(name:, type:, required: false, nullable: false, map: nil, **schema_options)
33
34
  raise ArgumentError, "Type must be one of #{allowed_types.join(", ")}" unless allowed_types.include?(type.to_s)
34
35
  raise ArgumentError, "You can not map fields to the root without a name" if map == "/"
35
36
 
@@ -48,14 +49,21 @@ module Verquest
48
49
  end
49
50
 
50
51
  @name = name.to_s
52
+ @nullable = nullable
51
53
  @map = map
54
+
55
+ if nullable
56
+ @type = [@type, "null"]
57
+ end
52
58
  end
53
59
 
54
60
  # Generate JSON schema definition for this field
55
61
  #
56
62
  # @return [Hash] The schema definition for this field
57
63
  def to_schema
58
- {name => {"type" => type}.merge(schema_options)}
64
+ {
65
+ name => {"type" => type}.merge(schema_options)
66
+ }
59
67
  end
60
68
 
61
69
  # Create mapping for this field property
@@ -15,16 +15,28 @@ module Verquest
15
15
  # Initialize a new Object property
16
16
  #
17
17
  # @param name [String, Symbol] The name of the property
18
- # @param required [Boolean] Whether this property is required
18
+ # @param required [Boolean, Array<Symbol>] Whether this property is required, or array of dependency names
19
+ # @param nullable [Boolean] Whether this property can be null
19
20
  # @param map [String, nil] The mapping path for this property
20
21
  # @param schema_options [Hash] Additional JSON schema options for this property
21
- def initialize(name:, required: false, map: nil, **schema_options)
22
+ def initialize(name:, required: false, nullable: false, map: nil, **schema_options)
22
23
  @properties = {}
23
24
 
24
25
  @name = name.to_s
25
26
  @required = required
27
+ @nullable = nullable
26
28
  @map = map
27
- @schema_options = schema_options&.transform_keys(&:to_s)
29
+ @schema_options = {
30
+ additionalProperties: Verquest.configuration.default_additional_properties
31
+ }.merge(schema_options)
32
+ .delete_if { |_, v| v.nil? }
33
+ .transform_keys(&:to_s)
34
+
35
+ @type = if nullable
36
+ %w[object null]
37
+ else
38
+ "object"
39
+ end
28
40
  end
29
41
 
30
42
  # Add a child property to this object
@@ -41,10 +53,12 @@ module Verquest
41
53
  def to_schema
42
54
  {
43
55
  name => {
44
- "type" => "object",
45
- "required" => properties.values.select(&:required).map(&:name),
56
+ "type" => type,
57
+ "required" => required_properties,
46
58
  "properties" => properties.transform_values { |property| property.to_schema[property.name] }
47
- }.merge(schema_options)
59
+ }.merge(schema_options).tap do |schema|
60
+ schema["dependentRequired"] = dependent_required_properties if dependent_required_properties.any?
61
+ end
48
62
  }
49
63
  end
50
64
 
@@ -55,10 +69,12 @@ module Verquest
55
69
  def to_validation_schema(version: nil)
56
70
  {
57
71
  name => {
58
- "type" => "object",
59
- "required" => properties.values.select(&:required).map(&:name),
72
+ "type" => type,
73
+ "required" => required_properties,
60
74
  "properties" => properties.transform_values { |property| property.to_validation_schema(version: version)[property.name] }
61
- }.merge(schema_options)
75
+ }.merge(schema_options).tap do |schema|
76
+ schema["dependentRequired"] = dependent_required_properties if dependent_required_properties.any?
77
+ end
62
78
  }
63
79
  end
64
80
 
@@ -26,12 +26,14 @@ module Verquest
26
26
  # @param name [String, Symbol] The name of the property
27
27
  # @param from [Class] The schema class to reference
28
28
  # @param property [Symbol, nil] Optional specific property to reference
29
+ # @param nullable [Boolean] Whether this property can be null
29
30
  # @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)
31
+ # @param required [Boolean, Array<Symbol>] Whether this property is required, or array of dependency names
32
+ def initialize(name:, from:, property: nil, nullable: false, map: nil, required: false)
32
33
  @name = name.to_s
33
34
  @from = from
34
35
  @property = property
36
+ @nullable = nullable
35
37
  @map = map
36
38
  @required = required
37
39
  end
@@ -40,9 +42,20 @@ module Verquest
40
42
  #
41
43
  # @return [Hash] The schema definition with a $ref pointer
42
44
  def to_schema
43
- {
44
- name => {"$ref" => from.to_ref(property: property)}
45
- }
45
+ if nullable
46
+ {
47
+ name => {
48
+ "oneOf" => [
49
+ {"$ref" => from.to_ref(property: property)},
50
+ {"type" => "null"}
51
+ ]
52
+ }
53
+ }
54
+ else
55
+ {
56
+ name => {"$ref" => from.to_ref(property: property)}
57
+ }
58
+ end
46
59
  end
47
60
 
48
61
  # Generate validation schema for this reference property
@@ -50,8 +63,14 @@ module Verquest
50
63
  # @param version [String, nil] The version to generate validation schema for
51
64
  # @return [Hash] The validation schema for this reference
52
65
  def to_validation_schema(version: nil)
66
+ schema = from.to_validation_schema(version:, property: property).dup
67
+
68
+ if nullable
69
+ schema["type"] = [schema["type"], "null"] unless schema["type"].include?("null")
70
+ end
71
+
53
72
  {
54
- name => from.to_validation_schema(version:, property: property)
73
+ name => schema
55
74
  }
56
75
  end
57
76
 
@@ -19,6 +19,8 @@ module Verquest
19
19
  # # Get mapping
20
20
  # mapping = version.mapping
21
21
  class Version
22
+ include HelperMethods::RequiredProperties
23
+
22
24
  # @!attribute [r] name
23
25
  # @return [String] The name/identifier of the version (e.g., "2023-01")
24
26
  #
@@ -102,6 +104,12 @@ module Verquest
102
104
  def prepare
103
105
  return if frozen?
104
106
 
107
+ unless schema_options.key?("additionalProperties")
108
+ schema_options["additionalProperties"] = Verquest.configuration.default_additional_properties
109
+ end
110
+
111
+ schema_options.delete_if { |_, v| v.nil? }
112
+
105
113
  prepare_schema
106
114
  prepare_validation_schema
107
115
  prepare_mapping
@@ -197,9 +205,11 @@ module Verquest
197
205
  @schema = {
198
206
  "type" => "object",
199
207
  "description" => description,
200
- "required" => properties.values.select(&:required).map(&:name),
208
+ "required" => required_properties,
201
209
  "properties" => properties.transform_values { |property| property.to_schema[property.name] }
202
- }.merge(schema_options).freeze
210
+ }.merge(schema_options).tap do |schema|
211
+ schema["dependentRequired"] = dependent_required_properties if dependent_required_properties.any?
212
+ end.freeze
203
213
  end
204
214
 
205
215
  # Generates the validation schema for this version
@@ -212,9 +222,11 @@ module Verquest
212
222
  @validation_schema = {
213
223
  "type" => "object",
214
224
  "description" => description,
215
- "required" => properties.values.select(&:required).map(&:name),
225
+ "required" => required_properties,
216
226
  "properties" => properties.transform_values { |property| property.to_validation_schema(version: name)[property.name] }
217
- }.merge(schema_options).freeze
227
+ }.merge(schema_options).tap do |schema|
228
+ schema["dependentRequired"] = dependent_required_properties if dependent_required_properties.any?
229
+ end.freeze
218
230
  end
219
231
 
220
232
  # Prepares the parameter mapping for this version
metadata CHANGED
@@ -1,14 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: verquest
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.0
4
+ version: 0.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Petr Hlavicka
8
- autorequire:
9
8
  bindir: exe
10
9
  cert_chain: []
11
- date: 2025-06-28 00:00:00.000000000 Z
10
+ date: 1980-01-02 00:00:00.000000000 Z
12
11
  dependencies:
13
12
  - !ruby/object:Gem::Dependency
14
13
  name: zeitwerk
@@ -62,6 +61,7 @@ files:
62
61
  - lib/verquest/base/public_class_methods.rb
63
62
  - lib/verquest/configuration.rb
64
63
  - lib/verquest/gem_version.rb
64
+ - lib/verquest/helper_methods/required_properties.rb
65
65
  - lib/verquest/properties.rb
66
66
  - lib/verquest/properties/array.rb
67
67
  - lib/verquest/properties/base.rb
@@ -81,7 +81,6 @@ metadata:
81
81
  homepage_uri: https://github.com/CiTroNaK/verquest
82
82
  source_code_uri: https://github.com/CiTroNaK/verquest
83
83
  changelog_uri: https://github.com/CiTroNaK/verquest/blob/main/CHANGELOG.md
84
- post_install_message:
85
84
  rdoc_options: []
86
85
  require_paths:
87
86
  - lib
@@ -96,8 +95,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
96
95
  - !ruby/object:Gem::Version
97
96
  version: '0'
98
97
  requirements: []
99
- rubygems_version: 3.4.19
100
- signing_key:
98
+ rubygems_version: 3.6.9
101
99
  specification_version: 4
102
100
  summary: Verquest is a Ruby gem that offers an elegant solution for versioning API
103
101
  requests