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 +7 -0
- data/CHANGELOG.md +8 -0
- data/MIT-LICENSE +21 -0
- data/README.md +71 -0
- data/app/controllers/swagger_docs_rails/swagger_controller.rb +228 -0
- data/config/routes.rb +7 -0
- data/lib/generators/swagger_docs_rails/install/install_generator.rb +23 -0
- data/lib/generators/swagger_docs_rails/install/templates/swagger_docs_rails.rb +14 -0
- data/lib/swagger_docs_rails/configuration.rb +22 -0
- data/lib/swagger_docs_rails/controller_parser.rb +556 -0
- data/lib/swagger_docs_rails/engine.rb +14 -0
- data/lib/swagger_docs_rails/generator.rb +40 -0
- data/lib/swagger_docs_rails/openapi_builder.rb +712 -0
- data/lib/swagger_docs_rails/schema_parser.rb +150 -0
- data/lib/swagger_docs_rails/version.rb +5 -0
- data/lib/swagger_docs_rails.rb +25 -0
- data/lib/tasks/swagger_docs_rails.rake +72 -0
- metadata +81 -0
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
# lib/swagger_docs_rails/schema_parser.rb
|
|
3
|
+
#
|
|
4
|
+
# Lê db/schema.rb e gera schemas OpenAPI 3.0 para cada tabela.
|
|
5
|
+
# Produz três schemas por tabela:
|
|
6
|
+
# - ModelName → schema completo (usado nas respostas)
|
|
7
|
+
# - ModelNameInput → campos editáveis (usado em POST/PATCH genéricos)
|
|
8
|
+
# - ModelNameList → wrapper paginado { data: [...], meta: {...} }
|
|
9
|
+
|
|
10
|
+
module SwaggerDocsRails
|
|
11
|
+
class SchemaParser
|
|
12
|
+
IGNORED_COLUMNS = %w[
|
|
13
|
+
id created_by updated_by deleted_at created_at updated_at
|
|
14
|
+
encrypted_password reset_password_token reset_password_sent_at
|
|
15
|
+
remember_created_at refresh_token refresh_token_expires_at
|
|
16
|
+
token_primeiro_acesso
|
|
17
|
+
].freeze
|
|
18
|
+
|
|
19
|
+
COLUMN_TYPE_MAP = {
|
|
20
|
+
string: { type: "string" },
|
|
21
|
+
text: { type: "string" },
|
|
22
|
+
integer: { type: "integer", format: "int64" },
|
|
23
|
+
bigint: { type: "integer", format: "int64" },
|
|
24
|
+
float: { type: "number", format: "float" },
|
|
25
|
+
decimal: { type: "number", format: "double" },
|
|
26
|
+
boolean: { type: "boolean" },
|
|
27
|
+
date: { type: "string", format: "date" },
|
|
28
|
+
datetime: { type: "string", format: "date-time" },
|
|
29
|
+
jsonb: { type: "object" },
|
|
30
|
+
json: { type: "object" }
|
|
31
|
+
}.freeze
|
|
32
|
+
|
|
33
|
+
attr_reader :tables
|
|
34
|
+
|
|
35
|
+
def initialize(schema_path = nil)
|
|
36
|
+
@schema_path = schema_path || Rails.root.join("db", "schema.rb")
|
|
37
|
+
@tables = {}
|
|
38
|
+
parse!
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Retorna Hash com todos os schemas OpenAPI gerados
|
|
42
|
+
def to_openapi_schemas
|
|
43
|
+
schemas = {}
|
|
44
|
+
@tables.each do |table_name, columns|
|
|
45
|
+
model = table_to_model_name(table_name)
|
|
46
|
+
schemas[model] = build_full_schema(columns)
|
|
47
|
+
schemas["#{model}Input"] = build_input_schema(columns)
|
|
48
|
+
schemas["#{model}List"] = build_list_schema(model)
|
|
49
|
+
end
|
|
50
|
+
schemas
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def model_name_for_table(table_name)
|
|
54
|
+
table_to_model_name(table_name)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
private
|
|
58
|
+
|
|
59
|
+
def parse!
|
|
60
|
+
content = File.read(@schema_path)
|
|
61
|
+
|
|
62
|
+
content.scan(/create_table\s+"(\w+)".*?do\s+\|t\|(.*?)end/m) do |table_name, body|
|
|
63
|
+
columns = [{ name: "id", type: :bigint, nullable: false, default: nil }]
|
|
64
|
+
|
|
65
|
+
body.scan(/t\.(\w+)\s+"(\w+)"(.*?)$/) do |col_type, col_name, opts|
|
|
66
|
+
next if col_type == "index"
|
|
67
|
+
columns << {
|
|
68
|
+
name: col_name,
|
|
69
|
+
type: col_type.to_sym,
|
|
70
|
+
nullable: !opts.include?("null: false"),
|
|
71
|
+
default: extract_default(opts)
|
|
72
|
+
}
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
columns << { name: "created_at", type: :datetime, nullable: false, default: nil }
|
|
76
|
+
columns << { name: "updated_at", type: :datetime, nullable: false, default: nil }
|
|
77
|
+
|
|
78
|
+
@tables[table_name] = columns
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def build_full_schema(columns)
|
|
83
|
+
props = {}
|
|
84
|
+
required = []
|
|
85
|
+
columns.each do |col|
|
|
86
|
+
prop = (COLUMN_TYPE_MAP[col[:type]] || { type: "string" }).dup
|
|
87
|
+
prop[:description] = col[:name].tr("_", " ").capitalize
|
|
88
|
+
prop[:default] = cast_default(col[:default], col[:type]) if col[:default]
|
|
89
|
+
props[col[:name]] = prop
|
|
90
|
+
required << col[:name] unless col[:nullable]
|
|
91
|
+
end
|
|
92
|
+
schema = { type: "object", properties: props }
|
|
93
|
+
schema[:required] = required unless required.empty?
|
|
94
|
+
schema
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def build_input_schema(columns)
|
|
98
|
+
writable = columns.reject { |c| IGNORED_COLUMNS.include?(c[:name]) }
|
|
99
|
+
props = {}
|
|
100
|
+
required = []
|
|
101
|
+
writable.each do |col|
|
|
102
|
+
prop = (COLUMN_TYPE_MAP[col[:type]] || { type: "string" }).dup
|
|
103
|
+
prop[:description] = col[:name].tr("_", " ").capitalize
|
|
104
|
+
props[col[:name]] = prop
|
|
105
|
+
required << col[:name] if !col[:nullable] && col[:default].nil?
|
|
106
|
+
end
|
|
107
|
+
schema = { type: "object", properties: props }
|
|
108
|
+
schema[:required] = required unless required.empty?
|
|
109
|
+
schema
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def build_list_schema(model)
|
|
113
|
+
{
|
|
114
|
+
type: "object",
|
|
115
|
+
properties: {
|
|
116
|
+
data: { type: "array", items: { "$ref" => "#/components/schemas/#{model}" } },
|
|
117
|
+
meta: {
|
|
118
|
+
type: "object",
|
|
119
|
+
properties: {
|
|
120
|
+
current_page: { type: "integer" },
|
|
121
|
+
total_pages: { type: "integer" },
|
|
122
|
+
total_count: { type: "integer" },
|
|
123
|
+
per_page: { type: "integer" }
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def table_to_model_name(table_name)
|
|
131
|
+
singular = table_name.singularize rescue table_name.sub(/s$/, "")
|
|
132
|
+
parts = singular.split("_")
|
|
133
|
+
parts.first == "g" ? "G" + parts[1..].map(&:capitalize).join : parts.map(&:capitalize).join
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def extract_default(opts)
|
|
137
|
+
m = opts.match(/default:\s*([^,\s]+)/)
|
|
138
|
+
m ? m[1] : nil
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def cast_default(value, type)
|
|
142
|
+
case type
|
|
143
|
+
when :boolean then value == "true"
|
|
144
|
+
when :integer, :bigint then value.to_i
|
|
145
|
+
when :float, :decimal then value.to_f
|
|
146
|
+
else value.to_s.gsub(/\A["']|["']\z/, "")
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require "fileutils"
|
|
5
|
+
require "swagger_docs_rails/version"
|
|
6
|
+
require "swagger_docs_rails/configuration"
|
|
7
|
+
require "swagger_docs_rails/schema_parser"
|
|
8
|
+
require "swagger_docs_rails/controller_parser"
|
|
9
|
+
require "swagger_docs_rails/openapi_builder"
|
|
10
|
+
require "swagger_docs_rails/generator"
|
|
11
|
+
require "swagger_docs_rails/engine" if defined?(Rails::Engine)
|
|
12
|
+
|
|
13
|
+
module SwaggerDocsRails
|
|
14
|
+
class << self
|
|
15
|
+
attr_writer :configuration
|
|
16
|
+
|
|
17
|
+
def configuration
|
|
18
|
+
@configuration ||= Configuration.new
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def configure
|
|
22
|
+
yield(configuration)
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
namespace :swagger do
|
|
4
|
+
desc "Gera a documentação OpenAPI 3.0 a partir do schema.rb e controllers"
|
|
5
|
+
task :generate, [:output] => :environment do |_task, args|
|
|
6
|
+
require "json"
|
|
7
|
+
require "swagger_docs_rails"
|
|
8
|
+
|
|
9
|
+
config = SwaggerDocsRails.configuration
|
|
10
|
+
output_path = args[:output] || ENV["SWAGGER_OUTPUT"] || config.output_path
|
|
11
|
+
|
|
12
|
+
app_name = begin
|
|
13
|
+
Rails.application.class.module_parent_name
|
|
14
|
+
rescue StandardError
|
|
15
|
+
"API"
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
dados_configuracao = {
|
|
19
|
+
title: ENV.fetch("SWAGGER_TITLE", config.title || app_name),
|
|
20
|
+
version: ENV.fetch("SWAGGER_VERSION", config.version || "1.0.0"),
|
|
21
|
+
description: ENV.fetch("SWAGGER_DESC", config.description || "Documentação OpenAPI gerada automaticamente"),
|
|
22
|
+
server_url: ENV.fetch("SWAGGER_SERVER", config.server_url || "/")
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
puts "→ Parsing db/schema.rb..."
|
|
26
|
+
schema_parser = SwaggerDocsRails::SchemaParser.new
|
|
27
|
+
puts " #{schema_parser.tables.size} tabelas encontradas"
|
|
28
|
+
schema_parser.tables.each_key { |tabela| puts " • #{tabela}" }
|
|
29
|
+
|
|
30
|
+
puts "\n→ Scanning app/controllers/api/..."
|
|
31
|
+
caminhos = [Rails.root.join("app", "controllers", "api"), *Array(config.additional_controller_paths)]
|
|
32
|
+
controller_parser = SwaggerDocsRails::ControllerParser.new(caminhos)
|
|
33
|
+
puts " #{controller_parser.to_resources.size} controllers encontradas"
|
|
34
|
+
controller_parser.to_resources.each do |recurso|
|
|
35
|
+
crud_label = recurso[:actions].any? ? "CRUD[#{recurso[:actions].join(",")}]" : "(sem ações CRUD)"
|
|
36
|
+
custom_label = recurso[:custom_actions].any? ? " + custom[#{recurso[:custom_actions].map { |acao| acao[:name] }.join(",")}]" : ""
|
|
37
|
+
puts " • #{recurso[:controller_class]} #{crud_label}#{custom_label}"
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
puts "\n→ Building OpenAPI document..."
|
|
41
|
+
documento = SwaggerDocsRails::OpenapiBuilder.new(schema_parser, controller_parser, dados_configuracao).build
|
|
42
|
+
|
|
43
|
+
puts " #{documento[:paths].size} paths"
|
|
44
|
+
puts " #{documento.dig(:components, :schemas)&.size || 0} schemas"
|
|
45
|
+
|
|
46
|
+
puts "\n→ Writing to #{output_path}..."
|
|
47
|
+
FileUtils.mkdir_p(File.dirname(output_path.to_s))
|
|
48
|
+
File.write(output_path.to_s, JSON.pretty_generate(documento))
|
|
49
|
+
|
|
50
|
+
tamanho_kb = (File.size(output_path.to_s) / 1024.0).round(1)
|
|
51
|
+
puts "\n✓ Concluído! #{output_path} (#{tamanho_kb} KB)"
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
desc "Valida o documento OpenAPI gerado (requer gem openapi_parser)"
|
|
55
|
+
task validate: :environment do
|
|
56
|
+
require "json"
|
|
57
|
+
require "openapi_parser"
|
|
58
|
+
|
|
59
|
+
path = SwaggerDocsRails.configuration.output_path
|
|
60
|
+
|
|
61
|
+
unless File.exist?(path)
|
|
62
|
+
puts "✗ Arquivo não encontrado. Rode `rails swagger:generate` primeiro."
|
|
63
|
+
exit 1
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
OpenAPIParser.parse(JSON.parse(File.read(path)), strict_reference_validation: true)
|
|
67
|
+
puts "✓ Documento OpenAPI válido (#{path})"
|
|
68
|
+
rescue OpenAPIParser::OpenAPIError => e
|
|
69
|
+
puts "✗ Validação falhou: #{e.message}"
|
|
70
|
+
exit 1
|
|
71
|
+
end
|
|
72
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: swagger_docs_rails
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Clic API
|
|
8
|
+
bindir: exe
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: rails
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - ">="
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '7.0'
|
|
19
|
+
- - "<"
|
|
20
|
+
- !ruby/object:Gem::Version
|
|
21
|
+
version: '9.0'
|
|
22
|
+
type: :runtime
|
|
23
|
+
prerelease: false
|
|
24
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
25
|
+
requirements:
|
|
26
|
+
- - ">="
|
|
27
|
+
- !ruby/object:Gem::Version
|
|
28
|
+
version: '7.0'
|
|
29
|
+
- - "<"
|
|
30
|
+
- !ruby/object:Gem::Version
|
|
31
|
+
version: '9.0'
|
|
32
|
+
description: Rails Engine para gerar documentação OpenAPI 3.0 a partir do schema.rb
|
|
33
|
+
e controllers Rails.
|
|
34
|
+
email:
|
|
35
|
+
- dev@clic.com.br
|
|
36
|
+
executables: []
|
|
37
|
+
extensions: []
|
|
38
|
+
extra_rdoc_files: []
|
|
39
|
+
files:
|
|
40
|
+
- CHANGELOG.md
|
|
41
|
+
- MIT-LICENSE
|
|
42
|
+
- README.md
|
|
43
|
+
- app/controllers/swagger_docs_rails/swagger_controller.rb
|
|
44
|
+
- config/routes.rb
|
|
45
|
+
- lib/generators/swagger_docs_rails/install/install_generator.rb
|
|
46
|
+
- lib/generators/swagger_docs_rails/install/templates/swagger_docs_rails.rb
|
|
47
|
+
- lib/swagger_docs_rails.rb
|
|
48
|
+
- lib/swagger_docs_rails/configuration.rb
|
|
49
|
+
- lib/swagger_docs_rails/controller_parser.rb
|
|
50
|
+
- lib/swagger_docs_rails/engine.rb
|
|
51
|
+
- lib/swagger_docs_rails/generator.rb
|
|
52
|
+
- lib/swagger_docs_rails/openapi_builder.rb
|
|
53
|
+
- lib/swagger_docs_rails/schema_parser.rb
|
|
54
|
+
- lib/swagger_docs_rails/version.rb
|
|
55
|
+
- lib/tasks/swagger_docs_rails.rake
|
|
56
|
+
homepage: https://rubygems.org/gems/swagger_docs_rails
|
|
57
|
+
licenses:
|
|
58
|
+
- MIT
|
|
59
|
+
metadata:
|
|
60
|
+
allowed_push_host: https://rubygems.org
|
|
61
|
+
homepage_uri: https://rubygems.org/gems/swagger_docs_rails
|
|
62
|
+
source_code_uri: https://rubygems.org/gems/swagger_docs_rails
|
|
63
|
+
changelog_uri: https://rubygems.org/gems/swagger_docs_rails/-/blob/main/CHANGELOG.md
|
|
64
|
+
rdoc_options: []
|
|
65
|
+
require_paths:
|
|
66
|
+
- lib
|
|
67
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
68
|
+
requirements:
|
|
69
|
+
- - ">="
|
|
70
|
+
- !ruby/object:Gem::Version
|
|
71
|
+
version: '3.2'
|
|
72
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
73
|
+
requirements:
|
|
74
|
+
- - ">="
|
|
75
|
+
- !ruby/object:Gem::Version
|
|
76
|
+
version: '0'
|
|
77
|
+
requirements: []
|
|
78
|
+
rubygems_version: 3.6.9
|
|
79
|
+
specification_version: 4
|
|
80
|
+
summary: Gerador OpenAPI e Swagger UI para APIs Rails.
|
|
81
|
+
test_files: []
|