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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 7990b0f6b4af407b5df408029221d095428b91e0f0cc71f798f77e6d37901eb5
4
- data.tar.gz: 4514fb71405d81e25a2e079e9e0ebcb03110f1430c327be386fd4d63c336eff1
3
+ metadata.gz: f937b3be2ac21a6d99687ca9ae2e0a625f89747c597fca146a7f04398d55cedc
4
+ data.tar.gz: e185d294c49caf0f33b1ad4cfc4a0512968f94ecb8f0f8184affcdd69230c94c
5
5
  SHA512:
6
- metadata.gz: 6f2f27ba8f415683fb454b707e90eec7b9bb3f6adf5c5c1835b1f4002d83afb898414d4a735cc372d9b6a5a47f636c7aa2d0bf24dee092c5fe2b40180828a218
7
- data.tar.gz: 666bb48fd0aaa60c3b25ab242e180735513416db9b34aaa22ec9dcabd54cc0dbcb2aeb5a50d36c7ae860f68fda83cb5261e0706561decce219f657fde4003853
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
- One caveat if you do guard: skipping `rails/test_help` also means `fixtures` is undefined, so any test file calling `fixtures :all` in its class body fails to *load*. Point `PATTERN` at just the files carrying `api_path` declarations:
538
+ #### When guarding backfires
503
539
 
504
- ```bash
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
- Suites using FactoryBot rather than fixtures don't hit this.
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 stop being optional: without them both test frameworks wire themselves into Rails' lazy-load hooks in the same process.
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. The guard is then no longer *required* but it's still worth keeping for the reasons in "Making generation cheaper" above.
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
- schema_name = params[:id]
7
- config = OpenapiRuby.configuration
6
+ document = Serving.schema_document(params[:id], request: request)
7
+ return head :not_found unless document
8
8
 
9
- schema_config = config.schemas[schema_name.to_sym]
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
- schemas = OpenapiRuby.configuration.schemas.keys.map(&:to_s)
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
- config = OpenapiRuby.configuration
9
- @schemas = config.schemas
10
- @ui_config = config.ui_config
11
-
12
- render html: swagger_ui_html.html_safe
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 = File.join(OpenapiRuby::Engine.root, "app", "views", "openapi_ruby", "oauth2_redirect.html")
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 schema_format
23
- (OpenapiRuby.configuration.schema_output_format == :json) ? :json : :yaml
24
- end
25
-
26
- def swagger_ui_html
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("app/api_components", component_type, "#{file_name}.rb")
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.filter_map { |param| param["name"]&.to_s }
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
- send(method, path, **request_args)
130
+ openapi_transport.dispatch(method, path, **request_args)
115
131
 
116
132
  # Validate response
117
- assert_equal expected_status, response.status,
118
- "Expected status #{expected_status}, got #{response.status}\nResponse body: #{response.body}"
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: response.status,
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.map { |p| p["name"] }
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 response.body.empty?
252
+ return nil if openapi_response.body.empty?
226
253
 
227
- JSON.parse(response.body)
254
+ JSON.parse(openapi_response.body)
228
255
  rescue JSON::ParserError
229
- response.body
256
+ openapi_response.body
230
257
  end
231
258
  end
232
259