Skip to content

Commit 28c50fb

Browse files
committed
Integrate rspec-openapi for automatic OpenAPI spec generation
Add rspec-openapi to Gemfile and configure it in spec/support/openapi.rb to regenerate docs/openapi.yml from request specs when running with OPENAPI=1. The gem merges actual JSON:API responses into the existing spec file, preserving hand-written component schemas. Also includes docs/openapi.yml in the gemspec file list and documents the workflow in the README. https://claude.ai/code/session_01F4qY2LA6JYqsvdLi4xkLVA
1 parent 3bcd707 commit 28c50fb

5 files changed

Lines changed: 79 additions & 1 deletion

File tree

Gemfile

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,3 +26,5 @@ gem "standard", "~> 1.25", require: false
2626
gem "pry-byebug"
2727

2828
gem "propshaft", "~> 1.3"
29+
30+
gem "rspec-openapi", require: false

README.md

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,30 @@ Alchemy::JsonApi.key_transform = :camel_lower
9797

9898
It defaults to `:underscore`.
9999

100+
## OpenAPI Documentation
101+
102+
The API is documented with an OpenAPI 3.0 spec at `docs/openapi.yml`. When the
103+
engine is mounted, the spec is served as JSON at:
104+
105+
```
106+
GET /jsonapi/openapi.json
107+
```
108+
109+
Point Swagger UI, Redoc, or any OpenAPI client generator at this URL.
110+
111+
### Regenerating the spec
112+
113+
The spec is auto-generated from request specs using
114+
[rspec-openapi](https://github.com/exoego/rspec-openapi). To update it after
115+
changing endpoints or serializers:
116+
117+
```bash
118+
OPENAPI=1 bundle exec rspec
119+
```
120+
121+
This merges actual API responses into `docs/openapi.yml`. Hand-written component
122+
schemas are preserved. Review the diff and commit the result.
123+
100124
## Contributing
101125

102126
Contribution directions go here.

alchemy-json_api.gemspec

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ Gem::Specification.new do |spec|
1616
spec.description = "A JSONAPI compliant API for AlchemyCMS"
1717
spec.license = "BSD-3-Clause"
1818

19-
spec.files = Dir["{app,config,db,lib}/**/*", "LICENSE", "Rakefile", "README.md"]
19+
spec.files = Dir["{app,config,db,docs,lib}/**/*", "LICENSE", "Rakefile", "README.md"]
2020

2121
spec.add_dependency "alchemy_cms", [">= 8.2.0.a", "< 9"]
2222
spec.add_dependency "jsonapi.rb", [">= 1.6.0", "< 2.2"]

spec/rails_helper.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
require "shoulda-matchers"
1414

1515
require "alchemy/json_api/test_support/ingredient_serializer_behaviour"
16+
require_relative "support/openapi"
1617

1718
Shoulda::Matchers.configure do |config|
1819
config.integrate do |with|

spec/support/openapi.rb

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
# frozen_string_literal: true
2+
3+
if ENV["OPENAPI"]
4+
require "rspec/openapi"
5+
6+
RSpec::OpenAPI.path = File.expand_path("../../docs/openapi.yml", __dir__)
7+
RSpec::OpenAPI.title = "Alchemy CMS JSON API"
8+
RSpec::OpenAPI.application_version = Alchemy::JsonApi::VERSION
9+
RSpec::OpenAPI.info = {
10+
description: <<~DESC.strip,
11+
A JSON:API compliant API for AlchemyCMS.
12+
13+
All responses follow the JSON:API specification. Use the `include` query parameter to
14+
sideload related resources, `filter` for Ransack-based filtering, and `page` for pagination.
15+
DESC
16+
license: {
17+
name: "BSD-3-Clause",
18+
url: "https://opensource.org/licenses/BSD-3-Clause"
19+
},
20+
contact: {
21+
name: "AlchemyCMS",
22+
url: "https://www.alchemy-cms.com/"
23+
}
24+
}
25+
26+
RSpec::OpenAPI.comment = <<~COMMENT
27+
This file is auto-generated by rspec-openapi.
28+
Run `OPENAPI=1 bundle exec rspec` to regenerate.
29+
COMMENT
30+
31+
RSpec::OpenAPI.servers = [{url: "/jsonapi", description: "Mounted engine path (default)"}]
32+
RSpec::OpenAPI.response_headers = %w[ETag Cache-Control Last-Modified]
33+
RSpec::OpenAPI.example_types = %i[request]
34+
35+
# Exclude the openapi endpoint itself from generation
36+
RSpec::OpenAPI.ignored_paths = [/openapi/]
37+
38+
# Tag endpoints by controller name
39+
RSpec::OpenAPI.tags_builder = ->(example) {
40+
controller = example.metadata.dig(:request, :controller) ||
41+
example.metadata[:described_class].to_s
42+
case controller
43+
when /Admin::LayoutPages/ then ["Admin Layout Pages"]
44+
when /Admin::Pages/ then ["Admin Pages"]
45+
when /LayoutPages/ then ["Layout Pages"]
46+
when /Nodes/ then ["Nodes"]
47+
when /Pages/ then ["Pages"]
48+
else []
49+
end
50+
}
51+
end

0 commit comments

Comments
 (0)