openapi-ruby 4.1.0 → 5.0.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 +4 -4
- data/README.md +252 -11
- data/Rakefile +32 -0
- data/app/controllers/openapi_ruby/schemas_controller.rb +4 -45
- data/app/controllers/openapi_ruby/ui_controller.rb +11 -68
- data/lib/generators/openapi_ruby/component/component_generator.rb +21 -1
- data/lib/openapi_ruby/adapters/context_resolution.rb +1 -1
- data/lib/openapi_ruby/adapters/minitest.rb +35 -8
- data/lib/openapi_ruby/adapters/rspec.rb +46 -15
- data/lib/openapi_ruby/components/loader.rb +15 -0
- data/lib/openapi_ruby/configuration.rb +13 -1
- data/lib/openapi_ruby/core/document_builder.rb +1 -0
- data/lib/openapi_ruby/dsl/context.rb +1 -1
- data/lib/openapi_ruby/dsl/operation_context.rb +1 -1
- data/lib/openapi_ruby/engine.rb +1 -38
- data/lib/openapi_ruby/generator/rake_task_support.rb +43 -1
- data/lib/openapi_ruby/hanami.rb +69 -0
- data/lib/openapi_ruby/host.rb +53 -0
- data/lib/openapi_ruby/middleware/installer.rb +50 -0
- data/lib/openapi_ruby/parameter_names.rb +89 -0
- data/lib/openapi_ruby/rack_app.rb +94 -0
- data/lib/openapi_ruby/rake_tasks.rb +39 -0
- data/lib/openapi_ruby/serving.rb +135 -0
- data/lib/openapi_ruby/testing/request_builder.rb +1 -1
- data/lib/openapi_ruby/testing/request_validator.rb +15 -4
- data/lib/openapi_ruby/testing/transport.rb +99 -0
- data/lib/openapi_ruby/version.rb +1 -1
- data/lib/openapi_ruby.rb +6 -0
- data/lib/tasks/openapi_ruby.rake +1 -19
- metadata +14 -20
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: f937b3be2ac21a6d99687ca9ae2e0a625f89747c597fca146a7f04398d55cedc
|
|
4
|
+
data.tar.gz: e185d294c49caf0f33b1ad4cfc4a0512968f94ecb8f0f8184affcdd69230c94c
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 7509404bf36980da8c58fde64c22b868e8216aaba20a13849904eedda75245f5015fece24a757e4d7c9c5b40c420554c9538afa30ab591e6b93086cce0c85928
|
|
7
|
+
data.tar.gz: 1a00f2de46c1d363686952541948bc20930bd27397a66381923c1a01262e833eedcc0e3298c3601d0c84ae288ead7a754f73825fca0d292711588823be3469bc
|
data/README.md
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
<h1 align="center">openapi_ruby</h1>
|
|
6
6
|
|
|
7
7
|
<p align="center">
|
|
8
|
-
A unified OpenAPI toolkit for Rails that combines test-driven spec generation, reusable schema components as Ruby classes, and runtime request/response validation middleware. Supports OpenAPI 3.0 and 3.1. Works with both RSpec and Minitest.
|
|
8
|
+
A unified OpenAPI toolkit for Rails, Hanami, and Rack that combines test-driven spec generation, reusable schema components as Ruby classes, and runtime request/response validation middleware. Supports OpenAPI 3.0 and 3.1. Works with both RSpec and Minitest.
|
|
9
9
|
</p>
|
|
10
10
|
|
|
11
11
|
Replaces [rswag](https://github.com/rswag/rswag), [rswag-schema-components](https://github.com/101skills-gmbh/rswag-schema-components), and [committee](https://github.com/interagent/committee) with a single gem.
|
|
@@ -14,6 +14,7 @@ Replaces [rswag](https://github.com/rswag/rswag), [rswag-schema-components](http
|
|
|
14
14
|
|
|
15
15
|
- **OpenAPI 3.0 & 3.1** with JSON Schema 2020-12 (via [json_schemer](https://github.com/davishmcclurg/json_schemer))
|
|
16
16
|
- **Test-framework agnostic** — works with RSpec and Minitest
|
|
17
|
+
- **Host-framework agnostic** — works on Rails, Hanami, Sinatra, and bare Rack
|
|
17
18
|
- **Schema components** as Ruby classes with inheritance
|
|
18
19
|
- **Runtime middleware** for request/response validation with deep type checking
|
|
19
20
|
- **Strong params** derived from schema components
|
|
@@ -23,7 +24,7 @@ Replaces [rswag](https://github.com/rswag/rswag), [rswag-schema-components](http
|
|
|
23
24
|
## Requirements
|
|
24
25
|
|
|
25
26
|
- Ruby >= 3.2
|
|
26
|
-
- Rails >= 7.0
|
|
27
|
+
- Rails >= 7.0, Hanami >= 2.3, or any Rack app (see [Host Frameworks](#host-frameworks))
|
|
27
28
|
|
|
28
29
|
## Installation
|
|
29
30
|
|
|
@@ -47,6 +48,8 @@ This creates:
|
|
|
47
48
|
- `openapi/` — output directory for generated specs
|
|
48
49
|
- Engine mount in `config/routes.rb`
|
|
49
50
|
|
|
51
|
+
The install generator is Rails-only — see [Host Frameworks](#host-frameworks) for the equivalent setup elsewhere.
|
|
52
|
+
|
|
50
53
|
## Configuration
|
|
51
54
|
|
|
52
55
|
```ruby
|
|
@@ -179,6 +182,37 @@ class Schemas::User
|
|
|
179
182
|
end
|
|
180
183
|
```
|
|
181
184
|
|
|
185
|
+
#### Parameter names
|
|
186
|
+
|
|
187
|
+
The same setting covers parameter names, so parameters match the component keys instead of contradicting them. Declare them in snake_case — that keeps `let(:page_size)` and `params: {page_size: 20}` idiomatic Ruby — and the camelCased name is what reaches both the document and the request:
|
|
188
|
+
|
|
189
|
+
```ruby
|
|
190
|
+
api_path "/users/{user_id}" do
|
|
191
|
+
parameter name: :user_id, in: :path, schema: { type: :integer }
|
|
192
|
+
|
|
193
|
+
get "List posts" do
|
|
194
|
+
parameter name: :page_size, in: :query, schema: { type: :integer }
|
|
195
|
+
# ...
|
|
196
|
+
end
|
|
197
|
+
end
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
```yaml
|
|
201
|
+
paths:
|
|
202
|
+
/users/{userId}: # template variables travel with the parameter names
|
|
203
|
+
parameters:
|
|
204
|
+
- name: userId
|
|
205
|
+
in: path
|
|
206
|
+
get:
|
|
207
|
+
parameters:
|
|
208
|
+
- name: pageSize
|
|
209
|
+
in: query
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
The test DSL sends what the document promises: `GET /users/7?pageSize=20`. Header parameters keep their declared spelling — HTTP header names are conventionally hyphenated and matched case-insensitively, so camelizing them would rename headers nobody asked to rename.
|
|
213
|
+
|
|
214
|
+
If your API takes snake_case parameters, set `config.camelize_keys = false` (or keep the parameter names camelCased in the declaration, which passes through unchanged either way).
|
|
215
|
+
|
|
182
216
|
### Scopes
|
|
183
217
|
|
|
184
218
|
Assign components to scopes for multiple API specs:
|
|
@@ -250,6 +284,8 @@ rails generate openapi_ruby:component User schemas
|
|
|
250
284
|
rails generate openapi_ruby:component BearerAuth security_schemes
|
|
251
285
|
```
|
|
252
286
|
|
|
287
|
+
Files land in the first entry of `config.component_paths`, so the generator follows whatever the loader reads.
|
|
288
|
+
|
|
253
289
|
## Testing with RSpec
|
|
254
290
|
|
|
255
291
|
```ruby
|
|
@@ -497,15 +533,29 @@ require "rails/test_help"
|
|
|
497
533
|
# ...other test-time setup...
|
|
498
534
|
```
|
|
499
535
|
|
|
500
|
-
This is purely an optimization — generation is already correct and database-free without it.
|
|
536
|
+
This is purely an optimization — generation is already correct and database-free without it. Reach for it only if generation is slow enough to bother you.
|
|
501
537
|
|
|
502
|
-
|
|
538
|
+
#### When guarding backfires
|
|
503
539
|
|
|
504
|
-
|
|
505
|
-
PATTERN="test/integration/api/**/*_test.rb" rake openapi_ruby:generate
|
|
506
|
-
```
|
|
540
|
+
The guard skips a require, so anything that require defines is gone for the whole generation run. That is fine for setup your files only touch while *running*, and fatal for anything they touch while *loading* — a file that fails to load contributes no declarations, and generation fails outright.
|
|
507
541
|
|
|
508
|
-
|
|
542
|
+
Loading a spec/test file executes its class body, so these all break under a guard:
|
|
543
|
+
|
|
544
|
+
- `fixtures :all` — `fixtures` is undefined without `rails/test_help`
|
|
545
|
+
- `include Devise::Test::IntegrationHelpers` — needs `rspec/rails` (or `rails/test_help`) already loaded
|
|
546
|
+
- `it_behaves_like "..."` / `include_examples` at the top level — needs the shared examples your helper loaded
|
|
547
|
+
|
|
548
|
+
Two ways out, and they compose:
|
|
549
|
+
|
|
550
|
+
1. **Narrow `PATTERN`** to just the files carrying `path` / `api_path` declarations, so the files with load-time dependencies are never loaded:
|
|
551
|
+
|
|
552
|
+
```bash
|
|
553
|
+
PATTERN="test/integration/api/**/*_test.rb" rake openapi_ruby:generate
|
|
554
|
+
```
|
|
555
|
+
|
|
556
|
+
2. **Leave that helper unguarded.** A helper whose constants are referenced at load time across the suite is often not worth guarding — you would be trading a working generation run for a faster one. Guarding is optional per helper; guard `test/test_helper.rb` and leave `spec/rails_helper.rb` alone if that is what your suite needs.
|
|
557
|
+
|
|
558
|
+
Suites using FactoryBot rather than fixtures, and keeping helper includes inside `before` blocks, tend not to hit any of this.
|
|
509
559
|
|
|
510
560
|
### How a request finds its api_path (Style 2)
|
|
511
561
|
|
|
@@ -570,7 +620,7 @@ declares a second path. Off by default.
|
|
|
570
620
|
|
|
571
621
|
When both `spec/spec_helper.rb` and `test/test_helper.rb` are present, the rake task auto-selects `FRAMEWORK=hybrid` — it requires both adapters and loads both glob patterns (`spec/**/*_spec.rb,test/**/*_test.rb`) into one process. Style 1 `path(...)` and Style 2 `api_path(...)` definitions register into the same `MetadataStore`, so a single schema file holds paths contributed by either DSL.
|
|
572
622
|
|
|
573
|
-
Here the guards described above
|
|
623
|
+
Here the guards described above carry more weight: without them both test frameworks wire themselves into Rails' lazy-load hooks in the same process. Guard what you can — but the "When guarding backfires" rules still apply, so a helper your suite leans on at load time stays unguarded even in hybrid mode.
|
|
574
624
|
|
|
575
625
|
```ruby
|
|
576
626
|
# test/test_helper.rb
|
|
@@ -590,7 +640,198 @@ end
|
|
|
590
640
|
|
|
591
641
|
`OpenapiRuby.schema_generating?` returns `true` when the rake task launched the current process (it sets `OPENAPI_RUBY_GENERATING=true` in the subprocess). With the guards in place, neither test framework boots its full Rails integration during generation — only the DSL needs to be live for `api_path` / `path` to register.
|
|
592
642
|
|
|
593
|
-
Once the migration completes and only one test framework remains, the rake task auto-detects that framework
|
|
643
|
+
Once the migration completes and only one test framework remains, the rake task auto-detects that framework, and the guard goes back to being a pure optimization.
|
|
644
|
+
|
|
645
|
+
## Host Frameworks
|
|
646
|
+
|
|
647
|
+
Rails picks up its wiring from the engine. Every other host makes the same calls itself — the gem only ever needs a Rack middleware stack, a way to mount a Rack app, and rack-test.
|
|
648
|
+
|
|
649
|
+
| | Rails | Hanami | Sinatra / Roda / bare Rack |
|
|
650
|
+
|---|---|---|---|
|
|
651
|
+
| Test DSL + generation | ✅ | ✅ | ✅ |
|
|
652
|
+
| Runtime validation middleware | automatic | one call | one call |
|
|
653
|
+
| Schema + Swagger UI endpoints | `mount OpenapiRuby::Engine` | `mount OpenapiRuby::RackApp` | `map`/`run OpenapiRuby::RackApp` |
|
|
654
|
+
| `openapi_ruby:install` / `:component` generators | ✅ | — | — |
|
|
655
|
+
| `openapi_permit` strong params | ✅ | — | — |
|
|
656
|
+
|
|
657
|
+
Versions covered by CI: Ruby 3.2–4.0, Rails 7.0–8.0, Hanami 2.3 and 3.0, Sinatra 3.2 and 4.2. Working reference apps live in [`spec/dummy`](spec/dummy), [`spec/hanami_dummy`](spec/hanami_dummy), and [`spec/sinatra_dummy`](spec/sinatra_dummy).
|
|
658
|
+
|
|
659
|
+
### Hanami
|
|
660
|
+
|
|
661
|
+
**1. Configure.** Anywhere that loads before your app class — `config/openapi_ruby.rb` is a natural home:
|
|
662
|
+
|
|
663
|
+
```ruby
|
|
664
|
+
require "openapi_ruby/hanami"
|
|
665
|
+
|
|
666
|
+
OpenapiRuby.configure do |config|
|
|
667
|
+
config.schemas = {
|
|
668
|
+
public_api: {
|
|
669
|
+
info: { title: "My API", version: "v1" },
|
|
670
|
+
servers: [{ url: "/api/v1" }],
|
|
671
|
+
prefix: "/api/v1" # scopes the validation middleware to the API
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
end
|
|
675
|
+
```
|
|
676
|
+
|
|
677
|
+
Components default to `config/api_components/` on Hanami instead of `app/api_components/`. Zeitwerk owns everything under `app/` and expects `app/api_components/schemas/user.rb` to define `MyApp::ApiComponents::Schemas::User`, while the component loader requires the file directly — which loads fine in tests and fails on eager load in production. Keeping components outside the autoload roots avoids the clash; openapi_ruby warns if `component_paths` points inside `app/`.
|
|
678
|
+
|
|
679
|
+
**2. Install the middleware** (only needed for runtime validation) in `config/app.rb`:
|
|
680
|
+
|
|
681
|
+
```ruby
|
|
682
|
+
require "hanami"
|
|
683
|
+
require_relative "openapi_ruby"
|
|
684
|
+
|
|
685
|
+
module MyApp
|
|
686
|
+
class App < Hanami::App
|
|
687
|
+
# Declared before :body_parser so the validation middleware reads and
|
|
688
|
+
# rewinds the request body first.
|
|
689
|
+
OpenapiRuby::Hanami.install_middleware!(config)
|
|
690
|
+
|
|
691
|
+
config.middleware.use :body_parser, :json
|
|
692
|
+
end
|
|
693
|
+
end
|
|
694
|
+
```
|
|
695
|
+
|
|
696
|
+
**3. Mount the docs endpoints** in `config/routes.rb`:
|
|
697
|
+
|
|
698
|
+
```ruby
|
|
699
|
+
module MyApp
|
|
700
|
+
class Routes < Hanami::Routes
|
|
701
|
+
mount OpenapiRuby::RackApp, at: "/api-docs"
|
|
702
|
+
end
|
|
703
|
+
end
|
|
704
|
+
```
|
|
705
|
+
|
|
706
|
+
For request specs, `require "openapi_ruby/rspec"` wires rack-test into `type: :openapi` example groups and points it at `Hanami.app`:
|
|
707
|
+
|
|
708
|
+
```ruby
|
|
709
|
+
# spec/spec_helper.rb
|
|
710
|
+
ENV["HANAMI_ENV"] ||= "test"
|
|
711
|
+
|
|
712
|
+
require "hanami/prepare"
|
|
713
|
+
require "openapi_ruby/rspec"
|
|
714
|
+
```
|
|
715
|
+
|
|
716
|
+
Define `let(:app)` in a group to drive a slice instead of the whole app.
|
|
717
|
+
|
|
718
|
+
### Sinatra, Roda, and bare Rack
|
|
719
|
+
|
|
720
|
+
Nothing here is Sinatra-specific — it is the same three steps against a plain Rack app.
|
|
721
|
+
|
|
722
|
+
**1. Configure**, and say where components live. There is no autoload convention to infer one from:
|
|
723
|
+
|
|
724
|
+
```ruby
|
|
725
|
+
# config/openapi_ruby.rb
|
|
726
|
+
require "openapi_ruby"
|
|
727
|
+
|
|
728
|
+
OpenapiRuby.configure do |config|
|
|
729
|
+
config.schemas = {
|
|
730
|
+
public_api: {
|
|
731
|
+
info: { title: "My API", version: "v1" },
|
|
732
|
+
servers: [{ url: "/api/v1" }],
|
|
733
|
+
prefix: "/api/v1"
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
config.component_paths = ["api_components"]
|
|
738
|
+
end
|
|
739
|
+
```
|
|
740
|
+
|
|
741
|
+
**2. Install the middleware** onto the app's stack. `Installer#install!` takes anything that responds to `use`, which a `Sinatra::Base` subclass does:
|
|
742
|
+
|
|
743
|
+
```ruby
|
|
744
|
+
class App < Sinatra::Base
|
|
745
|
+
OpenapiRuby::Middleware::Installer.install!(self, root: __dir__)
|
|
746
|
+
|
|
747
|
+
# ... routes
|
|
748
|
+
end
|
|
749
|
+
```
|
|
750
|
+
|
|
751
|
+
For Roda or bare Rack, hand it the builder instead — `OpenapiRuby::Middleware::Installer.install!(builder, root: __dir__)` inside `Rack::Builder.new { ... }`.
|
|
752
|
+
|
|
753
|
+
**3. Mount the docs endpoints** in `config.ru`:
|
|
754
|
+
|
|
755
|
+
```ruby
|
|
756
|
+
require_relative "app"
|
|
757
|
+
|
|
758
|
+
map "/api-docs" do
|
|
759
|
+
run OpenapiRuby::RackApp
|
|
760
|
+
end
|
|
761
|
+
|
|
762
|
+
map "/" do
|
|
763
|
+
run App
|
|
764
|
+
end
|
|
765
|
+
```
|
|
766
|
+
|
|
767
|
+
For request specs, `require "openapi_ruby/rspec"` includes rack-test for you; naming the app is the only wiring left, since no Rack host has a convention for which app is under test:
|
|
768
|
+
|
|
769
|
+
```ruby
|
|
770
|
+
# spec/spec_helper.rb
|
|
771
|
+
ENV["APP_ENV"] ||= "test"
|
|
772
|
+
|
|
773
|
+
require_relative "../app"
|
|
774
|
+
require "openapi_ruby/rspec"
|
|
775
|
+
|
|
776
|
+
module AppUnderTest
|
|
777
|
+
def app
|
|
778
|
+
App
|
|
779
|
+
end
|
|
780
|
+
end
|
|
781
|
+
|
|
782
|
+
RSpec.configure do |config|
|
|
783
|
+
config.include AppUnderTest, type: :openapi
|
|
784
|
+
end
|
|
785
|
+
```
|
|
786
|
+
|
|
787
|
+
Minitest is the same shape — including the DSL brings rack-test with it, and the class defines `app`:
|
|
788
|
+
|
|
789
|
+
```ruby
|
|
790
|
+
class ApiTest < Minitest::Test
|
|
791
|
+
include OpenapiRuby::Adapters::Minitest::DSL
|
|
792
|
+
|
|
793
|
+
def app
|
|
794
|
+
App
|
|
795
|
+
end
|
|
796
|
+
end
|
|
797
|
+
```
|
|
798
|
+
|
|
799
|
+
> **Getting 403 "Host not permitted"?** Sinatra only relaxes its host authorization outside `development`, and rack-test sends `Host: example.org`. Set `APP_ENV=test` (or `RACK_ENV=test`) in your test helper — the snippet above does.
|
|
800
|
+
|
|
801
|
+
### Specs and generation on every host
|
|
802
|
+
|
|
803
|
+
Specs are written identically regardless of host, in either DSL style:
|
|
804
|
+
|
|
805
|
+
```ruby
|
|
806
|
+
RSpec.describe "Posts API", type: :openapi do
|
|
807
|
+
openapi_schema :public_api
|
|
808
|
+
|
|
809
|
+
api_path "/posts" do
|
|
810
|
+
get "List posts" do
|
|
811
|
+
tags "Posts"
|
|
812
|
+
produces "application/json"
|
|
813
|
+
|
|
814
|
+
response 200, "returns posts" do
|
|
815
|
+
schema type: :array, items: { "$ref" => "#/components/schemas/Post" }
|
|
816
|
+
end
|
|
817
|
+
end
|
|
818
|
+
end
|
|
819
|
+
|
|
820
|
+
it "returns all posts" do
|
|
821
|
+
assert_api_response :get, 200 do
|
|
822
|
+
expect(parsed_body.length).to eq(2)
|
|
823
|
+
end
|
|
824
|
+
end
|
|
825
|
+
end
|
|
826
|
+
```
|
|
827
|
+
|
|
828
|
+
The Rails engine loads the rake task on its own. Elsewhere, add it to your Rakefile:
|
|
829
|
+
|
|
830
|
+
```ruby
|
|
831
|
+
require "openapi_ruby/rake_tasks"
|
|
832
|
+
```
|
|
833
|
+
|
|
834
|
+
`bundle exec rake openapi_ruby:generate` then behaves as it does on Rails. It detects the host and sets the environment variable that host reads — `RAILS_ENV`, `HANAMI_ENV`, or `APP_ENV`/`RACK_ENV` — for the generation subprocess.
|
|
594
835
|
|
|
595
836
|
## Runtime Middleware
|
|
596
837
|
|
|
@@ -632,7 +873,7 @@ Mount the engine to expose the schema endpoints:
|
|
|
632
873
|
mount OpenapiRuby::Engine => "/api-docs"
|
|
633
874
|
```
|
|
634
875
|
|
|
635
|
-
Schema files are served at `/api-docs/schemas/:name`.
|
|
876
|
+
Schema files are served at `/api-docs/schemas/:name`. On any other host, mount `OpenapiRuby::RackApp` instead — see [Host Frameworks](#host-frameworks).
|
|
636
877
|
|
|
637
878
|
To also serve the interactive Swagger UI at the mount root, opt in:
|
|
638
879
|
|
data/Rakefile
CHANGED
|
@@ -5,4 +5,36 @@ require "rspec/core/rake_task"
|
|
|
5
5
|
|
|
6
6
|
RSpec::Core::RakeTask.new(:spec)
|
|
7
7
|
|
|
8
|
+
# Each alternate host's dummy app runs in its own bundle, so Rails and the
|
|
9
|
+
# other framework never have to boot in one process.
|
|
10
|
+
#
|
|
11
|
+
# Unbundled: Bundler exports BUNDLE_LOCKFILE next to BUNDLE_GEMFILE, and an
|
|
12
|
+
# inherited one makes the child write its resolution into this bundle's
|
|
13
|
+
# Gemfile.lock.
|
|
14
|
+
def run_dummy_app_specs(gemfile, dir, command)
|
|
15
|
+
gemfile_path = File.expand_path("gemfiles/#{gemfile}", __dir__)
|
|
16
|
+
|
|
17
|
+
Bundler.with_unbundled_env do
|
|
18
|
+
Dir.chdir(dir) do
|
|
19
|
+
sh({"BUNDLE_GEMFILE" => gemfile_path}, command)
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
namespace :spec do
|
|
25
|
+
desc "Run the Hanami dummy app's specs in the Rails-free bundle"
|
|
26
|
+
task :hanami do
|
|
27
|
+
run_dummy_app_specs("hanami.gemfile", "spec/hanami_dummy", "bundle exec rspec")
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
desc "Run the Sinatra dummy app's specs and tests in the Rails-free bundle"
|
|
31
|
+
task :sinatra do
|
|
32
|
+
run_dummy_app_specs("sinatra.gemfile", "spec/sinatra_dummy", "bundle exec rspec")
|
|
33
|
+
run_dummy_app_specs("sinatra.gemfile", "spec/sinatra_dummy", "bundle exec ruby -Itest test/posts_test.rb")
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
desc "Run every host's dummy app suite"
|
|
37
|
+
task hosts: %i[hanami sinatra]
|
|
38
|
+
end
|
|
39
|
+
|
|
8
40
|
task default: :spec
|
|
@@ -3,56 +3,15 @@
|
|
|
3
3
|
module OpenapiRuby
|
|
4
4
|
class SchemasController < ActionController::API
|
|
5
5
|
def show
|
|
6
|
-
|
|
7
|
-
|
|
6
|
+
document = Serving.schema_document(params[:id], request: request)
|
|
7
|
+
return head :not_found unless document
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
return head :not_found unless schema_config
|
|
11
|
-
|
|
12
|
-
file_path = schema_file_path(schema_name)
|
|
13
|
-
return head :not_found unless file_path && File.exist?(file_path)
|
|
14
|
-
|
|
15
|
-
content = File.read(file_path)
|
|
16
|
-
|
|
17
|
-
# Apply filter if configured
|
|
18
|
-
if schema_config[:openapi_filter]
|
|
19
|
-
doc = parse_content(file_path, content)
|
|
20
|
-
schema_config[:openapi_filter].call(doc, request)
|
|
21
|
-
content = serialize_doc(file_path, doc)
|
|
22
|
-
end
|
|
23
|
-
|
|
24
|
-
content_type = file_path.end_with?(".json") ? "application/json" : "application/x-yaml"
|
|
9
|
+
content, content_type = document
|
|
25
10
|
render plain: content, content_type: content_type
|
|
26
11
|
end
|
|
27
12
|
|
|
28
13
|
def index
|
|
29
|
-
|
|
30
|
-
render json: {schemas: schemas}
|
|
31
|
-
end
|
|
32
|
-
|
|
33
|
-
private
|
|
34
|
-
|
|
35
|
-
def schema_file_path(schema_name)
|
|
36
|
-
config = OpenapiRuby.configuration
|
|
37
|
-
ext = (config.schema_output_format == :json) ? "json" : "yaml"
|
|
38
|
-
path = Rails.root.join(config.schema_output_dir, "#{schema_name}.#{ext}")
|
|
39
|
-
path.to_s
|
|
40
|
-
end
|
|
41
|
-
|
|
42
|
-
def parse_content(file_path, content)
|
|
43
|
-
if file_path.end_with?(".json")
|
|
44
|
-
JSON.parse(content)
|
|
45
|
-
else
|
|
46
|
-
YAML.safe_load(content, permitted_classes: [Date, Time])
|
|
47
|
-
end
|
|
48
|
-
end
|
|
49
|
-
|
|
50
|
-
def serialize_doc(file_path, doc)
|
|
51
|
-
if file_path.end_with?(".json")
|
|
52
|
-
JSON.pretty_generate(doc)
|
|
53
|
-
else
|
|
54
|
-
doc.to_yaml
|
|
55
|
-
end
|
|
14
|
+
render json: {schemas: Serving.schema_names}
|
|
56
15
|
end
|
|
57
16
|
end
|
|
58
17
|
end
|
|
@@ -5,83 +5,26 @@ module OpenapiRuby
|
|
|
5
5
|
layout false
|
|
6
6
|
|
|
7
7
|
def index
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
render html:
|
|
8
|
+
html = Serving.swagger_ui_html(
|
|
9
|
+
schema_urls: schema_urls,
|
|
10
|
+
ui_config: OpenapiRuby.configuration.ui_config
|
|
11
|
+
)
|
|
12
|
+
render html: html.html_safe
|
|
13
13
|
end
|
|
14
14
|
|
|
15
15
|
def oauth2_redirect
|
|
16
|
-
file
|
|
17
|
-
render file: file, layout: false, content_type: "text/html"
|
|
16
|
+
render file: Serving.oauth2_redirect_file, layout: false, content_type: "text/html"
|
|
18
17
|
end
|
|
19
18
|
|
|
20
19
|
private
|
|
21
20
|
|
|
22
|
-
def
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
<<~HTML
|
|
28
|
-
<!DOCTYPE html>
|
|
29
|
-
<html lang="en">
|
|
30
|
-
<head>
|
|
31
|
-
<meta charset="UTF-8">
|
|
32
|
-
<title>#{@ui_config[:title] || "API Documentation"}</title>
|
|
33
|
-
<link rel="stylesheet" href="https://unpkg.com/swagger-ui-dist@5/swagger-ui.css">
|
|
34
|
-
<style>
|
|
35
|
-
html { box-sizing: border-box; overflow-y: scroll; }
|
|
36
|
-
*, *:before, *:after { box-sizing: inherit; }
|
|
37
|
-
body { margin: 0; background: #fafafa; }
|
|
38
|
-
</style>
|
|
39
|
-
</head>
|
|
40
|
-
<body>
|
|
41
|
-
<div id="swagger-ui"></div>
|
|
42
|
-
<script src="https://unpkg.com/swagger-ui-dist@5/swagger-ui-bundle.js"></script>
|
|
43
|
-
<script src="https://unpkg.com/swagger-ui-dist@5/swagger-ui-standalone-preset.js"></script>
|
|
44
|
-
<script>
|
|
45
|
-
SwaggerUIBundle({
|
|
46
|
-
#{schema_urls_js},
|
|
47
|
-
dom_id: '#swagger-ui',
|
|
48
|
-
deepLinking: true,
|
|
49
|
-
presets: [
|
|
50
|
-
SwaggerUIBundle.presets.apis,
|
|
51
|
-
SwaggerUIStandalonePreset
|
|
52
|
-
],
|
|
53
|
-
plugins: [
|
|
54
|
-
SwaggerUIBundle.plugins.DownloadUrl
|
|
55
|
-
],
|
|
56
|
-
layout: "#{(@schemas.size > 1) ? "StandaloneLayout" : "BaseLayout"}",
|
|
57
|
-
#{ui_config_js}
|
|
58
|
-
});
|
|
59
|
-
</script>
|
|
60
|
-
</body>
|
|
61
|
-
</html>
|
|
62
|
-
HTML
|
|
63
|
-
end
|
|
64
|
-
|
|
65
|
-
def schema_urls_js
|
|
66
|
-
fmt = schema_format
|
|
67
|
-
if @schemas.size > 1
|
|
68
|
-
urls = @schemas.map { |name, schema_config|
|
|
69
|
-
title = schema_config.dig(:info, :title) || name.to_s
|
|
70
|
-
url = openapi_ruby.schema_path(name.to_s, format: fmt)
|
|
71
|
-
{url: url, name: title}
|
|
21
|
+
def schema_urls
|
|
22
|
+
OpenapiRuby.configuration.schemas.map do |name, schema_config|
|
|
23
|
+
{
|
|
24
|
+
url: openapi_ruby.schema_path(name.to_s, format: Serving.schema_format),
|
|
25
|
+
name: schema_config.dig(:info, :title) || name.to_s
|
|
72
26
|
}
|
|
73
|
-
"urls: #{urls.to_json}"
|
|
74
|
-
else
|
|
75
|
-
name = @schemas.keys.first.to_s
|
|
76
|
-
url = openapi_ruby.schema_path(name, format: fmt)
|
|
77
|
-
"url: \"#{url}\""
|
|
78
27
|
end
|
|
79
28
|
end
|
|
80
|
-
|
|
81
|
-
def ui_config_js
|
|
82
|
-
@ui_config.except(:title).map { |k, v|
|
|
83
|
-
"#{k}: #{v.to_json}"
|
|
84
|
-
}.join(",\n ")
|
|
85
|
-
end
|
|
86
29
|
end
|
|
87
30
|
end
|
|
@@ -12,11 +12,31 @@ module OpenapiRuby
|
|
|
12
12
|
|
|
13
13
|
def create_component_file
|
|
14
14
|
template "component.rb.tt",
|
|
15
|
-
File.join(
|
|
15
|
+
File.join(component_path, component_type, "#{file_name}.rb")
|
|
16
16
|
end
|
|
17
17
|
|
|
18
18
|
private
|
|
19
19
|
|
|
20
|
+
# Components::Loader only looks under the configured paths, so anything
|
|
21
|
+
# written to a hardcoded app/api_components is invisible on hosts that
|
|
22
|
+
# default elsewhere (Hanami) or configure their own.
|
|
23
|
+
def component_path
|
|
24
|
+
path = OpenapiRuby.configuration.component_paths.first ||
|
|
25
|
+
Configuration.default_component_paths.first
|
|
26
|
+
relativize(path)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# An initializer may hold an absolute Rails.root.join(...) path; Thor
|
|
30
|
+
# reports the destination verbatim, and an absolute one reads as noise.
|
|
31
|
+
def relativize(path)
|
|
32
|
+
pathname = Pathname.new(path)
|
|
33
|
+
return path unless pathname.absolute?
|
|
34
|
+
|
|
35
|
+
pathname.relative_path_from(Pathname.new(destination_root)).to_s
|
|
36
|
+
rescue ArgumentError
|
|
37
|
+
path
|
|
38
|
+
end
|
|
39
|
+
|
|
20
40
|
def class_name
|
|
21
41
|
name.camelize
|
|
22
42
|
end
|
|
@@ -69,7 +69,7 @@ module OpenapiRuby
|
|
|
69
69
|
def accounted_keys(context, method)
|
|
70
70
|
declared = context.path_parameters + (context.operations[method.to_s]&.parameters || [])
|
|
71
71
|
|
|
72
|
-
template_params(context) | declared.
|
|
72
|
+
template_params(context) | declared.flat_map { |param| OpenapiRuby::ParameterNames.lookup_names(param) }
|
|
73
73
|
end
|
|
74
74
|
|
|
75
75
|
def template_params(context)
|
|
@@ -13,6 +13,21 @@ module OpenapiRuby
|
|
|
13
13
|
base.extend ClassMethods
|
|
14
14
|
base.class_attribute :_openapi_contexts, default: []
|
|
15
15
|
base.class_attribute :_openapi_schema_name, default: nil
|
|
16
|
+
|
|
17
|
+
install_rack_test!(base)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# On Rails the test class already inherits ActionDispatch's integration
|
|
21
|
+
# helpers. Every other host drives requests through rack-test, and the
|
|
22
|
+
# class defines `app` itself.
|
|
23
|
+
def self.install_rack_test!(base)
|
|
24
|
+
return if OpenapiRuby.rails_host?
|
|
25
|
+
return if base.method_defined?(:last_response)
|
|
26
|
+
|
|
27
|
+
require "rack/test"
|
|
28
|
+
base.include ::Rack::Test::Methods
|
|
29
|
+
rescue LoadError
|
|
30
|
+
nil
|
|
16
31
|
end
|
|
17
32
|
|
|
18
33
|
module ClassMethods
|
|
@@ -75,6 +90,7 @@ module OpenapiRuby
|
|
|
75
90
|
|
|
76
91
|
# Build query params (exclude path params)
|
|
77
92
|
query_params = params.reject { |k, _| path_param_names(context).include?(k.to_s) }
|
|
93
|
+
query_params = ParameterNames.rename_keys(query_params, operation.parameters)
|
|
78
94
|
|
|
79
95
|
# Execute the request
|
|
80
96
|
if body
|
|
@@ -111,18 +127,18 @@ module OpenapiRuby
|
|
|
111
127
|
assert req_errors.empty?, "Request validation failed:\n#{req_errors.join("\n")}"
|
|
112
128
|
end
|
|
113
129
|
|
|
114
|
-
|
|
130
|
+
openapi_transport.dispatch(method, path, **request_args)
|
|
115
131
|
|
|
116
132
|
# Validate response
|
|
117
|
-
assert_equal expected_status,
|
|
118
|
-
"Expected status #{expected_status}, got #{
|
|
133
|
+
assert_equal expected_status, openapi_response.status,
|
|
134
|
+
"Expected status #{expected_status}, got #{openapi_response.status}\nResponse body: #{openapi_response.body}"
|
|
119
135
|
|
|
120
136
|
if response_ctx.schema_definition
|
|
121
137
|
validator = Testing::ResponseValidator.new
|
|
122
138
|
body_data = parse_response_body
|
|
123
139
|
errors = validator.validate(
|
|
124
140
|
response_body: body_data,
|
|
125
|
-
status_code:
|
|
141
|
+
status_code: openapi_response.status,
|
|
126
142
|
response_context: response_ctx
|
|
127
143
|
)
|
|
128
144
|
assert errors.empty?, "Response validation failed:\n#{errors.join("\n")}"
|
|
@@ -136,6 +152,17 @@ module OpenapiRuby
|
|
|
136
152
|
parse_response_body
|
|
137
153
|
end
|
|
138
154
|
|
|
155
|
+
# The seam between the DSL and the host's request API. Public so specs
|
|
156
|
+
# that drive requests themselves (rate limiting, pagination loops) can
|
|
157
|
+
# reach the same dispatcher and response the assertions use.
|
|
158
|
+
def openapi_transport
|
|
159
|
+
@openapi_transport ||= Testing::Transport.for(self)
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
def openapi_response
|
|
163
|
+
openapi_transport.response
|
|
164
|
+
end
|
|
165
|
+
|
|
139
166
|
private
|
|
140
167
|
|
|
141
168
|
def find_context_for(method, path_params, params, expected_status, api_path)
|
|
@@ -154,7 +181,7 @@ module OpenapiRuby
|
|
|
154
181
|
end
|
|
155
182
|
|
|
156
183
|
def path_param_names(context)
|
|
157
|
-
context.path_parameters.
|
|
184
|
+
context.path_parameters.flat_map { |p| ParameterNames.lookup_names(p) }
|
|
158
185
|
end
|
|
159
186
|
|
|
160
187
|
def resolve_base_path(schema_name)
|
|
@@ -222,11 +249,11 @@ module OpenapiRuby
|
|
|
222
249
|
end
|
|
223
250
|
|
|
224
251
|
def parse_response_body
|
|
225
|
-
return nil if
|
|
252
|
+
return nil if openapi_response.body.empty?
|
|
226
253
|
|
|
227
|
-
JSON.parse(
|
|
254
|
+
JSON.parse(openapi_response.body)
|
|
228
255
|
rescue JSON::ParserError
|
|
229
|
-
|
|
256
|
+
openapi_response.body
|
|
230
257
|
end
|
|
231
258
|
end
|
|
232
259
|
|