openapi_blocks 0.3.1 → 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,39 +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
164
146
  controller UsersController
165
147
 
166
- tags "Users"
148
+ tags "Usuários"
167
149
 
168
150
  operation :index do
169
- summary "List all users"
170
- 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"
171
153
 
172
- parameter :page, in: :query, type: :integer, description: "Page number"
173
- 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"
174
156
 
175
- response 200, description: "List of users", schema: { type: :array, items: :User }
176
- response 401, description: "Unauthorized"
157
+ response 200, description: "Lista de usuários", schema: { type: :array, items: :User }
158
+ response 401, description: "Não autorizado"
177
159
  end
178
160
 
179
161
  operation :show do
180
- summary "Get a user"
162
+ summary "Busca um usuário"
181
163
 
182
- response 200, description: "User found", schema: :User
183
- response 404, description: "User not found"
164
+ response 200, description: "Usuário encontrado", schema: :User
165
+ response 404, description: "Não encontrado"
184
166
 
185
167
  no_security!
186
168
  end
@@ -190,20 +172,20 @@ end
190
172
  ```ruby
191
173
  # app/controllers/users_controller.rb
192
174
  def index
193
- render json: UserResource.serialize(User.includes(:posts))
175
+ render json: UserSerializer.serialize(User.includes(:posts))
194
176
  end
195
177
 
196
178
  def show
197
- render json: UserResource.serialize(User.find(params[:id]))
179
+ render json: UserSerializer.serialize(User.find(params[:id]))
198
180
  end
199
181
  ```
200
182
 
201
- ### Base (legado, classe única)
183
+ ### Legado: Base (classe única)
202
184
 
203
185
  ```ruby
204
186
  # app/openapi/user_openapi.rb
205
187
  class UserOpenapi < OpenapiBlocks::Base
206
- tags "Users"
188
+ tags "Usuários"
207
189
 
208
190
  ignore :password_digest
209
191
 
@@ -212,8 +194,8 @@ class UserOpenapi < OpenapiBlocks::Base
212
194
  attribute :full_name, type: :string, read_only: true
213
195
 
214
196
  operation :index do
215
- summary "List all users"
216
- 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 }
217
199
  end
218
200
  end
219
201
  ```
@@ -227,35 +209,67 @@ end
227
209
 
228
210
  ---
229
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
+
230
244
  ## Serializer
231
245
 
232
- 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.
233
247
 
234
248
  ### Performance (200 registros, arm64, Ruby 4.0)
235
249
 
236
- | Método | i/s | μs/i | vs serialize |
237
- |---|---:|---:|---:|
238
- | serialize | 4 239 | 235 | — |
239
- | to_json | 1 444 | 692 | 2.94× mais lento |
240
- | as_json | 1 186 | 843 | 3.58× mais lento |
241
- | 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 |
242
256
 
243
- 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.
244
258
 
245
- ### Atributos virtuais e resolução de método
259
+ ### Atributos virtuais e resolução de métodos
246
260
 
247
- | Declarado com | Método no resource? | Chamada |
248
- |---|---:|---|
249
- | `attribute :full_name` | sim | `resource_instance.full_name` |
250
- | `attribute :full_name` | não | `object.full_name` (delegado ao model) |
251
- | 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) |
252
266
 
253
- ### Resolução de serializer de associação
267
+ ### Resolução do serializer de associações
254
268
 
255
- Para cada associação, a resolução procura na ordem:
269
+ Para cada associação, o serializer resolve a classe na seguinte ordem:
256
270
 
257
- 1. `PostResource` — se existir `serialize`, é usado diretamente.
258
- 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`.
259
273
  3. Fallback — chama `as_json` no valor da associação.
260
274
 
261
275
  ---
@@ -273,16 +287,16 @@ class User < ApplicationRecord
273
287
  end
274
288
  ```
275
289
 
276
- OpenapiBlocks gera:
290
+ O OpenapiBlocks gera:
277
291
 
278
- - `User` schema a partir de `db/schema.rb` (colunas e tipos)
279
- - `UserInput` schema para bodies de `POST`, `PUT` e `PATCH` (sem `id`, `created_at`, `updated_at` e campos `read_only`)
280
- - `required` a partir de validações `presence: true`
281
- - `minLength` e `maxLength` a partir de validações `length`
282
- - `minimum` e `maximum` a partir de validações `numericality`
283
- - `enum` a partir de validações `inclusion`
284
- - `format: "email"` a partir de validações de formato
285
- - 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`
286
300
 
287
301
  ---
288
302
 
@@ -292,20 +306,20 @@ Configure esquemas de segurança globais no initializer:
292
306
 
293
307
  ```ruby
294
308
  config.security do
295
- bearer_token format: "JWT" # Authorization: Bearer <token>
296
- 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>
297
311
  end
298
312
  ```
299
313
 
300
- Substitua a segurança por operação:
314
+ Sobrescreva a segurança por operação:
301
315
 
302
316
  ```ruby
303
317
  operation :index do
304
- security :bearerAuth # apenas bearer nesta operação
318
+ security :bearerAuth # bearer nesta operação
305
319
  end
306
320
 
307
321
  operation :show do
308
- no_security! # endpoint público — sem autenticação
322
+ no_security! # endpoint público — sem autenticação
309
323
  end
310
324
  ```
311
325
 
@@ -314,60 +328,61 @@ end
314
328
  ## Associações
315
329
 
316
330
  ```ruby
317
- association :company # belongs_to — $ref para Company schema
318
- association :posts, type: :array # has_many — array de $ref para Post schema
319
- 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)
320
334
  ```
321
335
 
322
336
  ---
323
337
 
324
338
  ## Atributos Virtuais
325
339
 
326
- 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.
327
341
 
328
- | Opção | Descrição | Aparece em User | Aparece em UserInput |
329
- |---|---|:---:|:---:|
330
- | `read_only: true` | Campos calculados ou gerados pelo sistema | SIM | NÃO |
331
- | `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 |
332
346
 
333
347
  ```ruby
334
- attribute :full_name, type: :string, read_only: true # response only
335
- attribute :access_token, type: :string, read_only: true # response only
336
- 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
337
351
  ```
338
352
 
339
353
  ---
340
354
 
341
- ## Mapeamento de tipos
342
-
343
- | Tipo do ActiveRecord | Tipo OpenAPI |
344
- |---|---|
345
- | integer | integer / int32 |
346
- | bigint | integer / int64 |
347
- | float | number / float |
348
- | decimal | number / double |
349
- | string | string |
350
- | text | string |
351
- | boolean | boolean |
352
- | date | string / date |
353
- | datetime | string / date-time |
354
- | uuid | string / uuid |
355
- | 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` |
356
370
 
357
371
  ---
358
372
 
359
- ## Auto-reload em desenvolvimento
373
+ ## Recarregamento Automático em Desenvolvimento
360
374
 
361
- OpenapiBlocks observa mudanças em:
375
+ O OpenapiBlocks monitora mudanças em:
362
376
 
363
377
  ```
378
+ app/serializers/**/*.rb
364
379
  app/openapi/**/*.rb
365
380
  app/models/**/*.rb
366
381
  config/routes.rb
367
382
  db/schema.rb
368
383
  ```
369
384
 
370
- 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.
371
386
 
372
387
  ---
373
388
 
@@ -380,4 +395,4 @@ O spec é regenerado automaticamente na próxima requisição para `/docs/openap
380
395
 
381
396
  ## Licença
382
397
 
383
- 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