openapi_first 1.0.0.beta5 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (86) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ruby.yml +2 -1
  3. data/CHANGELOG.md +23 -2
  4. data/Gemfile +2 -0
  5. data/Gemfile.lock +16 -18
  6. data/Gemfile.rack2 +15 -0
  7. data/Gemfile.rack2.lock +99 -0
  8. data/README.md +99 -130
  9. data/lib/openapi_first/body_parser.rb +29 -0
  10. data/lib/openapi_first/configuration.rb +20 -0
  11. data/lib/openapi_first/definition/cookie_parameters.rb +12 -0
  12. data/lib/openapi_first/definition/header_parameters.rb +12 -0
  13. data/lib/openapi_first/definition/operation.rb +116 -0
  14. data/lib/openapi_first/definition/parameters.rb +47 -0
  15. data/lib/openapi_first/definition/path_item.rb +32 -0
  16. data/lib/openapi_first/definition/path_parameters.rb +12 -0
  17. data/lib/openapi_first/definition/query_parameters.rb +12 -0
  18. data/lib/openapi_first/definition/request_body.rb +43 -0
  19. data/lib/openapi_first/definition/response.rb +25 -0
  20. data/lib/openapi_first/definition/responses.rb +83 -0
  21. data/lib/openapi_first/definition.rb +61 -8
  22. data/lib/openapi_first/error_response.rb +22 -27
  23. data/lib/openapi_first/errors.rb +2 -14
  24. data/lib/openapi_first/failure.rb +55 -0
  25. data/lib/openapi_first/middlewares/request_validation.rb +52 -0
  26. data/lib/openapi_first/middlewares/response_validation.rb +35 -0
  27. data/lib/openapi_first/plugins/default/error_response.rb +74 -0
  28. data/lib/openapi_first/plugins/default.rb +11 -0
  29. data/lib/openapi_first/plugins/jsonapi/error_response.rb +58 -0
  30. data/lib/openapi_first/plugins/jsonapi.rb +11 -0
  31. data/lib/openapi_first/plugins.rb +9 -7
  32. data/lib/openapi_first/request_validation/request_body_validator.rb +41 -0
  33. data/lib/openapi_first/request_validation/validator.rb +81 -0
  34. data/lib/openapi_first/response_validation/validator.rb +101 -0
  35. data/lib/openapi_first/runtime_request.rb +84 -0
  36. data/lib/openapi_first/runtime_response.rb +31 -0
  37. data/lib/openapi_first/schema/validation_error.rb +18 -0
  38. data/lib/openapi_first/schema/validation_result.rb +32 -0
  39. data/lib/openapi_first/{json_schema.rb → schema.rb} +9 -5
  40. data/lib/openapi_first/version.rb +1 -1
  41. data/lib/openapi_first.rb +32 -28
  42. data/openapi_first.gemspec +10 -9
  43. metadata +55 -67
  44. data/.rspec +0 -3
  45. data/.rubocop.yml +0 -14
  46. data/Rakefile +0 -15
  47. data/benchmarks/Gemfile +0 -16
  48. data/benchmarks/Gemfile.lock +0 -142
  49. data/benchmarks/README.md +0 -29
  50. data/benchmarks/apps/committee_with_hanami_api.ru +0 -26
  51. data/benchmarks/apps/committee_with_response_validation.ru +0 -29
  52. data/benchmarks/apps/committee_with_sinatra.ru +0 -31
  53. data/benchmarks/apps/grape.ru +0 -21
  54. data/benchmarks/apps/hanami_api.ru +0 -21
  55. data/benchmarks/apps/hanami_router.ru +0 -14
  56. data/benchmarks/apps/openapi.yaml +0 -268
  57. data/benchmarks/apps/openapi_first_with_hanami_api.ru +0 -24
  58. data/benchmarks/apps/openapi_first_with_plain_rack.ru +0 -32
  59. data/benchmarks/apps/openapi_first_with_response_validation.ru +0 -25
  60. data/benchmarks/apps/openapi_first_with_sinatra.ru +0 -29
  61. data/benchmarks/apps/roda.ru +0 -27
  62. data/benchmarks/apps/sinatra.ru +0 -26
  63. data/benchmarks/apps/syro.ru +0 -25
  64. data/benchmarks/benchmark-wrk.sh +0 -3
  65. data/benchmarks/benchmarks.rb +0 -48
  66. data/benchmarks/post.lua +0 -3
  67. data/bin/console +0 -15
  68. data/bin/setup +0 -8
  69. data/examples/README.md +0 -13
  70. data/examples/app.rb +0 -18
  71. data/examples/config.ru +0 -7
  72. data/examples/openapi.yaml +0 -29
  73. data/lib/openapi_first/body_parser_middleware.rb +0 -40
  74. data/lib/openapi_first/config.rb +0 -20
  75. data/lib/openapi_first/error_responses/default.rb +0 -58
  76. data/lib/openapi_first/error_responses/json_api.rb +0 -58
  77. data/lib/openapi_first/json_schema/result.rb +0 -17
  78. data/lib/openapi_first/operation.rb +0 -170
  79. data/lib/openapi_first/request_body_validator.rb +0 -41
  80. data/lib/openapi_first/request_validation.rb +0 -118
  81. data/lib/openapi_first/request_validation_error.rb +0 -31
  82. data/lib/openapi_first/response_validation.rb +0 -93
  83. data/lib/openapi_first/response_validator.rb +0 -21
  84. data/lib/openapi_first/router.rb +0 -102
  85. data/lib/openapi_first/string_keyed_hash.rb +0 -20
  86. data/lib/openapi_first/use_router.rb +0 -18
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 8e6c1f8f1a6fffd91827a74f95b932bfd5be9d4a897f3704efd6313bac9e6be4
4
- data.tar.gz: 59041cacdcb634bb25e5d23025c0f86afe97632918f1e10d6f67409d31ba5f9b
3
+ metadata.gz: 259995bd946bd53afc1b6ab3cc2d556811a8d775ba2714d1e13f0058f3049048
4
+ data.tar.gz: fe150a639f4ddbbf6190296c0fb70ee06c5116711a2716826934fc12b48b1622
5
5
  SHA512:
6
- metadata.gz: 1955264ba1b60f477123cd1bbb71a14d611d598664965548a9ebe3c6508d5ac6e205dfe971bc7c1ebe6b27da78a48f1bf5d27239c886a9b4aa7db303224e0cfc
7
- data.tar.gz: 2f25b5944e546a6619c2be01462008d358a0a80140594b0906ca62e9b152fa97b2b8225d0c137acbe29c64b7732bbd00d3f35459cd0b771b2b38c409303adde8
6
+ metadata.gz: ed09d09b5ad4c131134843ed0c7772eb6742b9bd98e6bb410d2c7fca3efe9a49875993b095a9590ee1a5ec9484301d5a58ed67b445fdd46aacbabedaf7e8160e
7
+ data.tar.gz: 9fe5c08c823aff23d979b9cd0652e4a63a8df424f98c037cb77e45c28c7fd232f24305df8024495b7bd50f4b99fbb8669d0f8a962b20ef2652907298ac7dd6cf
@@ -9,4 +9,5 @@ jobs:
9
9
  with:
10
10
  ruby-version: '3.1'
11
11
  bundler-cache: true # runs 'bundle install' and caches installed gems automatically
12
- - run: bundle exec rake
12
+ - run: BUNDLE_GEMFILE=Gemfile bundle exec rake
13
+ - run: BUNDLE_GEMFILE=Gemfile.rack2 bundle lock --add-platform x86_64-linux && bundle exec rake
data/CHANGELOG.md CHANGED
@@ -1,11 +1,32 @@
1
1
  # Changelog
2
2
 
3
- ## Unreleased
3
+ ## 1.0.0
4
+
5
+ - Breaking: The default error uses application/problem+json content-type
6
+ - Breaking: Moved rack middlewares to OpenapiFirst::Middlewares
7
+ - Breaking: Rename OpenapiFirst::ResponseInvalid to OpenapiFirst::ResponseInvalidError
8
+ - Breaking: Remove OpenapiFirst::Router
9
+ - Breaking: Remove `env[OpenapiFirst::OPERATION]`. Use `env[OpenapiFirst::REQUEST]` instead.
10
+ - Breaking: Remove `env[OpenapiFirst::REQUEST_BODY]`, `env[OpenapiFirst::PARAMS]`. Use `env[OpenapiFirst::REQUEST].body env[OpenapiFirst::REQUEST].params` instead.
11
+ - Add interface to validate requests / responses without middlewares (see "Manual validation" in README)
12
+ - Add OpenapiFirst.configure
13
+ - Add OpenapiFirst.register, OpenapiFirst.plugin
14
+ - Fix response header validation with Rack 3
15
+ - Fixed: Add support for paths like `/{a}..{b}`
16
+
17
+ ## 1.0.0.beta6
18
+
19
+ - Fix: Make response header validation work with rack 3
20
+ - Refactor router
21
+ - Remove dependency hanami-router
22
+ - PathItem and Operation for a request can be found by calling methods on the Definitnion
23
+ - Fixed https://github.com/ahx/openapi_first/issues/155
24
+ - Breaking / Regression: A paths like /pets/{from}-{to} if there is a path "/pets/{id}"
4
25
 
5
26
  ## 1.0.0.beta5
6
27
 
7
28
  - Added: `OpenapiFirst::Config.default_options=` to set default options globally
8
- - Added: You can define custom error responses by subclassing `OpenapiFirst::ErrorResponse` and register it via `OpenapiFirst::Plugins.register_error_response(name, MyCustomErrorResponse)`
29
+ - Added: You can define custom error responses by subclassing `OpenapiFirst::ErrorResponse` and register it via `OpenapiFirst.register_error_response(name, MyCustomErrorResponse)`
9
30
 
10
31
  ## 1.0.0.beta4
11
32
 
data/Gemfile CHANGED
@@ -5,6 +5,8 @@ source 'https://rubygems.org'
5
5
  # Specify your gem's dependencies in openapi_first.gemspec
6
6
  gemspec
7
7
 
8
+ gem 'rack', '>= 3.0.0'
9
+
8
10
  group :test, :development do
9
11
  gem 'bundler'
10
12
  gem 'rack-test'
data/Gemfile.lock CHANGED
@@ -1,12 +1,12 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- openapi_first (1.0.0.beta5)
5
- hanami-router (~> 2.0.0)
4
+ openapi_first (1.0.0)
6
5
  json_refs (~> 0.1, >= 0.1.7)
7
- json_schemer (~> 2.0.0)
6
+ json_schemer (~> 2.1.0)
8
7
  multi_json (~> 1.15)
9
- openapi_parameters (>= 0.3.1, < 2.0)
8
+ mustermann-contrib (~> 3.0.0)
9
+ openapi_parameters (>= 0.3.2, < 2.0)
10
10
  rack (>= 2.2, < 4.0)
11
11
 
12
12
  GEM
@@ -15,15 +15,11 @@ GEM
15
15
  ast (2.4.2)
16
16
  diff-lcs (1.5.0)
17
17
  hana (1.3.7)
18
- hanami-router (2.0.2)
19
- mustermann (~> 3.0)
20
- mustermann-contrib (~> 3.0)
21
- rack (~> 2.0)
22
18
  hansi (0.2.1)
23
- json (2.6.3)
19
+ json (2.7.1)
24
20
  json_refs (0.1.8)
25
21
  hana
26
- json_schemer (2.0.0)
22
+ json_schemer (2.1.1)
27
23
  hana (~> 1.3)
28
24
  regexp_parser (~> 2.0)
29
25
  simpleidn (~> 0.2)
@@ -34,20 +30,20 @@ GEM
34
30
  mustermann-contrib (3.0.0)
35
31
  hansi (~> 0.2.0)
36
32
  mustermann (= 3.0.0)
37
- openapi_parameters (0.3.1)
33
+ openapi_parameters (0.3.2)
38
34
  rack (>= 2.2)
39
35
  zeitwerk (~> 2.6)
40
- parallel (1.23.0)
41
- parser (3.2.2.4)
36
+ parallel (1.24.0)
37
+ parser (3.3.0.0)
42
38
  ast (~> 2.4.1)
43
39
  racc
44
40
  racc (1.7.3)
45
- rack (2.2.8)
41
+ rack (3.0.8)
46
42
  rack-test (2.1.0)
47
43
  rack (>= 1.3)
48
44
  rainbow (3.1.1)
49
45
  rake (13.1.0)
50
- regexp_parser (2.8.2)
46
+ regexp_parser (2.8.3)
51
47
  rexml (3.2.6)
52
48
  rspec (3.12.0)
53
49
  rspec-core (~> 3.12.0)
@@ -62,7 +58,7 @@ GEM
62
58
  diff-lcs (>= 1.2.0, < 2.0)
63
59
  rspec-support (~> 3.12.0)
64
60
  rspec-support (3.12.1)
65
- rubocop (1.57.2)
61
+ rubocop (1.59.0)
66
62
  json (~> 2.3)
67
63
  language_server-protocol (>= 3.17.0)
68
64
  parallel (~> 1.10)
@@ -70,7 +66,7 @@ GEM
70
66
  rainbow (>= 2.2.2, < 4.0)
71
67
  regexp_parser (>= 1.8, < 3.0)
72
68
  rexml (>= 3.2.5, < 4.0)
73
- rubocop-ast (>= 1.28.1, < 2.0)
69
+ rubocop-ast (>= 1.30.0, < 2.0)
74
70
  ruby-progressbar (~> 1.7)
75
71
  unicode-display_width (>= 2.4.0, < 3.0)
76
72
  rubocop-ast (1.30.0)
@@ -81,17 +77,19 @@ GEM
81
77
  unf (~> 0.1.4)
82
78
  unf (0.1.4)
83
79
  unf_ext
84
- unf_ext (0.0.9)
80
+ unf_ext (0.0.9.1)
85
81
  unicode-display_width (2.5.0)
86
82
  zeitwerk (2.6.12)
87
83
 
88
84
  PLATFORMS
89
85
  arm64-darwin-21
86
+ arm64-darwin-22
90
87
  x86_64-linux
91
88
 
92
89
  DEPENDENCIES
93
90
  bundler
94
91
  openapi_first!
92
+ rack (>= 3.0.0)
95
93
  rack-test
96
94
  rake
97
95
  rspec
data/Gemfile.rack2 ADDED
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ source 'https://rubygems.org'
4
+
5
+ gemspec
6
+
7
+ gem 'rack', '< 3.0.0'
8
+
9
+ group :test, :development do
10
+ gem 'bundler'
11
+ gem 'rack-test'
12
+ gem 'rake'
13
+ gem 'rspec'
14
+ gem 'rubocop'
15
+ end
@@ -0,0 +1,99 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ openapi_first (1.0.0.beta6)
5
+ json_refs (~> 0.1, >= 0.1.7)
6
+ json_schemer (~> 2.1.0)
7
+ multi_json (~> 1.15)
8
+ mustermann-contrib (~> 3.0.0)
9
+ openapi_parameters (>= 0.3.2, < 2.0)
10
+ rack (>= 2.2, < 4.0)
11
+
12
+ GEM
13
+ remote: https://rubygems.org/
14
+ specs:
15
+ ast (2.4.2)
16
+ diff-lcs (1.5.0)
17
+ hana (1.3.7)
18
+ hansi (0.2.1)
19
+ json (2.7.1)
20
+ json_refs (0.1.8)
21
+ hana
22
+ json_schemer (2.1.1)
23
+ hana (~> 1.3)
24
+ regexp_parser (~> 2.0)
25
+ simpleidn (~> 0.2)
26
+ language_server-protocol (3.17.0.3)
27
+ multi_json (1.15.0)
28
+ mustermann (3.0.0)
29
+ ruby2_keywords (~> 0.0.1)
30
+ mustermann-contrib (3.0.0)
31
+ hansi (~> 0.2.0)
32
+ mustermann (= 3.0.0)
33
+ openapi_parameters (0.3.2)
34
+ rack (>= 2.2)
35
+ zeitwerk (~> 2.6)
36
+ parallel (1.24.0)
37
+ parser (3.2.2.4)
38
+ ast (~> 2.4.1)
39
+ racc
40
+ racc (1.7.3)
41
+ rack (2.2.8)
42
+ rack-test (2.1.0)
43
+ rack (>= 1.3)
44
+ rainbow (3.1.1)
45
+ rake (13.1.0)
46
+ regexp_parser (2.8.3)
47
+ rexml (3.2.6)
48
+ rspec (3.12.0)
49
+ rspec-core (~> 3.12.0)
50
+ rspec-expectations (~> 3.12.0)
51
+ rspec-mocks (~> 3.12.0)
52
+ rspec-core (3.12.2)
53
+ rspec-support (~> 3.12.0)
54
+ rspec-expectations (3.12.3)
55
+ diff-lcs (>= 1.2.0, < 2.0)
56
+ rspec-support (~> 3.12.0)
57
+ rspec-mocks (3.12.6)
58
+ diff-lcs (>= 1.2.0, < 2.0)
59
+ rspec-support (~> 3.12.0)
60
+ rspec-support (3.12.1)
61
+ rubocop (1.59.0)
62
+ json (~> 2.3)
63
+ language_server-protocol (>= 3.17.0)
64
+ parallel (~> 1.10)
65
+ parser (>= 3.2.2.4)
66
+ rainbow (>= 2.2.2, < 4.0)
67
+ regexp_parser (>= 1.8, < 3.0)
68
+ rexml (>= 3.2.5, < 4.0)
69
+ rubocop-ast (>= 1.30.0, < 2.0)
70
+ ruby-progressbar (~> 1.7)
71
+ unicode-display_width (>= 2.4.0, < 3.0)
72
+ rubocop-ast (1.30.0)
73
+ parser (>= 3.2.1.0)
74
+ ruby-progressbar (1.13.0)
75
+ ruby2_keywords (0.0.5)
76
+ simpleidn (0.2.1)
77
+ unf (~> 0.1.4)
78
+ unf (0.1.4)
79
+ unf_ext
80
+ unf_ext (0.0.9.1)
81
+ unicode-display_width (2.5.0)
82
+ zeitwerk (2.6.12)
83
+
84
+ PLATFORMS
85
+ arm64-darwin-22
86
+ ruby
87
+ x86_64-linux
88
+
89
+ DEPENDENCIES
90
+ bundler
91
+ openapi_first!
92
+ rack (< 3.0.0)
93
+ rack-test
94
+ rake
95
+ rspec
96
+ rubocop
97
+
98
+ BUNDLED WITH
99
+ 2.5.3
data/README.md CHANGED
@@ -1,175 +1,144 @@
1
- # OpenapiFirst
1
+ # openapi_first
2
2
 
3
- [![Join the chat at https://gitter.im/openapi_first/community](https://badges.gitter.im/openapi_first/community.svg)](https://gitter.im/openapi_first/community?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
3
+ OpenapiFirst helps to implement HTTP APIs based on an [OpenAPI](https://www.openapis.org/) API description. It supports OpenAPI 3.0 and 3.1. It offers request and response validation to ensure that your implementation follows exactly the API description.
4
4
 
5
- OpenapiFirst helps to implement HTTP APIs based on an [OpenAPI](https://www.openapis.org/) API description. It supports OpenAPI 3.0 and 3.1.
5
+ This makes your API description reliable, reason about API design and use various tooling on top of OpenAPI.
6
6
 
7
- It provides these Rack middlewares:
7
+ ## Contents
8
8
 
9
- - [`OpenapiFirst::RequestValidation`](#request-validation) – Validates the request against the API description and returns 400 if the request is invalid.
10
- - [`OpenapiFirst::ResponseValidation`](#response-validation) Validates the response and raises an exception if the response body is invalid.
11
- - [`OpenapiFirst::Router`](#openapifirstrouter) – This internal middleware is added automatically when using request/response validation. It adds the OpenAPI operation for the current request to the Rack env.
9
+ <!-- TOC -->
12
10
 
13
- Using Request and Response validation together ensures that your implementation follows exactly the API description. This enables you to use the API description as a single source of truth for your API, reason about details and use various tooling.
11
+ - [Manual use](#manual-use)
12
+ - [Rack Middlewares](#rack-middlewares)
13
+ - [Configuration](#configuration)
14
+ - [Development](#development)
14
15
 
15
- ## Request Validation
16
+ <!-- /TOC -->
16
17
 
17
- The `OpenapiFirst::RequestValidation` middleware returns a 400 status code with a body that describes the error if the request is not valid.
18
+ ## Manual use
18
19
 
19
- ```ruby
20
- use OpenapiFirst::RequestValidation, spec: 'openapi.yaml'
21
- ```
22
-
23
- It adds these fields to the Rack env:
24
-
25
- - `env[OpenapiFirst::PARAMS]` – The parsed parameters (query, path) for the current request (string keyed)
26
- - `env[OpenapiFirst::REQUEST_BODY]` – The parsed request body (string keyed)
27
- - `env[OpenapiFirst::OPERATION]` (Added via Router) – The Operation object for the current request. This is an instance of `OpenapiFirst::Operation`.
28
-
29
- ### Options and defaults
30
-
31
- | Name | Possible values | Description | Default |
32
- | :---------------- | --------------------------------------------------------------- | -------------------------------------------------------------------------------------------------- | ---------------------------------- |
33
- | `spec:` | | The path to the spec file or spec loaded via `OpenapiFirst.load` |
34
- | `raise_error:` | `false`, `true` | If set to true the middleware raises `OpenapiFirst::RequestInvalidError` instead of returning 4xx. | `false` (don't raise an exception) |
35
- | `error_response:` | `:default`, `:json_api`, Your implementation of `ErrorResponse` | :default |
36
-
37
- The error responses conform with [JSON:API](https://jsonapi.org).
38
-
39
- Here's an example response body for a missing query parameter "search":
20
+ Load the API description:
40
21
 
41
- ```json
42
- http-status: 400
43
- content-type: "application/json"
22
+ ```ruby
23
+ require 'openapi_first'
44
24
 
45
- {
46
- "errors": [
47
- {
48
- "title": "is missing",
49
- "source": {
50
- "parameter": "search"
51
- }
52
- }
53
- ]
54
- }
25
+ definition = OpenapiFirst.load('petstore.yaml')
55
26
  ```
56
27
 
57
- ### Parameters
58
-
59
- The `RequestValidation` middleware adds `env[OpenapiFirst::PARAMS]` (or `env['openapi.params']` ) with the converted query and path parameters. This only includes the parameters that are defined in the API description. It supports every [`style` and `explode` value as described](https://spec.openapis.org/oas/latest.html#style-examples) in the OpenAPI 3.0 and 3.1 specs. So you can do things these:
28
+ Validate request / response:
60
29
 
61
30
  ```ruby
62
- # GET /pets/filter[id]=1,2,3
63
- env[OpenapiFirst::PARAMS] # => { 'filter[id]' => [1,2,3] }
64
31
 
65
- # GET /colors/.blue.black.brown?format=csv
66
- env[OpenapiFirst::PARAMS] # => { 'color_names' => ['blue', 'black', 'brown'], 'format' => 'csv' }
67
-
68
- # And a lot more.
32
+ # Find the request
33
+ rack_request = Rack::Request.new(env)
34
+ request = definition.request(rack_request)
35
+
36
+ # Inspect the request and access parsed parameters
37
+ request.known? # Is the request defined in the API description?
38
+ request.parsed_body # alias: body
39
+ request.path_parameters
40
+ request.query # alias: query_parameters
41
+ request.params # Merged path and query parameters
42
+ request.headers
43
+ request.cookies
44
+
45
+ # Validate the request
46
+ request.validate # Returns OpenapiFirst:::Failure if validation fails
47
+ request.validate! # Raises OpenapiFirst::RequestInvalidError or OpenapiFirst::NotFoundError if validation fails
48
+
49
+ # Find the response
50
+ rack_response = Rack::Response[*app.call(env)]
51
+ response = request.response(rack_response) # or definition.response(rack_request, rack_response)
52
+
53
+ # Validate response
54
+ response.validate # Returns OpenapiFirst::Failure
55
+ response.validate! # Raises OpenapiFirst::ResponseInvalidError or OpenapiFirst::ResponseNotFoundError if validation fails
69
56
  ```
70
57
 
71
- Integration for specific webframeworks is ongoing. Don't hesitate to create an issue with you specific needs.
72
-
73
- ### Request body validation
74
-
75
- This middleware adds the parsed request body to `env[OpenapiFirst::REQUEST_BODY]`.
76
-
77
- The middleware will return a status `415` if the requests content type does not match or `400` if the request body is invalid.
78
-
79
- ### Header, Cookie, Query and Path parameter validation
80
-
81
- The `RequestValidation` middleware validates the request headers, cookies and path parameters as defined in you API description. It returns a `400` status code if the request is invalid. It adds the parsed merged _path_ and _query_ parameters to `env['openapi.params']`.
82
- Separate parsed parameters are made available by location at `env['openapi.path_params']`, `env['openapi.query']`, `env['openapi.headers']`, `env['openapi.cookies']` as well if you need to access them separately.
83
-
84
- ### readOnly / writeOnly properties
85
-
86
- Request validation fails if request includes a property with `readOnly: true`.
87
-
88
- Response validation fails if response body includes a property with `writeOnly: true`.
89
-
90
- ## Response validation
91
-
92
- The `OpenapiFirst::ResponseValidation` middleware is especially useful when testing. It _always_ raises an error if the response is not valid.
58
+ OpenapiFirst uses [`multi_json`](https://rubygems.org/gems/multi_json).
93
59
 
94
- ```ruby
95
- use OpenapiFirst::ResponseValidation, spec: 'openapi.yaml' if ENV['RACK_ENV'] == 'test'
96
- ```
60
+ ## Rack Middlewares
97
61
 
98
- ### Options
62
+ All middlewares add a _request_ object to the current Rack env at `env[OpenapiFirst::REQUEST]`), which is in an instance of `OpenapiFirst::RuntimeRequest` that responds to `.params`, `.parsed_body` etc.
99
63
 
100
- | Name | Possible values | Description | Default |
101
- | :------ | --------------- | ---------------------------------------------------------------- | ------- |
102
- | `spec:` | | The path to the spec file or spec loaded via `OpenapiFirst.load` |
64
+ This gives you access to the converted request parameters and body exaclty as described in your API description instead of relying on Rack alone to parse the request. This only includes query parameters that are defined in the API description. It supports every [`style` and `explode` value as described](https://spec.openapis.org/oas/latest.html#style-examples) in the OpenAPI 3.0 and 3.1 specs.
103
65
 
104
- ## OpenapiFirst::Router
66
+ ### Request validation
105
67
 
106
- This middleware is used automatically, but you can add it to the top of your middleware stack if you want to customize the behavior via options.
68
+ The request validation middleware returns a 4xx if the request is invalid or not defined in the API description.
107
69
 
108
70
  ```ruby
109
- use OpenapiFirst::Router, spec: './openapi/openapi.yaml'
71
+ use OpenapiFirst::Middlewares::RequestValidation, spec: 'openapi.yaml'
110
72
  ```
111
73
 
112
- This middleware adds `env['openapi.operation']` which holds an instance of `OpenapiFirst::Operation` that responds to `#operation_id`, `#path` (and `#[]` to access raw fields).
74
+ #### Options
113
75
 
114
- ### Options and defaults
76
+ | Name | Possible values | Description |
77
+ | :---------------- | ------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------- |
78
+ | `spec:` | | The path to the spec file or spec loaded via `OpenapiFirst.load` |
79
+ | `raise_error:` | `false` (default), `true` | If set to true the middleware raises `OpenapiFirst::RequestInvalidError` or `OpenapiFirst::NotFoundError` instead of returning 4xx. |
80
+ | `error_response:` | `:default` (default), `:json_api`, Your implementation of `ErrorResponse` | :default |
115
81
 
116
- | Name | Possible values | Description | Default |
117
- | :------------- | -------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------- |
118
- | `spec:` | | The path to the spec file or spec loaded via `OpenapiFirst.load` | |
119
- | `raise_error:` | `false`, `true` | If set to true the middleware raises `OpenapiFirst::NotFoundError` when a path or method was not found in the API description. This is useful during testing to spot an incomplete API description. | `false` (don't raise an exception) |
120
- | `not_found:` | `:continue`, `:halt` | If set to `:continue` the middleware will not return 404 (405, 415), but just pass handling the request to the next middleware or application in the Rack stack. If combined with `raise_error: true` `raise_error` gets preference and an exception is raised. | `:halt` (return 4xx response) |
82
+ Here's an example response body about an invalid request body. See also [RFC 9457](https://www.rfc-editor.org/rfc/rfc9457).
121
83
 
122
- ## Global configuration
123
-
124
- You can configure default options gobally via `OpenapiFirst::Config`:
84
+ ```json
85
+ http-status: 400
86
+ content-type: "application/problem+json"
125
87
 
126
- ```ruby
127
- OpenapiFirst::Config.default_options = {
128
- error_response: :json_api,
129
- request_validation_raise_error: true
88
+ {
89
+ "title": "Bad Request Body",
90
+ "status": 400,
91
+ "errors": [
92
+ {
93
+ "message": "value at `/data/name` is not a string",
94
+ "pointer": "/data/name",
95
+ "code": "string"
96
+ },
97
+ {
98
+ "message": "number at `/data/numberOfLegs` is less than: 2",
99
+ "pointer": "/data/numberOfLegs",
100
+ "code": "minimum"
101
+ },
102
+ {
103
+ "message": "object at `/data` is missing required properties: mandatory",
104
+ "pointer": "/data",
105
+ "code": "required"
106
+ }
107
+ ]
130
108
  }
131
109
  ```
132
110
 
133
- ## Alternatives
134
-
135
- This gem is inspired by [committee](https://github.com/interagent/committee) (Ruby) and [connexion](https://github.com/zalando/connexion) (Python).
136
-
137
- Here's a [comparison between committee and openapi_first](https://gist.github.com/ahx/1538c31f0652f459861713b5259e366a).
111
+ #### readOnly / writeOnly properties
138
112
 
139
- ## Try it out
113
+ Request validation fails if request includes a property with `readOnly: true`.
140
114
 
141
- See [examples](examples).
115
+ Response validation fails if response body includes a property with `writeOnly: true`.
142
116
 
143
- ## Installation
117
+ ### Response validation
144
118
 
145
- Add this line to your application's Gemfile:
119
+ This middleware is especially useful when testing. It _always_ raises an error if the response is not valid.
146
120
 
147
121
  ```ruby
148
- gem 'openapi_first'
122
+ use OpenapiFirst::Middlewares::ResponseValidation, spec: 'openapi.yaml' if ENV['RACK_ENV'] == 'test'
149
123
  ```
150
124
 
151
- OpenapiFirst uses [`multi_json`](https://rubygems.org/gems/multi_json).
152
-
153
- ## Manual response validation
154
-
155
- Instead of using the ResponseValidation middleware you can validate the response in your test manually via [rack-test](https://github.com/rack-test/rack-test) and ResponseValidator.
125
+ #### Options
156
126
 
157
- ```ruby
158
- # In your test (rspec example):
159
- require 'openapi_first'
160
- validator = OpenapiFirst::ResponseValidator.new('petstore.yaml')
161
-
162
- # This will raise an exception if it found an error
163
- validator.validate(last_request, last_response)
164
- ```
127
+ | Name | Possible values | Description |
128
+ | :------ | --------------- | ---------------------------------------------------------------- |
129
+ | `spec:` | | The path to the spec file or spec loaded via `OpenapiFirst.load` |
165
130
 
166
- ## Handling only certain paths
131
+ ## Configuration
167
132
 
168
- You can filter the URIs that should be handled by passing `only` to `OpenapiFirst.load`:
133
+ You can configure default options globally:
169
134
 
170
135
  ```ruby
171
- spec = OpenapiFirst.load('./openapi/openapi.yaml', only: { |path| path.starts_with? '/pets' })
172
- run OpenapiFirst.app(spec, namespace: Pets)
136
+ OpenapiFirst.configure do |config|
137
+ # Specify which plugin is used to render error responses returned by the request validation middleware (defaults to :default)
138
+ config.request_validation_error_response = :json_api
139
+ # Configure if the response validation middleware should raise an exception (defaults to false)
140
+ config.request_validation_raise_error = true
141
+ end
173
142
  ```
174
143
 
175
144
  ## Development
@@ -180,11 +149,11 @@ See `bundle exec rake` to run the linter and the tests.
180
149
 
181
150
  Run `bundle exec rspec` to run the tests only.
182
151
 
183
- ## Benchmarks
152
+ ### Benchmarks
184
153
 
185
154
  [Results](https://gist.github.com/ahx/e6ffced58bd2e8d5baffb2f4d2c1f823)
186
155
 
187
- ### Run benchmarks
156
+ Run benchmarks:
188
157
 
189
158
  ```sh
190
159
  cd benchmarks
@@ -192,8 +161,8 @@ bundle
192
161
  bundle exec ruby benchmarks.rb
193
162
  ```
194
163
 
195
- ## Contributing
164
+ ### Contributing
196
165
 
197
- If you have a question or an idea or found a bug don't hesitate to [create an issue on GitHub](https://github.com/ahx/openapi_first/issues) or [reach out via chat](https://gitter.im/openapi_first/community).
166
+ If you have a question or an idea or found a bug don't hesitate to [create an issue](https://github.com/ahx/openapi_first/issues) or [start a discussion](https://github.com/ahx/openapi_first/discussions).
198
167
 
199
168
  Pull requests are very welcome as well, of course. Feel free to create a "draft" pull request early on, even if your change is still work in progress. 🤗
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'multi_json'
4
+
5
+ module OpenapiFirst
6
+ class BodyParser
7
+ class ParsingError < StandardError; end
8
+
9
+ def parse(request, content_type)
10
+ body = read_body(request)
11
+ return if body.empty?
12
+
13
+ return MultiJson.load(body) if content_type =~ (/json/i) && (content_type =~ /json/i)
14
+ return request.POST if request.form_data?
15
+
16
+ body
17
+ rescue MultiJson::ParseError
18
+ raise ParsingError, 'Failed to parse body as JSON'
19
+ end
20
+
21
+ private
22
+
23
+ def read_body(request)
24
+ body = request.body.read
25
+ request.body.rewind
26
+ body
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OpenapiFirst
4
+ class Configuration
5
+ def initialize
6
+ @request_validation_error_response = OpenapiFirst.plugin(:default)::ErrorResponse
7
+ @request_validation_raise_error = false
8
+ end
9
+
10
+ attr_reader :request_validation_error_response, :request_validation_raise_error
11
+
12
+ def request_validation_error_response=(mod)
13
+ @request_validation_error_response = if mod.is_a?(Symbol)
14
+ OpenapiFirst.plugin(:default)::ErrorResponse
15
+ else
16
+ mod
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'openapi_parameters'
4
+ require_relative 'parameters'
5
+
6
+ module OpenapiFirst
7
+ class CookieParameters < Parameters
8
+ def unpack(env)
9
+ OpenapiParameters::Cookie.new(@parameter_definitions).unpack(env['HTTP_COOKIE'])
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'openapi_parameters'
4
+ require_relative 'parameters'
5
+
6
+ module OpenapiFirst
7
+ class HeaderParameters < Parameters
8
+ def unpack(env)
9
+ OpenapiParameters::Header.new(@parameter_definitions).unpack_env(env)
10
+ end
11
+ end
12
+ end