openapi_blocks 0.2.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 +7 -0
- data/CHANGELOG.md +86 -0
- data/CODE_OF_CONDUCT.md +10 -0
- data/LICENSE.txt +21 -0
- data/README.md +304 -0
- data/README.pt-BR.md +495 -0
- data/Rakefile +12 -0
- data/app/controllers/openapi_blocks/spec_controller.rb +110 -0
- data/config/routes.rb +7 -0
- data/lib/openapi_blocks/base.rb +52 -0
- data/lib/openapi_blocks/builder.rb +23 -0
- data/lib/openapi_blocks/cache.rb +28 -0
- data/lib/openapi_blocks/configuration/contact_builder.rb +23 -0
- data/lib/openapi_blocks/configuration/info_builder.rb +42 -0
- data/lib/openapi_blocks/configuration/license_builder.rb +19 -0
- data/lib/openapi_blocks/configuration/security_builder.rb +33 -0
- data/lib/openapi_blocks/configuration/server_builder.rb +19 -0
- data/lib/openapi_blocks/configuration/servers_builder.rb +21 -0
- data/lib/openapi_blocks/configuration.rb +55 -0
- data/lib/openapi_blocks/engine.rb +13 -0
- data/lib/openapi_blocks/file_watcher.rb +42 -0
- data/lib/openapi_blocks/middleware.rb +54 -0
- data/lib/openapi_blocks/operation_builder.rb +57 -0
- data/lib/openapi_blocks/railtie.rb +19 -0
- data/lib/openapi_blocks/routing/extractor.rb +187 -0
- data/lib/openapi_blocks/routing/operation.rb +45 -0
- data/lib/openapi_blocks/schema/extractor.rb +103 -0
- data/lib/openapi_blocks/schema/types.rb +52 -0
- data/lib/openapi_blocks/schema/validator.rb +86 -0
- data/lib/openapi_blocks/spec/components.rb +67 -0
- data/lib/openapi_blocks/spec/document.rb +47 -0
- data/lib/openapi_blocks/spec/paths.rb +17 -0
- data/lib/openapi_blocks/version.rb +5 -0
- data/lib/openapi_blocks.rb +35 -0
- data/sig/openapi_blocks.rbs +4 -0
- metadata +177 -0
data/README.pt-BR.md
ADDED
|
@@ -0,0 +1,495 @@
|
|
|
1
|
+
# OpenapiBlocks
|
|
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).
|
|
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.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Instalação
|
|
10
|
+
|
|
11
|
+
Adicione ao seu `Gemfile`:
|
|
12
|
+
|
|
13
|
+
```ruby
|
|
14
|
+
gem "openapi_blocks"
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Depois execute:
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
bundle install
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
## Configuração
|
|
26
|
+
|
|
27
|
+
### 1. Monte a Engine
|
|
28
|
+
|
|
29
|
+
```ruby
|
|
30
|
+
# config/routes.rb
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
```ruby
|
|
34
|
+
Rails.application.routes.draw do
|
|
35
|
+
mount OpenapiBlocks::Engine => "/docs"
|
|
36
|
+
resources :users
|
|
37
|
+
end
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Isso expõe:
|
|
41
|
+
|
|
42
|
+
<br />
|
|
43
|
+
GET /docs/openapi.json
|
|
44
|
+
|
|
45
|
+
<br />
|
|
46
|
+
GET /docs/openapi.yaml
|
|
47
|
+
|
|
48
|
+
### 2. Configure o initializer
|
|
49
|
+
|
|
50
|
+
```ruby
|
|
51
|
+
# config/initializers/openapi_blocks.rb
|
|
52
|
+
|
|
53
|
+
OpenapiBlocks.configure do |config|
|
|
54
|
+
# Versões suportadas: "3.1.0" e "3.0.3". O padrão desta gem é "3.1.0".
|
|
55
|
+
config.openapi_version = "3.1.0" # "3.0.3" ou "3.1.0"
|
|
56
|
+
config.info do
|
|
57
|
+
title "Minha API"
|
|
58
|
+
version "1.0.0"
|
|
59
|
+
description "Documentação da API gerada automaticamente"
|
|
60
|
+
contact do
|
|
61
|
+
name "Minha equipe"
|
|
62
|
+
email "api@mycompany.com"
|
|
63
|
+
url "https://mycompany.com"
|
|
64
|
+
end
|
|
65
|
+
license do
|
|
66
|
+
name "MIT"
|
|
67
|
+
url "https://opensource.org/licenses/MIT"
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
config.servers do
|
|
71
|
+
server do
|
|
72
|
+
url "https://api.mycompany.com"
|
|
73
|
+
description "Produção"
|
|
74
|
+
end
|
|
75
|
+
server do
|
|
76
|
+
url "http://localhost:3000"
|
|
77
|
+
description "Desenvolvimento"
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
config.watch = :development # recarrega automaticamente em mudanças de arquivo
|
|
81
|
+
end
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
Observações:
|
|
85
|
+
|
|
86
|
+
- A interface Swagger UI fornecida pela engine prioriza a origem atual da
|
|
87
|
+
requisição (same-origin) como servidor primário para evitar problemas de
|
|
88
|
+
CORS ao usar o recurso "Try it out". Você ainda pode listar outros
|
|
89
|
+
servidores em `config.servers` apenas para fins informacionais; a UI
|
|
90
|
+
buscará o documento OpenAPI a partir da URL do spec na mesma origem,
|
|
91
|
+
construída automaticamente com o prefixo usado ao montar a engine.
|
|
92
|
+
|
|
93
|
+
---
|
|
94
|
+
|
|
95
|
+
## Uso
|
|
96
|
+
|
|
97
|
+
### Criando uma classe OpenAPI
|
|
98
|
+
|
|
99
|
+
Crie um arquivo em `app/openapi/` seguindo a mesma convenção de nomes do ActiveModel::Serializer:
|
|
100
|
+
|
|
101
|
+
```text
|
|
102
|
+
app/
|
|
103
|
+
openapi/
|
|
104
|
+
user_openapi.rb → User model
|
|
105
|
+
post_openapi.rb → Post model
|
|
106
|
+
order_openapi.rb → Order model
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
```ruby
|
|
110
|
+
# app/openapi/user_openapi.rb
|
|
111
|
+
|
|
112
|
+
class UserOpenapi < OpenapiBlocks::Base
|
|
113
|
+
# o model User é inferido automaticamente pelo nome da classe
|
|
114
|
+
|
|
115
|
+
# ignora campos sensíveis ou desnecessários
|
|
116
|
+
ignore :password_digest, :reset_password_token
|
|
117
|
+
|
|
118
|
+
# associações opt-in
|
|
119
|
+
association :company
|
|
120
|
+
association :posts, type: :array
|
|
121
|
+
|
|
122
|
+
# atributos virtuais (não existem no banco)
|
|
123
|
+
attribute :full_name, type: :string
|
|
124
|
+
attribute :token, type: :string, read_only: true
|
|
125
|
+
end
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
### O que é gerado automaticamente
|
|
129
|
+
|
|
130
|
+
Dado este model:
|
|
131
|
+
|
|
132
|
+
```ruby
|
|
133
|
+
class User < ApplicationRecord
|
|
134
|
+
validates :name, presence: true, length: { minimum: 2, maximum: 100 }
|
|
135
|
+
validates :email, presence: true, format: { with: URI::MailTo::EMAIL_REGEXP }
|
|
136
|
+
validates :age, numericality: { greater_than: 0 }
|
|
137
|
+
validates :role, inclusion: { in: %w[admin user guest] }
|
|
138
|
+
end
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
OpenapiBlocks gera:
|
|
142
|
+
|
|
143
|
+
- `User` schema a partir das colunas e tipos de `db/schema.rb`
|
|
144
|
+
- `UserInput` schema para bodies de request de `POST`, `PUT` e `PATCH` (sem `id`, `created_at`, `updated_at`)
|
|
145
|
+
- `UserInput` schema para bodies de request de `POST`, `PUT` e `PATCH` (sem `id`, `created_at`, `updated_at` e atributos virtuais marcados como `read_only`)
|
|
146
|
+
- campos `required` a partir de validações `presence: true`
|
|
147
|
+
- `minLength` e `maxLength` a partir de validações `length`
|
|
148
|
+
- `minimum` e `maximum` a partir de validações `numericality`
|
|
149
|
+
- `enum` a partir de validações `inclusion`
|
|
150
|
+
- `format: "email"` a partir de validações de formato
|
|
151
|
+
- todos os paths a partir de `config/routes.rb`
|
|
152
|
+
|
|
153
|
+
### Atributos Virtuais
|
|
154
|
+
|
|
155
|
+
Atributos virtuais são campos que existem apenas na resposta da API e não no banco de dados.
|
|
156
|
+
|
|
157
|
+
| Opção | Descrição | Aparece em User | Aparece em UserInput |
|
|
158
|
+
| ---------------- | ------------------------------------------ | :-------------: | :------------------: |
|
|
159
|
+
| read_only: true | Campos calculados ou gerados pelo sistema | SIM | NÃO |
|
|
160
|
+
| read_only: false | Campos que o cliente pode enviar e receber | SIM | SIM |
|
|
161
|
+
|
|
162
|
+
```ruby
|
|
163
|
+
attribute :full_name, type: :string, read_only: true # apenas resposta
|
|
164
|
+
attribute :access_token, type: :string, read_only: true # apenas resposta
|
|
165
|
+
attribute :nickname, type: :string # request e response
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
### Customizando operações
|
|
169
|
+
|
|
170
|
+
````ruby
|
|
171
|
+
# app/openapi/user_openapi.rb
|
|
172
|
+
class UserOpenapi < OpenapiBlocks::Base
|
|
173
|
+
operation :index do
|
|
174
|
+
summary "Listar todos os usuários"
|
|
175
|
+
description "Retorna uma lista paginada de usuários ativos"
|
|
176
|
+
parameter :page, in: :query, type: :integer, description: "Número da página"
|
|
177
|
+
parameter :per_page, in: :query, type: :integer, description: "Itens por página"
|
|
178
|
+
response 200, description: "Lista de usuários", schema: { type: :array, items: :User }
|
|
179
|
+
response 401, description: "Não autorizado"
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
operation :show do
|
|
183
|
+
summary "Buscar um usuário"
|
|
184
|
+
response 200, description: "Usuário encontrado", schema: :User
|
|
185
|
+
response 404, description: "Usuário não encontrado"
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
operation :create do
|
|
189
|
+
# OpenapiBlocks
|
|
190
|
+
|
|
191
|
+
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.
|
|
192
|
+
|
|
193
|
+
Sem anotações manuais. Sem ruído de DSL nos controllers. Basta declarar o que deve ser exposto e o spec é gerado automaticamente.
|
|
194
|
+
|
|
195
|
+
---
|
|
196
|
+
|
|
197
|
+
## Instalação
|
|
198
|
+
|
|
199
|
+
Adicione ao seu `Gemfile`:
|
|
200
|
+
|
|
201
|
+
```ruby
|
|
202
|
+
gem "openapi_blocks"
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
Depois execute:
|
|
206
|
+
|
|
207
|
+
```bash
|
|
208
|
+
bundle install
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
---
|
|
212
|
+
|
|
213
|
+
## Configuração
|
|
214
|
+
|
|
215
|
+
### 1. Monte a Engine
|
|
216
|
+
|
|
217
|
+
```ruby
|
|
218
|
+
# config/routes.rb
|
|
219
|
+
Rails.application.routes.draw do
|
|
220
|
+
mount OpenapiBlocks::Engine => "/docs"
|
|
221
|
+
|
|
222
|
+
resources :users
|
|
223
|
+
end
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
Isso expõe:
|
|
227
|
+
|
|
228
|
+
```
|
|
229
|
+
GET /docs -> Swagger UI
|
|
230
|
+
GET /docs/openapi.json -> OpenAPI spec em JSON
|
|
231
|
+
GET /docs/openapi.yaml -> OpenAPI spec em YAML
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
### 2. Configure o initializer
|
|
235
|
+
|
|
236
|
+
```ruby
|
|
237
|
+
# config/initializers/openapi_blocks.rb
|
|
238
|
+
OpenapiBlocks.configure do |config|
|
|
239
|
+
# Versões suportadas: "3.1.0" e "3.0.3". O padrão desta gem é "3.1.0".
|
|
240
|
+
config.openapi_version = "3.1.0"
|
|
241
|
+
|
|
242
|
+
config.info do
|
|
243
|
+
title "Minha API"
|
|
244
|
+
version "1.0.0"
|
|
245
|
+
description "Documentação da API gerada automaticamente"
|
|
246
|
+
|
|
247
|
+
contact do
|
|
248
|
+
name "Minha equipe"
|
|
249
|
+
email "api@mycompany.com"
|
|
250
|
+
url "https://mycompany.com"
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
license do
|
|
254
|
+
name "MIT"
|
|
255
|
+
url "https://opensource.org/licenses/MIT"
|
|
256
|
+
end
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
config.servers do
|
|
260
|
+
server do
|
|
261
|
+
url "https://api.mycompany.com"
|
|
262
|
+
description "Produção"
|
|
263
|
+
end
|
|
264
|
+
|
|
265
|
+
server do
|
|
266
|
+
url "http://localhost:3000"
|
|
267
|
+
description "Desenvolvimento"
|
|
268
|
+
end
|
|
269
|
+
end
|
|
270
|
+
|
|
271
|
+
config.watch = :development # recarrega automaticamente em mudanças de arquivo
|
|
272
|
+
|
|
273
|
+
# opcional: esquemas de segurança
|
|
274
|
+
config.security do
|
|
275
|
+
bearer_token format: "JWT"
|
|
276
|
+
api_key name: "X-API-Key", in: :header
|
|
277
|
+
end
|
|
278
|
+
end
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
Observações:
|
|
282
|
+
- A interface Swagger UI fornecida pela engine prioriza a origem atual da requisição (same-origin) como servidor primário para evitar problemas de CORS ao usar o recurso "Try it out". Você ainda pode listar outros servidores em `config.servers` apenas para fins informacionais; a UI buscará o documento OpenAPI a partir da URL do spec na mesma origem, construída automaticamente com o prefixo usado ao montar a engine.
|
|
283
|
+
|
|
284
|
+
---
|
|
285
|
+
|
|
286
|
+
## Uso
|
|
287
|
+
|
|
288
|
+
### Criando uma classe OpenAPI
|
|
289
|
+
|
|
290
|
+
Crie um arquivo em `app/openapi/` seguindo a mesma convenção de nomes do ActiveModel::Serializer:
|
|
291
|
+
|
|
292
|
+
```
|
|
293
|
+
app/
|
|
294
|
+
openapi/
|
|
295
|
+
user_openapi.rb -> User model
|
|
296
|
+
post_openapi.rb -> Post model
|
|
297
|
+
order_openapi.rb -> Order model
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
```ruby
|
|
301
|
+
# app/openapi/user_openapi.rb
|
|
302
|
+
class UserOpenapi < OpenapiBlocks::Base
|
|
303
|
+
# model User é inferido automaticamente a partir do nome da classe
|
|
304
|
+
|
|
305
|
+
# tags customizadas (padrão: inferido a partir do nome do controller/schema)
|
|
306
|
+
tags "Users"
|
|
307
|
+
|
|
308
|
+
# ignora campos sensíveis ou desnecessários
|
|
309
|
+
ignore :password_digest, :reset_password_token
|
|
310
|
+
|
|
311
|
+
# associações opt-in
|
|
312
|
+
association :company
|
|
313
|
+
association :posts, type: :array, input: false # excluído do UserInput
|
|
314
|
+
|
|
315
|
+
# atributos virtuais (não existem no banco de dados)
|
|
316
|
+
# read_only: true -> exposto apenas na resposta (User)
|
|
317
|
+
# read_only: false -> exposto em User e UserInput
|
|
318
|
+
attribute :full_name, type: :string, read_only: true
|
|
319
|
+
attribute :access_token, type: :string, read_only: true
|
|
320
|
+
attribute :nickname, type: :string
|
|
321
|
+
end
|
|
322
|
+
```
|
|
323
|
+
|
|
324
|
+
### O que é gerado automaticamente
|
|
325
|
+
|
|
326
|
+
Dado este model:
|
|
327
|
+
|
|
328
|
+
```ruby
|
|
329
|
+
class User < ApplicationRecord
|
|
330
|
+
validates :name, presence: true, length: { minimum: 2, maximum: 100 }
|
|
331
|
+
validates :email, presence: true, format: { with: URI::MailTo::EMAIL_REGEXP }
|
|
332
|
+
validates :age, numericality: { greater_than: 0 }
|
|
333
|
+
validates :role, inclusion: { in: %w[admin user guest] }
|
|
334
|
+
end
|
|
335
|
+
```
|
|
336
|
+
|
|
337
|
+
OpenapiBlocks gera:
|
|
338
|
+
|
|
339
|
+
- `User` schema a partir das colunas e tipos de `db/schema.rb`
|
|
340
|
+
- `UserInput` schema para bodies de request de `POST`, `PUT` e `PATCH` (sem `id`, `created_at`, `updated_at` e atributos virtuais marcados como `read_only`)
|
|
341
|
+
- campos `required` a partir de validações `presence: true`
|
|
342
|
+
- `minLength` e `maxLength` a partir de validações `length`
|
|
343
|
+
- `minimum` e `maximum` a partir de validações `numericality`
|
|
344
|
+
- `enum` a partir de validações `inclusion`
|
|
345
|
+
- `format: "email"` a partir de validações de formato
|
|
346
|
+
- todos os paths a partir de `config/routes.rb`
|
|
347
|
+
|
|
348
|
+
### Customizando operações
|
|
349
|
+
|
|
350
|
+
```ruby
|
|
351
|
+
# app/openapi/user_openapi.rb
|
|
352
|
+
class UserOpenapi < OpenapiBlocks::Base
|
|
353
|
+
tags "Users"
|
|
354
|
+
|
|
355
|
+
operation :index do
|
|
356
|
+
summary "List all users"
|
|
357
|
+
description "Returns a paginated list of active users"
|
|
358
|
+
tags "Users", "Admin" # sobrescreve tags em nível de operação
|
|
359
|
+
|
|
360
|
+
parameter :page, in: :query, type: :integer, description: "Page number"
|
|
361
|
+
parameter :per_page, in: :query, type: :integer, description: "Items per page"
|
|
362
|
+
|
|
363
|
+
response 200, description: "List of users", schema: { type: :array, items: :User }
|
|
364
|
+
response 401, description: "Unauthorized"
|
|
365
|
+
end
|
|
366
|
+
|
|
367
|
+
operation :show do
|
|
368
|
+
summary "Get a user"
|
|
369
|
+
|
|
370
|
+
response 200, description: "User found", schema: :User
|
|
371
|
+
response 404, description: "User not found"
|
|
372
|
+
end
|
|
373
|
+
|
|
374
|
+
operation :create do
|
|
375
|
+
summary "Create a user"
|
|
376
|
+
|
|
377
|
+
response 201, description: "User created", schema: :User
|
|
378
|
+
response 422, description: "Invalid data"
|
|
379
|
+
end
|
|
380
|
+
|
|
381
|
+
operation :update do
|
|
382
|
+
summary "Update a user"
|
|
383
|
+
|
|
384
|
+
response 200, description: "User updated", schema: :User
|
|
385
|
+
response 404, description: "User not found"
|
|
386
|
+
response 422, description: "Invalid data"
|
|
387
|
+
end
|
|
388
|
+
|
|
389
|
+
operation :destroy do
|
|
390
|
+
summary "Delete a user"
|
|
391
|
+
|
|
392
|
+
response 200, description: "User deleted"
|
|
393
|
+
response 404, description: "User not found"
|
|
394
|
+
end
|
|
395
|
+
end
|
|
396
|
+
```
|
|
397
|
+
|
|
398
|
+
---
|
|
399
|
+
|
|
400
|
+
## Segurança
|
|
401
|
+
|
|
402
|
+
Configure esquemas de segurança globais no initializer:
|
|
403
|
+
|
|
404
|
+
```ruby
|
|
405
|
+
config.security do
|
|
406
|
+
bearer_token format: "JWT" # Authorization: Bearer <token>
|
|
407
|
+
api_key name: "X-API-Key", in: :header # X-API-Key: <key>
|
|
408
|
+
end
|
|
409
|
+
```
|
|
410
|
+
|
|
411
|
+
Substitua a segurança por operação:
|
|
412
|
+
|
|
413
|
+
```ruby
|
|
414
|
+
operation :index do
|
|
415
|
+
security :bearerAuth # apenas bearer nesta operação
|
|
416
|
+
end
|
|
417
|
+
|
|
418
|
+
operation :show do
|
|
419
|
+
no_security! # endpoint público — sem autenticação
|
|
420
|
+
end
|
|
421
|
+
```
|
|
422
|
+
|
|
423
|
+
---
|
|
424
|
+
|
|
425
|
+
## Associações
|
|
426
|
+
|
|
427
|
+
```ruby
|
|
428
|
+
association :company # belongs_to — $ref para Company schema
|
|
429
|
+
association :posts, type: :array # has_many — array de $ref para Post schema
|
|
430
|
+
association :posts, type: :array, input: false # excluído do UserInput (response only)
|
|
431
|
+
```
|
|
432
|
+
|
|
433
|
+
---
|
|
434
|
+
|
|
435
|
+
## Atributos Virtuais
|
|
436
|
+
|
|
437
|
+
Atributos virtuais são campos que existem apenas na resposta da API e não no banco de dados.
|
|
438
|
+
|
|
439
|
+
| Opção | Descrição | Aparece em User | Aparece em UserInput |
|
|
440
|
+
| ---------------- | -------------------------------------- | :-------------: | :------------------: |
|
|
441
|
+
| read_only: true | Campos calculados ou gerados pelo sistema | SIM | NÃO |
|
|
442
|
+
| read_only: false | Campos que o cliente pode enviar e receber | SIM | SIM |
|
|
443
|
+
|
|
444
|
+
```ruby
|
|
445
|
+
attribute :full_name, type: :string, read_only: true # response only
|
|
446
|
+
attribute :access_token, type: :string, read_only: true # response only
|
|
447
|
+
attribute :nickname, type: :string # request and response
|
|
448
|
+
```
|
|
449
|
+
|
|
450
|
+
---
|
|
451
|
+
|
|
452
|
+
## Mapeamento de tipos
|
|
453
|
+
|
|
454
|
+
| Tipo do ActiveRecord | Tipo OpenAPI |
|
|
455
|
+
|----------------------|-------------------------|
|
|
456
|
+
| integer | integer / int32 |
|
|
457
|
+
| bigint | integer / int64 |
|
|
458
|
+
| float | number / float |
|
|
459
|
+
| decimal | number / double |
|
|
460
|
+
| string | string |
|
|
461
|
+
| text | string |
|
|
462
|
+
| boolean | boolean |
|
|
463
|
+
| date | string / date |
|
|
464
|
+
| datetime | string / date-time |
|
|
465
|
+
| uuid | string / uuid |
|
|
466
|
+
| json / jsonb | object |
|
|
467
|
+
|
|
468
|
+
---
|
|
469
|
+
|
|
470
|
+
## Auto-reload em desenvolvimento
|
|
471
|
+
|
|
472
|
+
OpenapiBlocks observa mudanças em:
|
|
473
|
+
|
|
474
|
+
```
|
|
475
|
+
app/openapi/**/*.rb
|
|
476
|
+
app/models/**/*.rb
|
|
477
|
+
config/routes.rb
|
|
478
|
+
db/schema.rb
|
|
479
|
+
```
|
|
480
|
+
|
|
481
|
+
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.
|
|
482
|
+
|
|
483
|
+
---
|
|
484
|
+
|
|
485
|
+
## Requisitos
|
|
486
|
+
|
|
487
|
+
- Ruby >= 3.2
|
|
488
|
+
- Rails >= 7.0
|
|
489
|
+
|
|
490
|
+
---
|
|
491
|
+
|
|
492
|
+
## Licença
|
|
493
|
+
|
|
494
|
+
[MIT](LICENSE.txt)
|
|
495
|
+
````
|
data/Rakefile
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "action_controller/api"
|
|
4
|
+
|
|
5
|
+
module OpenapiBlocks
|
|
6
|
+
class SpecController < ActionController::API # rubocop:disable Style/Documentation
|
|
7
|
+
SWAGGER_UI_CSS = "https://unpkg.com/swagger-ui-dist@5.11.0/swagger-ui.css"
|
|
8
|
+
SWAGGER_UI_STANDALONE_JS = "https://unpkg.com/swagger-ui-dist@5.11.0/swagger-ui-standalone-preset.js"
|
|
9
|
+
SWAGGER_UI_JS = "https://unpkg.com/swagger-ui-dist@5.11.0/swagger-ui-bundle.js"
|
|
10
|
+
|
|
11
|
+
def ui
|
|
12
|
+
render html: swagger_ui_html.html_safe
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def show
|
|
16
|
+
spec = OpenapiBlocks::Builder.build.deep_stringify_keys
|
|
17
|
+
spec["servers"] = swagger_ui_servers(spec)
|
|
18
|
+
|
|
19
|
+
if request.format.yaml?
|
|
20
|
+
render plain: spec.to_yaml, content_type: "application/yaml"
|
|
21
|
+
else
|
|
22
|
+
render json: spec
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
private
|
|
27
|
+
|
|
28
|
+
def swagger_ui_html # rubocop:disable Metrics/MethodLength
|
|
29
|
+
urls = swagger_ui_urls
|
|
30
|
+
|
|
31
|
+
<<~HTML
|
|
32
|
+
<!doctype html>
|
|
33
|
+
<html lang="en">
|
|
34
|
+
<head>
|
|
35
|
+
<meta charset="utf-8" />
|
|
36
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
37
|
+
<title>#{swagger_ui_title}</title>
|
|
38
|
+
<link rel="stylesheet" href="#{SWAGGER_UI_CSS}" />
|
|
39
|
+
<style>
|
|
40
|
+
html, body { margin: 0; padding: 0; height: 100%; background: #f6f7fb; }
|
|
41
|
+
#swagger-ui { height: 100%; }
|
|
42
|
+
</style>
|
|
43
|
+
</head>
|
|
44
|
+
<body>
|
|
45
|
+
<div id="swagger-ui"></div>
|
|
46
|
+
<script src="#{SWAGGER_UI_JS}"></script>
|
|
47
|
+
<script src="#{SWAGGER_UI_STANDALONE_JS}"></script>
|
|
48
|
+
<script>
|
|
49
|
+
window.ui = SwaggerUIBundle({
|
|
50
|
+
urls: #{urls.to_json},
|
|
51
|
+
'urls.primaryName': #{urls.first[:name].to_json},
|
|
52
|
+
dom_id: '#swagger-ui',
|
|
53
|
+
deepLinking: true,
|
|
54
|
+
displayRequestDuration: true,
|
|
55
|
+
docExpansion: 'list',
|
|
56
|
+
presets: [
|
|
57
|
+
SwaggerUIBundle.presets.apis,
|
|
58
|
+
SwaggerUIStandalonePreset
|
|
59
|
+
],
|
|
60
|
+
layout: 'StandaloneLayout'
|
|
61
|
+
});
|
|
62
|
+
</script>
|
|
63
|
+
</body>
|
|
64
|
+
</html>
|
|
65
|
+
HTML
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def swagger_ui_title
|
|
69
|
+
"#{OpenapiBlocks.configuration.info.title} - SwaggerUI"
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def swagger_ui_urls # rubocop:disable Metrics/MethodLength
|
|
73
|
+
spec_base = swagger_spec_base_url
|
|
74
|
+
servers = OpenapiBlocks.configuration.to_h[:servers]
|
|
75
|
+
|
|
76
|
+
return default_swagger_ui_urls if servers.blank?
|
|
77
|
+
|
|
78
|
+
servers.flat_map do |server|
|
|
79
|
+
[
|
|
80
|
+
{
|
|
81
|
+
url: "#{spec_base}.json",
|
|
82
|
+
name: "#{server[:url]} JSON"
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
url: "#{spec_base}.yaml",
|
|
86
|
+
name: "#{server[:url]} YAML"
|
|
87
|
+
}
|
|
88
|
+
]
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def default_swagger_ui_urls
|
|
93
|
+
spec_base = swagger_spec_base_url
|
|
94
|
+
|
|
95
|
+
[
|
|
96
|
+
{ url: "#{spec_base}.json", name: "OpenAPI JSON" },
|
|
97
|
+
{ url: "#{spec_base}.yaml", name: "OpenAPI YAML" }
|
|
98
|
+
]
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def swagger_spec_base_url
|
|
102
|
+
mount_path = request.script_name.to_s.chomp("/")
|
|
103
|
+
mount_path.present? ? "#{mount_path}/openapi" : "/openapi"
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def swagger_ui_servers(_spec)
|
|
107
|
+
[{ "url" => request.base_url, "description" => "Current" }]
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
end
|
data/config/routes.rb
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module OpenapiBlocks
|
|
4
|
+
class Base # rubocop:disable Style/Documentation
|
|
5
|
+
class << self
|
|
6
|
+
attr_reader :_model, :_ignored, :_associations, :_virtual_attributes, :_operations, :_tags
|
|
7
|
+
|
|
8
|
+
def model(klass = nil)
|
|
9
|
+
klass ? @_model = klass : @_model ||= infer_model # rubocop:disable Naming/MemoizedInstanceVariableName
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def ignore(*attributes)
|
|
13
|
+
@_ignored ||= []
|
|
14
|
+
@_ignored.concat(attributes.map(&:to_s))
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def association(name, type: nil, input: true)
|
|
18
|
+
@_associations ||= []
|
|
19
|
+
@_associations << { name: name, type: type, input: input }
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def attribute(name, **)
|
|
23
|
+
@_virtual_attributes ||= []
|
|
24
|
+
@_virtual_attributes << ({ name: name, ** })
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def operation(action, &block)
|
|
28
|
+
@_operations ||= {}
|
|
29
|
+
builder = OperationBuilder.new
|
|
30
|
+
builder.instance_eval(&block) if block
|
|
31
|
+
@_operations[action] = builder
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def tags(*values)
|
|
35
|
+
values.any? ? @_tags = values : @_tags
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
private
|
|
39
|
+
|
|
40
|
+
def infer_model
|
|
41
|
+
model_name = name
|
|
42
|
+
.gsub(/Openapi$/, "")
|
|
43
|
+
.split("::")
|
|
44
|
+
.last
|
|
45
|
+
|
|
46
|
+
Object.const_get(model_name)
|
|
47
|
+
rescue NameError
|
|
48
|
+
raise Error, "Could not infer model from #{name}. Use `model ModelClass` to define it explicitly."
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "spec/document"
|
|
4
|
+
|
|
5
|
+
module OpenapiBlocks
|
|
6
|
+
class Builder # rubocop:disable Style/Documentation
|
|
7
|
+
def self.build
|
|
8
|
+
new.build
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def build
|
|
12
|
+
Spec::Document.new(openapi_classes).build
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
private
|
|
16
|
+
|
|
17
|
+
def openapi_classes
|
|
18
|
+
ObjectSpace.each_object(Class).select do |klass|
|
|
19
|
+
klass < OpenapiBlocks::Base
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|