rails-autodoc 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/.github/workflows/ci.yml +63 -0
- data/.gitignore +16 -0
- data/.rspec +2 -0
- data/.rubocop.yml +81 -0
- data/.yardopts +3 -0
- data/Appraisals +26 -0
- data/CHANGELOG.md +20 -0
- data/CONTRIBUTING.md +54 -0
- data/Gemfile +10 -0
- data/Gemfile.lock +298 -0
- data/LICENSE.txt +21 -0
- data/README.md +111 -0
- data/Rakefile +26 -0
- data/app/controllers/rails_autodoc/spec_controller.rb +52 -0
- data/config/routes.rb +6 -0
- data/docs/annotation-dsl.md +56 -0
- data/docs/architecture.md +37 -0
- data/docs/ci-integration.md +32 -0
- data/docs/configuration.md +48 -0
- data/docs/faq.md +29 -0
- data/docs/getting-started.md +54 -0
- data/docs/index.md +21 -0
- data/docs/inference-rules.md +74 -0
- data/docs/limitations.md +34 -0
- data/docs/migration-from-rswag.md +42 -0
- data/docs/serializer-support.md +25 -0
- data/lib/generators/rails_autodoc/install_generator.rb +34 -0
- data/lib/generators/rails_autodoc/templates/autodoc-verify.yml +19 -0
- data/lib/generators/rails_autodoc/templates/initializer.rb +20 -0
- data/lib/rails_autodoc/ast_traversal.rb +74 -0
- data/lib/rails_autodoc/configuration.rb +45 -0
- data/lib/rails_autodoc/dsl/controller_extensions.rb +65 -0
- data/lib/rails_autodoc/engine.rb +13 -0
- data/lib/rails_autodoc/generator.rb +54 -0
- data/lib/rails_autodoc/openapi_spec_builder.rb +334 -0
- data/lib/rails_autodoc/railtie.rb +25 -0
- data/lib/rails_autodoc/registry.rb +71 -0
- data/lib/rails_autodoc/response_inferencer.rb +158 -0
- data/lib/rails_autodoc/route_inspector.rb +139 -0
- data/lib/rails_autodoc/schema_mapper.rb +142 -0
- data/lib/rails_autodoc/serializers/active_model_serializer.rb +27 -0
- data/lib/rails_autodoc/serializers/alba.rb +39 -0
- data/lib/rails_autodoc/serializers/base.rb +19 -0
- data/lib/rails_autodoc/serializers/blueprinter.rb +27 -0
- data/lib/rails_autodoc/serializers/registry.rb +29 -0
- data/lib/rails_autodoc/strong_params_parser.rb +188 -0
- data/lib/rails_autodoc/tasks/autodoc.rake +26 -0
- data/lib/rails_autodoc/version.rb +5 -0
- data/lib/rails_autodoc.rb +47 -0
- data/mkdocs.yml +16 -0
- data/rails-autodoc.gemspec +61 -0
- data/spec/combustion/config.ru +4 -0
- data/spec/dummy/app/assets/config/manifest.js +1 -0
- data/spec/dummy/app/controllers/api/v1/users_controller.rb +45 -0
- data/spec/dummy/app/models/user.rb +5 -0
- data/spec/dummy/config/application.rb +14 -0
- data/spec/dummy/config/boot.rb +5 -0
- data/spec/dummy/config/database.yml +3 -0
- data/spec/dummy/config/environment.rb +5 -0
- data/spec/dummy/config/environments/test.rb +12 -0
- data/spec/dummy/config/initializers/rails_autodoc.rb +8 -0
- data/spec/dummy/config/initializers/sqlite3_boolean.rb +8 -0
- data/spec/dummy/config/routes.rb +11 -0
- data/spec/dummy/db/migrate/001_create_users.rb +14 -0
- data/spec/dummy/db/schema.rb +12 -0
- data/spec/rails_autodoc/configuration_spec.rb +34 -0
- data/spec/rails_autodoc/dsl_integration_spec.rb +77 -0
- data/spec/rails_autodoc/engine_spec.rb +26 -0
- data/spec/rails_autodoc/gem_spec.rb +27 -0
- data/spec/rails_autodoc/generator_spec.rb +39 -0
- data/spec/rails_autodoc/golden_spec.rb +67 -0
- data/spec/rails_autodoc/integration_spec.rb +114 -0
- data/spec/rails_autodoc/registry_spec.rb +26 -0
- data/spec/rails_autodoc/response_inferencer_spec.rb +26 -0
- data/spec/rails_autodoc/route_inspector_spec.rb +56 -0
- data/spec/rails_autodoc/schema_mapper_spec.rb +42 -0
- data/spec/rails_autodoc/serializers/registry_spec.rb +33 -0
- data/spec/rails_autodoc/strong_params_parser_spec.rb +41 -0
- data/spec/spec_helper.rb +43 -0
- metadata +320 -0
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "parser/current"
|
|
4
|
+
|
|
5
|
+
module RailsAutodoc
|
|
6
|
+
class StrongParamsParser
|
|
7
|
+
include AstTraversal
|
|
8
|
+
|
|
9
|
+
ParamsResult = Struct.new(:root_key, :schema, :method_name, keyword_init: true)
|
|
10
|
+
|
|
11
|
+
def initialize(source_path:, class_name:)
|
|
12
|
+
@source_path = source_path
|
|
13
|
+
@class_name = class_name
|
|
14
|
+
@buffer, = Parser::CurrentRuby.parse_file(source_path)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def param_methods
|
|
18
|
+
methods = {}
|
|
19
|
+
each_method_definition(class_node) do |child|
|
|
20
|
+
method_name = method_name_for(child)
|
|
21
|
+
next unless method_name.end_with?("_params")
|
|
22
|
+
|
|
23
|
+
schema = extract_permit_schema(child)
|
|
24
|
+
next if schema.nil?
|
|
25
|
+
|
|
26
|
+
methods[method_name] = ParamsResult.new(
|
|
27
|
+
root_key: schema[:root_key],
|
|
28
|
+
schema: schema[:properties],
|
|
29
|
+
method_name: method_name
|
|
30
|
+
)
|
|
31
|
+
end
|
|
32
|
+
methods
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def params_for_action(action_name)
|
|
36
|
+
action_node = find_action_node(action_name)
|
|
37
|
+
return nil unless action_node
|
|
38
|
+
|
|
39
|
+
called_methods = extract_called_param_methods(action_node)
|
|
40
|
+
called_methods.each do |method_name|
|
|
41
|
+
result = param_methods[method_name]
|
|
42
|
+
return result if result
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
param_methods.values.first
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def query_params_for_action(action_name)
|
|
49
|
+
action_node = find_action_node(action_name)
|
|
50
|
+
return [] unless action_node
|
|
51
|
+
|
|
52
|
+
params = []
|
|
53
|
+
walk_nodes(action_node) do |node|
|
|
54
|
+
next unless node.type == :send
|
|
55
|
+
|
|
56
|
+
method_name = node.children[1]
|
|
57
|
+
case method_name
|
|
58
|
+
when :[]
|
|
59
|
+
param_name = literal_value(node.children[2])
|
|
60
|
+
if param_name && node.children[0]&.type == :send && node.children[0].children[1] == :params
|
|
61
|
+
params << param_name.to_s
|
|
62
|
+
end
|
|
63
|
+
when :fetch
|
|
64
|
+
param_name = literal_value(node.children[2])
|
|
65
|
+
if param_name && node.children[0]&.type == :send && node.children[0].children[1] == :params
|
|
66
|
+
params << param_name.to_s
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
params.uniq.map do |name|
|
|
72
|
+
{ name: name, type: "string", required: false }
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
private
|
|
77
|
+
|
|
78
|
+
def class_node
|
|
79
|
+
@class_node ||= find_class_node(@buffer, class_name: @class_name)
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def find_action_node(action_name)
|
|
83
|
+
each_method_definition(class_node) do |child|
|
|
84
|
+
next unless child.type == :def
|
|
85
|
+
|
|
86
|
+
return child if child.children[0].to_s == action_name.to_s
|
|
87
|
+
end
|
|
88
|
+
nil
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def extract_called_param_methods(action_node)
|
|
92
|
+
methods = []
|
|
93
|
+
walk_nodes(action_node) do |node|
|
|
94
|
+
next unless node.type == :send
|
|
95
|
+
|
|
96
|
+
method_name = node.children[1]
|
|
97
|
+
methods << method_name.to_s if method_name.to_s.end_with?("_params")
|
|
98
|
+
end
|
|
99
|
+
methods.uniq
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def extract_permit_schema(method_node)
|
|
103
|
+
root_key = nil
|
|
104
|
+
properties = nil
|
|
105
|
+
|
|
106
|
+
walk_nodes(method_node) do |node|
|
|
107
|
+
next unless node.type == :send
|
|
108
|
+
|
|
109
|
+
method_name = node.children[1]
|
|
110
|
+
if method_name == :require
|
|
111
|
+
root_key = literal_value(node.children[2])
|
|
112
|
+
elsif method_name == :permit
|
|
113
|
+
properties = parse_permit_args(node.children[2..])
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
return nil unless properties
|
|
118
|
+
|
|
119
|
+
{ root_key: root_key, properties: properties }
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def parse_permit_args(args)
|
|
123
|
+
schema = { type: "object", properties: {}, required: [] }
|
|
124
|
+
|
|
125
|
+
args.each do |arg|
|
|
126
|
+
case arg.type
|
|
127
|
+
when :sym
|
|
128
|
+
field = arg.children[0].to_s
|
|
129
|
+
schema[:properties][field] = { type: "string" }
|
|
130
|
+
schema[:required] << field
|
|
131
|
+
when :hash
|
|
132
|
+
each_hash_pair(arg) do |key_node, value_node|
|
|
133
|
+
field = literal_value(key_node).to_s
|
|
134
|
+
schema[:properties][field] = parse_nested_permit_value(value_node)
|
|
135
|
+
schema[:required] << field
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
schema[:required].uniq!
|
|
141
|
+
schema
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def each_hash_pair(hash_node, &block)
|
|
145
|
+
hash_node.children.each do |pair_node|
|
|
146
|
+
next unless pair_node.type == :pair
|
|
147
|
+
|
|
148
|
+
yield pair_node.children[0], pair_node.children[1]
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
def parse_nested_permit_value(node)
|
|
153
|
+
case node.type
|
|
154
|
+
when :array
|
|
155
|
+
symbols = node.children.compact.select { |child| child.type == :sym }
|
|
156
|
+
if symbols.any?
|
|
157
|
+
properties = symbols.to_h do |sym|
|
|
158
|
+
[sym.children[0].to_s, { type: "string" }]
|
|
159
|
+
end
|
|
160
|
+
return {
|
|
161
|
+
type: "object",
|
|
162
|
+
properties: properties,
|
|
163
|
+
required: properties.keys
|
|
164
|
+
}
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
inner = node.children[0]
|
|
168
|
+
if inner&.type == :hash
|
|
169
|
+
parse_permit_args([inner])
|
|
170
|
+
else
|
|
171
|
+
{ type: "array", items: { type: "string" } }
|
|
172
|
+
end
|
|
173
|
+
when :hash
|
|
174
|
+
parse_permit_args([node])
|
|
175
|
+
else
|
|
176
|
+
{ type: "string" }
|
|
177
|
+
end
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
def literal_value(node)
|
|
181
|
+
case node&.type
|
|
182
|
+
when :sym then node.children[0]
|
|
183
|
+
when :str then node.children[0]
|
|
184
|
+
else node.to_s
|
|
185
|
+
end
|
|
186
|
+
end
|
|
187
|
+
end
|
|
188
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
namespace :autodoc do
|
|
4
|
+
desc "Generate OpenAPI specification from Rails routes and controllers"
|
|
5
|
+
task generate: :environment do
|
|
6
|
+
spec = RailsAutodoc::Generator.new.generate!
|
|
7
|
+
path = RailsAutodoc.config.resolved_output_path
|
|
8
|
+
puts "Generated OpenAPI spec at #{path}"
|
|
9
|
+
puts "Operations: #{spec.fetch('paths', {}).values.flat_map(&:keys).size}"
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
desc "Verify OpenAPI specification is up to date"
|
|
13
|
+
task verify: :environment do
|
|
14
|
+
RailsAutodoc::Generator.new.verify!
|
|
15
|
+
puts "OpenAPI spec is up to date."
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
desc "List inferred API operations"
|
|
19
|
+
task routes: :environment do
|
|
20
|
+
operations = RailsAutodoc::RouteInspector.new.operations
|
|
21
|
+
operations.each do |operation|
|
|
22
|
+
puts "#{operation.verb.ljust(7)} #{operation.openapi_path.ljust(40)} #{operation.controller_class.name}##{operation.action}"
|
|
23
|
+
end
|
|
24
|
+
puts "\nTotal: #{operations.size} operations"
|
|
25
|
+
end
|
|
26
|
+
end
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require "pathname"
|
|
5
|
+
require "yaml"
|
|
6
|
+
|
|
7
|
+
require "active_support"
|
|
8
|
+
require "active_support/core_ext/object/blank"
|
|
9
|
+
require "active_support/core_ext/string/inflections"
|
|
10
|
+
require "active_support/dependencies"
|
|
11
|
+
|
|
12
|
+
require_relative "rails_autodoc/version"
|
|
13
|
+
require_relative "rails_autodoc/ast_traversal"
|
|
14
|
+
require_relative "rails_autodoc/configuration"
|
|
15
|
+
require_relative "rails_autodoc/registry"
|
|
16
|
+
require_relative "rails_autodoc/route_inspector"
|
|
17
|
+
require_relative "rails_autodoc/strong_params_parser"
|
|
18
|
+
require_relative "rails_autodoc/schema_mapper"
|
|
19
|
+
require_relative "rails_autodoc/response_inferencer"
|
|
20
|
+
require_relative "rails_autodoc/serializers/registry"
|
|
21
|
+
require_relative "rails_autodoc/dsl/controller_extensions"
|
|
22
|
+
require_relative "rails_autodoc/openapi_spec_builder"
|
|
23
|
+
require_relative "rails_autodoc/generator"
|
|
24
|
+
|
|
25
|
+
module RailsAutodoc
|
|
26
|
+
class << self
|
|
27
|
+
def config
|
|
28
|
+
@config ||= Configuration.new
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def configure
|
|
32
|
+
yield config
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def registry
|
|
36
|
+
@registry ||= Registry.new
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def reset!
|
|
40
|
+
@config = Configuration.new
|
|
41
|
+
@registry = Registry.new
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
require_relative "rails_autodoc/railtie" if defined?(Rails)
|
|
47
|
+
require_relative "rails_autodoc/engine" if defined?(Rails)
|
data/mkdocs.yml
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
site_name: rails-autodoc
|
|
2
|
+
site_description: Auto-generate OpenAPI documentation from Rails conventions
|
|
3
|
+
theme:
|
|
4
|
+
name: material
|
|
5
|
+
nav:
|
|
6
|
+
- Home: index.md
|
|
7
|
+
- Getting Started: getting-started.md
|
|
8
|
+
- Configuration: configuration.md
|
|
9
|
+
- Inference Rules: inference-rules.md
|
|
10
|
+
- Annotation DSL: annotation-dsl.md
|
|
11
|
+
- Serializer Support: serializer-support.md
|
|
12
|
+
- CI Integration: ci-integration.md
|
|
13
|
+
- Limitations: limitations.md
|
|
14
|
+
- Migration from rswag: migration-from-rswag.md
|
|
15
|
+
- Architecture: architecture.md
|
|
16
|
+
- FAQ: faq.md
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "lib/rails_autodoc/version"
|
|
4
|
+
|
|
5
|
+
DEFAULT_GEM_FILES = Dir[
|
|
6
|
+
"{app,config,docs,lib}/**/*",
|
|
7
|
+
"README.md",
|
|
8
|
+
"CHANGELOG.md",
|
|
9
|
+
"LICENSE.txt",
|
|
10
|
+
"Rakefile"
|
|
11
|
+
].select { |file| File.file?(File.join(__dir__, file)) }.freeze
|
|
12
|
+
|
|
13
|
+
Gem::Specification.new do |spec|
|
|
14
|
+
spec.name = "rails-autodoc"
|
|
15
|
+
spec.version = RailsAutodoc::VERSION
|
|
16
|
+
spec.authors = ["Prajjwalkumar Panzade"]
|
|
17
|
+
spec.email = ["prajjwalbpanzade22@gmail.com"]
|
|
18
|
+
|
|
19
|
+
spec.summary = "Auto-generate OpenAPI documentation from Rails routes, strong params, and schemas"
|
|
20
|
+
spec.description = "Generate and serve OpenAPI 3.0 specs from Rails conventions with optional annotation overrides."
|
|
21
|
+
spec.homepage = "https://github.com/prajjwalkumarpanzade/rails-autodoc"
|
|
22
|
+
spec.license = "MIT"
|
|
23
|
+
spec.required_ruby_version = ">= 2.7.0"
|
|
24
|
+
|
|
25
|
+
spec.metadata["homepage_uri"] = spec.homepage
|
|
26
|
+
spec.metadata["source_code_uri"] = spec.homepage
|
|
27
|
+
spec.metadata["changelog_uri"] = "#{spec.homepage}/blob/main/CHANGELOG.md"
|
|
28
|
+
spec.metadata["rubygems_mfa_required"] = "true"
|
|
29
|
+
|
|
30
|
+
spec.files = Dir.chdir(__dir__) do
|
|
31
|
+
files = begin
|
|
32
|
+
`git ls-files -z`.split("\x0")
|
|
33
|
+
rescue StandardError
|
|
34
|
+
[]
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
files = DEFAULT_GEM_FILES if files.empty?
|
|
38
|
+
|
|
39
|
+
files.reject do |file|
|
|
40
|
+
file.start_with?("spec/fixtures/") || file.end_with?(".gem")
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
spec.bindir = "exe"
|
|
45
|
+
spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
|
|
46
|
+
spec.require_paths = ["lib"]
|
|
47
|
+
|
|
48
|
+
spec.add_dependency "activesupport", ">= 5.2", "< 9"
|
|
49
|
+
spec.add_dependency "parser", "~> 3.3"
|
|
50
|
+
spec.add_dependency "psych", ">= 3.1"
|
|
51
|
+
spec.add_dependency "railties", ">= 5.2", "< 9"
|
|
52
|
+
|
|
53
|
+
spec.add_development_dependency "appraisal"
|
|
54
|
+
spec.add_development_dependency "combustion", "~> 1.4"
|
|
55
|
+
spec.add_development_dependency "rails", ">= 5.2", "< 9"
|
|
56
|
+
spec.add_development_dependency "rspec", "~> 3.12"
|
|
57
|
+
spec.add_development_dependency "rspec-rails"
|
|
58
|
+
spec.add_development_dependency "rubocop", "~> 1.60"
|
|
59
|
+
spec.add_development_dependency "sqlite3", ">= 1.4", "< 1.7"
|
|
60
|
+
spec.add_development_dependency "webmock"
|
|
61
|
+
end
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
//= link_tree ../images
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Api
|
|
4
|
+
module V1
|
|
5
|
+
class UsersController < ActionController::API
|
|
6
|
+
def index
|
|
7
|
+
render json: User.all
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def show
|
|
11
|
+
user = User.find(params[:id])
|
|
12
|
+
render json: user
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def create
|
|
16
|
+
user = User.new(user_params)
|
|
17
|
+
if user.save
|
|
18
|
+
render json: user, status: :created
|
|
19
|
+
else
|
|
20
|
+
render json: { errors: user.errors.full_messages }, status: :unprocessable_entity
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def update
|
|
25
|
+
user = User.find(params[:id])
|
|
26
|
+
if user.update(user_params)
|
|
27
|
+
render json: user
|
|
28
|
+
else
|
|
29
|
+
render json: { errors: user.errors.full_messages }, status: :unprocessable_entity
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def destroy
|
|
34
|
+
User.find(params[:id]).destroy!
|
|
35
|
+
head :no_content
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
private
|
|
39
|
+
|
|
40
|
+
def user_params
|
|
41
|
+
params.require(:user).permit(:name, :email, :age, address: %i[street city], tags: [])
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rails/all"
|
|
4
|
+
Bundler.require(*Rails.groups)
|
|
5
|
+
require "rails_autodoc"
|
|
6
|
+
|
|
7
|
+
module Dummy
|
|
8
|
+
class Application < Rails::Application
|
|
9
|
+
config.load_defaults Rails::VERSION::STRING.to_f >= 7.0 ? "7.1" : "6.1"
|
|
10
|
+
config.eager_load = false
|
|
11
|
+
config.secret_key_base = "0" * 64
|
|
12
|
+
config.active_record.maintain_test_schema = true
|
|
13
|
+
end
|
|
14
|
+
end
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
Rails.application.configure do
|
|
4
|
+
config.cache_classes = false
|
|
5
|
+
config.eager_load = false
|
|
6
|
+
config.public_file_server.enabled = true
|
|
7
|
+
config.public_file_server.headers = { "Cache-Control" => "public, max-age=3600" }
|
|
8
|
+
config.consider_all_requests_local = true
|
|
9
|
+
config.action_controller.perform_caching = false
|
|
10
|
+
config.active_support.deprecation = :stderr
|
|
11
|
+
config.active_record.migration_error = :page_load
|
|
12
|
+
end
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Rails 5.2 stores sqlite booleans as 't'/'f' by default; use integer 1/0 instead.
|
|
4
|
+
if Gem::Version.new(Rails.version) < Gem::Version.new("6.0")
|
|
5
|
+
ActiveSupport.on_load(:active_record_sqlite3adapter) do
|
|
6
|
+
ActiveRecord::ConnectionAdapters::SQLite3Adapter.represent_boolean_as_integer = true
|
|
7
|
+
end
|
|
8
|
+
end
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class CreateUsers < ActiveRecord::Migration[6.1]
|
|
4
|
+
def change
|
|
5
|
+
create_table :users do |t|
|
|
6
|
+
t.string :name, null: false
|
|
7
|
+
t.string :email, null: false
|
|
8
|
+
t.integer :age
|
|
9
|
+
t.boolean :active, default: true, null: false
|
|
10
|
+
|
|
11
|
+
t.timestamps
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
ActiveRecord::Schema.define(version: 1) do
|
|
4
|
+
create_table "users", force: :cascade do |t|
|
|
5
|
+
t.string "name", null: false
|
|
6
|
+
t.string "email", null: false
|
|
7
|
+
t.integer "age"
|
|
8
|
+
t.boolean "active", default: true, null: false
|
|
9
|
+
t.datetime "created_at", null: false
|
|
10
|
+
t.datetime "updated_at", null: false
|
|
11
|
+
end
|
|
12
|
+
end
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "spec_helper"
|
|
4
|
+
|
|
5
|
+
RSpec.describe RailsAutodoc::Configuration do
|
|
6
|
+
subject(:config) { described_class.new }
|
|
7
|
+
|
|
8
|
+
it "defines sensible defaults" do
|
|
9
|
+
expect(config.title).to eq("Rails API")
|
|
10
|
+
expect(config.version).to eq("1.0.0")
|
|
11
|
+
expect(config.mount_path).to eq("/api-docs")
|
|
12
|
+
expect(config.cache_spec_in_dev).to be(true)
|
|
13
|
+
expect(config.exclude_paths).not_to be_empty
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
it "matches excluded paths against patterns" do
|
|
17
|
+
expect(config.excluded_path?("/rails/info")).to be(true)
|
|
18
|
+
expect(config.excluded_path?("/api-docs/spec.json")).to be(true)
|
|
19
|
+
expect(config.excluded_path?("/api/v1/users")).to be(false)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
it "resolves output path from config when set" do
|
|
23
|
+
custom_path = Rails.root.join("tmp/custom-openapi.yaml")
|
|
24
|
+
config.output_path = custom_path
|
|
25
|
+
|
|
26
|
+
expect(config.resolved_output_path).to eq(custom_path)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
it "falls back to openapi/openapi.yaml under Rails.root" do
|
|
30
|
+
config.output_path = nil
|
|
31
|
+
|
|
32
|
+
expect(config.resolved_output_path).to eq(Rails.root.join("openapi/openapi.yaml"))
|
|
33
|
+
end
|
|
34
|
+
end
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "spec_helper"
|
|
4
|
+
|
|
5
|
+
RSpec.describe "RailsAutodoc annotation DSL" do
|
|
6
|
+
let(:generator) { RailsAutodoc::Generator.new }
|
|
7
|
+
|
|
8
|
+
before do
|
|
9
|
+
annotated_controller = Class.new(ActionController::API) do
|
|
10
|
+
include RailsAutodoc::DSL::ControllerExtensions
|
|
11
|
+
|
|
12
|
+
def self.name
|
|
13
|
+
"Api::V1::UsersController"
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def create
|
|
17
|
+
head :created
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
swagger_doc action: :create do
|
|
21
|
+
summary "Create a user"
|
|
22
|
+
description "Creates a user with validated params"
|
|
23
|
+
tag "Users"
|
|
24
|
+
deprecated true
|
|
25
|
+
body_param :role, :string, enum: %w[admin user]
|
|
26
|
+
query_param :include, :string, required: true
|
|
27
|
+
response 201, ref: "User", description: "Created"
|
|
28
|
+
response 422, ref: "ValidationError", description: "Invalid"
|
|
29
|
+
security :bearer_auth
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
stub_const("Api::V1::UsersController", annotated_controller)
|
|
34
|
+
|
|
35
|
+
RailsAutodoc.configure do |config|
|
|
36
|
+
config.default_security = nil
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
Rails.application.routes.draw do
|
|
40
|
+
namespace :api do
|
|
41
|
+
namespace :v1 do
|
|
42
|
+
resources :users, only: :create
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
it "applies DSL overrides to generated operations" do
|
|
49
|
+
operation = generator.generate.dig("paths", "/api/v1/users", "post")
|
|
50
|
+
|
|
51
|
+
expect(operation["summary"]).to eq("Create a user")
|
|
52
|
+
expect(operation["description"]).to eq("Creates a user with validated params")
|
|
53
|
+
expect(operation["deprecated"]).to be(true)
|
|
54
|
+
expect(operation["tags"]).to include("Users")
|
|
55
|
+
expect(operation["security"]).to eq([{ "bearer_auth" => [] }])
|
|
56
|
+
|
|
57
|
+
role_schema = operation.dig("requestBody", "content", "application/json", "schema", "properties", "role")
|
|
58
|
+
expect(role_schema).to include("type" => "string", "enum" => %w[admin user])
|
|
59
|
+
|
|
60
|
+
include_param = operation["parameters"].find { |entry| entry["name"] == "include" }
|
|
61
|
+
expect(include_param).to include("in" => "query", "required" => true)
|
|
62
|
+
|
|
63
|
+
expect(operation.dig("responses", "201", "description")).to eq("Created")
|
|
64
|
+
expect(operation.dig("responses", "201", "content", "application/json", "schema",
|
|
65
|
+
"$ref")).to eq("#/components/schemas/User")
|
|
66
|
+
expect(operation.dig("responses", "422", "content", "application/json", "schema",
|
|
67
|
+
"$ref")).to eq("#/components/schemas/ValidationError")
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
it "excludes operations when annotated with exclude" do
|
|
71
|
+
RailsAutodoc.registry.register(Api::V1::UsersController, :create) do
|
|
72
|
+
exclude true
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
expect(generator.generate.dig("paths", "/api/v1/users", "post")).to be_nil
|
|
76
|
+
end
|
|
77
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "spec_helper"
|
|
4
|
+
|
|
5
|
+
RSpec.describe "RailsAutodoc::Engine", type: :request do
|
|
6
|
+
before do
|
|
7
|
+
Rails.application.routes.draw do
|
|
8
|
+
mount RailsAutodoc::Engine => "/api-docs"
|
|
9
|
+
get "/health", to: proc { [200, {}, ["ok"]] }
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
it "serves Swagger UI" do
|
|
14
|
+
get "/api-docs/"
|
|
15
|
+
expect(response).to have_http_status(:ok)
|
|
16
|
+
expect(response.body).to include("swagger-ui")
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
it "serves generated OpenAPI JSON" do
|
|
20
|
+
get "/api-docs/spec.json"
|
|
21
|
+
expect(response).to have_http_status(:ok)
|
|
22
|
+
expect(response.media_type).to include("json")
|
|
23
|
+
body = JSON.parse(response.body)
|
|
24
|
+
expect(body["openapi"]).to eq("3.0.3")
|
|
25
|
+
end
|
|
26
|
+
end
|