openapi_blocks 0.2.1 → 0.3.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 +17 -0
- data/README.md +152 -85
- data/README.pt-BR.md +229 -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 +40 -0
- data/lib/openapi_blocks/resource.rb +20 -0
- 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: c466c6845f12b5170db8082aab7e11146666fd06776e7026f29fb85ab2a9beb2
|
|
4
|
+
data.tar.gz: 99c38fe1681484456900b72ce75def577a248865d885abd21bf0d4b462ed27ad
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 8f016873a16d2066461ae29891de560c5c4bd5867ec26a00fb99dbc9c4ca2c73887f9aefd64e272a5ed2c111f2a29b71756809ba19beba3fb8e3dde4c5bd7bb2
|
|
7
|
+
data.tar.gz: 70a4deb4c047856082593ada3c2c4476c5a5caf3aad08126d3505e35a233e8d12cc48ea49a95edfdc5ee47c70b870cfc0f87506f06a1e4dc31b1683e1f1df161
|
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,23 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.3.0] - 2026-06-01
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- Scalar UI served at `/docs/scalar` alongside Swagger UI
|
|
14
|
+
- `SpecController#scalar` action serving Scalar with `displayRequestDuration` and Ruby `net_http` as default client
|
|
15
|
+
- `Resource` and `Controller` classes to support serializer-style resources and controller-scoped OpenAPI classes (`lib/openapi_blocks/resource.rb`, `lib/openapi_blocks/controller.rb`)
|
|
16
|
+
|
|
17
|
+
### Changed
|
|
18
|
+
- `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
|
|
19
|
+
- Field classification (model / virtual / association) computed once via `classify_fields` and memoized — no runtime `respond_to?` or `Array#include?` per object
|
|
20
|
+
- Association metadata indexed by name in a `Hash` for O(1) lookup instead of `Array#find` per field per object
|
|
21
|
+
- Association serializer classes resolved at compile time inside `build_assoc_method` instead of per-object via `Object.const_get`
|
|
22
|
+
- 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
|
|
23
|
+
- `Builder#openapi_classes` expanded discovery to include classes ending with `Openapi` that inherit from `OpenapiBlocks::Base` or `OpenapiBlocks::Controller` (enables controller-scoped OpenAPI classes)
|
|
24
|
+
- `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.
|
|
25
|
+
- `Base#infer_model` updated to strip both `Openapi` and `Resource` suffixes when inferring the model class name.
|
|
26
|
+
|
|
10
27
|
## [0.2.1] - 2026-06-01
|
|
11
28
|
|
|
12
29
|
### 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,55 @@ 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
|
+
|
|
163
154
|
tags "Users"
|
|
164
155
|
|
|
165
156
|
operation :index do
|
|
166
157
|
summary "List all users"
|
|
167
158
|
description "Returns a paginated list of active users"
|
|
168
|
-
tags "Users", "Admin" # overrides class-level tags for this operation
|
|
169
159
|
|
|
170
160
|
parameter :page, in: :query, type: :integer, description: "Page number"
|
|
171
161
|
parameter :per_page, in: :query, type: :integer, description: "Items per page"
|
|
@@ -177,34 +167,111 @@ class UserOpenapi < OpenapiBlocks::Base
|
|
|
177
167
|
operation :show do
|
|
178
168
|
summary "Get a user"
|
|
179
169
|
|
|
180
|
-
response 200, description: "User found",
|
|
170
|
+
response 200, description: "User found", schema: :User
|
|
181
171
|
response 404, description: "User not found"
|
|
172
|
+
|
|
173
|
+
no_security!
|
|
182
174
|
end
|
|
175
|
+
end
|
|
176
|
+
```
|
|
183
177
|
|
|
184
|
-
|
|
185
|
-
|
|
178
|
+
```ruby
|
|
179
|
+
# app/controllers/users_controller.rb
|
|
180
|
+
def index
|
|
181
|
+
render json: UserResource.serialize(User.includes(:posts))
|
|
182
|
+
end
|
|
186
183
|
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
184
|
+
def show
|
|
185
|
+
render json: UserResource.serialize(User.find(params[:id]))
|
|
186
|
+
end
|
|
187
|
+
```
|
|
190
188
|
|
|
191
|
-
|
|
192
|
-
summary "Update a user"
|
|
189
|
+
### Base (legacy, single class)
|
|
193
190
|
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
191
|
+
```ruby
|
|
192
|
+
# app/openapi/user_openapi.rb
|
|
193
|
+
class UserOpenapi < OpenapiBlocks::Base
|
|
194
|
+
tags "Users"
|
|
198
195
|
|
|
199
|
-
|
|
200
|
-
summary "Delete a user"
|
|
196
|
+
ignore :password_digest
|
|
201
197
|
|
|
202
|
-
|
|
203
|
-
|
|
198
|
+
association :posts, type: :array, read_only: true
|
|
199
|
+
|
|
200
|
+
attribute :full_name, type: :string, read_only: true
|
|
201
|
+
|
|
202
|
+
operation :index do
|
|
203
|
+
summary "List all users"
|
|
204
|
+
response 200, description: "List of users", schema: { type: :array, items: :User }
|
|
204
205
|
end
|
|
205
206
|
end
|
|
206
207
|
```
|
|
207
208
|
|
|
209
|
+
```ruby
|
|
210
|
+
# app/controllers/users_controller.rb
|
|
211
|
+
def index
|
|
212
|
+
render json: UserOpenapi.serialize(User.includes(:posts))
|
|
213
|
+
end
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
---
|
|
217
|
+
|
|
218
|
+
## Serializer
|
|
219
|
+
|
|
220
|
+
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.
|
|
221
|
+
|
|
222
|
+
### Performance (200 records, arm64, Ruby 4.0)
|
|
223
|
+
|
|
224
|
+
| | i/s | μs/i | vs serialize |
|
|
225
|
+
|---|---|---|---|
|
|
226
|
+
| serialize | 4 239 | 235 | — |
|
|
227
|
+
| to_json | 1 444 | 692 | 2.94× slower |
|
|
228
|
+
| as_json | 1 186 | 843 | 3.58× slower |
|
|
229
|
+
| oj+as_json | 1 126 | 888 | 3.77× slower |
|
|
230
|
+
|
|
231
|
+
Scaling is linear — the 3.6× advantage over `as_json` holds from 10 to 5000 records.
|
|
232
|
+
|
|
233
|
+
### Virtual attributes and method resolution
|
|
234
|
+
|
|
235
|
+
| Declared with | Method in resource? | Calls |
|
|
236
|
+
|---|---|---|
|
|
237
|
+
| `attribute :full_name` | yes | `resource_instance.full_name` |
|
|
238
|
+
| `attribute :full_name` | no | `object.full_name` (delegated to model) |
|
|
239
|
+
| column in db | — | `object.full_name` (direct) |
|
|
240
|
+
|
|
241
|
+
### Association serializer resolution
|
|
242
|
+
|
|
243
|
+
For each association, the serializer resolves the serializer class in this order:
|
|
244
|
+
|
|
245
|
+
1. `PostResource` — has `serialize`, used directly.
|
|
246
|
+
2. `PostOpenapi` — is a `Controller`, delegates to its `_resource`.
|
|
247
|
+
3. Fallback — calls `as_json` on the association value.
|
|
248
|
+
|
|
249
|
+
---
|
|
250
|
+
|
|
251
|
+
## What is generated automatically
|
|
252
|
+
|
|
253
|
+
Given this model:
|
|
254
|
+
|
|
255
|
+
```ruby
|
|
256
|
+
class User < ApplicationRecord
|
|
257
|
+
validates :name, presence: true, length: { minimum: 2, maximum: 100 }
|
|
258
|
+
validates :email, presence: true, format: { with: URI::MailTo::EMAIL_REGEXP }
|
|
259
|
+
validates :age, numericality: { greater_than: 0 }
|
|
260
|
+
validates :role, inclusion: { in: %w[admin user guest] }
|
|
261
|
+
end
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
OpenapiBlocks generates:
|
|
265
|
+
|
|
266
|
+
- `User` schema from `db/schema.rb` columns and types
|
|
267
|
+
- `UserInput` schema for POST, PUT and PATCH request bodies (without `id`, `created_at`, `updated_at` and `read_only` fields)
|
|
268
|
+
- `required` fields from `presence: true` validations
|
|
269
|
+
- `minLength`, `maxLength` from `length` validations
|
|
270
|
+
- `minimum`, `maximum` from `numericality` validations
|
|
271
|
+
- `enum` from `inclusion` validations
|
|
272
|
+
- `format: "email"` from format validations
|
|
273
|
+
- All paths from `config/routes.rb`
|
|
274
|
+
|
|
208
275
|
---
|
|
209
276
|
|
|
210
277
|
## Security
|
|
@@ -235,9 +302,9 @@ end
|
|
|
235
302
|
## Associations
|
|
236
303
|
|
|
237
304
|
```ruby
|
|
238
|
-
association :company
|
|
239
|
-
association :posts, type: :array
|
|
240
|
-
association :posts, type: :array, read_only: true
|
|
305
|
+
association :company # belongs_to — $ref to Company schema
|
|
306
|
+
association :posts, type: :array # has_many — array of $ref to Post schema
|
|
307
|
+
association :posts, type: :array, read_only: true # excluded from UserInput (response only)
|
|
241
308
|
```
|
|
242
309
|
|
|
243
310
|
---
|
|
@@ -246,10 +313,10 @@ association :posts, type: :array, read_only: true # excluded from UserInput (r
|
|
|
246
313
|
|
|
247
314
|
Virtual attributes are fields that exist in the API response but not in the database.
|
|
248
315
|
|
|
249
|
-
| Option
|
|
250
|
-
|
|
251
|
-
| read_only: true
|
|
252
|
-
| read_only: false | Fields the client can send and receive |
|
|
316
|
+
| Option | Description | Appears in User | Appears in UserInput |
|
|
317
|
+
|---|---|:---:|:---:|
|
|
318
|
+
| `read_only: true` | Calculated or system-generated fields | YES | NO |
|
|
319
|
+
| `read_only: false` | Fields the client can send and receive | YES | YES |
|
|
253
320
|
|
|
254
321
|
```ruby
|
|
255
322
|
attribute :full_name, type: :string, read_only: true # response only
|
|
@@ -261,19 +328,19 @@ attribute :nickname, type: :string # request and response
|
|
|
261
328
|
|
|
262
329
|
## Type Mapping
|
|
263
330
|
|
|
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
|
|
331
|
+
| ActiveRecord type | OpenAPI type |
|
|
332
|
+
|---|---|
|
|
333
|
+
| integer | integer / int32 |
|
|
334
|
+
| bigint | integer / int64 |
|
|
335
|
+
| float | number / float |
|
|
336
|
+
| decimal | number / double |
|
|
337
|
+
| string | string |
|
|
338
|
+
| text | string |
|
|
339
|
+
| boolean | boolean |
|
|
340
|
+
| date | string / date |
|
|
341
|
+
| datetime | string / date-time |
|
|
342
|
+
| uuid | string / uuid |
|
|
343
|
+
| json / jsonb | object |
|
|
277
344
|
|
|
278
345
|
---
|
|
279
346
|
|
|
@@ -288,7 +355,7 @@ config/routes.rb
|
|
|
288
355
|
db/schema.rb
|
|
289
356
|
```
|
|
290
357
|
|
|
291
|
-
The spec is automatically regenerated on the next request to
|
|
358
|
+
The spec is automatically regenerated on the next request to `/docs/openapi.json` whenever any of these files change. No server restart needed.
|
|
292
359
|
|
|
293
360
|
---
|
|
294
361
|
|
|
@@ -301,4 +368,4 @@ The spec is automatically regenerated on the next request to /docs/openapi.json
|
|
|
301
368
|
|
|
302
369
|
## License
|
|
303
370
|
|
|
304
|
-
MIT (LICENSE.txt)
|
|
371
|
+
MIT (LICENSE.txt)
|