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.
Files changed (81) hide show
  1. checksums.yaml +7 -0
  2. data/.github/workflows/ci.yml +63 -0
  3. data/.gitignore +16 -0
  4. data/.rspec +2 -0
  5. data/.rubocop.yml +81 -0
  6. data/.yardopts +3 -0
  7. data/Appraisals +26 -0
  8. data/CHANGELOG.md +20 -0
  9. data/CONTRIBUTING.md +54 -0
  10. data/Gemfile +10 -0
  11. data/Gemfile.lock +298 -0
  12. data/LICENSE.txt +21 -0
  13. data/README.md +111 -0
  14. data/Rakefile +26 -0
  15. data/app/controllers/rails_autodoc/spec_controller.rb +52 -0
  16. data/config/routes.rb +6 -0
  17. data/docs/annotation-dsl.md +56 -0
  18. data/docs/architecture.md +37 -0
  19. data/docs/ci-integration.md +32 -0
  20. data/docs/configuration.md +48 -0
  21. data/docs/faq.md +29 -0
  22. data/docs/getting-started.md +54 -0
  23. data/docs/index.md +21 -0
  24. data/docs/inference-rules.md +74 -0
  25. data/docs/limitations.md +34 -0
  26. data/docs/migration-from-rswag.md +42 -0
  27. data/docs/serializer-support.md +25 -0
  28. data/lib/generators/rails_autodoc/install_generator.rb +34 -0
  29. data/lib/generators/rails_autodoc/templates/autodoc-verify.yml +19 -0
  30. data/lib/generators/rails_autodoc/templates/initializer.rb +20 -0
  31. data/lib/rails_autodoc/ast_traversal.rb +74 -0
  32. data/lib/rails_autodoc/configuration.rb +45 -0
  33. data/lib/rails_autodoc/dsl/controller_extensions.rb +65 -0
  34. data/lib/rails_autodoc/engine.rb +13 -0
  35. data/lib/rails_autodoc/generator.rb +54 -0
  36. data/lib/rails_autodoc/openapi_spec_builder.rb +334 -0
  37. data/lib/rails_autodoc/railtie.rb +25 -0
  38. data/lib/rails_autodoc/registry.rb +71 -0
  39. data/lib/rails_autodoc/response_inferencer.rb +158 -0
  40. data/lib/rails_autodoc/route_inspector.rb +139 -0
  41. data/lib/rails_autodoc/schema_mapper.rb +142 -0
  42. data/lib/rails_autodoc/serializers/active_model_serializer.rb +27 -0
  43. data/lib/rails_autodoc/serializers/alba.rb +39 -0
  44. data/lib/rails_autodoc/serializers/base.rb +19 -0
  45. data/lib/rails_autodoc/serializers/blueprinter.rb +27 -0
  46. data/lib/rails_autodoc/serializers/registry.rb +29 -0
  47. data/lib/rails_autodoc/strong_params_parser.rb +188 -0
  48. data/lib/rails_autodoc/tasks/autodoc.rake +26 -0
  49. data/lib/rails_autodoc/version.rb +5 -0
  50. data/lib/rails_autodoc.rb +47 -0
  51. data/mkdocs.yml +16 -0
  52. data/rails-autodoc.gemspec +61 -0
  53. data/spec/combustion/config.ru +4 -0
  54. data/spec/dummy/app/assets/config/manifest.js +1 -0
  55. data/spec/dummy/app/controllers/api/v1/users_controller.rb +45 -0
  56. data/spec/dummy/app/models/user.rb +5 -0
  57. data/spec/dummy/config/application.rb +14 -0
  58. data/spec/dummy/config/boot.rb +5 -0
  59. data/spec/dummy/config/database.yml +3 -0
  60. data/spec/dummy/config/environment.rb +5 -0
  61. data/spec/dummy/config/environments/test.rb +12 -0
  62. data/spec/dummy/config/initializers/rails_autodoc.rb +8 -0
  63. data/spec/dummy/config/initializers/sqlite3_boolean.rb +8 -0
  64. data/spec/dummy/config/routes.rb +11 -0
  65. data/spec/dummy/db/migrate/001_create_users.rb +14 -0
  66. data/spec/dummy/db/schema.rb +12 -0
  67. data/spec/rails_autodoc/configuration_spec.rb +34 -0
  68. data/spec/rails_autodoc/dsl_integration_spec.rb +77 -0
  69. data/spec/rails_autodoc/engine_spec.rb +26 -0
  70. data/spec/rails_autodoc/gem_spec.rb +27 -0
  71. data/spec/rails_autodoc/generator_spec.rb +39 -0
  72. data/spec/rails_autodoc/golden_spec.rb +67 -0
  73. data/spec/rails_autodoc/integration_spec.rb +114 -0
  74. data/spec/rails_autodoc/registry_spec.rb +26 -0
  75. data/spec/rails_autodoc/response_inferencer_spec.rb +26 -0
  76. data/spec/rails_autodoc/route_inspector_spec.rb +56 -0
  77. data/spec/rails_autodoc/schema_mapper_spec.rb +42 -0
  78. data/spec/rails_autodoc/serializers/registry_spec.rb +33 -0
  79. data/spec/rails_autodoc/strong_params_parser_spec.rb +41 -0
  80. data/spec/spec_helper.rb +43 -0
  81. 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,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RailsAutodoc
4
+ VERSION = "0.1.0"
5
+ 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,4 @@
1
+ # frozen_string_literal: true
2
+
3
+ require File.expand_path("../config/environment", __dir__)
4
+ run Rails.application
@@ -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,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ class User < ActiveRecord::Base
4
+ validates :name, :email, presence: true
5
+ 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,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../../Gemfile", __dir__)
4
+
5
+ require "bundler/setup"
@@ -0,0 +1,3 @@
1
+ test:
2
+ adapter: sqlite3
3
+ database: ":memory:"
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "application"
4
+
5
+ Rails.application.initialize!
@@ -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
+ RailsAutodoc.configure do |config|
4
+ config.title = "Dummy API"
5
+ config.version = "1.0.0"
6
+ config.output_path = Rails.root.join("tmp/openapi.yaml")
7
+ config.exclude_paths = [%r{^/rails/}, %r{^/api-docs}]
8
+ 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,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ Rails.application.routes.draw do
4
+ mount RailsAutodoc::Engine => "/api-docs"
5
+
6
+ namespace :api do
7
+ namespace :v1 do
8
+ resources :users, only: %i[index show create update destroy]
9
+ end
10
+ end
11
+ 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