verquest 0.6.4 → 0.7.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 +4 -4
- data/CHANGELOG.md +43 -1
- data/README.md +299 -45
- data/Rakefile +5 -0
- data/docs/rails.md +169 -0
- data/lib/verquest/base/condition_class_methods.rb +211 -0
- data/lib/verquest/base/private_class_methods.rb +31 -14
- data/lib/verquest/base/public_class_methods.rb +51 -1
- data/lib/verquest/base.rb +1 -0
- data/lib/verquest/conditions/branch.rb +111 -0
- data/lib/verquest/conditions/condition.rb +194 -0
- data/lib/verquest/conditions/conjunction.rb +39 -0
- data/lib/verquest/conditions/fragment_merge.rb +52 -0
- data/lib/verquest/conditions/property_merge.rb +47 -0
- data/lib/verquest/conditions/rule_set.rb +58 -0
- data/lib/verquest/conditions/rules/absent.rb +25 -0
- data/lib/verquest/conditions/rules/alternatives.rb +28 -0
- data/lib/verquest/conditions/rules/any.rb +27 -0
- data/lib/verquest/conditions/rules/equals.rb +22 -0
- data/lib/verquest/conditions/rules/names_rule.rb +23 -0
- data/lib/verquest/conditions/rules/none.rb +24 -0
- data/lib/verquest/conditions/rules/present.rb +22 -0
- data/lib/verquest/conditions/rules/property_rule.rb +48 -0
- data/lib/verquest/conditions/rules/satisfies.rb +22 -0
- data/lib/verquest/conditions/rules/value_in.rb +22 -0
- data/lib/verquest/conditions/rules.rb +17 -0
- data/lib/verquest/conditions.rb +36 -0
- data/lib/verquest/configuration.rb +19 -1
- data/lib/verquest/gem_version.rb +1 -1
- data/lib/verquest/helper_methods/conditional_properties.rb +126 -0
- data/lib/verquest/helper_methods/unnamed_one_of.rb +35 -0
- data/lib/verquest/properties/array.rb +15 -1
- data/lib/verquest/properties/base.rb +104 -10
- data/lib/verquest/properties/collection.rb +51 -18
- data/lib/verquest/properties/const.rb +2 -1
- data/lib/verquest/properties/object.rb +38 -4
- data/lib/verquest/properties/one_of/discriminated_schema.rb +84 -0
- data/lib/verquest/properties/one_of.rb +83 -44
- data/lib/verquest/properties/reference.rb +47 -16
- data/lib/verquest/query/coercer.rb +165 -0
- data/lib/verquest/query/definition_check.rb +91 -0
- data/lib/verquest/query/parameters.rb +79 -0
- data/lib/verquest/query.rb +25 -0
- data/lib/verquest/transformer/collection.rb +32 -0
- data/lib/verquest/transformer/const_as_enum.rb +47 -0
- data/lib/verquest/transformer/layout.rb +83 -0
- data/lib/verquest/transformer/mapping_layout.rb +166 -0
- data/lib/verquest/transformer/one_of.rb +111 -0
- data/lib/verquest/transformer/path.rb +188 -0
- data/lib/verquest/transformer/plan.rb +183 -0
- data/lib/verquest/transformer/scope.rb +64 -0
- data/lib/verquest/transformer.rb +43 -880
- data/lib/verquest/version.rb +152 -79
- data/lib/verquest.rb +6 -0
- metadata +42 -7
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 8bc88f55be50f80f099c989c617954b1600ed5d61adfcf35fccd430fbbce21cc
|
|
4
|
+
data.tar.gz: 8741893b6b9c3d5e91db1b6d6c3e3bf72e84ac43babd34f62a04859ee5bf6a7e
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: d77a640d8afade1d2578899ef72a39550d870452f16e81bfef9c2aa61506c1b09aea07e7f23b3ef35ccc38218295c620389c1cda3e6eb9cdf1bb3227638358b1
|
|
7
|
+
data.tar.gz: 308cdf6806df22ec89108911cfe9a6e351e9b9c3c7b925a8a15dd31c108adc19f51d8f361528ecd566e057042b1382603ec6b3652e4f9604a5e45c711e7d3d81
|
data/CHANGELOG.md
CHANGED
|
@@ -1,6 +1,48 @@
|
|
|
1
1
|
## [Unreleased]
|
|
2
2
|
|
|
3
|
-
## [0.
|
|
3
|
+
## [0.7.0] - 2026-11-05
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
- Query parameters: `location :query` on a request class converts query strings to the types its schema expects (numbers, booleans, null, comma-separated or repeated arrays, one level of `deepObject`) before `process` validates and maps them. `to_parameters(version:)` returns OpenAPI parameter objects with symbol keys for rswag. Arrays take `explode:`, defaulting to the new `config.query_array_explode` (`true`). `parameter_examples:` on `field`, `enum`, `const`, `array`, `object` and `reference` adds named OpenAPI examples to the parameter without putting them in the JSON schemas. Shapes query strings can't express raise the new `Verquest::DefinitionError` when the version is defined.
|
|
7
|
+
- Path parameters: `location :path` converts and validates path params like query params, and `to_parameters` documents them as `in: :path`. Properties must be required scalars, otherwise the version raises `Verquest::DefinitionError`.
|
|
8
|
+
- Conditional schemas: `condition` with `given`, `then_apply` and `else_apply` compiles to JSON Schema `if` / `then` / `else` in a version, an `object` or an inline `collection`. Rules: `present`, `absent`, `equals`, `value_in`, `satisfies`, `any` and `none`. Branch properties are listed in the scope's `properties` (so `additionalProperties: false` keeps working and the mapping stays flat), and each branch forbids the properties only the other branch declares, which gives one validation error per problem. Also adds the `depends_on` shorthand and `exclude_conditions` for later versions. Conditions need `json_schema_version` `:draft7` or later, or `:openapi31`; other versions raise `ArgumentError`.
|
|
9
|
+
- [Rails and rswag guide](docs/rails.md): a controller concern for body, query and path classes, and an rswag shared context.
|
|
10
|
+
- `condition ..., document_as: :one_of` documents a condition in a version as a `oneOf` of its branches in `to_schema`, while the validation schema keeps `if` / `then` / `else`. `then_apply` and `else_apply` take `title`, `description` and `deprecated` for their variant.
|
|
11
|
+
|
|
12
|
+
### Changed
|
|
13
|
+
- A `one_of` with a `discriminator:` keeps the discriminator in the validation schema. Its variants move into local `definitions` referenced by `$ref`, under a content-hash id written as both `$id` and `id` (`https://verquest.local/one_of/...`). With `json_schema_version = :openapi31` or `:openapi30`, validation errors come only from the variant the discriminator selects, and a missing, unknown or non-object value gets a single `discriminator` error. Under `:openapi30`, valid payloads that were rejected with `oneOf` errors (OpenAPI 3.0 has no `const`, so they matched every variant) now pass. Other versions validate as before, but `to_validation_schema` output changes. If you register these schemas with another validator, note the new ids.
|
|
14
|
+
- The transformer builds one plan at initialization and is frozen. `Verquest::NOT_FOUND` moved to `Verquest::Transformer::Path::NOT_FOUND`. In the mapping, `_variant_path` and `_nullable_path` hold the full source path inside collections too (`entries[]/content` instead of `content`).
|
|
15
|
+
- A `null` oneOf maps to `null` whether it has a discriminator or not, and inside multiple oneOfs.
|
|
16
|
+
- A oneOf inside a `collection` is listed in the mapping's `_oneOfs` array, like several oneOfs, instead of putting its metadata at the top level.
|
|
17
|
+
- A oneOf variant property moved out of a collection item with `map: "/..."` raises `Verquest::MappingError` when the class is defined. Each item selects its own variant, so it can't be collected outside the items.
|
|
18
|
+
- The transformer allocates two to four times fewer objects per call and runs 15–40% faster. `rake bench` measures it.
|
|
19
|
+
- A version compiles its validation schema once per configuration instead of on every `process`, which makes `process` with validation up to 4 times faster.
|
|
20
|
+
- Under `json_schema_version` `:draft4` and `:openapi30`, which have no `const`, a `const` property is written as a one-value `enum` in `to_schema` and `to_validation_schema`, so it is enforced.
|
|
21
|
+
- An unnamed `one_of` next to other properties raises `ArgumentError` when it is declared. It was dropped from the schema.
|
|
22
|
+
- Versions build their transformer from their properties instead of from their `mapping` hash, so the transformer no longer depends on the mapping's metadata keys.
|
|
23
|
+
|
|
24
|
+
### Deprecated
|
|
25
|
+
- `Verquest::Transformer.new(mapping:, discriminator:)`. Use `MyRequest.process`, or `MyRequest.resolve(version).transformer`; versions build their transformer from their properties, and the mapping constructor will be removed in 1.0.0. It warns when deprecation warnings are on (`Warning[:deprecated] = true` or `ruby -W:deprecated`), which Ruby leaves off by default.
|
|
26
|
+
|
|
27
|
+
### Fixed
|
|
28
|
+
- A `one_of` inside an `object` mapped a valid payload to an empty hash, and two of them clashed. It also works inside an object in a collection.
|
|
29
|
+
- Empty collection items are kept (they were dropped, or became `nil` before a later item), and a `null` nested object inside an item stays `null`.
|
|
30
|
+
- A `null` nullable oneOf with a nested `map:` (e.g. `"billing/payment"`) is written as a nested key, not as the literal key `"billing/payment"`.
|
|
31
|
+
- A collection item without its optional oneOf, or with a `null` nullable one, no longer raises "No matching schema found". A nullable oneOf without a discriminator in a collection can be defined (the duplicate-mapping check counted its metadata).
|
|
32
|
+
- Properties whose names start with `_` are mapped next to oneOfs and inverted in `external_mapping`.
|
|
33
|
+
- A collection with `map:` and a oneOf no longer nests every item under the collection name again, also when a variant's first property is moved out with `map: "/..."`.
|
|
34
|
+
- Without validation, a present oneOf without its discriminator raises "Missing discriminator" instead of mapping to an empty hash, and an unknown value raises in collection items too. A value naming a metadata key no longer crashes with `NoMethodError`.
|
|
35
|
+
- Variant inference (a oneOf without a discriminator) reads the variant schemas in the configured `json_schema_version`. Under draft 4, a boolean `exclusive_maximum` crashed it. Under draft 4 and OpenAPI 3.0, which have no `const`, it reads `const` as a one-value `enum`, so variants that differ only by a const are still told apart.
|
|
36
|
+
- oneOfs in a collection inside a collection, and in several collections of one version, are mapped. Before, the discriminator read an array of values, and the second collection's oneOf overwrote the first one's.
|
|
37
|
+
- A property moved out of collection items with `map: "/..."` writes `null` for items without the value, instead of an internal object. In a collection holding a oneOf, it is collected outside the items again.
|
|
38
|
+
- `external_mapping` inverts the oneOfs nested in objects and collections, which were left as a hash key.
|
|
39
|
+
- A `reference`, or a `collection` with `item:`, to a request holding a oneOf no longer drops its data.
|
|
40
|
+
- A oneOf inside a oneOf variant, inline or referenced, maps its data, and so does a collection of oneOfs inside a variant. The data was dropped. In `mapping` and `external_mapping`, such a nested oneOf is listed in the variant's `_oneOfs` array, where it used to show up as prefixed metadata keys and stringified hashes.
|
|
41
|
+
- In `mapping` and `external_mapping`, an absolute `map:` in a request used as a oneOf reference variant points to the root, as the transformer always mapped it. It was nested under the variant.
|
|
42
|
+
- A `reference` with `property:` to an object, a collection or a oneOf maps its data. Only scalar properties worked.
|
|
43
|
+
- A collection whose only property is a named `one_of` keeps the name in its item schema, so valid items pass validation.
|
|
44
|
+
|
|
45
|
+
## [0.6.4] - 2026-09-23
|
|
4
46
|
|
|
5
47
|
### Fixed
|
|
6
48
|
- Handling `nullable: true` ([#18](https://github.com/verquest/verquest/pull/18), [@CiTroNaK](https://github.com/CiTroNaK))
|
data/README.md
CHANGED
|
@@ -3,25 +3,33 @@
|
|
|
3
3
|
[](https://badge.fury.io/rb/verquest)
|
|
4
4
|
[](LICENSE.txt)
|
|
5
5
|
|
|
6
|
-
Verquest
|
|
6
|
+
Verquest describes your API's requests (bodies, query strings and path parameters) with a small Ruby DSL, one definition per API version. From that definition you get:
|
|
7
7
|
|
|
8
|
-
-
|
|
9
|
-
-
|
|
10
|
-
-
|
|
11
|
-
-
|
|
12
|
-
|
|
13
|
-
|
|
8
|
+
- validation of incoming params against [JSON Schema](https://json-schema.org/), with errors that point at the invalid value (`/address/city`);
|
|
9
|
+
- mapping from your public API structure to the structure your app uses (`full_name` → `name`);
|
|
10
|
+
- JSON Schema components and OpenAPI parameters for your docs, ready for [rswag](https://github.com/rswag/rswag);
|
|
11
|
+
- version resolution: ask for `2025-07` and get the closest earlier definition, so unchanged requests need no new version.
|
|
12
|
+
|
|
13
|
+
It works in any Ruby app and has no Rails dependency. [Rails and rswag](docs/rails.md) shows how to plug it into controllers and request specs.
|
|
14
14
|
|
|
15
15
|
Sponsored by [Stora](https://stora.co)
|
|
16
16
|
|
|
17
|
-
>
|
|
17
|
+
> Verquest is not 1.0 yet, so minor releases can change the API; the [changelog](CHANGELOG.md) lists every change. `anyOf` and `allOf` aren't in the DSL yet, see the open [issues](https://github.com/verquest/verquest/issues?q=sort:updated-desc%20is:issue%20is:open%20label:enhancement).
|
|
18
|
+
|
|
19
|
+
## Contents
|
|
20
|
+
|
|
21
|
+
- [Installation](#installation)
|
|
22
|
+
- [Quick Start](#quick-start)
|
|
23
|
+
- [Core Features](#core-features): [schema definition](#schema-definition-and-validation), [oneOf](#oneof), [conditional schemas](#conditional-schemas), [custom field types](#custom-field-types), [versioning](#versioning), [mapping](#mapping-request-structure), [query parameters](#query-parameters), [path parameters](#path-parameters), [OpenAPI components](#component-generation-for-openapi)
|
|
24
|
+
- [Rails and rswag](docs/rails.md)
|
|
25
|
+
- [Configuration](#configuration)
|
|
18
26
|
|
|
19
27
|
## Installation
|
|
20
28
|
|
|
21
29
|
Add this line to your application's Gemfile:
|
|
22
30
|
|
|
23
31
|
```ruby
|
|
24
|
-
gem "verquest", "~> 0.
|
|
32
|
+
gem "verquest", "~> 0.7"
|
|
25
33
|
```
|
|
26
34
|
|
|
27
35
|
And then execute:
|
|
@@ -32,9 +40,10 @@ bundle install
|
|
|
32
40
|
|
|
33
41
|
## Quick Start
|
|
34
42
|
|
|
35
|
-
### Define a
|
|
43
|
+
### Define a request
|
|
44
|
+
|
|
45
|
+
A request class inherits from `Verquest::Base` and describes each version in a `version` block. An address, reused below:
|
|
36
46
|
|
|
37
|
-
Address Create Request
|
|
38
47
|
```ruby
|
|
39
48
|
class AddressCreateRequest < Verquest::Base
|
|
40
49
|
description "Address Create Request"
|
|
@@ -49,9 +58,10 @@ class AddressCreateRequest < Verquest::Base
|
|
|
49
58
|
end
|
|
50
59
|
end
|
|
51
60
|
end
|
|
52
|
-
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
A user request that uses the address:
|
|
53
64
|
|
|
54
|
-
User Create Request that uses the `AddressCreateRequest`
|
|
55
65
|
```ruby
|
|
56
66
|
class UserCreateRequest < Verquest::Base
|
|
57
67
|
description "User Create Request"
|
|
@@ -97,31 +107,32 @@ class UserCreateRequest < Verquest::Base
|
|
|
97
107
|
end
|
|
98
108
|
```
|
|
99
109
|
|
|
100
|
-
###
|
|
110
|
+
### Process params in a Rails controller
|
|
101
111
|
|
|
102
|
-
|
|
112
|
+
`process` validates the params and returns them mapped to your internal structure. It takes `ActionController::Parameters` as they are, so it replaces strong parameters:
|
|
103
113
|
|
|
114
|
+
```ruby
|
|
104
115
|
class UsersController < ApplicationController
|
|
105
|
-
rescue_from Verquest::InvalidParamsError
|
|
106
|
-
|
|
107
|
-
def create
|
|
108
|
-
result = Users::Create.call(params: user_params) # service object to handle the creation logic
|
|
109
|
-
|
|
110
|
-
if result.success?
|
|
111
|
-
# render success response
|
|
112
|
-
else
|
|
113
|
-
# render error response
|
|
114
|
-
end
|
|
116
|
+
rescue_from Verquest::InvalidParamsError do |error|
|
|
117
|
+
render json: {errors: error.errors}, status: :unprocessable_content
|
|
115
118
|
end
|
|
116
119
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
UserCreateRequest.process(params, version: params[:api_version])
|
|
120
|
+
def create
|
|
121
|
+
user = Users::Create.call(params: UserCreateRequest.process(params, version: params[:api_version]))
|
|
122
|
+
render json: user, status: :created
|
|
121
123
|
end
|
|
122
124
|
end
|
|
123
125
|
```
|
|
124
126
|
|
|
127
|
+
Each error points at the invalid value:
|
|
128
|
+
|
|
129
|
+
```ruby
|
|
130
|
+
error.errors
|
|
131
|
+
# => [{pointer: "/email", type: "format", message: "value at `/email` does not match format: email", details: nil}]
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
Set `config.current_version` to drop the `version:` argument, or `config.validation_error_handling = :result` to get a `Verquest::Result` instead of an exception (see [Configuration](#configuration)). For query and path parameters in controllers, see [Rails and rswag](docs/rails.md).
|
|
135
|
+
|
|
125
136
|
### JSON schema for OpenAPI
|
|
126
137
|
You can generate JSON Schema for your versioned requests, which can be used for API documentation:
|
|
127
138
|
|
|
@@ -268,19 +279,22 @@ The JSON schema can be used for both validation of incoming parameters and for g
|
|
|
268
279
|
|
|
269
280
|
#### Component types
|
|
270
281
|
|
|
271
|
-
- `field`:
|
|
272
|
-
- `enum`:
|
|
273
|
-
- `object`:
|
|
274
|
-
- `array`:
|
|
275
|
-
- `collection`:
|
|
276
|
-
- `reference`:
|
|
277
|
-
- `const`:
|
|
282
|
+
- `field`: a scalar value (string, integer, boolean, etc.).
|
|
283
|
+
- `enum`: a value from a fixed list.
|
|
284
|
+
- `object`: a JSON object with its own properties.
|
|
285
|
+
- `array`: a JSON array of scalar items.
|
|
286
|
+
- `collection`: a JSON array of objects, defined inline or with `item:` pointing at another request.
|
|
287
|
+
- `reference`: another request reused in place, or one of its properties with `property:`.
|
|
288
|
+
- `const`: a [constant](https://json-schema.org/understanding-json-schema/reference/const#constant-values) value.
|
|
289
|
+
- `one_of`: exactly one of several request shapes, see [oneOf](#oneof).
|
|
278
290
|
|
|
279
291
|
#### Helper methods
|
|
280
292
|
|
|
281
|
-
- `description`:
|
|
282
|
-
- `schema_options`:
|
|
283
|
-
- `with_options`:
|
|
293
|
+
- `description`: a description for the request, or for one version.
|
|
294
|
+
- `schema_options`: extra JSON Schema keywords for the request or one version, such as `additional_properties: false`.
|
|
295
|
+
- `with_options`: shared options for every property in the block.
|
|
296
|
+
|
|
297
|
+
Properties (except `reference`) take JSON Schema keywords in snake case, such as `format`, `min_length` or `max_length`, and Verquest writes them in camel case (`minLength`).
|
|
284
298
|
|
|
285
299
|
#### Required properties
|
|
286
300
|
|
|
@@ -396,6 +410,166 @@ Will produce this validation schema:
|
|
|
396
410
|
}
|
|
397
411
|
```
|
|
398
412
|
|
|
413
|
+
#### Conditional schemas
|
|
414
|
+
|
|
415
|
+
Use `condition` when a request changes shape depending on the payload. It compiles to JSON Schema `if` / `then` / `else` and works in a version, an `object`, and an inline `collection`:
|
|
416
|
+
|
|
417
|
+
```ruby
|
|
418
|
+
version "2025-09" do
|
|
419
|
+
field :effective_from, type: :string, required: true
|
|
420
|
+
field :credit_card, type: :string
|
|
421
|
+
field :billing_address, type: :string
|
|
422
|
+
|
|
423
|
+
condition :pricing do
|
|
424
|
+
given { present :percentages }
|
|
425
|
+
then_apply do
|
|
426
|
+
object :percentages, required: true, min_properties: 1 do
|
|
427
|
+
field :unit_types, type: :number, maximum: 9999.99
|
|
428
|
+
end
|
|
429
|
+
end
|
|
430
|
+
else_apply do
|
|
431
|
+
field :percentage, type: :number, required: true, deprecated: true, not: {const: 0}
|
|
432
|
+
object :applies_to, deprecated: true do
|
|
433
|
+
field :unit_types, type: :boolean, map: "/apply_to_unit_types"
|
|
434
|
+
end
|
|
435
|
+
end
|
|
436
|
+
end
|
|
437
|
+
|
|
438
|
+
depends_on :credit_card do # condition { given { present :credit_card }; then_apply { ... } }
|
|
439
|
+
present :billing_address
|
|
440
|
+
end
|
|
441
|
+
end
|
|
442
|
+
```
|
|
443
|
+
|
|
444
|
+
- `given` (the `if`) holds rules only. `then_apply` and `else_apply` hold rules and property declarations. A condition needs `given` and at least one branch.
|
|
445
|
+
- Rules: `present :a, :b`, `absent :a`, `equals :kind, "company"`, `value_in :kind, %w[a b]`, `satisfies :age, minimum: 18`, `any { ... }` and `none { ... }`. Rules in one block must all hold. Inside `given`, `equals`, `value_in` and `satisfies` also require their property.
|
|
446
|
+
- Branch properties are listed in the scope's `properties`, so `additionalProperties: false` keeps working and the mapping is the flat union. Each branch gets only rules: its required names, and `false` for properties that only the other branch declares. A missing branch is generated.
|
|
447
|
+
- Several conditions on one scope go into `allOf`, one entry each.
|
|
448
|
+
- `exclude_conditions :pricing` removes an inherited named condition in a later version. A named condition can be declared only once per scope, so exclude an inherited one before redeclaring it.
|
|
449
|
+
- `exclude_properties` only sees regular properties. To drop a branch property, exclude its condition.
|
|
450
|
+
- Conditions need `json_schema_version` `:draft7` or later, or `:openapi31`. Other versions ignore `if` / `then` / `else`, so Verquest raises `ArgumentError` when the version is prepared.
|
|
451
|
+
|
|
452
|
+
The example gives this schema:
|
|
453
|
+
|
|
454
|
+
```json
|
|
455
|
+
{
|
|
456
|
+
"type": "object",
|
|
457
|
+
"required": ["effective_from"],
|
|
458
|
+
"properties": {
|
|
459
|
+
"effective_from": {"type": "string"}, "credit_card": {"type": "string"}, "billing_address": {"type": "string"},
|
|
460
|
+
"percentages": {"...": "..."}, "percentage": {"...": "..."}, "applies_to": {"...": "..."}
|
|
461
|
+
},
|
|
462
|
+
"additionalProperties": false,
|
|
463
|
+
"allOf": [
|
|
464
|
+
{
|
|
465
|
+
"if": {"required": ["percentages"]},
|
|
466
|
+
"then": {"required": ["percentages"], "properties": {"percentage": false, "applies_to": false}},
|
|
467
|
+
"else": {"required": ["percentage"], "properties": {"percentages": false}}
|
|
468
|
+
},
|
|
469
|
+
{"if": {"required": ["credit_card"]}, "then": {"required": ["billing_address"]}}
|
|
470
|
+
]
|
|
471
|
+
}
|
|
472
|
+
```
|
|
473
|
+
|
|
474
|
+
##### Documenting a condition as a oneOf
|
|
475
|
+
|
|
476
|
+
`document_as: :one_of` keeps `if` / `then` / `else` in the validation schema, but `to_schema` (the documentation) renders a `oneOf` with one variant per branch. `then_apply` and `else_apply` take a `title`, `description` and `deprecated` for their variant:
|
|
477
|
+
|
|
478
|
+
```ruby
|
|
479
|
+
version "2025-09" do
|
|
480
|
+
field :effective_from, type: :string, required: true
|
|
481
|
+
|
|
482
|
+
condition :pricing, document_as: :one_of do
|
|
483
|
+
given { present :percentages }
|
|
484
|
+
then_apply title: "Per-type percentages" do
|
|
485
|
+
object :percentages, required: true, min_properties: 1 do
|
|
486
|
+
field :unit_types, type: :number, maximum: 9999.99
|
|
487
|
+
end
|
|
488
|
+
end
|
|
489
|
+
else_apply title: "Shared percentage", deprecated: true do
|
|
490
|
+
field :percentage, type: :number, required: true
|
|
491
|
+
end
|
|
492
|
+
end
|
|
493
|
+
end
|
|
494
|
+
```
|
|
495
|
+
|
|
496
|
+
```json
|
|
497
|
+
{
|
|
498
|
+
"oneOf": [
|
|
499
|
+
{"type": "object", "title": "Per-type percentages", "required": ["effective_from", "percentages"], "properties": {"effective_from": {"...": "..."}, "percentages": {"...": "..."}}, "additionalProperties": false},
|
|
500
|
+
{"type": "object", "title": "Shared percentage", "deprecated": true, "required": ["effective_from", "percentage"], "properties": {"effective_from": {"...": "..."}, "percentage": {"...": "..."}}, "additionalProperties": false}
|
|
501
|
+
]
|
|
502
|
+
}
|
|
503
|
+
```
|
|
504
|
+
|
|
505
|
+
- Each variant lists the version's properties without the ones only the other branch declares, and adds its branch's rules. The `then` variant also holds the `given` rules, and the `else` variant `not` the `given` rules, unless it already forbids a property `given` requires. Both schemas accept the same payloads. When a rule clashes with a property's own schema (for example `given { absent :note }` against a declared `note`, or `satisfies :age, minimum: 18` against `minimum: 0`), the variant keeps both by adding the rule under `allOf`.
|
|
506
|
+
- Validation still uses the `if` / `then` / `else` schema, so validation errors point at those rules, not at a `oneOf` variant.
|
|
507
|
+
- When `additionalProperties` isn't `false`, the other branch's properties are forbidden with `false` in each variant.
|
|
508
|
+
- Only for a condition in a version, and it must be the only condition in that version. The version's `description` stays at the root of the schema.
|
|
509
|
+
- The schema has no root `properties`, so `to_ref(property:)` can't point into it.
|
|
510
|
+
|
|
511
|
+
#### oneOf
|
|
512
|
+
|
|
513
|
+
`one_of` accepts exactly one of several shapes. Each variant is a `reference` to another request, and `discriminator:` names the property that tells them apart:
|
|
514
|
+
|
|
515
|
+
```ruby
|
|
516
|
+
class CardPaymentComponent < Verquest::Base
|
|
517
|
+
version "2025-06" do
|
|
518
|
+
const :method, value: "card", required: true
|
|
519
|
+
field :card_number, type: :string, required: true
|
|
520
|
+
field :expiry, type: :string, required: true
|
|
521
|
+
end
|
|
522
|
+
end
|
|
523
|
+
|
|
524
|
+
class OrderRequest < Verquest::Base
|
|
525
|
+
version "2025-06" do
|
|
526
|
+
field :order_id, type: :string, required: true
|
|
527
|
+
|
|
528
|
+
one_of name: :payment, discriminator: :method, required: true do
|
|
529
|
+
reference :card, from: CardPaymentComponent
|
|
530
|
+
reference :bank, from: BankPaymentComponent
|
|
531
|
+
end
|
|
532
|
+
end
|
|
533
|
+
end
|
|
534
|
+
|
|
535
|
+
OrderRequest.process({"order_id" => "o1", "payment" => {"method" => "card", "card_number" => "4242", "expiry" => "12/30"}})
|
|
536
|
+
# => {"order_id" => "o1", "payment" => {"method" => "card", "card_number" => "4242", "expiry" => "12/30"}}
|
|
537
|
+
```
|
|
538
|
+
|
|
539
|
+
`to_schema` documents it as an OpenAPI `oneOf` with a `discriminator` mapping to the variants' components:
|
|
540
|
+
|
|
541
|
+
```ruby
|
|
542
|
+
{"oneOf" => [{"$ref" => "#/components/schemas/CardPaymentComponent"}, {"$ref" => "#/components/schemas/BankPaymentComponent"}],
|
|
543
|
+
"discriminator" => {"propertyName" => "method", "mapping" => {"card" => "#/components/schemas/CardPaymentComponent", "bank" => "#/components/schemas/BankPaymentComponent"}}}
|
|
544
|
+
```
|
|
545
|
+
|
|
546
|
+
- Without `name:`, the whole request is one of the variants (`one_of discriminator: :type do ... end` as the only property of a version).
|
|
547
|
+
- Without `discriminator:`, the value must match exactly one variant's schema. Validation rejects it otherwise; with validation off, mapping raises `Verquest::MappingError`.
|
|
548
|
+
- `one_of` works inside objects, collections and other variants, and with `nullable: true` and `map:`.
|
|
549
|
+
|
|
550
|
+
#### oneOf discriminators in validation
|
|
551
|
+
|
|
552
|
+
A `one_of` with a `discriminator:` keeps the discriminator in the validation schema. Its variants move into local `definitions` that `oneOf` references with `$ref`. An id, written as both `$id` and `id` so every JSON Schema version reads it, makes those references resolve wherever the schema is embedded:
|
|
553
|
+
|
|
554
|
+
```ruby
|
|
555
|
+
one_of name: :pet, discriminator: :type do
|
|
556
|
+
reference :dog, from: DogComponent
|
|
557
|
+
reference :cat, from: CatComponent
|
|
558
|
+
end
|
|
559
|
+
```
|
|
560
|
+
|
|
561
|
+
```json
|
|
562
|
+
{
|
|
563
|
+
"$id": "https://verquest.local/one_of/3f1c9a0b2d4e5f67",
|
|
564
|
+
"id": "https://verquest.local/one_of/3f1c9a0b2d4e5f67",
|
|
565
|
+
"definitions": {"dog": {"type": "object", "...": "..."}, "cat": {"type": "object", "...": "..."}},
|
|
566
|
+
"oneOf": [{"$ref": "#/definitions/dog"}, {"$ref": "#/definitions/cat"}],
|
|
567
|
+
"discriminator": {"propertyName": "type", "mapping": {"dog": "#/definitions/dog", "cat": "#/definitions/cat"}}
|
|
568
|
+
}
|
|
569
|
+
```
|
|
570
|
+
|
|
571
|
+
With `json_schema_version = :openapi31` or `:openapi30`, the validator checks only the variant the discriminator selects, so validation errors come from that variant only. A value that isn't an object, or that has no known discriminator value, gets a single `discriminator` error. Other versions ignore `discriminator` and check every variant.
|
|
572
|
+
|
|
399
573
|
#### Custom Field Types
|
|
400
574
|
|
|
401
575
|
You can define custom field types that can be used in `field` and `array` in the configuration.
|
|
@@ -559,7 +733,7 @@ Will be transformed to:
|
|
|
559
733
|
"address_city": "Springfield",
|
|
560
734
|
"address_zip": "12345"
|
|
561
735
|
}
|
|
562
|
-
|
|
736
|
+
```
|
|
563
737
|
|
|
564
738
|
What you can use:
|
|
565
739
|
- `/` to reference the root of the request structure
|
|
@@ -585,9 +759,81 @@ Will produce the following mapping:
|
|
|
585
759
|
}
|
|
586
760
|
```
|
|
587
761
|
|
|
588
|
-
|
|
762
|
+
The mapping tests in [`test/verquest/base_test.rb`](test/verquest/base_test.rb) have more examples.
|
|
763
|
+
|
|
764
|
+
### Query parameters
|
|
765
|
+
|
|
766
|
+
A request class with `location :query` describes query parameters. `process` converts the query strings to the types the schema expects before it validates and maps them, and `to_parameters` documents them as OpenAPI parameters.
|
|
767
|
+
|
|
768
|
+
```ruby
|
|
769
|
+
class DealsIndexQuery < Verquest::Base
|
|
770
|
+
location :query # inherited, so it can be set once on a base class
|
|
589
771
|
|
|
590
|
-
|
|
772
|
+
version "2025-06" do
|
|
773
|
+
field :page, type: :integer, minimum: 1, default: 1
|
|
774
|
+
field :archived, type: :boolean
|
|
775
|
+
enum :sort_direction, values: %w[ASC DESC], default: "DESC"
|
|
776
|
+
array :status, type: :string, explode: false, item_schema_options: {enum: %w[open won lost]}
|
|
777
|
+
|
|
778
|
+
object :created do # sent as created[after]=...
|
|
779
|
+
field :after, type: :string, format: "date-time", map: "/created_after"
|
|
780
|
+
end
|
|
781
|
+
end
|
|
782
|
+
end
|
|
783
|
+
|
|
784
|
+
DealsIndexQuery.process(request.query_parameters, version: "2025-06")
|
|
785
|
+
# ?page=2&archived=true&status=open,won&created[after]=2025-01-01T00:00:00Z
|
|
786
|
+
# => {"page" => 2, "archived" => true, "sort_direction" => "DESC", "status" => ["open", "won"], "created_after" => "2025-01-01T00:00:00Z"}
|
|
787
|
+
```
|
|
788
|
+
|
|
789
|
+
Conversion follows the validation schema:
|
|
790
|
+
- `integer` and `number` from digits (`"2"`, `"-1.5"`), `boolean` from `"true"` and `"false"`, `null` from an empty string when the schema allows it;
|
|
791
|
+
- arrays split on commas with `explode: false` (`status=open,won`), or come from repeated keys with `explode: true` (`status[]=open&status[]=won`; Rails keeps only the last value of a plain repeated key). `config.query_array_explode` sets the default (`true`);
|
|
792
|
+
- objects (`deepObject`, one level deep) convert each property.
|
|
793
|
+
|
|
794
|
+
A value that can't be converted is left as it is, so the validation reports it. Shapes a query string can't express (`one_of` properties, arrays of objects, objects inside objects) raise `Verquest::DefinitionError` when the version is defined.
|
|
795
|
+
|
|
796
|
+
`to_parameters` returns one parameter object per property, with symbol keys and the schema inlined, ready for rswag:
|
|
797
|
+
|
|
798
|
+
```ruby
|
|
799
|
+
DealsIndexQuery.to_parameters(version: "2025-06").each { |p| parameter(p) }
|
|
800
|
+
# {name: "status", in: :query, required: false, style: :form, explode: false,
|
|
801
|
+
# schema: {type: "array", items: {type: "string", enum: ["open", "won", "lost"]}}}
|
|
802
|
+
```
|
|
803
|
+
|
|
804
|
+
Named OpenAPI examples go on the parameter with `parameter_examples:`. They are left out of the JSON schemas, because JSON Schema `examples` is an array:
|
|
805
|
+
|
|
806
|
+
```ruby
|
|
807
|
+
array :status, type: :string, explode: false, item_schema_options: {enum: %w[open won lost]},
|
|
808
|
+
parameter_examples: {single: {value: %w[open]}, multiple: {value: %w[open won]}}
|
|
809
|
+
# => {name: "status", ..., examples: {single: {value: ["open"]}, multiple: {value: ["open", "won"]}}, schema: {...}}
|
|
810
|
+
```
|
|
811
|
+
|
|
812
|
+
A property reference to another query class reuses its definition, e.g. a shared `expand` parameter: `reference :expand, from: ExpandQuery, property: :expand`.
|
|
813
|
+
|
|
814
|
+
### Path parameters
|
|
815
|
+
|
|
816
|
+
`location :path` works like `location :query`, but documents `in: :path` parameters. Path parameters must be required scalars; arrays, objects, one_of and optional properties raise `Verquest::DefinitionError`.
|
|
817
|
+
|
|
818
|
+
```ruby
|
|
819
|
+
class DealPath < Verquest::Base
|
|
820
|
+
location :path
|
|
821
|
+
|
|
822
|
+
version "2025-06" do
|
|
823
|
+
field :id, type: :string, required: true, pattern: "^deal_", description: "The ID of the deal"
|
|
824
|
+
end
|
|
825
|
+
end
|
|
826
|
+
|
|
827
|
+
DealPath.process({"id" => params[:id]}) # raises Verquest::InvalidParamsError for "con_123"
|
|
828
|
+
DealPath.to_parameters(version: "2025-06")
|
|
829
|
+
# => [{name: "id", in: :path, required: true, description: "The ID of the deal", schema: {type: "string", pattern: "^deal_"}}]
|
|
830
|
+
```
|
|
831
|
+
|
|
832
|
+
The parameter name comes from the property. When a route uses another name (`/deals/{deal_id}/win`), rename the key before `process` and the `name` of the parameter object, as the [Rails and rswag](docs/rails.md) example does.
|
|
833
|
+
|
|
834
|
+
### Rails and rswag
|
|
835
|
+
|
|
836
|
+
See [Rails and rswag](docs/rails.md) for a controller concern that processes body, query and path classes, and an rswag shared context that documents them.
|
|
591
837
|
|
|
592
838
|
### Component Generation for OpenAPI
|
|
593
839
|
|
|
@@ -621,20 +867,28 @@ Verquest.configure do |config|
|
|
|
621
867
|
config.remove_extra_root_keys = true # default
|
|
622
868
|
|
|
623
869
|
# Set custom version resolver
|
|
624
|
-
config.version_resolver =
|
|
870
|
+
config.version_resolver = CustomVersionResolver # default is `Verquest::VersionResolver`
|
|
625
871
|
|
|
626
872
|
# Set default value for additional properties
|
|
627
873
|
config.default_additional_properties = false # default
|
|
874
|
+
|
|
875
|
+
# Default explode value of query arrays (true: status[]=a&status[]=b, false: status=a,b)
|
|
876
|
+
config.query_array_explode = true # default
|
|
877
|
+
|
|
878
|
+
# Reusable field types, see Custom Field Types
|
|
879
|
+
config.custom_field_types = {} # default
|
|
628
880
|
end
|
|
629
881
|
```
|
|
630
882
|
|
|
631
883
|
## Documentation
|
|
632
884
|
|
|
633
|
-
|
|
885
|
+
The API reference is on [RubyDoc](https://www.rubydoc.info/gems/verquest). Guides:
|
|
886
|
+
|
|
887
|
+
- [Rails and rswag](docs/rails.md): controllers and request specs for body, query and path parameters.
|
|
634
888
|
|
|
635
889
|
## Development
|
|
636
890
|
|
|
637
|
-
After checking out the repo, run `bin/setup` to install dependencies.
|
|
891
|
+
After checking out the repo, run `bin/setup` to install dependencies. `bundle exec rake` runs the tests, RuboCop and the YARD documentation check; `COVERAGE=1 bundle exec rake` adds a coverage report. `bin/console` opens an interactive prompt with the gem loaded.
|
|
638
892
|
|
|
639
893
|
To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `gem_version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
|
|
640
894
|
|
data/Rakefile
CHANGED