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.
Files changed (36) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +86 -0
  3. data/CODE_OF_CONDUCT.md +10 -0
  4. data/LICENSE.txt +21 -0
  5. data/README.md +304 -0
  6. data/README.pt-BR.md +495 -0
  7. data/Rakefile +12 -0
  8. data/app/controllers/openapi_blocks/spec_controller.rb +110 -0
  9. data/config/routes.rb +7 -0
  10. data/lib/openapi_blocks/base.rb +52 -0
  11. data/lib/openapi_blocks/builder.rb +23 -0
  12. data/lib/openapi_blocks/cache.rb +28 -0
  13. data/lib/openapi_blocks/configuration/contact_builder.rb +23 -0
  14. data/lib/openapi_blocks/configuration/info_builder.rb +42 -0
  15. data/lib/openapi_blocks/configuration/license_builder.rb +19 -0
  16. data/lib/openapi_blocks/configuration/security_builder.rb +33 -0
  17. data/lib/openapi_blocks/configuration/server_builder.rb +19 -0
  18. data/lib/openapi_blocks/configuration/servers_builder.rb +21 -0
  19. data/lib/openapi_blocks/configuration.rb +55 -0
  20. data/lib/openapi_blocks/engine.rb +13 -0
  21. data/lib/openapi_blocks/file_watcher.rb +42 -0
  22. data/lib/openapi_blocks/middleware.rb +54 -0
  23. data/lib/openapi_blocks/operation_builder.rb +57 -0
  24. data/lib/openapi_blocks/railtie.rb +19 -0
  25. data/lib/openapi_blocks/routing/extractor.rb +187 -0
  26. data/lib/openapi_blocks/routing/operation.rb +45 -0
  27. data/lib/openapi_blocks/schema/extractor.rb +103 -0
  28. data/lib/openapi_blocks/schema/types.rb +52 -0
  29. data/lib/openapi_blocks/schema/validator.rb +86 -0
  30. data/lib/openapi_blocks/spec/components.rb +67 -0
  31. data/lib/openapi_blocks/spec/document.rb +47 -0
  32. data/lib/openapi_blocks/spec/paths.rb +17 -0
  33. data/lib/openapi_blocks/version.rb +5 -0
  34. data/lib/openapi_blocks.rb +35 -0
  35. data/sig/openapi_blocks.rbs +4 -0
  36. 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,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "rspec/core/rake_task"
5
+
6
+ RSpec::Core::RakeTask.new(:spec)
7
+
8
+ require "rubocop/rake_task"
9
+
10
+ RuboCop::RakeTask.new
11
+
12
+ task default: %i[spec rubocop]
@@ -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,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ OpenapiBlocks::Engine.routes.draw do
4
+ root to: "spec#ui"
5
+ get "openapi.json", to: "spec#show", defaults: { format: "json" }
6
+ get "openapi.yaml", to: "spec#show", defaults: { format: "yaml" }
7
+ end
@@ -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