openapi_first 1.0.0.beta4 → 1.0.0.beta6

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.
Files changed (70) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ruby.yml +2 -1
  3. data/CHANGELOG.md +13 -0
  4. data/Gemfile +2 -1
  5. data/Gemfile.lock +17 -22
  6. data/Gemfile.rack2 +15 -0
  7. data/README.md +17 -7
  8. data/lib/openapi_first/body_parser.rb +28 -0
  9. data/lib/openapi_first/config.rb +4 -3
  10. data/lib/openapi_first/definition/cookie_parameters.rb +12 -0
  11. data/lib/openapi_first/definition/has_content.rb +37 -0
  12. data/lib/openapi_first/definition/header_parameters.rb +12 -0
  13. data/lib/openapi_first/definition/operation.rb +103 -0
  14. data/lib/openapi_first/definition/parameters.rb +47 -0
  15. data/lib/openapi_first/definition/path_item.rb +23 -0
  16. data/lib/openapi_first/definition/path_parameters.rb +13 -0
  17. data/lib/openapi_first/definition/query_parameters.rb +12 -0
  18. data/lib/openapi_first/definition/request_body.rb +32 -0
  19. data/lib/openapi_first/definition/response.rb +37 -0
  20. data/lib/openapi_first/definition/schema/result.rb +17 -0
  21. data/lib/openapi_first/{schema_validation.rb → definition/schema.rb} +6 -6
  22. data/lib/openapi_first/definition.rb +26 -6
  23. data/lib/openapi_first/error_response.rb +28 -12
  24. data/lib/openapi_first/error_responses/default.rb +58 -0
  25. data/lib/openapi_first/error_responses/json_api.rb +58 -0
  26. data/lib/openapi_first/request_body_validator.rb +18 -22
  27. data/lib/openapi_first/request_validation.rb +68 -58
  28. data/lib/openapi_first/request_validation_error.rb +31 -0
  29. data/lib/openapi_first/response_validation.rb +33 -13
  30. data/lib/openapi_first/response_validator.rb +1 -0
  31. data/lib/openapi_first/router.rb +20 -62
  32. data/lib/openapi_first/version.rb +1 -1
  33. data/lib/openapi_first.rb +2 -13
  34. data/openapi_first.gemspec +8 -5
  35. metadata +44 -57
  36. data/.rspec +0 -3
  37. data/.rubocop.yml +0 -14
  38. data/Rakefile +0 -15
  39. data/benchmarks/Gemfile +0 -16
  40. data/benchmarks/Gemfile.lock +0 -131
  41. data/benchmarks/README.md +0 -29
  42. data/benchmarks/apps/committee_with_hanami_api.ru +0 -26
  43. data/benchmarks/apps/committee_with_response_validation.ru +0 -29
  44. data/benchmarks/apps/committee_with_sinatra.ru +0 -31
  45. data/benchmarks/apps/grape.ru +0 -21
  46. data/benchmarks/apps/hanami_api.ru +0 -21
  47. data/benchmarks/apps/hanami_router.ru +0 -14
  48. data/benchmarks/apps/openapi.yaml +0 -268
  49. data/benchmarks/apps/openapi_first_with_hanami_api.ru +0 -24
  50. data/benchmarks/apps/openapi_first_with_plain_rack.ru +0 -32
  51. data/benchmarks/apps/openapi_first_with_response_validation.ru +0 -25
  52. data/benchmarks/apps/openapi_first_with_sinatra.ru +0 -29
  53. data/benchmarks/apps/roda.ru +0 -27
  54. data/benchmarks/apps/sinatra.ru +0 -26
  55. data/benchmarks/apps/syro.ru +0 -25
  56. data/benchmarks/benchmark-wrk.sh +0 -3
  57. data/benchmarks/benchmarks.rb +0 -48
  58. data/benchmarks/post.lua +0 -3
  59. data/bin/console +0 -15
  60. data/bin/setup +0 -8
  61. data/examples/README.md +0 -13
  62. data/examples/app.rb +0 -18
  63. data/examples/config.ru +0 -7
  64. data/examples/openapi.yaml +0 -29
  65. data/lib/openapi_first/body_parser_middleware.rb +0 -53
  66. data/lib/openapi_first/default_error_response.rb +0 -47
  67. data/lib/openapi_first/operation.rb +0 -142
  68. data/lib/openapi_first/operation_schemas.rb +0 -52
  69. data/lib/openapi_first/string_keyed_hash.rb +0 -20
  70. data/lib/openapi_first/validation_result.rb +0 -15
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c02c053192b6b39cb8f05acd35f7a257ee98b449c49f8ceeec4ac6e542f09e15
4
- data.tar.gz: c36d3598b263ebd3d1a9bd23e6aa0b66256890f68e2f70fe245ca932d2f004f0
3
+ metadata.gz: 8173c11075add6c2c7a1a6cf024ea86ba448b185574e346fe288afa3aafcf677
4
+ data.tar.gz: f3dcd95b9c06adcfcada5430a8412a3ee6ea37c1c117e79546436ade496c5778
5
5
  SHA512:
6
- metadata.gz: e70192d7c2cb58734b8ebb6ccf53b2f243a98e78adbea271a3b0df1468f400d60080ead1ea68c07dd33bd9985d67de02ec4e0200e3ca0d49272a893c97611ffb
7
- data.tar.gz: c59e62cd24dd767330f7e9ce6ad96a9c13005a4498c39f3ccf10bc801b49b6bddccf3ac62f858ae3ecf56fa4c695b4ad010fea7292171f98b640dec968ea357d
6
+ metadata.gz: d29578cdcc1573ff8903a7060a85e2b334b1ff908d4b3b7973e64b336ba414bfdf7b6fb762aff48cf0feaf78f5a29403d2d063b117fb1278bed628195363dd74
7
+ data.tar.gz: 0a6409930941ea9a1f4b734f49752ec18f8e9779ce6f5279e2db65022066500d8b7f9067af23625e780e20cf0d62f542c0a4b8388fea6f7cc37f63cd372baaf5
@@ -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
@@ -2,6 +2,19 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## 1.0.0.beta6
6
+ - Fix: Make response header validation work with rack 3
7
+ - Refactor router
8
+ - Remove dependency hanami-router
9
+ - PathItem and Operation for a request can be found by calling methods on the Definitnion
10
+ - Fixed https://github.com/ahx/openapi_first/issues/155
11
+ - Breaking / Regression: A paths like /pets/{from}-{to} if there is a path "/pets/{id}"
12
+
13
+ ## 1.0.0.beta5
14
+
15
+ - Added: `OpenapiFirst::Config.default_options=` to set default options globally
16
+ - Added: You can define custom error responses by subclassing `OpenapiFirst::ErrorResponse` and register it via `OpenapiFirst::Plugins.register_error_response(name, MyCustomErrorResponse)`
17
+
5
18
  ## 1.0.0.beta4
6
19
 
7
20
  - Update json_schemer to version 2.0
data/Gemfile CHANGED
@@ -2,9 +2,10 @@
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
 
7
+ gem 'rack', '>= 3.0.0'
8
+
8
9
  group :test, :development do
9
10
  gem 'bundler'
10
11
  gem 'rack-test'
data/Gemfile.lock CHANGED
@@ -1,25 +1,20 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- openapi_first (1.0.0.beta4)
5
- hanami-router (~> 2.0.0)
4
+ openapi_first (1.0.0.beta6)
6
5
  json_refs (~> 0.1, >= 0.1.7)
7
6
  json_schemer (~> 2.0.0)
8
- multi_json (~> 1.14)
9
- openapi_parameters (~> 0.2.2)
7
+ multi_json (~> 1.15)
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
13
13
  remote: https://rubygems.org/
14
14
  specs:
15
15
  ast (2.4.2)
16
- base64 (0.1.1)
17
16
  diff-lcs (1.5.0)
18
17
  hana (1.3.7)
19
- hanami-router (2.0.2)
20
- mustermann (~> 3.0)
21
- mustermann-contrib (~> 3.0)
22
- rack (~> 2.0)
23
18
  hansi (0.2.1)
24
19
  json (2.6.3)
25
20
  json_refs (0.1.8)
@@ -35,20 +30,20 @@ GEM
35
30
  mustermann-contrib (3.0.0)
36
31
  hansi (~> 0.2.0)
37
32
  mustermann (= 3.0.0)
38
- openapi_parameters (0.2.2)
33
+ openapi_parameters (0.3.2)
39
34
  rack (>= 2.2)
40
35
  zeitwerk (~> 2.6)
41
36
  parallel (1.23.0)
42
- parser (3.2.2.3)
37
+ parser (3.2.2.4)
43
38
  ast (~> 2.4.1)
44
39
  racc
45
- racc (1.7.1)
46
- rack (2.2.8)
40
+ racc (1.7.3)
41
+ rack (3.0.8)
47
42
  rack-test (2.1.0)
48
43
  rack (>= 1.3)
49
44
  rainbow (3.1.1)
50
- rake (13.0.6)
51
- regexp_parser (2.8.1)
45
+ rake (13.1.0)
46
+ regexp_parser (2.8.2)
52
47
  rexml (3.2.6)
53
48
  rspec (3.12.0)
54
49
  rspec-core (~> 3.12.0)
@@ -63,19 +58,18 @@ GEM
63
58
  diff-lcs (>= 1.2.0, < 2.0)
64
59
  rspec-support (~> 3.12.0)
65
60
  rspec-support (3.12.1)
66
- rubocop (1.56.3)
67
- base64 (~> 0.1.1)
61
+ rubocop (1.57.2)
68
62
  json (~> 2.3)
69
63
  language_server-protocol (>= 3.17.0)
70
64
  parallel (~> 1.10)
71
- parser (>= 3.2.2.3)
65
+ parser (>= 3.2.2.4)
72
66
  rainbow (>= 2.2.2, < 4.0)
73
67
  regexp_parser (>= 1.8, < 3.0)
74
68
  rexml (>= 3.2.5, < 4.0)
75
69
  rubocop-ast (>= 1.28.1, < 2.0)
76
70
  ruby-progressbar (~> 1.7)
77
71
  unicode-display_width (>= 2.4.0, < 3.0)
78
- rubocop-ast (1.29.0)
72
+ rubocop-ast (1.30.0)
79
73
  parser (>= 3.2.1.0)
80
74
  ruby-progressbar (1.13.0)
81
75
  ruby2_keywords (0.0.5)
@@ -83,9 +77,9 @@ GEM
83
77
  unf (~> 0.1.4)
84
78
  unf (0.1.4)
85
79
  unf_ext
86
- unf_ext (0.0.8.2)
87
- unicode-display_width (2.4.2)
88
- zeitwerk (2.6.11)
80
+ unf_ext (0.0.9)
81
+ unicode-display_width (2.5.0)
82
+ zeitwerk (2.6.12)
89
83
 
90
84
  PLATFORMS
91
85
  arm64-darwin-21
@@ -94,6 +88,7 @@ PLATFORMS
94
88
  DEPENDENCIES
95
89
  bundler
96
90
  openapi_first!
91
+ rack (>= 3.0.0)
97
92
  rack-test
98
93
  rake
99
94
  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
data/README.md CHANGED
@@ -28,11 +28,11 @@ It adds these fields to the Rack env:
28
28
 
29
29
  ### Options and defaults
30
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`, Your implementation of `ErrorResponse` | :default
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
36
 
37
37
  The error responses conform with [JSON:API](https://jsonapi.org).
38
38
 
@@ -40,7 +40,7 @@ Here's an example response body for a missing query parameter "search":
40
40
 
41
41
  ```json
42
42
  http-status: 400
43
- content-type: "application/vnd.api+json"
43
+ content-type: "application/json"
44
44
 
45
45
  {
46
46
  "errors": [
@@ -54,7 +54,6 @@ content-type: "application/vnd.api+json"
54
54
  }
55
55
  ```
56
56
 
57
-
58
57
  ### Parameters
59
58
 
60
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:
@@ -120,6 +119,17 @@ This middleware adds `env['openapi.operation']` which holds an instance of `Open
120
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) |
121
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) |
122
121
 
122
+ ## Global configuration
123
+
124
+ You can configure default options gobally via `OpenapiFirst::Config`:
125
+
126
+ ```ruby
127
+ OpenapiFirst::Config.default_options = {
128
+ error_response: :json_api,
129
+ request_validation_raise_error: true
130
+ }
131
+ ```
132
+
123
133
  ## Alternatives
124
134
 
125
135
  This gem is inspired by [committee](https://github.com/interagent/committee) (Ruby) and [connexion](https://github.com/zalando/connexion) (Python).
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'multi_json'
4
+
5
+ module OpenapiFirst
6
+ class BodyParser
7
+ def parse_body(env)
8
+ request = Rack::Request.new(env)
9
+ body = read_body(request)
10
+ return if body.empty?
11
+
12
+ return MultiJson.load(body) if request.media_type =~ (/json/i) && (request.media_type =~ /json/i)
13
+ return request.POST if request.form_data?
14
+
15
+ body
16
+ rescue MultiJson::ParseError
17
+ raise BodyParsingError, 'Failed to parse body as application/json'
18
+ end
19
+
20
+ private
21
+
22
+ def read_body(request)
23
+ body = request.body.read
24
+ request.body.rewind
25
+ body
26
+ end
27
+ end
28
+ end
@@ -2,11 +2,12 @@
2
2
 
3
3
  module OpenapiFirst
4
4
  class Config
5
- def initialize(error_response: :default)
6
- @error_response = error_response
5
+ def initialize(error_response: :default, request_validation_raise_error: false)
6
+ @error_response = Plugins.find_error_response(error_response)
7
+ @request_validation_raise_error = request_validation_raise_error
7
8
  end
8
9
 
9
- attr_reader :error_response
10
+ attr_reader :error_response, :request_validation_raise_error
10
11
 
11
12
  def self.default_options
12
13
  @default_options ||= new
@@ -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,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'schema'
4
+
5
+ module OpenapiFirst
6
+ module HasContent
7
+ def schema_for(content_type)
8
+ return unless content&.any?
9
+
10
+ content_schemas&.fetch(content_type) do
11
+ type = content_type.split(';')[0]
12
+ content_schemas[type] || content_schemas["#{type.split('/')[0]}/*"] || content_schemas['*/*']
13
+ end
14
+ end
15
+
16
+ private
17
+
18
+ def content
19
+ raise NotImplementedError
20
+ end
21
+
22
+ def schema_write?
23
+ raise NotImplementedError
24
+ end
25
+
26
+ def content_schemas
27
+ @content_schemas ||= content&.each_with_object({}) do |kv, result|
28
+ type, media_type = kv
29
+ schema_object = media_type['schema']
30
+ next unless schema_object
31
+
32
+ result[type] = Schema.new(schema_object, write: schema_write?,
33
+ openapi_version: @operation.openapi_version)
34
+ end
35
+ end
36
+ end
37
+ 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
@@ -0,0 +1,103 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'forwardable'
4
+ require 'set'
5
+ require_relative 'request_body'
6
+ require_relative 'response'
7
+ require_relative 'query_parameters'
8
+ require_relative 'header_parameters'
9
+ require_relative 'path_parameters'
10
+ require_relative 'cookie_parameters'
11
+ require_relative 'schema'
12
+
13
+ module OpenapiFirst
14
+ class Operation
15
+ extend Forwardable
16
+ def_delegators :operation_object,
17
+ :[],
18
+ :dig
19
+
20
+ WRITE_METHODS = Set.new(%w[post put patch delete]).freeze
21
+ private_constant :WRITE_METHODS
22
+
23
+ attr_reader :path, :method, :openapi_version
24
+
25
+ def initialize(path, request_method, path_item_object, openapi_version:)
26
+ @path = path
27
+ @method = request_method
28
+ @path_item_object = path_item_object
29
+ @openapi_version = openapi_version
30
+ @operation_object = @path_item_object[request_method]
31
+ end
32
+
33
+ def operation_id
34
+ operation_object['operationId']
35
+ end
36
+
37
+ def read?
38
+ !write?
39
+ end
40
+
41
+ def write?
42
+ WRITE_METHODS.include?(method)
43
+ end
44
+
45
+ def request_body
46
+ @request_body ||= RequestBody.new(operation_object['requestBody'], self) if operation_object['requestBody']
47
+ end
48
+
49
+ def response_for(status)
50
+ response_object = operation_object.dig('responses', status.to_s) ||
51
+ operation_object.dig('responses', "#{status / 100}XX") ||
52
+ operation_object.dig('responses', "#{status / 100}xx") ||
53
+ operation_object.dig('responses', 'default')
54
+ Response.new(status, response_object, self) if response_object
55
+ end
56
+
57
+ def name
58
+ @name ||= "#{method.upcase} #{path} (#{operation_id})"
59
+ end
60
+
61
+ def query_parameters
62
+ @query_parameters ||= build_parameters(all_parameters.filter { |p| p['in'] == 'query' }, QueryParameters)
63
+ end
64
+
65
+ def path_parameters
66
+ @path_parameters ||= build_parameters(all_parameters.filter { |p| p['in'] == 'path' }, PathParameters)
67
+ end
68
+
69
+ IGNORED_HEADERS = Set['Content-Type', 'Accept', 'Authorization'].freeze
70
+ private_constant :IGNORED_HEADERS
71
+
72
+ def header_parameters
73
+ @header_parameters ||= build_parameters(find_header_parameters, HeaderParameters)
74
+ end
75
+
76
+ def cookie_parameters
77
+ @cookie_parameters ||= build_parameters(all_parameters.filter { |p| p['in'] == 'cookie' }, CookieParameters)
78
+ end
79
+
80
+ def all_parameters
81
+ @all_parameters ||= begin
82
+ parameters = @path_item_object['parameters']&.dup || []
83
+ parameters_on_operation = operation_object['parameters']
84
+ parameters.concat(parameters_on_operation) if parameters_on_operation
85
+ parameters
86
+ end
87
+ end
88
+
89
+ private
90
+
91
+ attr_reader :operation_object
92
+
93
+ def build_parameters(parameters, klass)
94
+ klass.new(parameters, openapi_version:) if parameters.any?
95
+ end
96
+
97
+ def find_header_parameters
98
+ all_parameters.filter do |p|
99
+ p['in'] == 'header' && !IGNORED_HEADERS.include?(p['name'])
100
+ end
101
+ end
102
+ end
103
+ end
@@ -0,0 +1,47 @@
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
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'operation'
4
+
5
+ module OpenapiFirst
6
+ class PathItem
7
+ def initialize(path, path_item_object, openapi_version:)
8
+ @path = path
9
+ @path_item_object = path_item_object
10
+ @openapi_version = openapi_version
11
+ end
12
+
13
+ attr_reader :path
14
+
15
+ def find_operation(request_method)
16
+ return unless @path_item_object[request_method]
17
+
18
+ Operation.new(
19
+ @path, request_method, @path_item_object, openapi_version: @openapi_version
20
+ )
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'openapi_parameters'
4
+ require_relative 'parameters'
5
+ require_relative '../router'
6
+
7
+ module OpenapiFirst
8
+ class PathParameters < Parameters
9
+ def unpack(env)
10
+ OpenapiParameters::Path.new(@parameter_definitions).unpack(env[Router::RAW_PATH_PARAMS])
11
+ end
12
+ end
13
+ 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 QueryParameters < Parameters
8
+ def unpack(env)
9
+ OpenapiParameters::Query.new(@parameter_definitions).unpack(env['QUERY_STRING'])
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'has_content'
4
+
5
+ module OpenapiFirst
6
+ class RequestBody
7
+ include HasContent
8
+
9
+ def initialize(request_body_object, operation)
10
+ @object = request_body_object
11
+ @operation = operation
12
+ end
13
+
14
+ def description
15
+ @object['description']
16
+ end
17
+
18
+ def required?
19
+ !!@object['required']
20
+ end
21
+
22
+ private
23
+
24
+ def schema_write?
25
+ @operation.write?
26
+ end
27
+
28
+ def content
29
+ @object['content']
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'has_content'
4
+
5
+ module OpenapiFirst
6
+ class Response
7
+ include HasContent
8
+
9
+ def initialize(status, response_object, operation)
10
+ @status = status&.to_i
11
+ @object = response_object
12
+ @operation = operation
13
+ end
14
+
15
+ attr_reader :status
16
+
17
+ def description
18
+ @object['description']
19
+ end
20
+
21
+ def headers
22
+ @object['headers']
23
+ end
24
+
25
+ def content?
26
+ !!content&.any?
27
+ end
28
+
29
+ private
30
+
31
+ def schema_write? = false
32
+
33
+ def content
34
+ @object['content']
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OpenapiFirst
4
+ class Schema
5
+ Result = Struct.new(:output, :schema, :data, keyword_init: true) do
6
+ def valid? = output['valid']
7
+ def error? = !output['valid']
8
+
9
+ # Returns a message that is used in exception messages.
10
+ def message
11
+ return if valid?
12
+
13
+ (output['errors']&.map { |e| e['error'] }&.join('. ') || output['error'])
14
+ end
15
+ end
16
+ end
17
+ end
@@ -1,11 +1,11 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'json_schemer'
4
- require_relative 'validation_result'
4
+ require_relative 'schema/result'
5
5
 
6
6
  module OpenapiFirst
7
- class SchemaValidation
8
- attr_reader :raw_schema
7
+ class Schema
8
+ attr_reader :schema
9
9
 
10
10
  SCHEMAS = {
11
11
  '3.1' => 'https://spec.openapis.org/oas/3.1/dialect/base',
@@ -13,7 +13,7 @@ module OpenapiFirst
13
13
  }.freeze
14
14
 
15
15
  def initialize(schema, openapi_version:, write: true)
16
- @raw_schema = schema
16
+ @schema = schema
17
17
  @schemer = JSONSchemer.schema(
18
18
  schema,
19
19
  access_mode: write ? 'write' : 'read',
@@ -25,9 +25,9 @@ module OpenapiFirst
25
25
  end
26
26
 
27
27
  def validate(data)
28
- ValidationResult.new(
28
+ Result.new(
29
29
  output: @schemer.validate(data),
30
- schema: raw_schema,
30
+ schema:,
31
31
  data:
32
32
  )
33
33
  end