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.
data/README.pt-BR.md CHANGED
@@ -1,38 +1,16 @@
1
1
  # OpenapiBlocks
2
2
 
3
- OpenapiBlocks é uma gem Rails que gera automaticamente documentação OpenAPI 3.0/3.1 a partir dos seus modelos ActiveRecord, validações do ActiveModel e rotas do Rails, inspirada em [ActiveModel::Serializer](https://github.com/rails-api/active_model_serializers).
3
+ OpenapiBlocks é uma gem Rails que gera automaticamente documentação OpenAPI 3.0/3.1 a partir dos seus models ActiveRecord, validações ActiveModel e rotas do Rails inspirada no [ActiveModel::Serializer](https://github.com/rails-api/active_model_serializers).
4
4
 
5
- Sem anotações manuais. Sem ruído de DSL nos controllers. Basta declarar o que deve ser exposto e o spec é gerado automaticamente.
5
+ English version: [README.md](README.md)
6
6
 
7
- ## Principais mudanças (recentes)
8
- - Versão padrão do OpenAPI: `3.1.0` (suportado: `3.1.0`, `3.0.3`).
9
- - A Swagger UI é servida no caminho onde a engine foi montada e usa endpoints do mesmo origin (same-origin) para evitar CORS — a UI mostra uma lista de servidores, mas buscará o spec a partir da URL montada.
10
- - A saída YAML é normalizada para chaves em string (`deep_stringify_keys`) para que o campo `openapi` seja reconhecido pelo Swagger UI.
11
- - O DSL `association` usa `read_only: true` para marcar associações como somente resposta e excluí-las dos schemas `*Input`; associações/atributos `read_only` continuam presentes nas respostas.
12
- - O `tags` é gerado no nível do documento a partir dos paths e pode ser customizado via `tags` nas classes e operações.
13
- - Referências de schema aceitam `Symbol` (ex.: `schema: :user`) e arrays com items como símbolos (ex.: `items: :user`).
14
- # OpenapiBlocks
15
-
16
- OpenapiBlocks é uma gem Rails que gera automaticamente documentação OpenAPI 3.0/3.1 a partir dos seus modelos ActiveRecord, validações do ActiveModel e rotas do Rails — inspirada em ActiveModel::Serializer.
17
-
18
- Sem anotações manuais. Sem ruído de DSL nos controllers. Basta declarar o que deve ser exposto e o spec é gerado automaticamente. Inclui um serializer interno de alto desempenho — aproximadamente 3.6× mais rápido que `as_json` com escalabilidade linear consistente.
19
-
20
- ## Principais mudanças (recentes)
21
- - `OpenapiBlocks::Resource` e `OpenapiBlocks::Controller` foram introduzidos para separar responsabilidades de serialização e documentação.
22
- - Versão padrão do OpenAPI: `3.1.0` (suportado: `3.1.0`, `3.0.3`).
23
- - Scalar UI agora é servido em `/docs/scalar` ao lado da Swagger UI em `/docs`.
24
- - A Swagger UI usa endpoints same-origin para evitar problemas de CORS ao usar "Try it out"; a UI mostra servidores configurados, mas busca o spec a partir da URL montada da engine.
25
- - A saída YAML é normalizada para chaves em string (`deep_stringify_keys`) para que o campo `openapi` seja reconhecido pelo Swagger UI.
26
- - O DSL `association` utiliza `read_only: true` para marcar associações como somente-resposta e excluí-las dos schemas `*Input`; atributos/associações `read_only` continuam presentes em respostas.
27
- - `tags` são gerados no nível do documento a partir dos paths e podem ser customizados via `tags` nas classes e operações.
28
- - Referências de schema aceitam `Symbol` (ex.: `schema: :user`) e arrays com `items` como símbolos (ex.: `items: :user`).
29
- - O serializer compila um método extrator monolítico por classe em tempo de boot usando `class_eval`, eliminando ramificações por objeto e chamadas lambda em tempo de execução.
7
+ Sem anotações manuais. Sem DSL nos controllers. Basta declarar o que expor e a spec é gerada automaticamente. Inclui um serializer de alta performance — ~3.6× mais rápido que `as_json` com escalabilidade linear de 10 a 5000 registros.
30
8
 
31
9
  ---
32
10
 
33
11
  ## Instalação
34
12
 
35
- Adicione ao seu Gemfile:
13
+ Adicione ao seu `Gemfile`:
36
14
 
37
15
  ```ruby
38
16
  gem "openapi_blocks"
@@ -48,7 +26,7 @@ bundle install
48
26
 
49
27
  ## Configuração
50
28
 
51
- ### 1. Monte a Engine
29
+ ### 1. Monte o Engine
52
30
 
53
31
  ```ruby
54
32
  # config/routes.rb
@@ -62,28 +40,30 @@ end
62
40
  Isso expõe:
63
41
 
64
42
  ```
65
- GET /docs -> Scalar UI
43
+ GET /docs -> Scalar UI (padrão)
66
44
  GET /docs/swagger -> Swagger UI
67
- GET /docs/openapi.json -> OpenAPI spec in JSON
68
- GET /docs/openapi.yaml -> OpenAPI spec in YAML
45
+ GET /docs/openapi.json -> Spec OpenAPI em JSON
46
+ GET /docs/openapi.yaml -> Spec OpenAPI em YAML
69
47
  ```
70
48
 
71
49
  ### 2. Configure o initializer
72
50
 
51
+ `OpenapiBlocks.configure` é obrigatório. A gem lança `OpenapiBlocks::Error` na primeira requisição se nunca foi chamado ou se `info.title` / `info.version` estiverem em branco.
52
+
73
53
  ```ruby
74
54
  # config/initializers/openapi_blocks.rb
75
55
  OpenapiBlocks.configure do |config|
76
- config.openapi_version = "3.1.0" # "3.0.3" ou "3.1.0"
56
+ config.openapi_version = "3.1.0" # obrigatório — "3.0.3" ou "3.1.0"
77
57
 
78
58
  config.info do
79
- title "Minha API"
80
- version "1.0.0"
81
- description "Documentação da API gerada automaticamente"
59
+ title "Minha API" # obrigatório
60
+ version "1.0.0" # obrigatório
61
+ description "Documentação gerada automaticamente"
82
62
 
83
63
  contact do
84
- name "Minha equipe"
85
- email "api@mycompany.com"
86
- url "https://mycompany.com"
64
+ name "Meu Time"
65
+ email "api@minhaempresa.com.br"
66
+ url "https://minhaempresa.com.br"
87
67
  end
88
68
 
89
69
  license do
@@ -94,7 +74,7 @@ OpenapiBlocks.configure do |config|
94
74
 
95
75
  config.servers do
96
76
  server do
97
- url "https://api.mycompany.com"
77
+ url "https://api.minhaempresa.com.br"
98
78
  description "Produção"
99
79
  end
100
80
 
@@ -104,7 +84,8 @@ OpenapiBlocks.configure do |config|
104
84
  end
105
85
  end
106
86
 
107
- config.watch = :development # auto-reload em mudanças de arquivo
87
+ config.watch = :development # recarrega automaticamente em desenvolvimento
88
+ config.auto_serialize = true # opcional — veja Serialização Automática abaixo
108
89
 
109
90
  # opcional: esquemas de segurança
110
91
  config.security do
@@ -118,27 +99,28 @@ end
118
99
 
119
100
  ## Uso
120
101
 
121
- OpenapiBlocks fornece duas classes base com responsabilidades distintas:
102
+ OpenapiBlocks oferece duas classes base com responsabilidades distintas:
122
103
 
123
- - `OpenapiBlocks::Resource` — define o model, campos, associações e lógica de serialização.
124
- - `OpenapiBlocks::Controller` — define operações da API, parâmetros e respostas para documentação.
125
- - `OpenapiBlocks::Base` — classe legada que combina ambas as responsabilidades. Ainda suportada.
104
+ - `OpenapiBlocks::Serializer` — define o model, campos, associações e lógica de serialização. Fica em `app/serializers/`.
105
+ - `OpenapiBlocks::Controller` — define operações, parâmetros e respostas para documentação. Fica em `app/openapi/`.
106
+ - `OpenapiBlocks::Base` — classe base legada que combina ambas as responsabilidades. Ainda suportada.
126
107
 
127
- ### Resource + Controller (recomendado)
108
+ ### Recomendado: Serializer + Controller
128
109
 
129
110
  ```
130
111
  app/
112
+ serializers/
113
+ user_serializer.rb -> serialização + schema
114
+ post_serializer.rb
131
115
  openapi/
132
- user_resource.rb -> serialização + schema
133
- user_openapi.rb -> documentação da API
134
- post_resource.rb
116
+ user_openapi.rb -> documentação da API
135
117
  post_openapi.rb
136
118
  ```
137
119
 
138
120
  ```ruby
139
- # app/openapi/user_resource.rb
140
- class UserResource < OpenapiBlocks::Resource
141
- # o model User é inferido automaticamente pelo nome da classe
121
+ # app/serializers/user_serializer.rb
122
+ class UserSerializer < OpenapiBlocks::Serializer
123
+ # model User inferido automaticamente pelo nome da classe
142
124
 
143
125
  ignore :password_digest, :reset_password_token
144
126
 
@@ -148,38 +130,39 @@ class UserResource < OpenapiBlocks::Resource
148
130
  attribute :access_token, type: :string, read_only: true
149
131
  attribute :nickname, type: :string
150
132
 
151
- # método definido aqui — chamado na instância do recurso
133
+ # método definido aqui — chamado na instância do serializer
152
134
  def full_name
153
135
  "#{object.name} (#{object.email})"
154
136
  end
155
137
 
156
- # ou omita o método e ele será delegado ao model automaticamente
138
+ # ou omita o método e ele delega para o model automaticamente
157
139
  end
158
140
  ```
159
141
 
160
142
  ```ruby
161
143
  # app/openapi/user_openapi.rb
162
144
  class UserOpenapi < OpenapiBlocks::Controller
163
- resource UserResource
145
+ resource UserSerializer
146
+ controller UsersController
164
147
 
165
- tags "Users"
148
+ tags "Usuários"
166
149
 
167
150
  operation :index do
168
- summary "List all users"
169
- description "Returns a paginated list of active users"
151
+ summary "Lista todos os usuários"
152
+ description "Retorna uma lista paginada de usuários ativos"
170
153
 
171
- parameter :page, in: :query, type: :integer, description: "Page number"
172
- parameter :per_page, in: :query, type: :integer, description: "Items per page"
154
+ parameter :page, in: :query, type: :integer, description: "Número da página"
155
+ parameter :per_page, in: :query, type: :integer, description: "Itens por página"
173
156
 
174
- response 200, description: "List of users", schema: { type: :array, items: :User }
175
- response 401, description: "Unauthorized"
157
+ response 200, description: "Lista de usuários", schema: { type: :array, items: :User }
158
+ response 401, description: "Não autorizado"
176
159
  end
177
160
 
178
161
  operation :show do
179
- summary "Get a user"
162
+ summary "Busca um usuário"
180
163
 
181
- response 200, description: "User found", schema: :User
182
- response 404, description: "User not found"
164
+ response 200, description: "Usuário encontrado", schema: :User
165
+ response 404, description: "Não encontrado"
183
166
 
184
167
  no_security!
185
168
  end
@@ -189,20 +172,20 @@ end
189
172
  ```ruby
190
173
  # app/controllers/users_controller.rb
191
174
  def index
192
- render json: UserResource.serialize(User.includes(:posts))
175
+ render json: UserSerializer.serialize(User.includes(:posts))
193
176
  end
194
177
 
195
178
  def show
196
- render json: UserResource.serialize(User.find(params[:id]))
179
+ render json: UserSerializer.serialize(User.find(params[:id]))
197
180
  end
198
181
  ```
199
182
 
200
- ### Base (legado, classe única)
183
+ ### Legado: Base (classe única)
201
184
 
202
185
  ```ruby
203
186
  # app/openapi/user_openapi.rb
204
187
  class UserOpenapi < OpenapiBlocks::Base
205
- tags "Users"
188
+ tags "Usuários"
206
189
 
207
190
  ignore :password_digest
208
191
 
@@ -211,8 +194,8 @@ class UserOpenapi < OpenapiBlocks::Base
211
194
  attribute :full_name, type: :string, read_only: true
212
195
 
213
196
  operation :index do
214
- summary "List all users"
215
- response 200, description: "List of users", schema: { type: :array, items: :User }
197
+ summary "Lista todos os usuários"
198
+ response 200, description: "Lista de usuários", schema: { type: :array, items: :User }
216
199
  end
217
200
  end
218
201
  ```
@@ -226,35 +209,67 @@ end
226
209
 
227
210
  ---
228
211
 
212
+ ## Serialização Automática
213
+
214
+ Quando `config.auto_serialize = true`, o OpenapiBlocks intercepta todas as chamadas `render json:` e aplica automaticamente o serializer registrado — sem precisar chamar o serializer explicitamente nos 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 # serializado automaticamente pelo UserSerializer
225
+ end
226
+
227
+ def show
228
+ render json: @user # serializado automaticamente pelo UserSerializer
229
+ end
230
+ ```
231
+
232
+ O registro do serializer é automático por convenção (`UserSerializer` -> `User`). Para registro explícito:
233
+
234
+ ```ruby
235
+ class AdminUserSerializer < OpenapiBlocks::Serializer
236
+ serializes User # mapeia explicitamente este serializer para o model User
237
+ end
238
+ ```
239
+
240
+ Se nenhum serializer for encontrado, o OpenapiBlocks usa o comportamento padrão do Rails e registra um aviso no log.
241
+
242
+ ---
243
+
229
244
  ## Serializer
230
245
 
231
- O serializer interno compila um método extrator monolítico por classe em tempo de boot usando `class_eval`. Não loops, nem indirection por lambda e nem ramificações em tempo de execução por objeto.
246
+ O serializer compila um método extrator monolítico por classe no boot usando `class_eval`. Sem loops, sem indireção via lambda e sem branching por objeto em tempo de execução.
232
247
 
233
248
  ### Performance (200 registros, arm64, Ruby 4.0)
234
249
 
235
- | Método | i/s | μs/i | vs serialize |
236
- |---|---:|---:|---:|
237
- | serialize | 4 239 | 235 | — |
238
- | to_json | 1 444 | 692 | 2.94× mais lento |
239
- | as_json | 1 186 | 843 | 3.58× mais lento |
240
- | oj+as_json | 1 126 | 888 | 3.77× mais lento |
250
+ | Método | i/s | μs/i | vs serialize |
251
+ |------------|-------|------|--------------|
252
+ | serialize | 4 239 | 235 | — |
253
+ | to_json | 1 444 | 692 | 2.94× mais lento |
254
+ | as_json | 1 186 | 843 | 3.58× mais lento |
255
+ | oj+as_json | 1 126 | 888 | 3.77× mais lento |
241
256
 
242
- Escalamento é linear — a vantagem ~3.6× em relação a `as_json` se mantém de 10 a 5000 registros.
257
+ A escalabilidade é linear — a vantagem de 3.6× sobre o `as_json` se mantém de 10 a 5000 registros.
243
258
 
244
- ### Atributos virtuais e resolução de método
259
+ ### Atributos virtuais e resolução de métodos
245
260
 
246
- | Declarado com | Método no resource? | Chamada |
247
- |---|---:|---|
248
- | `attribute :full_name` | sim | `resource_instance.full_name` |
249
- | `attribute :full_name` | não | `object.full_name` (delegado ao model) |
250
- | coluna no db | — | `object.full_name` (direto) |
261
+ | Declarado com | Método no serializer? | Chama |
262
+ |------------------------|-----------------------|-----------------------------------------|
263
+ | `attribute :full_name` | sim | `serializer_instance.full_name` |
264
+ | `attribute :full_name` | não | `object.full_name` (delegado ao model) |
265
+ | coluna no banco | — | `object.attribute` (direto) |
251
266
 
252
- ### Resolução de serializer de associação
267
+ ### Resolução do serializer de associações
253
268
 
254
- Para cada associação, a resolução procura na ordem:
269
+ Para cada associação, o serializer resolve a classe na seguinte ordem:
255
270
 
256
- 1. `PostResource` — se existir `serialize`, é usado diretamente.
257
- 2. `PostOpenapi` — se for um `Controller`, delega ao seu `_resource`.
271
+ 1. `PostSerializer` — tem `serialize`, usado diretamente.
272
+ 2. `PostOpenapi` — é um `Controller`, delega para o `_resource`.
258
273
  3. Fallback — chama `as_json` no valor da associação.
259
274
 
260
275
  ---
@@ -272,16 +287,16 @@ class User < ApplicationRecord
272
287
  end
273
288
  ```
274
289
 
275
- OpenapiBlocks gera:
290
+ O OpenapiBlocks gera:
276
291
 
277
- - `User` schema a partir de `db/schema.rb` (colunas e tipos)
278
- - `UserInput` schema para bodies de `POST`, `PUT` e `PATCH` (sem `id`, `created_at`, `updated_at` e campos `read_only`)
279
- - `required` a partir de validações `presence: true`
280
- - `minLength` e `maxLength` a partir de validações `length`
281
- - `minimum` e `maximum` a partir de validações `numericality`
282
- - `enum` a partir de validações `inclusion`
283
- - `format: "email"` a partir de validações de formato
284
- - Todos os paths a partir de `config/routes.rb`
292
+ - Schema `User` a partir das colunas e tipos do `db/schema.rb`
293
+ - Schema `UserInput` para os request bodies de `POST`, `PUT` e `PATCH` (sem `id`, `created_at`, `updated_at` e campos `read_only`)
294
+ - Campos `required` a partir das validações `presence: true`
295
+ - `minLength`, `maxLength` a partir das validações `length`
296
+ - `minimum`, `maximum` a partir das validações `numericality`
297
+ - `enum` a partir das validações `inclusion`
298
+ - `format: "email"` a partir das validações de formato
299
+ - Todos os paths a partir do `config/routes.rb`
285
300
 
286
301
  ---
287
302
 
@@ -291,20 +306,20 @@ Configure esquemas de segurança globais no initializer:
291
306
 
292
307
  ```ruby
293
308
  config.security do
294
- bearer_token format: "JWT" # Authorization: Bearer <token>
295
- 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>
296
311
  end
297
312
  ```
298
313
 
299
- Substitua a segurança por operação:
314
+ Sobrescreva a segurança por operação:
300
315
 
301
316
  ```ruby
302
317
  operation :index do
303
- security :bearerAuth # apenas bearer nesta operação
318
+ security :bearerAuth # bearer nesta operação
304
319
  end
305
320
 
306
321
  operation :show do
307
- no_security! # endpoint público — sem autenticação
322
+ no_security! # endpoint público — sem autenticação
308
323
  end
309
324
  ```
310
325
 
@@ -313,60 +328,61 @@ end
313
328
  ## Associações
314
329
 
315
330
  ```ruby
316
- association :company # belongs_to — $ref para Company schema
317
- association :posts, type: :array # has_many — array de $ref para Post schema
318
- association :posts, type: :array, read_only: true # excluído do UserInput (response only)
331
+ association :company # belongs_to — $ref para schema Company
332
+ association :posts, type: :array # has_many — array de $ref para schema Post
333
+ association :posts, type: :array, read_only: true # excluído do UserInput (somente resposta)
319
334
  ```
320
335
 
321
336
  ---
322
337
 
323
338
  ## Atributos Virtuais
324
339
 
325
- Atributos virtuais são campos que existem apenas na resposta da API e não no banco de dados.
340
+ Atributos virtuais são campos que existem na resposta da API mas não no banco de dados.
326
341
 
327
- | Opção | Descrição | Aparece em User | Aparece em UserInput |
328
- |---|---|:---:|:---:|
329
- | `read_only: true` | Campos calculados ou gerados pelo sistema | SIM | NÃO |
330
- | `read_only: false` | Campos que o cliente pode enviar e receber | SIM | SIM |
342
+ | Opção | Descrição | Aparece em User | Aparece em UserInput |
343
+ |--------------------|----------------------------------------|:---------------:|:--------------------:|
344
+ | `read_only: true` | Campos calculados ou gerados pelo sistema | SIM | NÃO |
345
+ | `read_only: false` | Campos que o cliente pode enviar e receber | SIM | SIM |
331
346
 
332
347
  ```ruby
333
- attribute :full_name, type: :string, read_only: true # response only
334
- attribute :access_token, type: :string, read_only: true # response only
335
- attribute :nickname, type: :string # request and response
348
+ attribute :full_name, type: :string, read_only: true # somente resposta
349
+ attribute :access_token, type: :string, read_only: true # somente resposta
350
+ attribute :nickname, type: :string # requisição e resposta
336
351
  ```
337
352
 
338
353
  ---
339
354
 
340
- ## Mapeamento de tipos
341
-
342
- | Tipo do ActiveRecord | Tipo OpenAPI |
343
- |---|---|
344
- | integer | integer / int32 |
345
- | bigint | integer / int64 |
346
- | float | number / float |
347
- | decimal | number / double |
348
- | string | string |
349
- | text | string |
350
- | boolean | boolean |
351
- | date | string / date |
352
- | datetime | string / date-time |
353
- | uuid | string / uuid |
354
- | json / jsonb | object |
355
+ ## Mapeamento de Tipos
356
+
357
+ | Tipo ActiveRecord | Tipo OpenAPI |
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` |
355
370
 
356
371
  ---
357
372
 
358
- ## Auto-reload em desenvolvimento
373
+ ## Recarregamento Automático em Desenvolvimento
359
374
 
360
- OpenapiBlocks observa mudanças em:
375
+ O OpenapiBlocks monitora mudanças em:
361
376
 
362
377
  ```
378
+ app/serializers/**/*.rb
363
379
  app/openapi/**/*.rb
364
380
  app/models/**/*.rb
365
381
  config/routes.rb
366
382
  db/schema.rb
367
383
  ```
368
384
 
369
- O spec é regenerado automaticamente na próxima requisição para `/docs/openapi.json` sempre que qualquer um desses arquivos muda. Não é necessário reiniciar o servidor.
385
+ A spec é regenerada automaticamente na próxima requisição a `/docs/openapi.json` sempre que algum desses arquivos for alterado. Sem precisar reiniciar o servidor.
370
386
 
371
387
  ---
372
388
 
@@ -379,4 +395,4 @@ O spec é regenerado automaticamente na próxima requisição para `/docs/openap
379
395
 
380
396
  ## Licença
381
397
 
382
- MIT (LICENSE.txt)
398
+ [MIT](LICENSE.txt)
@@ -3,7 +3,7 @@
3
3
  require "action_controller/api"
4
4
 
5
5
  module OpenapiBlocks
6
- class SpecController < ActionController::API # rubocop:disable Style/Documentation
6
+ class SpecController < ActionController::API # rubocop:disable Style/Documentation,Metrics/ClassLength
7
7
  SWAGGER_UI_CSS = "https://unpkg.com/swagger-ui-dist@5.11.0/swagger-ui.css"
8
8
  SWAGGER_UI_STANDALONE_JS = "https://unpkg.com/swagger-ui-dist@5.11.0/swagger-ui-standalone-preset.js"
9
9
  SWAGGER_UI_JS = "https://unpkg.com/swagger-ui-dist@5.11.0/swagger-ui-bundle.js"
@@ -30,7 +30,7 @@ module OpenapiBlocks
30
30
 
31
31
  private
32
32
 
33
- def scalar_html
33
+ def scalar_html # rubocop:disable Metrics/MethodLength
34
34
  spec_url = "#{swagger_spec_base_url}.json"
35
35
  title = "#{OpenapiBlocks.configuration.info.title} - Scalar"
36
36
 
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OpenapiBlocks
4
+ module AutoSerialize # rubocop:disable Style/Documentation
5
+ def render(options = nil, extra = nil, &) # rubocop:disable Metrics/MethodLength
6
+ if auto_serialize_candidate?(options)
7
+ object = options[:json]
8
+ serializer = Registry.resolve(object)
9
+
10
+ if serializer
11
+ log_serializer(object, serializer)
12
+ options = options.merge(json: serializer.serialize(object))
13
+ else
14
+ warn_no_serializer(object)
15
+ end
16
+ end
17
+
18
+ super
19
+ end
20
+
21
+ private
22
+
23
+ def auto_serialize_candidate?(options)
24
+ OpenapiBlocks.configuration.auto_serialize &&
25
+ options.is_a?(Hash) &&
26
+ options.key?(:json)
27
+ end
28
+
29
+ def log_serializer(object, serializer)
30
+ model = extract_model(object)
31
+ Rails.logger.debug(
32
+ "[OpenapiBlocks] #{model.name} serialized by #{serializer.name}"
33
+ )
34
+ end
35
+
36
+ def warn_no_serializer(object)
37
+ model = extract_model(object)
38
+ return unless model
39
+
40
+ Rails.logger.warn(
41
+ "[OpenapiBlocks] No serializer found for #{model.name}. " \
42
+ "Falling back to default Rails rendering. " \
43
+ "Create #{model.name}Serializer or use `serializes #{model.name}` explicitly."
44
+ )
45
+ end
46
+
47
+ def extract_model(object)
48
+ case object
49
+ when Array then object.first&.class
50
+ else object.respond_to?(:klass) ? object.klass : object.class
51
+ end
52
+ end
53
+ end
54
+ end
@@ -1,48 +1,19 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module OpenapiBlocks
4
- class Base # rubocop:disable Style/Documentation
5
- include Serializer
4
+ # <b>DEPRECATED:</b> please use <tt>OpenapiBlocks::Controllers</tt> and <tt>OpenapiBlocks::Resources</tt> instead.
5
+ class Base
6
+ include Concerns::Schemable
7
+ include Concerns::Documentable
8
+ include Serialization
6
9
 
7
10
  class << self
8
- attr_reader :_model, :_ignored, :_associations, :_virtual_attributes, :_operations, :_tags
9
-
10
- def model(klass = nil)
11
- klass ? @_model = klass : @_model ||= infer_model # rubocop:disable Naming/MemoizedInstanceVariableName
12
- end
13
-
14
- def ignore(*attributes)
15
- @_ignored ||= []
16
- @_ignored.concat(attributes.map(&:to_s))
17
- end
18
-
19
- def association(name, type: nil, read_only: false)
20
- @_associations ||= []
21
- @_associations << { name: name, type: type, read_only: read_only }
22
- end
23
-
24
- def attribute(name, **)
25
- @_virtual_attributes ||= []
26
- @_virtual_attributes << ({ name: name, ** })
27
- end
28
-
29
- def operation(action, &block)
30
- @_operations ||= {}
31
- builder = OperationBuilder.new
32
- builder.instance_eval(&block) if block
33
- @_operations[action] = builder
34
- end
35
-
36
- def tags(*values)
37
- values.any? ? @_tags = values : @_tags
38
- end
39
-
40
11
  private
41
12
 
42
13
  def infer_model
43
14
  model_name = name
44
15
  .gsub(/Openapi$/, "")
45
- .gsub(/Resource$/, "")
16
+ .gsub(/Serializer$/, "")
46
17
  .split("::")
47
18
  .last
48
19
 
@@ -4,21 +4,53 @@ require_relative "spec/document"
4
4
 
5
5
  module OpenapiBlocks
6
6
  class Builder # rubocop:disable Style/Documentation
7
+ REQUIRED_CONFIG_ERROR = <<~MSG
8
+ OpenapiBlocks is not configured. Add an initializer:
9
+
10
+ # config/initializers/openapi_blocks.rb
11
+ OpenapiBlocks.configure do |config|
12
+ config.openapi_version = "3.1.0" # required: "3.0.3" or "3.1.0"
13
+
14
+ config.info do
15
+ title "My API" # required
16
+ version "1.0.0" # required
17
+ end
18
+ end
19
+ MSG
20
+
7
21
  def self.build
8
22
  new.build
9
23
  end
10
24
 
11
25
  def build
26
+ validate_configuration!
12
27
  Spec::Document.new(openapi_classes).build
13
28
  end
14
29
 
15
30
  private
16
31
 
32
+ def validate_configuration! # rubocop:disable Metrics/CyclomaticComplexity
33
+ config = OpenapiBlocks.configuration
34
+ errors = []
35
+
36
+ unless config.configured?
37
+ errors << "config.openapi_version or config.info must be defined — call OpenapiBlocks.configure"
38
+ end
39
+ errors << "config.info.title is required" if config.info&.title.blank?
40
+ errors << "config.info.version is required" if config.info&.version.blank?
41
+
42
+ return if errors.empty?
43
+
44
+ raise Error, "#{REQUIRED_CONFIG_ERROR}\nMissing:\n#{errors.map { |e| " - #{e}" }.join("\n")}"
45
+ end
46
+
17
47
  def openapi_classes
18
48
  ObjectSpace.each_object(Class).select do |klass|
19
49
  name = Module.instance_method(:name).bind_call(klass)
20
- name&.end_with?("Openapi") &&
21
- (klass < OpenapiBlocks::Base || klass < OpenapiBlocks::Controller)
50
+ next unless name&.end_with?("Openapi")
51
+
52
+ klass < OpenapiBlocks::Base ||
53
+ klass < OpenapiBlocks::Controller
22
54
  end
23
55
  end
24
56
  end