openapi_first 1.0.0 → 1.1.1

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: 259995bd946bd53afc1b6ab3cc2d556811a8d775ba2714d1e13f0058f3049048
4
- data.tar.gz: fe150a639f4ddbbf6190296c0fb70ee06c5116711a2716826934fc12b48b1622
3
+ metadata.gz: 4bb6d0ee4dbe98020ef124e79a99ced0d0111b1e2891cc8cb3ed8bb697973d2b
4
+ data.tar.gz: 77ce896a68f885614f64fd680af0dde1860c07ae772ce7d5f7d1b75b5abb4528
5
5
  SHA512:
6
- metadata.gz: ed09d09b5ad4c131134843ed0c7772eb6742b9bd98e6bb410d2c7fca3efe9a49875993b095a9590ee1a5ec9484301d5a58ed67b445fdd46aacbabedaf7e8160e
7
- data.tar.gz: 9fe5c08c823aff23d979b9cd0652e4a63a8df424f98c037cb77e45c28c7fd232f24305df8024495b7bd50f4b99fbb8669d0f8a962b20ef2652907298ac7dd6cf
6
+ metadata.gz: 841e32068646969d92ab26fff9c332e849056463c99805c8e0dbbb437918113a09750fb5ceffd3940716911ea50bc20685acbcd862aa6acc270e43fe43675c87
7
+ data.tar.gz: d9a225b3e3119512fc3ed585f00f6f915e21f3199206f520d20733999441567e651fcb129588092c4e7ea9dcc4ce1e77c24365b04b8a8d9d232b8a1a5cd8e1da
data/CHANGELOG.md CHANGED
@@ -1,5 +1,15 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.1.1
4
+
5
+ - Fix reading response body for example when running Rails (`ActionDispatch::Response::RackBody`)
6
+
7
+ ## 1.1.0
8
+
9
+ - Add `known?`, `status`, `body`, `headers`, `content_type` methods to inspect the parsed response (`RuntimeResponse`)
10
+ - Add `OpenapiFirst::ParseError` which is raised by low-level interfaces like `request.body` if the body could not be parsed.
11
+ - Add "code" field to errors in JSON:API error response
12
+
3
13
  ## 1.0.0
4
14
 
5
15
  - Breaking: The default error uses application/problem+json content-type
data/Gemfile CHANGED
@@ -2,15 +2,17 @@
2
2
 
3
3
  source 'https://rubygems.org'
4
4
 
5
- # Specify your gem's dependencies in openapi_first.gemspec
6
5
  gemspec
7
6
 
8
7
  gem 'rack', '>= 3.0.0'
8
+ gem 'rackup'
9
9
 
10
10
  group :test, :development do
11
+ gem 'actionpack'
11
12
  gem 'bundler'
12
13
  gem 'rack-test'
13
14
  gem 'rake'
14
15
  gem 'rspec'
15
16
  gem 'rubocop'
17
+ gem 'simplecov'
16
18
  end
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- openapi_first (1.0.0)
4
+ openapi_first (1.1.1)
5
5
  json_refs (~> 0.1, >= 0.1.7)
6
6
  json_schemer (~> 2.1.0)
7
7
  multi_json (~> 1.15)
@@ -12,10 +12,48 @@ PATH
12
12
  GEM
13
13
  remote: https://rubygems.org/
14
14
  specs:
15
+ actionpack (7.1.2)
16
+ actionview (= 7.1.2)
17
+ activesupport (= 7.1.2)
18
+ nokogiri (>= 1.8.5)
19
+ racc
20
+ rack (>= 2.2.4)
21
+ rack-session (>= 1.0.1)
22
+ rack-test (>= 0.6.3)
23
+ rails-dom-testing (~> 2.2)
24
+ rails-html-sanitizer (~> 1.6)
25
+ actionview (7.1.2)
26
+ activesupport (= 7.1.2)
27
+ builder (~> 3.1)
28
+ erubi (~> 1.11)
29
+ rails-dom-testing (~> 2.2)
30
+ rails-html-sanitizer (~> 1.6)
31
+ activesupport (7.1.2)
32
+ base64
33
+ bigdecimal
34
+ concurrent-ruby (~> 1.0, >= 1.0.2)
35
+ connection_pool (>= 2.2.5)
36
+ drb
37
+ i18n (>= 1.6, < 2)
38
+ minitest (>= 5.1)
39
+ mutex_m
40
+ tzinfo (~> 2.0)
15
41
  ast (2.4.2)
42
+ base64 (0.2.0)
43
+ bigdecimal (3.1.5)
44
+ builder (3.2.4)
45
+ concurrent-ruby (1.2.2)
46
+ connection_pool (2.4.1)
47
+ crass (1.0.6)
16
48
  diff-lcs (1.5.0)
49
+ docile (1.4.0)
50
+ drb (2.2.0)
51
+ ruby2_keywords
52
+ erubi (1.12.0)
17
53
  hana (1.3.7)
18
54
  hansi (0.2.1)
55
+ i18n (1.14.1)
56
+ concurrent-ruby (~> 1.0)
19
57
  json (2.7.1)
20
58
  json_refs (0.1.8)
21
59
  hana
@@ -24,26 +62,47 @@ GEM
24
62
  regexp_parser (~> 2.0)
25
63
  simpleidn (~> 0.2)
26
64
  language_server-protocol (3.17.0.3)
65
+ loofah (2.22.0)
66
+ crass (~> 1.0.2)
67
+ nokogiri (>= 1.12.0)
68
+ minitest (5.21.1)
27
69
  multi_json (1.15.0)
28
70
  mustermann (3.0.0)
29
71
  ruby2_keywords (~> 0.0.1)
30
72
  mustermann-contrib (3.0.0)
31
73
  hansi (~> 0.2.0)
32
74
  mustermann (= 3.0.0)
75
+ mutex_m (0.2.0)
76
+ nokogiri (1.16.0-arm64-darwin)
77
+ racc (~> 1.4)
78
+ nokogiri (1.16.0-x86_64-linux)
79
+ racc (~> 1.4)
33
80
  openapi_parameters (0.3.2)
34
81
  rack (>= 2.2)
35
82
  zeitwerk (~> 2.6)
36
83
  parallel (1.24.0)
37
- parser (3.3.0.0)
84
+ parser (3.3.0.3)
38
85
  ast (~> 2.4.1)
39
86
  racc
40
87
  racc (1.7.3)
41
88
  rack (3.0.8)
89
+ rack-session (2.0.0)
90
+ rack (>= 3.0.0)
42
91
  rack-test (2.1.0)
43
92
  rack (>= 1.3)
93
+ rackup (2.1.0)
94
+ rack (>= 3)
95
+ webrick (~> 1.8)
96
+ rails-dom-testing (2.2.0)
97
+ activesupport (>= 5.0.0)
98
+ minitest
99
+ nokogiri (>= 1.6)
100
+ rails-html-sanitizer (1.6.0)
101
+ loofah (~> 2.21)
102
+ nokogiri (~> 1.14)
44
103
  rainbow (3.1.1)
45
104
  rake (13.1.0)
46
- regexp_parser (2.8.3)
105
+ regexp_parser (2.9.0)
47
106
  rexml (3.2.6)
48
107
  rspec (3.12.0)
49
108
  rspec-core (~> 3.12.0)
@@ -73,12 +132,21 @@ GEM
73
132
  parser (>= 3.2.1.0)
74
133
  ruby-progressbar (1.13.0)
75
134
  ruby2_keywords (0.0.5)
135
+ simplecov (0.22.0)
136
+ docile (~> 1.1)
137
+ simplecov-html (~> 0.11)
138
+ simplecov_json_formatter (~> 0.1)
139
+ simplecov-html (0.12.3)
140
+ simplecov_json_formatter (0.1.4)
76
141
  simpleidn (0.2.1)
77
142
  unf (~> 0.1.4)
143
+ tzinfo (2.0.6)
144
+ concurrent-ruby (~> 1.0)
78
145
  unf (0.1.4)
79
146
  unf_ext
80
147
  unf_ext (0.0.9.1)
81
148
  unicode-display_width (2.5.0)
149
+ webrick (1.8.1)
82
150
  zeitwerk (2.6.12)
83
151
 
84
152
  PLATFORMS
@@ -87,13 +155,16 @@ PLATFORMS
87
155
  x86_64-linux
88
156
 
89
157
  DEPENDENCIES
158
+ actionpack
90
159
  bundler
91
160
  openapi_first!
92
161
  rack (>= 3.0.0)
93
162
  rack-test
163
+ rackup
94
164
  rake
95
165
  rspec
96
166
  rubocop
167
+ simplecov
97
168
 
98
169
  BUNDLED WITH
99
170
  2.3.10
data/README.md CHANGED
@@ -1,8 +1,6 @@
1
1
  # openapi_first
2
2
 
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
-
5
- This makes your API description reliable, reason about API design and use various tooling on top of OpenAPI.
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 and it ensures that your implementation follows exactly the API description.
6
4
 
7
5
  ## Contents
8
6
 
@@ -10,8 +8,12 @@ This makes your API description reliable, reason about API design and use variou
10
8
 
11
9
  - [Manual use](#manual-use)
12
10
  - [Rack Middlewares](#rack-middlewares)
11
+ - [Request validation](#request-validation)
12
+ - [Response validation](#response-validation)
13
13
  - [Configuration](#configuration)
14
14
  - [Development](#development)
15
+ - [Benchmarks](#benchmarks)
16
+ - [Contributing](#contributing)
15
17
 
16
18
  <!-- /TOC -->
17
19
 
@@ -30,17 +32,21 @@ Validate request / response:
30
32
  ```ruby
31
33
 
32
34
  # Find the request
33
- rack_request = Rack::Request.new(env)
35
+ rack_request = Rack::Request.new(env) # GET /pets/42
34
36
  request = definition.request(rack_request)
35
37
 
36
38
  # Inspect the request and access parsed parameters
37
39
  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
40
+ request.content_type
41
+ request.body # alias: parsed_body
42
+ request.path_parameters # => { "pet_id" => 42 }
43
+ request.query_parameters # alias: query
41
44
  request.params # Merged path and query parameters
42
45
  request.headers
43
46
  request.cookies
47
+ request.request_method # => "get"
48
+ request.path # => "/pets/42"
49
+ request.path_definition # => "/pets/{pet_id}"
44
50
 
45
51
  # Validate the request
46
52
  request.validate # Returns OpenapiFirst:::Failure if validation fails
@@ -50,8 +56,15 @@ request.validate! # Raises OpenapiFirst::RequestInvalidError or OpenapiFirst::No
50
56
  rack_response = Rack::Response[*app.call(env)]
51
57
  response = request.response(rack_response) # or definition.response(rack_request, rack_response)
52
58
 
59
+ # Inspect the response
60
+ response.known? # Is the response defined in the API description?
61
+ response.status # => 200
62
+ response.content_type
63
+ response.body
64
+ request.headers # parsed response headers
65
+
53
66
  # Validate response
54
- response.validate # Returns OpenapiFirst::Failure
67
+ response.validate # Returns OpenapiFirst::Failure if validation fails
55
68
  response.validate! # Raises OpenapiFirst::ResponseInvalidError or OpenapiFirst::ResponseNotFoundError if validation fails
56
69
  ```
57
70
 
@@ -73,13 +86,13 @@ use OpenapiFirst::Middlewares::RequestValidation, spec: 'openapi.yaml'
73
86
 
74
87
  #### Options
75
88
 
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 |
89
+ | Name | Possible values | Description |
90
+ | :---------------- | ------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------- |
91
+ | `spec:` | | The path to the spec file or spec loaded via `OpenapiFirst.load` |
92
+ | `raise_error:` | `false` (default), `true` | If set to true the middleware raises `OpenapiFirst::RequestInvalidError` or `OpenapiFirst::NotFoundError` instead of returning 4xx. |
93
+ | `error_response:` | `:default` (default), `:jsonapi`, Your implementation of `ErrorResponse` |
81
94
 
82
- Here's an example response body about an invalid request body. See also [RFC 9457](https://www.rfc-editor.org/rfc/rfc9457).
95
+ Here in an example response body about an invalid request body. See also [RFC 9457](https://www.rfc-editor.org/rfc/rfc9457).
83
96
 
84
97
  ```json
85
98
  http-status: 400
@@ -108,6 +121,50 @@ content-type: "application/problem+json"
108
121
  }
109
122
  ```
110
123
 
124
+ openapi_first offers a [JSON:API](https://jsonapi.org/) error response as well:
125
+
126
+ ```ruby
127
+ use OpenapiFirst::Middlewares::RequestValidation, spec: 'openapi.yaml, error_response: :jsonapi'
128
+ ```
129
+
130
+ Here is an example error response:
131
+
132
+ ```json
133
+ // http-status: 400
134
+ // content-type: "application/vnd.api+json"
135
+
136
+ {
137
+ "errors": [
138
+ {
139
+ "status": "400",
140
+ "source": {
141
+ "pointer": "/data/name"
142
+ },
143
+ "title": "value at `/data/name` is not a string",
144
+ "code": "string"
145
+ },
146
+ {
147
+ "status": "400",
148
+ "source": {
149
+ "pointer": "/data/numberOfLegs"
150
+ },
151
+ "title": "number at `/data/numberOfLegs` is less than: 2",
152
+ "code": "minimum"
153
+ },
154
+ {
155
+ "status": "400",
156
+ "source": {
157
+ "pointer": "/data"
158
+ },
159
+ "title": "object at `/data` is missing required properties: mandatory",
160
+ "code": "required"
161
+ }
162
+ ]
163
+ }
164
+ ```
165
+
166
+ You can build your own custom error response with `error_response: MyCustomClass` that implements `OpenapiFirst::ErrorResponse`.
167
+
111
168
  #### readOnly / writeOnly properties
112
169
 
113
170
  Request validation fails if request includes a property with `readOnly: true`.
@@ -135,7 +192,7 @@ You can configure default options globally:
135
192
  ```ruby
136
193
  OpenapiFirst.configure do |config|
137
194
  # 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
195
+ config.request_validation_error_response = :jsonapi
139
196
  # Configure if the response validation middleware should raise an exception (defaults to false)
140
197
  config.request_validation_raise_error = true
141
198
  end
@@ -4,7 +4,11 @@ require 'multi_json'
4
4
 
5
5
  module OpenapiFirst
6
6
  class BodyParser
7
- class ParsingError < StandardError; end
7
+ def self.const_missing(const_name)
8
+ super unless const_name == :ParsingError
9
+ warn 'DEPRECATION WARNING: OpenapiFirst::BodyParser::ParsingError is deprecated. Use OpenapiFirst::ParseError instead.' # rubocop:disable Layout/LineLength
10
+ OpenapiFirst::ParseError
11
+ end
8
12
 
9
13
  def parse(request, content_type)
10
14
  body = read_body(request)
@@ -15,7 +19,7 @@ module OpenapiFirst
15
19
 
16
20
  body
17
21
  rescue MultiJson::ParseError
18
- raise ParsingError, 'Failed to parse body as JSON'
22
+ raise ParseError, 'Failed to parse body as JSON'
19
23
  end
20
24
 
21
25
  private
@@ -2,11 +2,8 @@
2
2
 
3
3
  require 'forwardable'
4
4
  require 'set'
5
+ require 'openapi_parameters'
5
6
  require_relative 'request_body'
6
- require_relative 'query_parameters'
7
- require_relative 'header_parameters'
8
- require_relative 'path_parameters'
9
- require_relative 'cookie_parameters'
10
7
  require_relative 'responses'
11
8
 
12
9
  module OpenapiFirst
@@ -20,8 +17,6 @@ module OpenapiFirst
20
17
  WRITE_METHODS = Set.new(%w[post put patch delete]).freeze
21
18
  private_constant :WRITE_METHODS
22
19
 
23
- attr_reader :path, :method, :openapi_version
24
-
25
20
  def initialize(path, request_method, path_item_object, openapi_version:)
26
21
  @path = path
27
22
  @method = request_method
@@ -30,6 +25,9 @@ module OpenapiFirst
30
25
  @operation_object = @path_item_object[request_method]
31
26
  end
32
27
 
28
+ attr_reader :path, :method, :openapi_version
29
+ alias request_method method
30
+
33
31
  def operation_id
34
32
  operation_object['operationId']
35
33
  end
@@ -66,34 +64,63 @@ module OpenapiFirst
66
64
  @name ||= "#{method.upcase} #{path} (#{operation_id})"
67
65
  end
68
66
 
69
- def query_parameters
70
- @query_parameters ||= build_parameters(all_parameters.filter { |p| p['in'] == 'query' }, QueryParameters)
71
- end
72
-
73
67
  def path_parameters
74
- @path_parameters ||= build_parameters(all_parameters.filter { |p| p['in'] == 'path' }, PathParameters)
68
+ all_parameters['path']
75
69
  end
76
70
 
77
- IGNORED_HEADERS = Set['Content-Type', 'Accept', 'Authorization'].freeze
78
- private_constant :IGNORED_HEADERS
71
+ def query_parameters
72
+ all_parameters['query']
73
+ end
79
74
 
80
75
  def header_parameters
81
- @header_parameters ||= build_parameters(find_header_parameters, HeaderParameters)
76
+ all_parameters['header']
82
77
  end
83
78
 
84
79
  def cookie_parameters
85
- @cookie_parameters ||= build_parameters(all_parameters.filter { |p| p['in'] == 'cookie' }, CookieParameters)
80
+ all_parameters['cookie']
81
+ end
82
+
83
+ def path_parameters_schema
84
+ @path_parameters_schema ||= build_schema(path_parameters)
85
+ end
86
+
87
+ def query_parameters_schema
88
+ @query_parameters_schema ||= build_schema(query_parameters)
89
+ end
90
+
91
+ def header_parameters_schema
92
+ @header_parameters_schema ||= build_schema(header_parameters)
93
+ end
94
+
95
+ def cookie_parameters_schema
96
+ @cookie_parameters_schema ||= build_schema(cookie_parameters)
86
97
  end
87
98
 
88
99
  private
89
100
 
101
+ IGNORED_HEADERS = Set['Content-Type', 'Accept', 'Authorization'].freeze
102
+ private_constant :IGNORED_HEADERS
103
+
90
104
  def all_parameters
91
- @all_parameters ||= begin
92
- parameters = @path_item_object['parameters']&.dup || []
93
- parameters_on_operation = operation_object['parameters']
94
- parameters.concat(parameters_on_operation) if parameters_on_operation
95
- parameters
105
+ @all_parameters ||= (@path_item_object.fetch('parameters', []) + operation_object.fetch('parameters', []))
106
+ .reject { |p| p['in'] == 'header' && IGNORED_HEADERS.include?(p['name']) }
107
+ .group_by { _1['in'] }
108
+ end
109
+
110
+ def build_schema(parameters)
111
+ return unless parameters&.any?
112
+
113
+ init_schema = {
114
+ 'type' => 'object',
115
+ 'properties' => {},
116
+ 'required' => []
117
+ }
118
+ schema = parameters.each_with_object(init_schema) do |parameter_def, result|
119
+ parameter = OpenapiParameters::Parameter.new(parameter_def)
120
+ result['properties'][parameter.name] = parameter.schema if parameter.schema
121
+ result['required'] << parameter.name if parameter.required?
96
122
  end
123
+ Schema.new(schema, openapi_version: @openapi_version)
97
124
  end
98
125
 
99
126
  def responses
@@ -105,12 +132,6 @@ module OpenapiFirst
105
132
  def build_parameters(parameters, klass)
106
133
  klass.new(parameters, openapi_version:) if parameters.any?
107
134
  end
108
-
109
- def find_header_parameters
110
- all_parameters.filter do |p|
111
- p['in'] == 'header' && !IGNORED_HEADERS.include?(p['name'])
112
- end
113
- end
114
135
  end
115
136
  end
116
137
  end
@@ -3,40 +3,42 @@
3
3
  require_relative '../schema'
4
4
 
5
5
  module OpenapiFirst
6
- class RequestBody
7
- def initialize(request_body_object, operation)
8
- @request_body_object = request_body_object
9
- @operation = operation
10
- end
6
+ class Definition
7
+ class RequestBody
8
+ def initialize(request_body_object, operation)
9
+ @request_body_object = request_body_object
10
+ @operation = operation
11
+ end
11
12
 
12
- def description
13
- @request_body_object['description']
14
- end
13
+ def description
14
+ @request_body_object['description']
15
+ end
15
16
 
16
- def required?
17
- !!@request_body_object['required']
18
- end
17
+ def required?
18
+ !!@request_body_object['required']
19
+ end
19
20
 
20
- def schema_for(content_type)
21
- content = @request_body_object['content']
22
- return unless content&.any?
21
+ def schema_for(content_type)
22
+ content = @request_body_object['content']
23
+ return unless content&.any?
23
24
 
24
- content_schemas&.fetch(content_type) do
25
- type = content_type.split(';')[0]
26
- content_schemas[type] || content_schemas["#{type.split('/')[0]}/*"] || content_schemas['*/*']
25
+ content_schemas&.fetch(content_type) do
26
+ type = content_type.split(';')[0]
27
+ content_schemas[type] || content_schemas["#{type.split('/')[0]}/*"] || content_schemas['*/*']
28
+ end
27
29
  end
28
- end
29
30
 
30
- private
31
+ private
31
32
 
32
- def content_schemas
33
- @content_schemas ||= @request_body_object['content']&.each_with_object({}) do |kv, result|
34
- type, media_type = kv
35
- schema_object = media_type['schema']
36
- next unless schema_object
33
+ def content_schemas
34
+ @content_schemas ||= @request_body_object['content']&.each_with_object({}) do |kv, result|
35
+ type, media_type = kv
36
+ schema_object = media_type['schema']
37
+ next unless schema_object
37
38
 
38
- result[type] = Schema.new(schema_object, write: @operation.write?,
39
- openapi_version: @operation.openapi_version)
39
+ result[type] = Schema.new(schema_object, write: @operation.write?,
40
+ openapi_version: @operation.openapi_version)
41
+ end
40
42
  end
41
43
  end
42
44
  end
@@ -2,6 +2,7 @@
2
2
 
3
3
  module OpenapiFirst
4
4
  class Error < StandardError; end
5
+ class ParseError < Error; end
5
6
  class NotFoundError < Error; end
6
7
  class RequestInvalidError < Error; end
7
8
  class ResponseNotFoundError < Error; end
@@ -20,7 +20,7 @@ module OpenapiFirst
20
20
  private_constant :TYPES
21
21
 
22
22
  # @param error_type [Symbol] See Failure::TYPES.keys
23
- # @param errors [Array<OpenapiFirst::Schema::ValidationResult>]
23
+ # @param errors [Array<OpenapiFirst::Schema::ValidationError>]
24
24
  def self.fail!(error_type, message: nil, errors: nil)
25
25
  throw FAILURE, new(
26
26
  error_type,
@@ -19,10 +19,13 @@ module OpenapiFirst
19
19
 
20
20
  def call(env)
21
21
  request = find_request(env)
22
- response = @app.call(env)
23
- request.response(response).validate!
22
+ status, headers, body = @app.call(env)
24
23
 
25
- response
24
+ body = body.to_ary if body.respond_to?(:to_ary)
25
+
26
+ request.response(Rack::Response[status, headers, body]).validate!
27
+
28
+ [status, headers, body]
26
29
  end
27
30
 
28
31
  private
@@ -16,7 +16,7 @@ module OpenapiFirst
16
16
  invalid_query: 'Bad Query Parameter',
17
17
  invalid_header: 'Bad Request Header',
18
18
  invalid_path: 'Bad Request Path',
19
- invalid_cookie: 'Bod Request Cookie'
19
+ invalid_cookie: 'Bad Request Cookie'
20
20
  }.freeze
21
21
  private_constant :TITLES
22
22
 
@@ -22,7 +22,8 @@ module OpenapiFirst
22
22
  {
23
23
  status: status.to_s,
24
24
  source: { key => pointer(error.instance_location) },
25
- title: error.error
25
+ title: error.error,
26
+ code: error.type
26
27
  }
27
28
  end
28
29
  end
@@ -38,34 +38,34 @@ module OpenapiFirst
38
38
  end
39
39
 
40
40
  def validate_path_params!(request)
41
- parameters = operation.path_parameters
42
- return unless parameters
41
+ schema = operation.path_parameters_schema
42
+ return unless schema
43
43
 
44
- validation = parameters.schema.validate(request.path_parameters)
44
+ validation = schema.validate(request.path_parameters)
45
45
  Failure.fail!(:invalid_path, errors: validation.errors) if validation.error?
46
46
  end
47
47
 
48
48
  def validate_query_params!(request)
49
- parameters = operation.query_parameters
50
- return unless parameters
49
+ schema = operation.query_parameters_schema
50
+ return unless schema
51
51
 
52
- validation = parameters.schema.validate(request.query)
52
+ validation = schema.validate(request.query)
53
53
  Failure.fail!(:invalid_query, errors: validation.errors) if validation.error?
54
54
  end
55
55
 
56
56
  def validate_cookie_params!(request)
57
- parameters = operation.cookie_parameters
58
- return unless parameters
57
+ schema = operation.cookie_parameters_schema
58
+ return unless schema
59
59
 
60
- validation = parameters.schema.validate(request.cookies)
60
+ validation = schema.validate(request.cookies)
61
61
  Failure.fail!(:invalid_cookie, errors: validation.errors) if validation.error?
62
62
  end
63
63
 
64
64
  def validate_header_params!(request)
65
- parameters = operation.header_parameters
66
- return unless parameters
65
+ schema = operation.header_parameters_schema
66
+ return unless schema
67
67
 
68
- validation = parameters.schema.validate(request.headers)
68
+ validation = schema.validate(request.headers)
69
69
  Failure.fail!(:invalid_header, errors: validation.errors) if validation.error?
70
70
  end
71
71
 
@@ -73,7 +73,7 @@ module OpenapiFirst
73
73
  return unless operation.request_body
74
74
 
75
75
  RequestBodyValidator.new(operation).validate!(request.body, request.content_type)
76
- rescue BodyParser::ParsingError => e
76
+ rescue ParseError => e
77
77
  Failure.fail!(:invalid_body, message: e.message)
78
78
  end
79
79
  end
@@ -9,14 +9,14 @@ module OpenapiFirst
9
9
  @operation = operation
10
10
  end
11
11
 
12
- def validate(rack_response)
12
+ def validate(runtime_response)
13
13
  return unless operation
14
14
 
15
- response = Rack::Response[*rack_response.to_a]
16
15
  catch Failure::FAILURE do
17
- response_definition = response_for(operation, response.status, response.content_type)
18
- validate_response_body(response_definition.content_schema, response.body)
19
- validate_response_headers(response_definition.headers, response.headers)
16
+ validate_defined(runtime_response)
17
+ response_definition = runtime_response.response_definition
18
+ validate_response_body(response_definition.content_schema, runtime_response)
19
+ validate_response_headers(response_definition.headers, runtime_response.headers)
20
20
  nil
21
21
  end
22
22
  end
@@ -25,37 +25,40 @@ module OpenapiFirst
25
25
 
26
26
  attr_reader :operation
27
27
 
28
- def response_for(operation, status, content_type)
29
- response = operation.response_for(status, content_type)
30
- return response if response
28
+ def validate_defined(runtime_response)
29
+ return if runtime_response.known?
31
30
 
32
- unless operation.response_status_defined?(status)
33
- message = "Response status '#{status}' not found for '#{operation.name}'"
31
+ unless runtime_response.known_status?
32
+ message = "Response status '#{runtime_response.status}' not found for '#{runtime_response.name}'"
34
33
  Failure.fail!(:response_not_found, message:)
35
34
  end
35
+
36
+ content_type = runtime_response.content_type
36
37
  if content_type.nil? || content_type.empty?
37
- message = "Content-Type for '#{operation.name}' must not be empty"
38
+ message = "Content-Type for '#{runtime_response.name}' must not be empty"
38
39
  Failure.fail!(:invalid_response_header, message:)
39
40
  end
40
41
 
41
- message = "Content-Type '#{content_type}' is not defined for '#{operation.name}'"
42
+ message = "Content-Type '#{content_type}' is not defined for '#{runtime_response.name}'"
42
43
  Failure.fail!(:invalid_response_header, message:)
43
44
  end
44
45
 
45
- def validate_response_body(schema, response)
46
+ def validate_response_body(schema, runtime_response)
46
47
  return unless schema
47
48
 
48
- full_body = +''
49
- response.each { |chunk| full_body << chunk }
50
- data = full_body.empty? ? {} : load_json(full_body)
51
- validation = schema.validate(data)
49
+ begin
50
+ parsed_body = runtime_response.body
51
+ rescue ParseError => e
52
+ Failure.fail!(:invalid_response_body, message: e.message)
53
+ end
54
+
55
+ validation = schema.validate(parsed_body)
52
56
  Failure.fail!(:invalid_response_body, errors: validation.errors) if validation.error?
53
57
  end
54
58
 
55
- def validate_response_headers(response_header_definitions, response_headers)
59
+ def validate_response_headers(response_header_definitions, unpacked_headers)
56
60
  return unless response_header_definitions
57
61
 
58
- unpacked_headers = unpack_response_headers(response_header_definitions, response_headers)
59
62
  response_header_definitions.each do |name, definition|
60
63
  next if name == 'Content-Type'
61
64
 
@@ -84,13 +87,6 @@ module OpenapiFirst
84
87
  errors: validation_result.errors)
85
88
  end
86
89
 
87
- def unpack_response_headers(response_header_definitions, response_headers)
88
- headers_as_parameters = response_header_definitions.map do |name, definition|
89
- definition.merge('name' => name, 'in' => 'header')
90
- end
91
- OpenapiParameters::Header.new(headers_as_parameters).unpack(response_headers)
92
- end
93
-
94
90
  def load_json(string)
95
91
  MultiJson.load(string)
96
92
  rescue MultiJson::ParseError
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'forwardable'
4
+ require 'openapi_parameters'
4
5
  require_relative 'runtime_response'
5
6
  require_relative 'body_parser'
6
7
  require_relative 'request_validation/validator'
@@ -17,8 +18,11 @@ module OpenapiFirst
17
18
  @original_path_params = path_params
18
19
  end
19
20
 
20
- def_delegators :@request, :content_type, :media_type
21
- def_delegators :@operation, :operation_id
21
+ def_delegators :@request, :content_type, :media_type, :path
22
+ def_delegators :@operation, :operation_id, :request_method
23
+ def_delegator :@path_item, :path, :path_definition
24
+
25
+ attr_reader :path_item
22
26
 
23
27
  def known?
24
28
  known_path? && known_request_method?
@@ -38,25 +42,32 @@ module OpenapiFirst
38
42
  end
39
43
 
40
44
  def path_parameters
45
+ return {} unless operation.path_parameters
46
+
41
47
  @path_parameters ||=
42
- operation.path_parameters&.unpack(@original_path_params) || {}
48
+ OpenapiParameters::Path.new(operation.path_parameters).unpack(@original_path_params) || {}
43
49
  end
44
50
 
45
51
  def query
52
+ return {} unless operation.query_parameters
53
+
46
54
  @query ||=
47
- operation.query_parameters&.unpack(request.env) || {}
55
+ OpenapiParameters::Query.new(operation.query_parameters).unpack(request.env[Rack::QUERY_STRING]) || {}
48
56
  end
49
57
 
50
58
  alias query_parameters query
51
59
 
52
60
  def headers
53
- @headers ||=
54
- operation.header_parameters&.unpack(request.env) || {}
61
+ return {} unless operation.header_parameters
62
+
63
+ @headers ||= OpenapiParameters::Header.new(operation.header_parameters).unpack_env(request.env) || {}
55
64
  end
56
65
 
57
66
  def cookies
67
+ return {} unless operation.cookie_parameters
68
+
58
69
  @cookies ||=
59
- operation.cookie_parameters&.unpack(request.env) || {}
70
+ OpenapiParameters::Cookie.new(operation.cookie_parameters).unpack(request.env[Rack::HTTP_COOKIE]) || {}
60
71
  end
61
72
 
62
73
  def body
@@ -79,6 +90,6 @@ module OpenapiFirst
79
90
 
80
91
  private
81
92
 
82
- attr_reader :request, :operation, :path_item
93
+ attr_reader :request, :operation
83
94
  end
84
95
  end
@@ -1,20 +1,43 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'forwardable'
4
+ require_relative 'body_parser'
3
5
  require_relative 'response_validation/validator'
4
6
 
5
7
  module OpenapiFirst
6
8
  class RuntimeResponse
9
+ extend Forwardable
10
+
7
11
  def initialize(operation, rack_response)
8
12
  @operation = operation
9
13
  @rack_response = rack_response
10
14
  end
11
15
 
16
+ def_delegators :@rack_response, :status, :content_type
17
+ def_delegators :@operation, :name
18
+
19
+ def known?
20
+ !!response_definition
21
+ end
22
+
23
+ def known_status?
24
+ @operation.response_status_defined?(status)
25
+ end
26
+
12
27
  def description
13
28
  response_definition&.description
14
29
  end
15
30
 
31
+ def body
32
+ @body ||= content_type =~ /json/i ? load_json(original_body) : original_body
33
+ end
34
+
35
+ def headers
36
+ @headers ||= unpack_response_headers
37
+ end
38
+
16
39
  def validate
17
- ResponseValidation::Validator.new(@operation).validate(@rack_response)
40
+ ResponseValidation::Validator.new(@operation).validate(self)
18
41
  end
19
42
 
20
43
  def validate!
@@ -22,10 +45,31 @@ module OpenapiFirst
22
45
  error&.raise!
23
46
  end
24
47
 
48
+ def response_definition
49
+ @response_definition ||= @operation.response_for(status, content_type)
50
+ end
51
+
25
52
  private
26
53
 
27
- def response_definition
28
- @response_definition ||= @operation.response_for(@rack_response.status, @rack_response.content_type)
54
+ def original_body
55
+ buffered_body = String.new
56
+ @rack_response.body.each { |chunk| buffered_body << chunk }
57
+ buffered_body
58
+ end
59
+
60
+ def load_json(string)
61
+ MultiJson.load(string)
62
+ rescue MultiJson::ParseError
63
+ raise ParseError, 'Failed to parse response body as JSON'
64
+ end
65
+
66
+ def unpack_response_headers
67
+ return {} if response_definition&.headers.nil?
68
+
69
+ headers_as_parameters = response_definition.headers.map do |name, definition|
70
+ definition.merge('name' => name, 'in' => 'header')
71
+ end
72
+ OpenapiParameters::Header.new(headers_as_parameters).unpack(@rack_response.headers)
29
73
  end
30
74
  end
31
75
  end
@@ -32,10 +32,6 @@ module OpenapiFirst
32
32
  )
33
33
  end
34
34
 
35
- def [](key)
36
- @schema[key]
37
- end
38
-
39
35
  private
40
36
 
41
37
  def before_property_validation(data, property, property_schema, parent)
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module OpenapiFirst
4
- VERSION = '1.0.0'
4
+ VERSION = '1.1.1'
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: openapi_first
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.0
4
+ version: 1.1.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Andreas Haller
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2024-01-06 00:00:00.000000000 Z
11
+ date: 2024-01-12 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: json_refs
@@ -133,13 +133,8 @@ files:
133
133
  - lib/openapi_first/body_parser.rb
134
134
  - lib/openapi_first/configuration.rb
135
135
  - lib/openapi_first/definition.rb
136
- - lib/openapi_first/definition/cookie_parameters.rb
137
- - lib/openapi_first/definition/header_parameters.rb
138
136
  - lib/openapi_first/definition/operation.rb
139
- - lib/openapi_first/definition/parameters.rb
140
137
  - lib/openapi_first/definition/path_item.rb
141
- - lib/openapi_first/definition/path_parameters.rb
142
- - lib/openapi_first/definition/query_parameters.rb
143
138
  - lib/openapi_first/definition/request_body.rb
144
139
  - lib/openapi_first/definition/response.rb
145
140
  - lib/openapi_first/definition/responses.rb
@@ -1,12 +0,0 @@
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
@@ -1,12 +0,0 @@
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
@@ -1,47 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'forwardable'
4
- require_relative '../schema'
5
-
6
- module OpenapiFirst
7
- class Parameters
8
- extend Forwardable
9
-
10
- def initialize(parameter_definitions, openapi_version:)
11
- @parameter_definitions = parameter_definitions
12
- @openapi_version = openapi_version
13
- end
14
-
15
- def_delegators :parameters, :map
16
-
17
- def empty?
18
- @parameter_definitions.empty?
19
- end
20
-
21
- def schema
22
- @schema ||= build_schema
23
- end
24
-
25
- def parameters
26
- @parameter_definitions.map do |parameter_object|
27
- OpenapiParameters::Parameter.new(parameter_object)
28
- end
29
- end
30
-
31
- private
32
-
33
- def build_schema
34
- init_schema = {
35
- 'type' => 'object',
36
- 'properties' => {},
37
- 'required' => []
38
- }
39
- schema = @parameter_definitions.each_with_object(init_schema) do |parameter_def, result|
40
- parameter = OpenapiParameters::Parameter.new(parameter_def)
41
- result['properties'][parameter.name] = parameter.schema if parameter.schema
42
- result['required'] << parameter.name if parameter.required?
43
- end
44
- Schema.new(schema, openapi_version: @openapi_version)
45
- end
46
- end
47
- end
@@ -1,12 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'openapi_parameters'
4
- require_relative 'parameters'
5
-
6
- module OpenapiFirst
7
- class PathParameters < Parameters
8
- def unpack(original_path_params)
9
- OpenapiParameters::Path.new(@parameter_definitions).unpack(original_path_params)
10
- end
11
- end
12
- end
@@ -1,12 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'openapi_parameters'
4
- require_relative 'parameters'
5
-
6
- module OpenapiFirst
7
- class QueryParameters < Parameters
8
- def unpack(env)
9
- OpenapiParameters::Query.new(@parameter_definitions).unpack(env['QUERY_STRING'])
10
- end
11
- end
12
- end