openapi_blocks 0.2.1 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +22 -0
- data/README.md +153 -85
- data/README.pt-BR.md +230 -342
- data/app/controllers/openapi_blocks/spec_controller.rb +48 -13
- data/config/routes.rb +2 -1
- data/lib/openapi_blocks/base.rb +3 -0
- data/lib/openapi_blocks/builder.rb +3 -1
- data/lib/openapi_blocks/controller.rb +44 -0
- data/lib/openapi_blocks/resource.rb +20 -0
- data/lib/openapi_blocks/routing/extractor.rb +16 -1
- data/lib/openapi_blocks/serializer.rb +193 -0
- data/lib/openapi_blocks/spec/components.rb +14 -6
- data/lib/openapi_blocks/version.rb +1 -1
- data/lib/openapi_blocks.rb +4 -0
- metadata +18 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: a45ac9389f6202430fb9c21ded74a327d221a9925e5b6a9a3622ea712a216290
|
|
4
|
+
data.tar.gz: bc02bd7c04f7ab2e2c6981b23ca317c500b070bfddb4120808d1b801d300c679
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 28ab89af18ab105d20c37ea9882239925d2afba4a8f644fb038d1d93c444fc29b94f4f117429a36e0d6442116fe984c48bafbb37fbef0ebd4131a994e2d04141
|
|
7
|
+
data.tar.gz: a679b294432e62db66c3e3e774d042319aaf4d9371a3e788bcbcedecc3f71906a7b924b04090196c79a608a177a245fb12f545ad272307882e3d3ed459079f67
|
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,28 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.3.1] - 2026-06-02
|
|
11
|
+
|
|
12
|
+
### Changed
|
|
13
|
+
- `OpenapiBlocks::Controller` now uses `controller` to specifies exact controller
|
|
14
|
+
|
|
15
|
+
## [0.3.0] - 2026-06-01
|
|
16
|
+
|
|
17
|
+
### Added
|
|
18
|
+
- Scalar UI served at `/docs/scalar` alongside Swagger UI
|
|
19
|
+
- `SpecController#scalar` action serving Scalar with `displayRequestDuration` and Ruby `net_http` as default client
|
|
20
|
+
- `Resource` and `Controller` classes to support serializer-style resources and controller-scoped OpenAPI classes (`lib/openapi_blocks/resource.rb`, `lib/openapi_blocks/controller.rb`)
|
|
21
|
+
|
|
22
|
+
### Changed
|
|
23
|
+
- `OpenapiBlocks::Serializer` now uses `class_eval` to compile a monolithic extractor method per serializer class at boot time, eliminating per-object branching and lambda indirection
|
|
24
|
+
- Field classification (model / virtual / association) computed once via `classify_fields` and memoized — no runtime `respond_to?` or `Array#include?` per object
|
|
25
|
+
- Association metadata indexed by name in a `Hash` for O(1) lookup instead of `Array#find` per field per object
|
|
26
|
+
- Association serializer classes resolved at compile time inside `build_assoc_method` instead of per-object via `Object.const_get`
|
|
27
|
+
- Serializer is now **1.86× faster** than the original implementation and **3.6× faster** than `as_json` across 10–5000 records with consistent linear scaling
|
|
28
|
+
- `Builder#openapi_classes` expanded discovery to include classes ending with `Openapi` that inherit from `OpenapiBlocks::Base` or `OpenapiBlocks::Controller` (enables controller-scoped OpenAPI classes)
|
|
29
|
+
- `Serializer` now includes virtual attributes and associations marked `read_only` in serialized output (they previously were omitted). This ensures `read_only: true` fields are present in responses/listings while still being excluded from `*Input` schemas.
|
|
30
|
+
- `Base#infer_model` updated to strip both `Openapi` and `Resource` suffixes when inferring the model class name.
|
|
31
|
+
|
|
10
32
|
## [0.2.1] - 2026-06-01
|
|
11
33
|
|
|
12
34
|
### Changed
|
data/README.md
CHANGED
|
@@ -1,10 +1,21 @@
|
|
|
1
1
|
# OpenapiBlocks
|
|
2
2
|
|
|
3
|
-
OpenapiBlocks is a Rails gem that automatically generates OpenAPI 3.0/3.1 documentation from your ActiveRecord models, ActiveModel validations, and Rails routes — inspired by ActiveModel::Serializer
|
|
3
|
+
OpenapiBlocks is a Rails gem that automatically generates OpenAPI 3.0/3.1 documentation from your ActiveRecord models, ActiveModel validations, and Rails routes — inspired by [ActiveModel::Serializer](https://github.com/rails-api/active_model_serializers).
|
|
4
4
|
|
|
5
5
|
Versão em português brasileiro: README.pt-BR.md
|
|
6
6
|
|
|
7
|
-
No manual annotation. No DSL noise in your controllers. Just declare what to expose and the spec is generated automatically.
|
|
7
|
+
No manual annotation. No DSL noise in your controllers. Just declare what to expose and the spec is generated automatically. Includes a high-performance built-in serializer — ~3.6× faster than `as_json` with consistent linear scaling from 10 to 5000 records.
|
|
8
|
+
|
|
9
|
+
## Key changes (recent)
|
|
10
|
+
- `OpenapiBlocks::Resource` and `OpenapiBlocks::Controller` introduced as a cleaner alternative to `OpenapiBlocks::Base` — separating serialization from documentation concerns.
|
|
11
|
+
- Default OpenAPI version is `3.1.0` (supported: `3.1.0`, `3.0.3`).
|
|
12
|
+
- Scalar UI is now served at `/docs/scalar` alongside Swagger UI at `/docs`.
|
|
13
|
+
- Swagger UI uses same-origin spec endpoints to avoid CORS issues.
|
|
14
|
+
- YAML output is normalized to use string keys so Swagger UI accepts the `openapi` version field.
|
|
15
|
+
- `association` DSL uses `read_only: true` to mark fields as response-only and exclude them from `*Input` schemas.
|
|
16
|
+
- `tags` are generated at the document root from paths and can be customized via the `tags` DSL on classes and operations.
|
|
17
|
+
- Schema references accept `Symbol` (e.g. `schema: :user`) and array items can be symbol references (e.g. `items: :user`).
|
|
18
|
+
- Serializer uses `class_eval` to compile a monolithic extractor method per class at boot — eliminating per-object branching, lambda indirection, and runtime `respond_to?` checks.
|
|
8
19
|
|
|
9
20
|
---
|
|
10
21
|
|
|
@@ -40,7 +51,8 @@ end
|
|
|
40
51
|
This exposes:
|
|
41
52
|
|
|
42
53
|
```
|
|
43
|
-
GET /docs ->
|
|
54
|
+
GET /docs -> Scalar UI
|
|
55
|
+
GET /docs/swagger -> Swagger UI
|
|
44
56
|
GET /docs/openapi.json -> OpenAPI spec in JSON
|
|
45
57
|
GET /docs/openapi.yaml -> OpenAPI spec in YAML
|
|
46
58
|
```
|
|
@@ -50,7 +62,7 @@ GET /docs/openapi.yaml -> OpenAPI spec in YAML
|
|
|
50
62
|
```ruby
|
|
51
63
|
# config/initializers/openapi_blocks.rb
|
|
52
64
|
OpenapiBlocks.configure do |config|
|
|
53
|
-
config.openapi_version = "3.1" # "3.0" or "3.1"
|
|
65
|
+
config.openapi_version = "3.1.0" # "3.0.3" or "3.1.0"
|
|
54
66
|
|
|
55
67
|
config.info do
|
|
56
68
|
title "My API"
|
|
@@ -95,77 +107,56 @@ end
|
|
|
95
107
|
|
|
96
108
|
## Usage
|
|
97
109
|
|
|
98
|
-
|
|
110
|
+
OpenapiBlocks provides two base classes with distinct responsibilities:
|
|
111
|
+
|
|
112
|
+
- `OpenapiBlocks::Resource` — defines the model, fields, associations, and serialization logic.
|
|
113
|
+
- `OpenapiBlocks::Controller` — defines the API operations, parameters, and responses for documentation.
|
|
114
|
+
- `OpenapiBlocks::Base` — legacy base class that combines both concerns. Still supported.
|
|
99
115
|
|
|
100
|
-
|
|
116
|
+
### Resource + Controller (recommended)
|
|
101
117
|
|
|
102
118
|
```
|
|
103
119
|
app/
|
|
104
120
|
openapi/
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
121
|
+
user_resource.rb -> serialization + schema
|
|
122
|
+
user_openapi.rb -> API documentation
|
|
123
|
+
post_resource.rb
|
|
124
|
+
post_openapi.rb
|
|
108
125
|
```
|
|
109
126
|
|
|
110
127
|
```ruby
|
|
111
|
-
# app/openapi/
|
|
112
|
-
class
|
|
128
|
+
# app/openapi/user_resource.rb
|
|
129
|
+
class UserResource < OpenapiBlocks::Resource
|
|
113
130
|
# model User is inferred automatically from the class name
|
|
114
131
|
|
|
115
|
-
# custom tags (default: inferred from controller name)
|
|
116
|
-
tags "Users"
|
|
117
|
-
|
|
118
|
-
# opt-out sensitive or unnecessary fields
|
|
119
132
|
ignore :password_digest, :reset_password_token
|
|
120
133
|
|
|
121
|
-
|
|
122
|
-
association :company
|
|
123
|
-
association :posts, type: :array, read_only: true # excluded from UserInput
|
|
134
|
+
association :posts, type: :array, read_only: true
|
|
124
135
|
|
|
125
|
-
# virtual attributes (not in the database)
|
|
126
|
-
# read_only: true -> exposed in response (User), excluded from request body (UserInput)
|
|
127
|
-
# read_only: false -> exposed in both User and UserInput
|
|
128
136
|
attribute :full_name, type: :string, read_only: true
|
|
129
137
|
attribute :access_token, type: :string, read_only: true
|
|
130
138
|
attribute :nickname, type: :string
|
|
131
|
-
end
|
|
132
|
-
```
|
|
133
139
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
140
|
+
# method defined here — called on the resource instance
|
|
141
|
+
def full_name
|
|
142
|
+
"#{object.name} (#{object.email})"
|
|
143
|
+
end
|
|
137
144
|
|
|
138
|
-
|
|
139
|
-
class User < ApplicationRecord
|
|
140
|
-
validates :name, presence: true, length: { minimum: 2, maximum: 100 }
|
|
141
|
-
validates :email, presence: true, format: { with: URI::MailTo::EMAIL_REGEXP }
|
|
142
|
-
validates :age, numericality: { greater_than: 0 }
|
|
143
|
-
validates :role, inclusion: { in: %w[admin user guest] }
|
|
145
|
+
# or omit the method and it delegates to the model automatically
|
|
144
146
|
end
|
|
145
147
|
```
|
|
146
148
|
|
|
147
|
-
OpenapiBlocks generates:
|
|
148
|
-
|
|
149
|
-
- User schema from db/schema.rb columns and types
|
|
150
|
-
- UserInput schema for POST, PUT and PATCH request bodies (without id, created_at, updated_at and read_only virtual attributes)
|
|
151
|
-
- required fields from presence: true validations
|
|
152
|
-
- minLength, maxLength from length validations
|
|
153
|
-
- minimum, maximum from numericality validations
|
|
154
|
-
- enum from inclusion validations
|
|
155
|
-
- format: "email" from format validations
|
|
156
|
-
- All paths from config/routes.rb
|
|
157
|
-
|
|
158
|
-
### Customizing operations
|
|
159
|
-
|
|
160
149
|
```ruby
|
|
161
150
|
# app/openapi/user_openapi.rb
|
|
162
|
-
class UserOpenapi < OpenapiBlocks::
|
|
151
|
+
class UserOpenapi < OpenapiBlocks::Controller
|
|
152
|
+
resource UserResource
|
|
153
|
+
controller UsersController
|
|
154
|
+
|
|
163
155
|
tags "Users"
|
|
164
156
|
|
|
165
157
|
operation :index do
|
|
166
158
|
summary "List all users"
|
|
167
159
|
description "Returns a paginated list of active users"
|
|
168
|
-
tags "Users", "Admin" # overrides class-level tags for this operation
|
|
169
160
|
|
|
170
161
|
parameter :page, in: :query, type: :integer, description: "Page number"
|
|
171
162
|
parameter :per_page, in: :query, type: :integer, description: "Items per page"
|
|
@@ -177,34 +168,111 @@ class UserOpenapi < OpenapiBlocks::Base
|
|
|
177
168
|
operation :show do
|
|
178
169
|
summary "Get a user"
|
|
179
170
|
|
|
180
|
-
response 200, description: "User found",
|
|
171
|
+
response 200, description: "User found", schema: :User
|
|
181
172
|
response 404, description: "User not found"
|
|
173
|
+
|
|
174
|
+
no_security!
|
|
182
175
|
end
|
|
176
|
+
end
|
|
177
|
+
```
|
|
183
178
|
|
|
184
|
-
|
|
185
|
-
|
|
179
|
+
```ruby
|
|
180
|
+
# app/controllers/users_controller.rb
|
|
181
|
+
def index
|
|
182
|
+
render json: UserResource.serialize(User.includes(:posts))
|
|
183
|
+
end
|
|
186
184
|
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
185
|
+
def show
|
|
186
|
+
render json: UserResource.serialize(User.find(params[:id]))
|
|
187
|
+
end
|
|
188
|
+
```
|
|
190
189
|
|
|
191
|
-
|
|
192
|
-
summary "Update a user"
|
|
190
|
+
### Base (legacy, single class)
|
|
193
191
|
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
192
|
+
```ruby
|
|
193
|
+
# app/openapi/user_openapi.rb
|
|
194
|
+
class UserOpenapi < OpenapiBlocks::Base
|
|
195
|
+
tags "Users"
|
|
198
196
|
|
|
199
|
-
|
|
200
|
-
summary "Delete a user"
|
|
197
|
+
ignore :password_digest
|
|
201
198
|
|
|
202
|
-
|
|
203
|
-
|
|
199
|
+
association :posts, type: :array, read_only: true
|
|
200
|
+
|
|
201
|
+
attribute :full_name, type: :string, read_only: true
|
|
202
|
+
|
|
203
|
+
operation :index do
|
|
204
|
+
summary "List all users"
|
|
205
|
+
response 200, description: "List of users", schema: { type: :array, items: :User }
|
|
204
206
|
end
|
|
205
207
|
end
|
|
206
208
|
```
|
|
207
209
|
|
|
210
|
+
```ruby
|
|
211
|
+
# app/controllers/users_controller.rb
|
|
212
|
+
def index
|
|
213
|
+
render json: UserOpenapi.serialize(User.includes(:posts))
|
|
214
|
+
end
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
---
|
|
218
|
+
|
|
219
|
+
## Serializer
|
|
220
|
+
|
|
221
|
+
The built-in serializer compiles a monolithic extractor method per class at boot time using `class_eval`. There are no loops, no lambda indirection, and no runtime branching per object.
|
|
222
|
+
|
|
223
|
+
### Performance (200 records, arm64, Ruby 4.0)
|
|
224
|
+
|
|
225
|
+
| | i/s | μs/i | vs serialize |
|
|
226
|
+
|---|---|---|---|
|
|
227
|
+
| serialize | 4 239 | 235 | — |
|
|
228
|
+
| to_json | 1 444 | 692 | 2.94× slower |
|
|
229
|
+
| as_json | 1 186 | 843 | 3.58× slower |
|
|
230
|
+
| oj+as_json | 1 126 | 888 | 3.77× slower |
|
|
231
|
+
|
|
232
|
+
Scaling is linear — the 3.6× advantage over `as_json` holds from 10 to 5000 records.
|
|
233
|
+
|
|
234
|
+
### Virtual attributes and method resolution
|
|
235
|
+
|
|
236
|
+
| Declared with | Method in resource? | Calls |
|
|
237
|
+
|---|---|---|
|
|
238
|
+
| `attribute :full_name` | yes | `resource_instance.full_name` |
|
|
239
|
+
| `attribute :full_name` | no | `object.full_name` (delegated to model) |
|
|
240
|
+
| column in db | — | `object.full_name` (direct) |
|
|
241
|
+
|
|
242
|
+
### Association serializer resolution
|
|
243
|
+
|
|
244
|
+
For each association, the serializer resolves the serializer class in this order:
|
|
245
|
+
|
|
246
|
+
1. `PostResource` — has `serialize`, used directly.
|
|
247
|
+
2. `PostOpenapi` — is a `Controller`, delegates to its `_resource`.
|
|
248
|
+
3. Fallback — calls `as_json` on the association value.
|
|
249
|
+
|
|
250
|
+
---
|
|
251
|
+
|
|
252
|
+
## What is generated automatically
|
|
253
|
+
|
|
254
|
+
Given this model:
|
|
255
|
+
|
|
256
|
+
```ruby
|
|
257
|
+
class User < ApplicationRecord
|
|
258
|
+
validates :name, presence: true, length: { minimum: 2, maximum: 100 }
|
|
259
|
+
validates :email, presence: true, format: { with: URI::MailTo::EMAIL_REGEXP }
|
|
260
|
+
validates :age, numericality: { greater_than: 0 }
|
|
261
|
+
validates :role, inclusion: { in: %w[admin user guest] }
|
|
262
|
+
end
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
OpenapiBlocks generates:
|
|
266
|
+
|
|
267
|
+
- `User` schema from `db/schema.rb` columns and types
|
|
268
|
+
- `UserInput` schema for POST, PUT and PATCH request bodies (without `id`, `created_at`, `updated_at` and `read_only` fields)
|
|
269
|
+
- `required` fields from `presence: true` validations
|
|
270
|
+
- `minLength`, `maxLength` from `length` validations
|
|
271
|
+
- `minimum`, `maximum` from `numericality` validations
|
|
272
|
+
- `enum` from `inclusion` validations
|
|
273
|
+
- `format: "email"` from format validations
|
|
274
|
+
- All paths from `config/routes.rb`
|
|
275
|
+
|
|
208
276
|
---
|
|
209
277
|
|
|
210
278
|
## Security
|
|
@@ -235,9 +303,9 @@ end
|
|
|
235
303
|
## Associations
|
|
236
304
|
|
|
237
305
|
```ruby
|
|
238
|
-
association :company
|
|
239
|
-
association :posts, type: :array
|
|
240
|
-
association :posts, type: :array, read_only: true
|
|
306
|
+
association :company # belongs_to — $ref to Company schema
|
|
307
|
+
association :posts, type: :array # has_many — array of $ref to Post schema
|
|
308
|
+
association :posts, type: :array, read_only: true # excluded from UserInput (response only)
|
|
241
309
|
```
|
|
242
310
|
|
|
243
311
|
---
|
|
@@ -246,10 +314,10 @@ association :posts, type: :array, read_only: true # excluded from UserInput (r
|
|
|
246
314
|
|
|
247
315
|
Virtual attributes are fields that exist in the API response but not in the database.
|
|
248
316
|
|
|
249
|
-
| Option
|
|
250
|
-
|
|
251
|
-
| read_only: true
|
|
252
|
-
| read_only: false | Fields the client can send and receive |
|
|
317
|
+
| Option | Description | Appears in User | Appears in UserInput |
|
|
318
|
+
|---|---|:---:|:---:|
|
|
319
|
+
| `read_only: true` | Calculated or system-generated fields | YES | NO |
|
|
320
|
+
| `read_only: false` | Fields the client can send and receive | YES | YES |
|
|
253
321
|
|
|
254
322
|
```ruby
|
|
255
323
|
attribute :full_name, type: :string, read_only: true # response only
|
|
@@ -261,19 +329,19 @@ attribute :nickname, type: :string # request and response
|
|
|
261
329
|
|
|
262
330
|
## Type Mapping
|
|
263
331
|
|
|
264
|
-
| ActiveRecord type | OpenAPI type
|
|
265
|
-
|
|
266
|
-
| integer
|
|
267
|
-
| bigint
|
|
268
|
-
| float
|
|
269
|
-
| decimal
|
|
270
|
-
| string
|
|
271
|
-
| text
|
|
272
|
-
| boolean
|
|
273
|
-
| date
|
|
274
|
-
| datetime
|
|
275
|
-
| uuid
|
|
276
|
-
| json / jsonb
|
|
332
|
+
| ActiveRecord type | OpenAPI type |
|
|
333
|
+
|---|---|
|
|
334
|
+
| integer | integer / int32 |
|
|
335
|
+
| bigint | integer / int64 |
|
|
336
|
+
| float | number / float |
|
|
337
|
+
| decimal | number / double |
|
|
338
|
+
| string | string |
|
|
339
|
+
| text | string |
|
|
340
|
+
| boolean | boolean |
|
|
341
|
+
| date | string / date |
|
|
342
|
+
| datetime | string / date-time |
|
|
343
|
+
| uuid | string / uuid |
|
|
344
|
+
| json / jsonb | object |
|
|
277
345
|
|
|
278
346
|
---
|
|
279
347
|
|
|
@@ -288,7 +356,7 @@ config/routes.rb
|
|
|
288
356
|
db/schema.rb
|
|
289
357
|
```
|
|
290
358
|
|
|
291
|
-
The spec is automatically regenerated on the next request to
|
|
359
|
+
The spec is automatically regenerated on the next request to `/docs/openapi.json` whenever any of these files change. No server restart needed.
|
|
292
360
|
|
|
293
361
|
---
|
|
294
362
|
|
|
@@ -301,4 +369,4 @@ The spec is automatically regenerated on the next request to /docs/openapi.json
|
|
|
301
369
|
|
|
302
370
|
## License
|
|
303
371
|
|
|
304
|
-
MIT (LICENSE.txt)
|
|
372
|
+
MIT (LICENSE.txt)
|