openapi_blocks 0.3.0 → 0.4.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c466c6845f12b5170db8082aab7e11146666fd06776e7026f29fb85ab2a9beb2
4
- data.tar.gz: 99c38fe1681484456900b72ce75def577a248865d885abd21bf0d4b462ed27ad
3
+ metadata.gz: eafa1f0685d4c659609b5116dfac6e7204cee4bd5c60ea882417aebbe4cd67dd
4
+ data.tar.gz: db613ff23df3f705b70f126f02a0d03334fffe8a8fcbfd3dbe563343fce69ef8
5
5
  SHA512:
6
- metadata.gz: 8f016873a16d2066461ae29891de560c5c4bd5867ec26a00fb99dbc9c4ca2c73887f9aefd64e272a5ed2c111f2a29b71756809ba19beba3fb8e3dde4c5bd7bb2
7
- data.tar.gz: 70a4deb4c047856082593ada3c2c4476c5a5caf3aad08126d3505e35a233e8d12cc48ea49a95edfdc5ee47c70b870cfc0f87506f06a1e4dc31b1683e1f1df161
6
+ metadata.gz: f88f54dcc99be63820da03cedaa9062d5e1c3e185c1cbf0d6aa3ac2c00f066b9ea75002dd1e8d4061f51c0f4bb71d606dc5b632de4f80870f3c598bd47312ef7
7
+ data.tar.gz: 75027794cbdebde235211302cc5da8495f2dba128441cdd8d94bfea4d7808acd1a66ef40569f768698c5c2b32fba205f99c71305673105b0229276d4c9c9e505
data/CHANGELOG.md CHANGED
@@ -7,26 +7,68 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.4.1] - 2026-06-02
11
+
12
+ - Change parallel version
13
+
14
+ ## [0.4.0] - 2026-06-02
15
+
16
+ ### Added
17
+
18
+ - `OpenapiBlocks::Serializer` base class for standalone serializers in `app/serializers/` — infers model from class name (e.g. `UserSerializer` -> `User`)
19
+ - `OpenapiBlocks::Concerns::Schemable` module extracted from `Base` — provides `model`, `ignore`, `association`, `attribute`, `serializes` DSL
20
+ - `OpenapiBlocks::Concerns::Documentable` module extracted from `Base` — provides `operation`, `tags` DSL
21
+ - `OpenapiBlocks::Serialization` module — internal serialization engine shared by `Base` and `Serializer`
22
+ - `OpenapiBlocks::Registry` — maps models to serializers at boot; supports convention-based (`UserSerializer` -> `User`) and explicit (`serializes User`) registration
23
+ - `OpenapiBlocks::AutoSerialize` — Rack middleware that intercepts `render json:` calls and applies the registered serializer automatically when `config.auto_serialize = true`
24
+ - `Configuration#auto_serialize` flag — opt-in automatic serialization via `config.auto_serialize = true`
25
+ - `Configuration#configured?` flag — set to `true` when `config.info` block is called
26
+ - `Builder#validate_configuration!` — raises `OpenapiBlocks::Error` with a descriptive message if `configure` was never called or `info.title`/`info.version` are blank
27
+ - `Railtie` now eager loads `app/serializers/**/*.rb` alongside `app/openapi/**/*.rb`
28
+ - `Railtie` builds `Registry` and injects `AutoSerialize` into `ActionController::Base` and `ActionController::API` when `auto_serialize` is enabled
29
+
30
+ ### Changed
31
+
32
+ - `OpenapiBlocks::Resource` removed — replaced by `OpenapiBlocks::Serializer`
33
+ - `OpenapiBlocks::Serialization` hierarchy traversal stops at `serialization_sentinel` instead of hardcoded `OpenapiBlocks::Base`
34
+ - `resolve_assoc_serializer` lookup order: `PostSerializer` -> `PostOpenapi` (delegates to `_resource` if Controller) -> fallback to `as_json`
35
+ - `Spec::Components#build` resolves model via `resolve_schema_klass` — supports both `Controller` (via `_resource`) and `Serializer` as schema source
36
+ - `Base` now includes `Concerns::Schemable` and `Concerns::Documentable` — no behavioral change for existing users
37
+ - Supported `openapi_version` values changed to `"3.1.0"` and `"3.0.3"` (previously `"3.1"` and `"3.0"`)
38
+
39
+ ### Removed
40
+
41
+ - `lib/openapi_blocks/resource.rb` — `OpenapiBlocks::Resource` is no longer part of the public API
42
+
43
+ ## [0.3.1] - 2026-06-02
44
+
45
+ ### Changed
46
+
47
+ - `OpenapiBlocks::Controller` now uses `controller` to specifies exact controller
48
+
10
49
  ## [0.3.0] - 2026-06-01
11
50
 
12
51
  ### Added
52
+
13
53
  - Scalar UI served at `/docs/scalar` alongside Swagger UI
14
54
  - `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`)
55
+ - `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
56
 
17
57
  ### Changed
58
+
18
59
  - `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
60
  - Field classification (model / virtual / association) computed once via `classify_fields` and memoized — no runtime `respond_to?` or `Array#include?` per object
20
61
  - Association metadata indexed by name in a `Hash` for O(1) lookup instead of `Array#find` per field per object
21
62
  - Association serializer classes resolved at compile time inside `build_assoc_method` instead of per-object via `Object.const_get`
22
63
  - 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.
64
+ - `Builder#openapi_classes` expanded discovery to include classes ending with `Openapi` that inherit from `OpenapiBlocks::Base` or `OpenapiBlocks::Controller` (enables controller-scoped OpenAPI classes)
65
+ - `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.
66
+ - `Base#infer_model` updated to strip both `Openapi` and `Resource` suffixes when inferring the model class name.
26
67
 
27
68
  ## [0.2.1] - 2026-06-01
28
69
 
29
70
  ### Changed
71
+
30
72
  - `association` DSL now uses `read_only: true` instead of `input: false` for consistency with `attribute` DSL
31
73
 
32
74
  ## [0.2.0] - 2026-06-01
data/README.md CHANGED
@@ -2,26 +2,15 @@
2
2
 
3
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
- Versão em português brasileiro: README.pt-BR.md
5
+ Versão em português brasileiro: [README.pt-BR.md](README.pt-BR.md)
6
6
 
7
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
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.
19
-
20
9
  ---
21
10
 
22
11
  ## Installation
23
12
 
24
- Add to your Gemfile:
13
+ Add to your `Gemfile`:
25
14
 
26
15
  ```ruby
27
16
  gem "openapi_blocks"
@@ -51,7 +40,7 @@ end
51
40
  This exposes:
52
41
 
53
42
  ```
54
- GET /docs -> Scalar UI
43
+ GET /docs -> Scalar UI (default)
55
44
  GET /docs/swagger -> Swagger UI
56
45
  GET /docs/openapi.json -> OpenAPI spec in JSON
57
46
  GET /docs/openapi.yaml -> OpenAPI spec in YAML
@@ -59,14 +48,16 @@ GET /docs/openapi.yaml -> OpenAPI spec in YAML
59
48
 
60
49
  ### 2. Configure the initializer
61
50
 
51
+ `OpenapiBlocks.configure` is required. The gem raises `OpenapiBlocks::Error` on the first request if it was never called or if `info.title` / `info.version` are blank.
52
+
62
53
  ```ruby
63
54
  # config/initializers/openapi_blocks.rb
64
55
  OpenapiBlocks.configure do |config|
65
- config.openapi_version = "3.1.0" # "3.0.3" or "3.1.0"
56
+ config.openapi_version = "3.1.0" # required — "3.0.3" or "3.1.0"
66
57
 
67
58
  config.info do
68
- title "My API"
69
- version "1.0.0"
59
+ title "My API" # required
60
+ version "1.0.0" # required
70
61
  description "API documentation generated automatically"
71
62
 
72
63
  contact do
@@ -93,7 +84,8 @@ OpenapiBlocks.configure do |config|
93
84
  end
94
85
  end
95
86
 
96
- config.watch = :development # auto-reload on file changes
87
+ config.watch = :development # auto-reload on file changes
88
+ config.auto_serialize = true # optional — see Auto Serialization below
97
89
 
98
90
  # optional: security schemes
99
91
  config.security do
@@ -109,24 +101,25 @@ end
109
101
 
110
102
  OpenapiBlocks provides two base classes with distinct responsibilities:
111
103
 
112
- - `OpenapiBlocks::Resource` — defines the model, fields, associations, and serialization logic.
113
- - `OpenapiBlocks::Controller` — defines the API operations, parameters, and responses for documentation.
104
+ - `OpenapiBlocks::Serializer` — defines the model, fields, associations, and serialization logic. Lives in `app/serializers/`.
105
+ - `OpenapiBlocks::Controller` — defines API operations, parameters, and responses for documentation. Lives in `app/openapi/`.
114
106
  - `OpenapiBlocks::Base` — legacy base class that combines both concerns. Still supported.
115
107
 
116
- ### Resource + Controller (recommended)
108
+ ### Recommended: Serializer + Controller
117
109
 
118
110
  ```
119
111
  app/
112
+ serializers/
113
+ user_serializer.rb -> serialization + schema
114
+ post_serializer.rb
120
115
  openapi/
121
- user_resource.rb -> serialization + schema
122
- user_openapi.rb -> API documentation
123
- post_resource.rb
116
+ user_openapi.rb -> API documentation
124
117
  post_openapi.rb
125
118
  ```
126
119
 
127
120
  ```ruby
128
- # app/openapi/user_resource.rb
129
- class UserResource < OpenapiBlocks::Resource
121
+ # app/serializers/user_serializer.rb
122
+ class UserSerializer < OpenapiBlocks::Serializer
130
123
  # model User is inferred automatically from the class name
131
124
 
132
125
  ignore :password_digest, :reset_password_token
@@ -137,7 +130,7 @@ class UserResource < OpenapiBlocks::Resource
137
130
  attribute :access_token, type: :string, read_only: true
138
131
  attribute :nickname, type: :string
139
132
 
140
- # method defined here — called on the resource instance
133
+ # method defined here — called on the serializer instance
141
134
  def full_name
142
135
  "#{object.name} (#{object.email})"
143
136
  end
@@ -149,7 +142,8 @@ end
149
142
  ```ruby
150
143
  # app/openapi/user_openapi.rb
151
144
  class UserOpenapi < OpenapiBlocks::Controller
152
- resource UserResource
145
+ resource UserSerializer
146
+ controller UsersController
153
147
 
154
148
  tags "Users"
155
149
 
@@ -178,15 +172,15 @@ end
178
172
  ```ruby
179
173
  # app/controllers/users_controller.rb
180
174
  def index
181
- render json: UserResource.serialize(User.includes(:posts))
175
+ render json: UserSerializer.serialize(User.includes(:posts))
182
176
  end
183
177
 
184
178
  def show
185
- render json: UserResource.serialize(User.find(params[:id]))
179
+ render json: UserSerializer.serialize(User.find(params[:id]))
186
180
  end
187
181
  ```
188
182
 
189
- ### Base (legacy, single class)
183
+ ### Legacy: Base (single class)
190
184
 
191
185
  ```ruby
192
186
  # app/openapi/user_openapi.rb
@@ -215,35 +209,67 @@ end
215
209
 
216
210
  ---
217
211
 
212
+ ## Auto Serialization
213
+
214
+ When `config.auto_serialize = true`, OpenapiBlocks intercepts every `render json:` call and automatically applies the registered serializer — no explicit serializer call needed in controllers.
215
+
216
+ ```ruby
217
+ # config/initializers/openapi_blocks.rb
218
+ config.auto_serialize = true
219
+ ```
220
+
221
+ ```ruby
222
+ # app/controllers/users_controller.rb
223
+ def index
224
+ render json: User.all # automatically serialized by UserSerializer
225
+ end
226
+
227
+ def show
228
+ render json: @user # automatically serialized by UserSerializer
229
+ end
230
+ ```
231
+
232
+ Serializer registration is automatic by convention (`UserSerializer` -> `User`). For explicit registration:
233
+
234
+ ```ruby
235
+ class AdminUserSerializer < OpenapiBlocks::Serializer
236
+ serializes User # explicitly maps this serializer to the User model
237
+ end
238
+ ```
239
+
240
+ If no serializer is found, OpenapiBlocks falls back to default Rails rendering and logs a warning.
241
+
242
+ ---
243
+
218
244
  ## Serializer
219
245
 
220
246
  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
247
 
222
248
  ### Performance (200 records, arm64, Ruby 4.0)
223
249
 
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 |
250
+ | Method | i/s | μs/i | vs serialize |
251
+ | ---------- | ----- | ---- | ------------ |
252
+ | serialize | 4 239 | 235 | — |
253
+ | to_json | 1 444 | 692 | 2.94× slower |
254
+ | as_json | 1 186 | 843 | 3.58× slower |
255
+ | oj+as_json | 1 126 | 888 | 3.77× slower |
230
256
 
231
257
  Scaling is linear — the 3.6× advantage over `as_json` holds from 10 to 5000 records.
232
258
 
233
259
  ### Virtual attributes and method resolution
234
260
 
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) |
261
+ | Declared with | Method in serializer? | Calls |
262
+ | ---------------------- | --------------------- | --------------------------------------- |
263
+ | `attribute :full_name` | yes | `serializer_instance.full_name` |
264
+ | `attribute :full_name` | no | `object.full_name` (delegated to model) |
265
+ | column in db | — | `object.attribute` (direct) |
240
266
 
241
267
  ### Association serializer resolution
242
268
 
243
269
  For each association, the serializer resolves the serializer class in this order:
244
270
 
245
- 1. `PostResource` — has `serialize`, used directly.
246
- 2. `PostOpenapi` — is a `Controller`, delegates to its `_resource`.
271
+ 1. `PostSerializer` — has `serialize`, used directly.
272
+ 2. `PostOpenapi` — is a `Controller`, delegates to its `resource`.
247
273
  3. Fallback — calls `as_json` on the association value.
248
274
 
249
275
  ---
@@ -264,7 +290,7 @@ end
264
290
  OpenapiBlocks generates:
265
291
 
266
292
  - `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)
293
+ - `UserInput` schema for `POST`, `PUT` and `PATCH` request bodies (without `id`, `created_at`, `updated_at` and `read_only` fields)
268
294
  - `required` fields from `presence: true` validations
269
295
  - `minLength`, `maxLength` from `length` validations
270
296
  - `minimum`, `maximum` from `numericality` validations
@@ -280,8 +306,8 @@ Configure global security schemes in the initializer:
280
306
 
281
307
  ```ruby
282
308
  config.security do
283
- bearer_token format: "JWT" # Authorization: Bearer <token>
284
- api_key name: "X-API-Key", in: :header # X-API-Key: <key>
309
+ bearer_token format: "JWT" # Authorization: Bearer <token>
310
+ api_key name: "X-API-Key", in: :header # X-API-Key: <key>
285
311
  end
286
312
  ```
287
313
 
@@ -289,11 +315,11 @@ Override security per operation:
289
315
 
290
316
  ```ruby
291
317
  operation :index do
292
- security :bearerAuth # only bearer on this operation
318
+ security :bearerAuth # only bearer on this operation
293
319
  end
294
320
 
295
321
  operation :show do
296
- no_security! # public endpoint — no auth required
322
+ no_security! # public endpoint — no auth required
297
323
  end
298
324
  ```
299
325
 
@@ -302,9 +328,9 @@ end
302
328
  ## Associations
303
329
 
304
330
  ```ruby
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)
331
+ association :company # belongs_to — $ref to Company schema
332
+ association :posts, type: :array # has_many — array of $ref to Post schema
333
+ association :posts, type: :array, read_only: true # excluded from UserInput (response only)
308
334
  ```
309
335
 
310
336
  ---
@@ -313,34 +339,34 @@ association :posts, type: :array, read_only: true # excluded from UserInput
313
339
 
314
340
  Virtual attributes are fields that exist in the API response but not in the database.
315
341
 
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 |
342
+ | Option | Description | Appears in User | Appears in UserInput |
343
+ | ------------------ | -------------------------------------- | :-------------: | :------------------: |
344
+ | `read_only: true` | Calculated or system-generated fields | YES | NO |
345
+ | `read_only: false` | Fields the client can send and receive | YES | YES |
320
346
 
321
347
  ```ruby
322
- attribute :full_name, type: :string, read_only: true # response only
323
- attribute :access_token, type: :string, read_only: true # response only
324
- attribute :nickname, type: :string # request and response
348
+ attribute :full_name, type: :string, read_only: true # response only
349
+ attribute :access_token, type: :string, read_only: true # response only
350
+ attribute :nickname, type: :string # request and response
325
351
  ```
326
352
 
327
353
  ---
328
354
 
329
355
  ## Type Mapping
330
356
 
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 |
357
+ | ActiveRecord type | OpenAPI type |
358
+ | ----------------- | ---------------------- |
359
+ | `integer` | `integer` / `int32` |
360
+ | `bigint` | `integer` / `int64` |
361
+ | `float` | `number` / `float` |
362
+ | `decimal` | `number` / `double` |
363
+ | `string` | `string` |
364
+ | `text` | `string` |
365
+ | `boolean` | `boolean` |
366
+ | `date` | `string` / `date` |
367
+ | `datetime` | `string` / `date-time` |
368
+ | `uuid` | `string` / `uuid` |
369
+ | `json` / `jsonb` | `object` |
344
370
 
345
371
  ---
346
372
 
@@ -349,6 +375,7 @@ attribute :nickname, type: :string # request and response
349
375
  OpenapiBlocks watches for changes in:
350
376
 
351
377
  ```
378
+ app/serializers/**/*.rb
352
379
  app/openapi/**/*.rb
353
380
  app/models/**/*.rb
354
381
  config/routes.rb
@@ -368,4 +395,4 @@ The spec is automatically regenerated on the next request to `/docs/openapi.json
368
395
 
369
396
  ## License
370
397
 
371
- MIT (LICENSE.txt)
398
+ [MIT](LICENSE.txt)