structured_params-openapi 0.1.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 +7 -0
- data/CHANGELOG.md +23 -0
- data/LICENSE.txt +21 -0
- data/README.md +34 -0
- data/README_ja.md +34 -0
- data/docs/drift-check.md +379 -0
- data/docs/drift-check_ja.md +379 -0
- data/lib/structured_params/openapi/configuration.rb +131 -0
- data/lib/structured_params/openapi/drift_check/issue.rb +16 -0
- data/lib/structured_params/openapi/drift_check/result.rb +61 -0
- data/lib/structured_params/openapi/drift_check.rb +531 -0
- data/lib/structured_params/openapi/registry.rb +92 -0
- data/lib/structured_params/openapi/version.rb +8 -0
- data/lib/structured_params/openapi.rb +28 -0
- data/lib/tasks/structured_params_openapi.rake +15 -0
- data/sig/structured_params/openapi/configuration.rbs +74 -0
- data/sig/structured_params/openapi/drift_check/issue.rbs +19 -0
- data/sig/structured_params/openapi/drift_check/result.rbs +40 -0
- data/sig/structured_params/openapi/drift_check.rbs +201 -0
- data/sig/structured_params/openapi/registry.rbs +46 -0
- data/sig/structured_params/openapi/version.rbs +7 -0
- data/sig/structured_params/openapi.rbs +14 -0
- metadata +82 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 42d370e916a8b590b35d26a0e8b81ca443762ba037b88fec8561da53f354f4f4
|
|
4
|
+
data.tar.gz: 6e2ff90a8fdaae1fabffc1e1fc88dbf62d7ed174ef535ebc94051d719e7c3161
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: e8a417a0e94cb639d6b5c03bfbd708d16a7ef515aacf15f8a384994c1c93c7358c7c2c7a3a0bb38eeaac7611271fb50d65b4f9e113e5a239d5c47c8c5a198c69
|
|
7
|
+
data.tar.gz: 35b25fcb63864522759c5f1ac0f2dcd7c143dfbba5a0b50c118fbe1d6c5b261df727b9c2fdb726446b6afee7ab4021ad23cc71387860c73db81671df15d5ef0d
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## [0.1.0] - 2026-10-14
|
|
4
|
+
|
|
5
|
+
## structured_params-openapi 0.1.0
|
|
6
|
+
|
|
7
|
+
First release of the OpenAPI drift check for StructuredParams, extracted into its own gem.
|
|
8
|
+
|
|
9
|
+
### Features
|
|
10
|
+
- Compares OpenAPI request schemas with `StructuredParams::Params` classes: properties, scalar and nested object/array types, and `required` against presence validation.
|
|
11
|
+
- `structured_params:openapi:drift_check` Rake task with automatic class discovery (`parameter_class_filter`) and explicit mappings (`Registry.register`).
|
|
12
|
+
- Component naming: default convention, `schema_name_resolver`, `schema:` / `pointer:`.
|
|
13
|
+
- Several OpenAPI files: `document_resolver` and `Registry.register(openapi:)`.
|
|
14
|
+
- `ignore_drift(path:, kind:, parameter_class:)` to accept known findings.
|
|
15
|
+
- OpenAPI 3.1 nullable forms and scalar `oneOf`/`anyOf` are compared; missing schemas and broken `$ref`s are errors; skipped checks are printed and `STRUCTURED_PARAMS_STRICT=1` fails on them.
|
|
16
|
+
- English and Japanese guides.
|
|
17
|
+
|
|
18
|
+
### Installation
|
|
19
|
+
```ruby
|
|
20
|
+
gem 'structured_params-openapi'
|
|
21
|
+
```
|
|
22
|
+
Requires `structured_params >= 0.9.5`.
|
|
23
|
+
|
data/LICENSE.txt
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 Mizuki Yamamoto
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
|
13
|
+
all copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# structured_params-openapi
|
|
2
|
+
|
|
3
|
+
English | [日本語](README_ja.md)
|
|
4
|
+
|
|
5
|
+
OpenAPI drift check for [StructuredParams](https://github.com/Syati/structured_params).
|
|
6
|
+
It detects differences between OpenAPI request schemas and `StructuredParams::Params`
|
|
7
|
+
classes, so a change on one side cannot silently diverge from the other.
|
|
8
|
+
|
|
9
|
+
## Installation
|
|
10
|
+
|
|
11
|
+
```ruby
|
|
12
|
+
# Gemfile
|
|
13
|
+
gem 'structured_params-openapi'
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
Bundler requires it automatically (`structured_params/openapi`). In a Rails application
|
|
17
|
+
the Rake task `structured_params:openapi:drift_check` is registered for you.
|
|
18
|
+
|
|
19
|
+
## Usage
|
|
20
|
+
|
|
21
|
+
```ruby
|
|
22
|
+
# config/initializers/structured_params_openapi.rb
|
|
23
|
+
StructuredParams::OpenAPI.configure do |config|
|
|
24
|
+
config.document = Rails.root.join('docs/api/openapi.yaml').to_s
|
|
25
|
+
config.parameter_class_filter = ->(klass) { klass.name&.start_with?('Api::') }
|
|
26
|
+
end
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
```sh
|
|
30
|
+
bundle exec rake structured_params:openapi:drift_check
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
See the [guide](docs/drift-check.md) for naming conventions, multiple OpenAPI files,
|
|
34
|
+
ignoring findings, and the supported schema features.
|
data/README_ja.md
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# structured_params-openapi
|
|
2
|
+
|
|
3
|
+
[English](README.md) | 日本語
|
|
4
|
+
|
|
5
|
+
[StructuredParams](https://github.com/Syati/structured_params) 用の OpenAPI drift check です。
|
|
6
|
+
OpenAPI のリクエスト schema と `StructuredParams::Params` クラスの差分を検出し、片方だけが
|
|
7
|
+
変更されて乖離することを防ぎます。
|
|
8
|
+
|
|
9
|
+
## インストール
|
|
10
|
+
|
|
11
|
+
```ruby
|
|
12
|
+
# Gemfile
|
|
13
|
+
gem 'structured_params-openapi'
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
Bundler が自動で require します(`structured_params/openapi`)。Rails アプリケーションでは、
|
|
17
|
+
Rake タスク `structured_params:openapi:drift_check` も自動で登録されます。
|
|
18
|
+
|
|
19
|
+
## 使い方
|
|
20
|
+
|
|
21
|
+
```ruby
|
|
22
|
+
# config/initializers/structured_params_openapi.rb
|
|
23
|
+
StructuredParams::OpenAPI.configure do |config|
|
|
24
|
+
config.document = Rails.root.join('docs/api/openapi.yaml').to_s
|
|
25
|
+
config.parameter_class_filter = ->(klass) { klass.name&.start_with?('Api::') }
|
|
26
|
+
end
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
```sh
|
|
30
|
+
bundle exec rake structured_params:openapi:drift_check
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
命名規則、複数の OpenAPI ファイル、検出結果の無視、対応している schema の機能については、
|
|
34
|
+
[ガイド](docs/drift-check_ja.md)を参照してください。
|
data/docs/drift-check.md
ADDED
|
@@ -0,0 +1,379 @@
|
|
|
1
|
+
# OpenAPI Drift Check
|
|
2
|
+
|
|
3
|
+
English | [日本語](drift-check_ja.md)
|
|
4
|
+
|
|
5
|
+
Detects differences between OpenAPI request schemas and `StructuredParams::Params`
|
|
6
|
+
classes, so a change on one side cannot silently diverge from the other.
|
|
7
|
+
|
|
8
|
+
## Table of contents
|
|
9
|
+
|
|
10
|
+
- [Terms](#terms)
|
|
11
|
+
- [Quick start](#quick-start)
|
|
12
|
+
- [Matching classes to schemas](#matching-classes-to-schemas)
|
|
13
|
+
- [Default naming convention](#default-naming-convention)
|
|
14
|
+
- [Custom naming convention](#custom-naming-convention)
|
|
15
|
+
- [Exceptions for a single class](#exceptions-for-a-single-class)
|
|
16
|
+
- [What is compared](#what-is-compared)
|
|
17
|
+
- [Scalar types](#scalar-types)
|
|
18
|
+
- [Attributes and required properties](#attributes-and-required-properties)
|
|
19
|
+
- [Property names](#property-names)
|
|
20
|
+
- [Using it in RSpec](#using-it-in-rspec)
|
|
21
|
+
- [Ignoring a specific finding](#ignoring-a-specific-finding)
|
|
22
|
+
- [Rake task and Registry](#rake-task-and-registry)
|
|
23
|
+
- [Auto-discovery](#auto-discovery)
|
|
24
|
+
- [Registering exceptions](#registering-exceptions)
|
|
25
|
+
- [Multiple OpenAPI files](#multiple-openapi-files)
|
|
26
|
+
- [Running in CI](#running-in-ci)
|
|
27
|
+
- [Scope, skips and missing schemas](#scope-skips-and-missing-schemas)
|
|
28
|
+
|
|
29
|
+
## Terms
|
|
30
|
+
|
|
31
|
+
In this guide, a **component** means a named schema under `components.schemas` in the
|
|
32
|
+
OpenAPI document. Each `Params` class is checked against one component, found by name
|
|
33
|
+
(for example `CreateUserParams` → `CreateUser`).
|
|
34
|
+
|
|
35
|
+
```yaml
|
|
36
|
+
# openapi.yml
|
|
37
|
+
components:
|
|
38
|
+
schemas:
|
|
39
|
+
CreateUser: # <- a "component" (its JSON Pointer is #/components/schemas/CreateUser)
|
|
40
|
+
type: object
|
|
41
|
+
properties:
|
|
42
|
+
name: { type: string }
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## Quick start
|
|
46
|
+
|
|
47
|
+
Add an initializer and keep only the parts you need. The OpenAPI document here is
|
|
48
|
+
generated from TypeSpec, so its components are named like `V1.Orders.CreateRequest`, for
|
|
49
|
+
classes named like `Api::V1::Orders::CreateRequestParameter`.
|
|
50
|
+
|
|
51
|
+
```ruby
|
|
52
|
+
# config/initializers/structured_params_openapi.rb
|
|
53
|
+
StructuredParams::OpenAPI.configure do |config|
|
|
54
|
+
# Required: the OpenAPI document and the classes to check.
|
|
55
|
+
config.document = Rails.root.join('docs/api/openapi.yaml').to_s
|
|
56
|
+
config.parameter_class_filter = ->(klass) { klass.name&.start_with?('Api::') }
|
|
57
|
+
|
|
58
|
+
# Only if your component names differ from the default convention
|
|
59
|
+
# (CreateUserParams -> CreateUser):
|
|
60
|
+
# Api::V1::Orders::CreateRequestParameter -> "V1.Orders.CreateRequest"
|
|
61
|
+
config.schema_name_resolver = lambda do |klass|
|
|
62
|
+
klass.name.delete_prefix('Api::').delete_suffix('Parameter').gsub('::', '.')
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# Only for classes that should have a schema but do not yet:
|
|
66
|
+
# record them instead of failing.
|
|
67
|
+
%w[
|
|
68
|
+
Api::V1::Reports::ExportRequestParameter
|
|
69
|
+
Api::V1::Reports::SummaryRequestParameter
|
|
70
|
+
].each do |name|
|
|
71
|
+
config.ignore_drift(kind: :missing_schema, parameter_class: name)
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Only for classes whose component name does not follow the convention above.
|
|
76
|
+
{
|
|
77
|
+
'Api::V1::Orders::KonbiniRequestParameter' => 'V1.Orders.CreateKonbiniRequest',
|
|
78
|
+
'Api::V1::Orders::BankTransferRequestParameter' => 'V1.Orders.CreateBankTransferRequest'
|
|
79
|
+
}.each do |parameter_class, schema|
|
|
80
|
+
StructuredParams::OpenAPI::Registry.register(parameter_class, schema: schema)
|
|
81
|
+
end
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
- `parameter_class_filter` selects the classes to check, so a new `Api::*` class is
|
|
85
|
+
picked up without registration.
|
|
86
|
+
- `schema_name_resolver` maps every class to its component by one rule.
|
|
87
|
+
- `ignore_drift` records a class that has no schema yet, and it stays listed in the Rake
|
|
88
|
+
output. A class that is never meant to be checked is better left out of the filter
|
|
89
|
+
(see [Scope, skips and missing schemas](#scope-skips-and-missing-schemas)).
|
|
90
|
+
- `Registry.register` covers the few classes that break the naming rule.
|
|
91
|
+
|
|
92
|
+
Then run the Rake task to see what is left to fix:
|
|
93
|
+
|
|
94
|
+
```sh
|
|
95
|
+
bundle exec rake structured_params:openapi:drift_check
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
## Matching classes to schemas
|
|
99
|
+
|
|
100
|
+
### Default naming convention
|
|
101
|
+
|
|
102
|
+
By default, `CreateUserParams` is matched with `#/components/schemas/CreateUser`: the
|
|
103
|
+
class's namespace and a trailing `Param`, `Params`, `Parameter`, `Parameters`, or `Form`
|
|
104
|
+
are removed.
|
|
105
|
+
|
|
106
|
+
### Custom naming convention
|
|
107
|
+
|
|
108
|
+
If your components follow a different naming convention, override it globally with
|
|
109
|
+
`schema_name_resolver`. For example, when the OpenAPI document names the components
|
|
110
|
+
for `Api::V1::CreateUserParams` and `Api::V2::CreateUserParams` as `V1CreateUserRequest`
|
|
111
|
+
and `V2CreateUserRequest`:
|
|
112
|
+
|
|
113
|
+
```yaml
|
|
114
|
+
# openapi.yml
|
|
115
|
+
components:
|
|
116
|
+
schemas:
|
|
117
|
+
V1CreateUserRequest: # <- the default convention would look for CreateUser
|
|
118
|
+
type: object
|
|
119
|
+
properties:
|
|
120
|
+
name: { type: string }
|
|
121
|
+
V2CreateUserRequest: ...
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
```ruby
|
|
125
|
+
StructuredParams::OpenAPI.configure do |config|
|
|
126
|
+
config.schema_name_resolver = ->(parameter_class) do
|
|
127
|
+
# Api::V1::CreateUserParams -> "V1CreateUser" -> "V1CreateUserRequest"
|
|
128
|
+
name = parameter_class.name.delete_prefix('Api::').delete_suffix('Params').gsub('::', '')
|
|
129
|
+
"#{name}Request"
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
The resolver returns a component name, which replaces the default convention for every
|
|
135
|
+
class. Only the suffix `Params` is removed here, so a `CreateUserForm` would become
|
|
136
|
+
`CreateUserFormRequest`.
|
|
137
|
+
|
|
138
|
+
Keep the namespace in the returned name. If you drop it (for example with `demodulize`),
|
|
139
|
+
`Api::V1::CreateUserParams` and `Api::V2::CreateUserParams` both look for
|
|
140
|
+
`CreateUserRequest`, which does not exist, and report `missing_schema`. If each version
|
|
141
|
+
lives in its own OpenAPI file instead, keep the component name as is and switch files with
|
|
142
|
+
`document_resolver` (see [Multiple OpenAPI files](#multiple-openapi-files)).
|
|
143
|
+
|
|
144
|
+
### Exceptions for a single class
|
|
145
|
+
|
|
146
|
+
For an exceptional class, pass its component name as `schema:`. It also accepts a
|
|
147
|
+
JSON Pointer (`'/components/schemas/CreateUserRequest'`, `'#/…'`) or an inline Hash.
|
|
148
|
+
`pointer:` selects a pointer explicitly, and `openapi:` overrides the configured
|
|
149
|
+
document for one call (a file path or a Hash).
|
|
150
|
+
|
|
151
|
+
```ruby
|
|
152
|
+
StructuredParams::OpenAPI::DriftCheck.call(
|
|
153
|
+
parameter_class: CreateUserParams,
|
|
154
|
+
schema: 'CreateUserRequest'
|
|
155
|
+
)
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
## What is compared
|
|
159
|
+
|
|
160
|
+
### Scalar types
|
|
161
|
+
|
|
162
|
+
| Schema `type` | Accepted attribute types |
|
|
163
|
+
|---------------|--------------------------|
|
|
164
|
+
| `string` | `:string`, `:date`, `:datetime`, `:time` (`format` is not inspected) |
|
|
165
|
+
| `integer` | `:integer`, `:big_integer` |
|
|
166
|
+
| `number` | `:float`, `:decimal` |
|
|
167
|
+
| `boolean`, `object`, `array` | the same type |
|
|
168
|
+
|
|
169
|
+
A schema with neither `type` nor `properties` (for example an enum-only schema) is
|
|
170
|
+
reported as a `type_mismatch`. Nested object and array types are compared recursively.
|
|
171
|
+
|
|
172
|
+
### Attributes and required properties
|
|
173
|
+
|
|
174
|
+
Schema properties are compared with `attribute` declarations.
|
|
175
|
+
|
|
176
|
+
Schema `required` is compared with presence validation, and this mapping is only an
|
|
177
|
+
approximation. OpenAPI's `required` means one thing: the key must exist. In Rails,
|
|
178
|
+
a missing key and a `nil` value both leave the attribute `nil`, so no validator can
|
|
179
|
+
express "the key exists", and nothing says whether `presence`, `allow_nil` or
|
|
180
|
+
`allow_blank` was intended. The check therefore uses an unconditional
|
|
181
|
+
`validates ..., presence: true` as a stand-in ("unconditional" means none of the `if:`,
|
|
182
|
+
`unless:`, `on:`, `allow_nil:` or `allow_blank:` options).
|
|
183
|
+
|
|
184
|
+
A required property that the schema allows to be null (`nullable: true`, a `type` array
|
|
185
|
+
including `'null'`, or a `oneOf`/`anyOf` with a `null` option) needs no `presence`, since
|
|
186
|
+
`presence` would reject the null the contract permits, so it is not reported.
|
|
187
|
+
|
|
188
|
+
A `presence` that carries one of those options may let a missing key through, so the
|
|
189
|
+
check cannot confirm it satisfies `required` and reports `missing_presence_validation`.
|
|
190
|
+
That is not necessarily a bug. When the Ruby side is intentional, accept it per
|
|
191
|
+
property with [`ignore_drift`](#ignoring-a-specific-finding). When `presence` is
|
|
192
|
+
too strict for the whole API (it also rejects `false` and `""`, which `required` allows),
|
|
193
|
+
pass `required_validation: :none` and check required-key behavior separately.
|
|
194
|
+
|
|
195
|
+
### Property names
|
|
196
|
+
|
|
197
|
+
Use `property_names` when the JSON property name differs from the Ruby attribute name.
|
|
198
|
+
|
|
199
|
+
```ruby
|
|
200
|
+
StructuredParams::OpenAPI::DriftCheck.call(
|
|
201
|
+
parameter_class: CreateUserParams,
|
|
202
|
+
property_names: { user_id: 'userId' }
|
|
203
|
+
)
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
## Using it in RSpec
|
|
207
|
+
|
|
208
|
+
Use the result as the failure message so a CI failure lists every detected difference.
|
|
209
|
+
|
|
210
|
+
```ruby
|
|
211
|
+
it 'matches the request schema' do
|
|
212
|
+
result = StructuredParams::OpenAPI::DriftCheck.call(
|
|
213
|
+
parameter_class: CreateUserParams
|
|
214
|
+
)
|
|
215
|
+
|
|
216
|
+
expect(result).to be_valid, result.to_message
|
|
217
|
+
end
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
## Ignoring a specific finding
|
|
221
|
+
|
|
222
|
+
`required_validation: :none` and the Registry both operate on a whole class, so they
|
|
223
|
+
aren't precise enough to accept one known finding while still checking everything else
|
|
224
|
+
at that path. For that, configure an ignore rule instead:
|
|
225
|
+
|
|
226
|
+
```ruby
|
|
227
|
+
StructuredParams::OpenAPI.configure do |config|
|
|
228
|
+
config.ignore_drift(path: 'wallet_information.wallet_type', kind: :missing_presence_validation)
|
|
229
|
+
end
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
- The rule applies wherever that path occurs, including once per parent class for a
|
|
233
|
+
nested object reused by several top-level parameter classes.
|
|
234
|
+
- Omit `kind:` to ignore every error kind at that path. Valid kinds are
|
|
235
|
+
`missing_attribute`, `unexpected_attribute`, `missing_presence_validation`,
|
|
236
|
+
`type_mismatch`, `missing_schema` and `unresolved_reference`; anything else raises
|
|
237
|
+
`ArgumentError`.
|
|
238
|
+
- Pass `parameter_class:` (a class or its name) to limit a rule to one class.
|
|
239
|
+
- Paths use attribute names joined with `.`, with `[]` for array items (for example
|
|
240
|
+
`items[].sku`). Each of `path:`, `kind:` and `parameter_class:` narrows a rule and an
|
|
241
|
+
omitted one matches everything, but at least one is required. A missing schema
|
|
242
|
+
(`missing_schema`) concerns the whole class rather than a property, so omit `path:`.
|
|
243
|
+
- Skips are never ignored.
|
|
244
|
+
|
|
245
|
+
Ignored findings move to `result.ignored` instead of `result.errors`, so `valid?`
|
|
246
|
+
treats them as resolved and they don't appear in `to_message` — but they stay available
|
|
247
|
+
on the result object for anyone who wants to audit what's being suppressed and why,
|
|
248
|
+
rather than disappearing silently.
|
|
249
|
+
|
|
250
|
+
## Rake task and Registry
|
|
251
|
+
|
|
252
|
+
### Auto-discovery
|
|
253
|
+
|
|
254
|
+
The gem provides `structured_params:openapi:drift_check` in Rails applications. The task
|
|
255
|
+
automatically checks classes selected by `parameter_class_filter`, so a new
|
|
256
|
+
`Api::*Params` class needs no registration.
|
|
257
|
+
|
|
258
|
+
- Without a filter, only registered classes are checked, and the task warns when
|
|
259
|
+
nothing was checked.
|
|
260
|
+
- Anonymous classes are never auto-discovered, which is also why the filter example
|
|
261
|
+
guards with `name&.`.
|
|
262
|
+
|
|
263
|
+
### Registering exceptions
|
|
264
|
+
|
|
265
|
+
Use the Registry only for an exceptional component name or property mapping; explicit
|
|
266
|
+
registrations override auto-discovery for the same class, and a later registration for
|
|
267
|
+
the same class replaces an earlier one.
|
|
268
|
+
|
|
269
|
+
```ruby
|
|
270
|
+
StructuredParams::OpenAPI::Registry.register(
|
|
271
|
+
LegacyUserParams,
|
|
272
|
+
schema: 'LegacyCreateUserRequest'
|
|
273
|
+
)
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
`parameter_class` may also be a class name String, which is resolved lazily when the
|
|
277
|
+
task runs. This is useful for registering from a plain `config/initializers/*.rb` file,
|
|
278
|
+
where Rails hasn't finished setting up autoloading yet and referencing the class
|
|
279
|
+
constant directly would raise `NameError`.
|
|
280
|
+
|
|
281
|
+
```ruby
|
|
282
|
+
StructuredParams::OpenAPI::Registry.register(
|
|
283
|
+
'LegacyUserParams',
|
|
284
|
+
schema: 'LegacyCreateUserRequest'
|
|
285
|
+
)
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
### Multiple OpenAPI files
|
|
289
|
+
|
|
290
|
+
When an application has several OpenAPI files (for example `admin_api.yml` and
|
|
291
|
+
`front_api.yml`), set `document_resolver` so each auto-discovered class is checked
|
|
292
|
+
against its own file. Return `nil` to fall back to `config.document`.
|
|
293
|
+
|
|
294
|
+
```ruby
|
|
295
|
+
StructuredParams::OpenAPI.configure do |config|
|
|
296
|
+
config.parameter_class_filter = ->(klass) { klass.name&.match?(/\A(AdminApi|FrontApi)::/) }
|
|
297
|
+
config.document_resolver = ->(klass) do
|
|
298
|
+
case klass.name
|
|
299
|
+
when /\AAdminApi::/ then Rails.root.join('openapi/admin_api.yml').to_s
|
|
300
|
+
when /\AFrontApi::/ then Rails.root.join('openapi/front_api.yml').to_s
|
|
301
|
+
end
|
|
302
|
+
end
|
|
303
|
+
end
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
For a one-off class, `Registry.register(..., openapi:)` (a file path or Hash) overrides
|
|
307
|
+
the resolver. Without either, the configured `document` is used.
|
|
308
|
+
|
|
309
|
+
```ruby
|
|
310
|
+
StructuredParams::OpenAPI::Registry.register(
|
|
311
|
+
AdminUserParams,
|
|
312
|
+
openapi: Rails.root.join('openapi/admin.yml').to_s
|
|
313
|
+
)
|
|
314
|
+
```
|
|
315
|
+
|
|
316
|
+
### Running in CI
|
|
317
|
+
|
|
318
|
+
```sh
|
|
319
|
+
bundle exec rake structured_params:openapi:drift_check
|
|
320
|
+
```
|
|
321
|
+
|
|
322
|
+
Set `STRUCTURED_PARAMS_STRICT=1` to also fail on skipped checks (see below).
|
|
323
|
+
|
|
324
|
+
## Scope, skips and missing schemas
|
|
325
|
+
|
|
326
|
+
Handled as if they were plain schemas:
|
|
327
|
+
|
|
328
|
+
- OpenAPI 3.1 nullable forms: `oneOf`/`anyOf` with a `{type: null}` option and a `type`
|
|
329
|
+
array with `'null'` (for example `[string, 'null']`). Once the null option is removed
|
|
330
|
+
they are compared like 3.0 `nullable: true`;
|
|
331
|
+
- `oneOf`/`anyOf` of plain scalars (for example `string` or `integer`), which match when
|
|
332
|
+
the attribute fits any option;
|
|
333
|
+
- a single-entry `allOf`, as TypeSpec emits for `$ref` plus a `description`.
|
|
334
|
+
|
|
335
|
+
The current scope deliberately skips:
|
|
336
|
+
|
|
337
|
+
- `oneOf`/`anyOf` of objects, and multi-entry `allOf`;
|
|
338
|
+
- a `type` array of several non-null types;
|
|
339
|
+
- external references.
|
|
340
|
+
|
|
341
|
+
Recursive schemas are compared once per ancestor chain; the repeated visit is dropped
|
|
342
|
+
silently.
|
|
343
|
+
|
|
344
|
+
Skips never fail a run, but they are not silent: the Rake task prints every skipped and
|
|
345
|
+
ignored entry with a summary line, and `result.skipped` / `result.to_report` expose them
|
|
346
|
+
in RSpec. Set `STRUCTURED_PARAMS_STRICT=1` (or pass `strict: true` to
|
|
347
|
+
`Registry.check_all!`) to fail on skipped checks too.
|
|
348
|
+
|
|
349
|
+
A class whose resolved schema name has no matching component is reported as a
|
|
350
|
+
`missing_schema` error, as is a dangling or circular local `$ref`
|
|
351
|
+
(`unresolved_reference`), so a renamed component or a typo cannot silently drop
|
|
352
|
+
coverage. A class that intentionally has no schema (for example one backed by query
|
|
353
|
+
parameters) can be handled in two ways:
|
|
354
|
+
|
|
355
|
+
- If it is never meant to be checked, leave it out of `parameter_class_filter`. The
|
|
356
|
+
class is not checked at all, and nothing is recorded about it.
|
|
357
|
+
|
|
358
|
+
```ruby
|
|
359
|
+
NO_SCHEMA = %w[Api::V1::SearchParams].freeze
|
|
360
|
+
|
|
361
|
+
StructuredParams::OpenAPI.configure do |config|
|
|
362
|
+
config.parameter_class_filter = ->(klass) do
|
|
363
|
+
klass.name&.start_with?('Api::') && NO_SCHEMA.exclude?(klass.name)
|
|
364
|
+
end
|
|
365
|
+
end
|
|
366
|
+
```
|
|
367
|
+
|
|
368
|
+
- If a schema should exist but does not yet, ignore the finding for that class. It moves
|
|
369
|
+
to `result.ignored` and is listed by the Rake task, so it can be revisited later.
|
|
370
|
+
|
|
371
|
+
```ruby
|
|
372
|
+
StructuredParams::OpenAPI.configure do |config|
|
|
373
|
+
config.ignore_drift(kind: :missing_schema, parameter_class: 'Api::V1::SearchParams')
|
|
374
|
+
end
|
|
375
|
+
```
|
|
376
|
+
|
|
377
|
+
`result.to_message` is prefixed with the checked class's name, so `check_all!`'s
|
|
378
|
+
combined `DriftError` message reads as one block per class instead of a flat,
|
|
379
|
+
unattributed list of paths.
|