openapi_first 1.0.0.beta4 → 1.0.0.beta6

Sign up to get free protection for your applications and to get access to all the features.
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