swagger_docs_rails 0.1.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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 44980ee6b53ffdfe7cfa48ee2f6226255f5e310a59d64898ae6296fa0b9613d4
4
+ data.tar.gz: dac2a21c73832ec25dd4e06cd0ec0ed312f337a601d9757b46a32ac72bb5eadf
5
+ SHA512:
6
+ metadata.gz: bc22c1ad42adc28b3b172d46aefdedcb40b3a5a9103a5a27aa2c1be2a97a71807026fa5beb72b102d008e918fb6742a0827e716413414e423e9c4698f64fa7fa
7
+ data.tar.gz: a7208d0e7729930892d1032f65a842be201e36c5089efeef6fba07a79d5a08d2f5b18edefc5ac2ce21ada1092fde7e48d1843d07851849688a89110d615755f4
data/CHANGELOG.md ADDED
@@ -0,0 +1,8 @@
1
+ # Changelog
2
+
3
+ ## 0.1.0
4
+
5
+ - Primeira versão da gem `swagger_docs_rails`.
6
+ - Geração de OpenAPI 3.0 a partir de `db/schema.rb` e controllers Rails.
7
+ - Swagger UI via Rails Engine.
8
+ - Tasks `swagger:generate` e `swagger:validate`.
data/MIT-LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Clic API
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,71 @@
1
+ # Swagger Docs Rails
2
+
3
+ Gem Rails Engine para gerar documentação OpenAPI 3.0 a partir do `db/schema.rb`, controllers Rails e rotas da aplicação.
4
+
5
+ ## Instalação
6
+
7
+ Adicione ao `Gemfile` da API:
8
+
9
+ ```ruby
10
+ gem "swagger_docs_rails"
11
+ ```
12
+
13
+ Para desenvolvimento local:
14
+
15
+ ```ruby
16
+ gem "swagger_docs_rails", path: "../swagger_docs_rails"
17
+ ```
18
+
19
+ Execute:
20
+
21
+ ```bash
22
+ bundle install
23
+ rails generate swagger_docs_rails:install
24
+ ```
25
+
26
+ Adicione as rotas:
27
+
28
+ ```ruby
29
+ if ENV["ENABLE_SWAGGER"] == "true"
30
+ mount SwaggerDocsRails::Engine => "/"
31
+ end
32
+ ```
33
+
34
+ ## Configuração
35
+
36
+ O generator cria `config/initializers/swagger_docs_rails.rb`:
37
+
38
+ ```ruby
39
+ SwaggerDocsRails.configure do |config|
40
+ config.enabled = !Rails.env.production?
41
+ config.title = Rails.application.class.module_parent_name
42
+ config.version = "1.0.0"
43
+ config.description = "Documentação OpenAPI gerada automaticamente"
44
+ config.server_url = ENV.fetch("HOST_URL", "/")
45
+ config.live_reload = Rails.env.development?
46
+ config.username = ENV.fetch("SWAGGER_USER", "admin")
47
+ config.password = ENV.fetch("SWAGGER_PASSWORD", "")
48
+ config.additional_controller_paths = []
49
+ config.output_path = Rails.root.join("public", "swagger", "openapi.json")
50
+ end
51
+ ```
52
+
53
+ ## Uso
54
+
55
+ Gere o arquivo OpenAPI:
56
+
57
+ ```bash
58
+ rails swagger:generate
59
+ ```
60
+
61
+ Valide o arquivo gerado, se a aplicação possuir `openapi_parser`:
62
+
63
+ ```bash
64
+ rails swagger:validate
65
+ ```
66
+
67
+ Com `ENABLE_SWAGGER=true`, acesse:
68
+
69
+ - `/swagger`
70
+ - `/swagger/doc`
71
+ - `/swagger/cache` com método `DELETE`
@@ -0,0 +1,228 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SwaggerDocsRails
4
+ class SwaggerController < ActionController::Base
5
+ SWAGGER_UI_CDN = "https://cdn.jsdelivr.net/npm/swagger-ui-dist@5"
6
+
7
+ before_action :authenticate_swagger!
8
+
9
+ def ui
10
+ render html: html_page.html_safe, layout: false
11
+ end
12
+
13
+ def doc
14
+ render json: JSON.parse(build_doc(resolved_server_url))
15
+ end
16
+
17
+ def reset
18
+ self.class.cached_doc = nil
19
+ render json: { message: "Cache do Swagger limpo. Acesse /swagger para regenerar." }, status: :ok
20
+ end
21
+
22
+ class << self
23
+ attr_accessor :cached_doc
24
+ end
25
+
26
+ private
27
+
28
+ def authenticate_swagger!
29
+ return true if swagger_username.blank? && swagger_password.blank?
30
+
31
+ authenticate_or_request_with_http_basic("Swagger API Docs") do |user, password|
32
+ user_ok = ActiveSupport::SecurityUtils.secure_compare(user.to_s, swagger_username)
33
+ password_ok = ActiveSupport::SecurityUtils.secure_compare(password.to_s, swagger_password)
34
+ user_ok && password_ok
35
+ end
36
+ end
37
+
38
+ def swagger_username
39
+ configuracao.username.to_s
40
+ end
41
+
42
+ def swagger_password
43
+ configuracao.password.to_s
44
+ end
45
+
46
+ def configuracao
47
+ SwaggerDocsRails.configuration
48
+ end
49
+
50
+ def resolved_server_url
51
+ configured = configuracao.server_url.to_s.strip
52
+ if configured.start_with?("http://", "https://")
53
+ configured.chomp("/")
54
+ else
55
+ request.base_url.chomp("/")
56
+ end
57
+ end
58
+
59
+ def doc_url
60
+ "#{request.base_url}/swagger/doc"
61
+ end
62
+
63
+ def build_doc(server_url)
64
+ if configuracao.live_reload || self.class.cached_doc.nil?
65
+ config = {
66
+ title: configuracao.title,
67
+ version: configuracao.version,
68
+ description: configuracao.description,
69
+ server_url: server_url
70
+ }
71
+
72
+ document = SwaggerDocsRails::Generator.new(config: config).generate
73
+ self.class.cached_doc = JSON.generate(document)
74
+
75
+ output = configuracao.output_path
76
+ FileUtils.mkdir_p(File.dirname(output.to_s))
77
+ File.write(output.to_s, JSON.pretty_generate(document))
78
+ end
79
+
80
+ update_server_url_in_doc(server_url)
81
+ end
82
+
83
+ def update_server_url_in_doc(server_url)
84
+ doc = JSON.parse(self.class.cached_doc)
85
+ doc["servers"] = [{ "url" => server_url, "description" => "Servidor da API" }]
86
+ JSON.generate(doc)
87
+ end
88
+
89
+ def html_page
90
+ cdn = SWAGGER_UI_CDN
91
+ title = configuracao.title
92
+ absolute_doc_url = doc_url
93
+ gem_version = SwaggerDocsRails::VERSION
94
+
95
+ <<~HTML
96
+ <!DOCTYPE html>
97
+ <html lang="pt-BR">
98
+ <head>
99
+ <meta charset="UTF-8" />
100
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
101
+ <title>#{title} - Documentação da API</title>
102
+ <link rel="stylesheet" href="#{cdn}/swagger-ui.css" />
103
+ <style>
104
+ * { box-sizing: border-box; }
105
+ body { margin: 0; background: #f5f7fa; font-family: sans-serif; }
106
+ .topbar { display: none !important; }
107
+ #swagger-ui { max-width: 1400px; margin: 0 auto; padding: 1.5rem 1rem; }
108
+ #swagger-regen-bar {
109
+ background: #1b1b1b; color: #fff; padding: 8px 16px;
110
+ display: flex; align-items: center; gap: 12px; font-size: 13px;
111
+ }
112
+ #swagger-regen-bar strong { font-size: 14px; }
113
+ #swagger-gem-version {
114
+ background: #2f3b52; border-radius: 999px; color: #dce8ff;
115
+ font-size: 12px; font-weight: 600; padding: 4px 10px;
116
+ }
117
+ #btn-regen {
118
+ background: #4CAF50; color: white; border: none; border-radius: 4px;
119
+ padding: 5px 14px; cursor: pointer; font-size: 13px; font-weight: 600;
120
+ }
121
+ #btn-regen:hover { background: #45a049; }
122
+ #btn-regen.loading { background: #888; cursor: default; }
123
+ #regen-status { font-style: italic; color: #aef; }
124
+ </style>
125
+ </head>
126
+ <body>
127
+ <div id="swagger-regen-bar">
128
+ <strong>#{title}</strong>
129
+ <span id="swagger-gem-version">swagger_docs_rails v#{gem_version}</span>
130
+ <button id="btn-regen" onclick="regenSwagger()">Regenerar documentação</button>
131
+ <span id="regen-status"></span>
132
+ </div>
133
+ <div id="swagger-ui"></div>
134
+ <script src="#{cdn}/swagger-ui-bundle.js"></script>
135
+ <script src="#{cdn}/swagger-ui-standalone-preset.js"></script>
136
+ <script>
137
+ function regenSwagger() {
138
+ const btn = document.getElementById('btn-regen');
139
+ const status = document.getElementById('regen-status');
140
+ btn.classList.add('loading');
141
+ btn.disabled = true;
142
+ status.textContent = 'Limpando cache...';
143
+
144
+ fetch('/swagger/cache', { method: 'DELETE' })
145
+ .then(() => {
146
+ status.textContent = 'Regenerando...';
147
+ return fetch('#{absolute_doc_url}');
148
+ })
149
+ .then(() => {
150
+ status.textContent = 'Recarregando UI...';
151
+ setTimeout(() => window.location.reload(), 300);
152
+ })
153
+ .catch(() => {
154
+ status.textContent = 'Erro ao regenerar.';
155
+ btn.classList.remove('loading');
156
+ btn.disabled = false;
157
+ });
158
+ }
159
+
160
+ window.onload = () => {
161
+ const CaseInsensitiveFilterPlugin = () => ({
162
+ fn: {
163
+ opsFilter: (taggedOps, phrase) => {
164
+ if (!phrase || phrase.trim() === "") return taggedOps;
165
+ const q = phrase.toLowerCase().trim();
166
+ return taggedOps
167
+ .filter((tagObj) => {
168
+ const tag = (tagObj.get("tagDetails")?.get("name") || "").toLowerCase();
169
+ if (tag.includes(q)) return true;
170
+ const ops = tagObj.get("operations");
171
+ return ops?.some((op) => {
172
+ const path = (op.get("path") || "").toLowerCase();
173
+ const method = (op.get("method") || "").toLowerCase();
174
+ const summary = (op.getIn(["operation", "summary"]) || "").toLowerCase();
175
+ const opId = (op.getIn(["operation", "operationId"]) || "").toLowerCase();
176
+ const desc = (op.getIn(["operation", "description"]) || "").toLowerCase();
177
+ return path.includes(q) || method.includes(q) ||
178
+ summary.includes(q) || opId.includes(q) || desc.includes(q);
179
+ });
180
+ })
181
+ .map((tagObj) => {
182
+ const ops = tagObj.get("operations");
183
+ if (!ops) return tagObj;
184
+ const filtered = ops.filter((op) => {
185
+ const tag = (tagObj.get("tagDetails")?.get("name") || "").toLowerCase();
186
+ if (tag.includes(q)) return true;
187
+ const path = (op.get("path") || "").toLowerCase();
188
+ const method = (op.get("method") || "").toLowerCase();
189
+ const summary = (op.getIn(["operation", "summary"]) || "").toLowerCase();
190
+ const opId = (op.getIn(["operation", "operationId"]) || "").toLowerCase();
191
+ const desc = (op.getIn(["operation", "description"]) || "").toLowerCase();
192
+ return path.includes(q) || method.includes(q) ||
193
+ summary.includes(q) || opId.includes(q) || desc.includes(q);
194
+ });
195
+ return tagObj.set("operations", filtered);
196
+ });
197
+ }
198
+ }
199
+ });
200
+
201
+ SwaggerUIBundle({
202
+ url: "#{absolute_doc_url}",
203
+ dom_id: "#swagger-ui",
204
+ deepLinking: true,
205
+ presets: [
206
+ SwaggerUIBundle.presets.apis,
207
+ SwaggerUIStandalonePreset
208
+ ],
209
+ plugins: [
210
+ SwaggerUIBundle.plugins.DownloadUrl,
211
+ CaseInsensitiveFilterPlugin
212
+ ],
213
+ layout: "StandaloneLayout",
214
+ defaultModelsExpandDepth: 1,
215
+ defaultModelExpandDepth: 2,
216
+ displayRequestDuration: true,
217
+ filter: true,
218
+ tryItOutEnabled: true,
219
+ persistAuthorization: true
220
+ });
221
+ };
222
+ </script>
223
+ </body>
224
+ </html>
225
+ HTML
226
+ end
227
+ end
228
+ end
data/config/routes.rb ADDED
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ SwaggerDocsRails::Engine.routes.draw do
4
+ get "/swagger", to: "swagger#ui", as: :swagger_ui
5
+ get "/swagger/doc", to: "swagger#doc", as: :swagger_doc
6
+ delete "/swagger/cache", to: "swagger#reset", as: :swagger_reset
7
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails/generators"
4
+
5
+ module SwaggerDocsRails
6
+ module Generators
7
+ class InstallGenerator < Rails::Generators::Base
8
+ source_root File.expand_path("templates", __dir__)
9
+
10
+ def copiar_initializer
11
+ template "swagger_docs_rails.rb", "config/initializers/swagger_docs_rails.rb"
12
+ end
13
+
14
+ def exibir_rota
15
+ say "Adicione ao config/routes.rb:", :green
16
+ say ""
17
+ say "if ENV[\"ENABLE_SWAGGER\"] == \"true\""
18
+ say " mount SwaggerDocsRails::Engine => \"/\""
19
+ say "end"
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ SwaggerDocsRails.configure do |config|
4
+ config.enabled = !Rails.env.production?
5
+ config.title = Rails.application.class.module_parent_name
6
+ config.version = "1.0.0"
7
+ config.description = "Documentação OpenAPI gerada automaticamente"
8
+ config.server_url = ENV.fetch("HOST_URL", "/")
9
+ config.live_reload = Rails.env.development?
10
+ config.username = ENV.fetch("SWAGGER_USER", "admin")
11
+ config.password = ENV.fetch("SWAGGER_PASSWORD", "")
12
+ config.additional_controller_paths = []
13
+ config.output_path = Rails.root.join("public", "swagger", "openapi.json")
14
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SwaggerDocsRails
4
+ class Configuration
5
+ attr_accessor :enabled, :title, :version, :description,
6
+ :server_url, :live_reload, :username, :password,
7
+ :additional_controller_paths, :output_path
8
+
9
+ def initialize
10
+ @enabled = !Rails.env.production?
11
+ @title = Rails.application.class.module_parent_name
12
+ @version = "1.0.0"
13
+ @description = "Documentação OpenAPI gerada automaticamente"
14
+ @server_url = ENV.fetch("HOST_URL", "/")
15
+ @live_reload = Rails.env.development?
16
+ @username = ENV.fetch("SWAGGER_USER", "admin")
17
+ @password = ENV.fetch("SWAGGER_PASSWORD", "")
18
+ @additional_controller_paths = []
19
+ @output_path = Rails.root.join("public", "swagger", "openapi.json")
20
+ end
21
+ end
22
+ end