openapi_first 0.7.0.alpha1 → 3.1.0.beta1
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 +4 -4
- data/CHANGELOG.md +585 -2
- data/LICENSE.txt +21 -0
- data/README.md +360 -114
- data/lib/openapi_first/builder.rb +214 -0
- data/lib/openapi_first/child_configuration.rb +26 -0
- data/lib/openapi_first/configuration.rb +76 -0
- data/lib/openapi_first/definition.rb +98 -13
- data/lib/openapi_first/error_response.rb +40 -0
- data/lib/openapi_first/error_responses/default.rb +74 -0
- data/lib/openapi_first/error_responses/jsonapi.rb +60 -0
- data/lib/openapi_first/errors.rb +40 -0
- data/lib/openapi_first/failure.rb +77 -0
- data/lib/openapi_first/file_loader.rb +22 -0
- data/lib/openapi_first/header.rb +9 -0
- data/lib/openapi_first/json.rb +25 -0
- data/lib/openapi_first/middlewares/request_validation.rb +58 -0
- data/lib/openapi_first/middlewares/response_validation.rb +41 -0
- data/lib/openapi_first/parsed_request.rb +5 -0
- data/lib/openapi_first/ref_resolver.rb +193 -0
- data/lib/openapi_first/registry.rb +44 -0
- data/lib/openapi_first/request.rb +79 -0
- data/lib/openapi_first/request_body_parsers.rb +64 -0
- data/lib/openapi_first/request_validator.rb +32 -0
- data/lib/openapi_first/response.rb +43 -0
- data/lib/openapi_first/response_body_parsers.rb +31 -0
- data/lib/openapi_first/response_parser.rb +50 -0
- data/lib/openapi_first/response_validator.rb +16 -48
- data/lib/openapi_first/router/find_content.rb +17 -0
- data/lib/openapi_first/router/find_response.rb +53 -0
- data/lib/openapi_first/router/path_template.rb +48 -0
- data/lib/openapi_first/router.rb +84 -57
- data/lib/openapi_first/schema/hash.rb +39 -0
- data/lib/openapi_first/schema/validation_error.rb +45 -0
- data/lib/openapi_first/schema/validation_result.rb +30 -0
- data/lib/openapi_first/test/app.rb +30 -0
- data/lib/openapi_first/test/callable.rb +25 -0
- data/lib/openapi_first/test/configuration.rb +76 -0
- data/lib/openapi_first/test/coverage/covered_request.rb +11 -0
- data/lib/openapi_first/test/coverage/covered_response.rb +11 -0
- data/lib/openapi_first/test/coverage/plan.rb +77 -0
- data/lib/openapi_first/test/coverage/request_task.rb +42 -0
- data/lib/openapi_first/test/coverage/response_task.rb +42 -0
- data/lib/openapi_first/test/coverage/route_task.rb +13 -0
- data/lib/openapi_first/test/coverage/terminal_formatter.rb +115 -0
- data/lib/openapi_first/test/coverage/tracker.rb +32 -0
- data/lib/openapi_first/test/coverage.rb +102 -0
- data/lib/openapi_first/test/methods.rb +62 -0
- data/lib/openapi_first/test/minitest_helpers.rb +28 -0
- data/lib/openapi_first/test/observe.rb +37 -0
- data/lib/openapi_first/test/observer_middleware.rb +23 -0
- data/lib/openapi_first/test/plain_helpers.rb +31 -0
- data/lib/openapi_first/test.rb +171 -0
- data/lib/openapi_first/validated_request.rb +79 -0
- data/lib/openapi_first/validated_response.rb +53 -0
- data/lib/openapi_first/validators/request_body.rb +25 -0
- data/lib/openapi_first/validators/request_parameters.rb +49 -0
- data/lib/openapi_first/validators/response_body.rb +27 -0
- data/lib/openapi_first/validators/response_headers.rb +40 -0
- data/lib/openapi_first/version.rb +1 -1
- data/lib/openapi_first.rb +67 -35
- metadata +99 -133
- data/.github/CODEOWNERS +0 -1
- data/.gitignore +0 -11
- data/.rspec +0 -3
- data/.rubocop.yml +0 -14
- data/.travis.yml +0 -8
- data/Gemfile +0 -11
- data/Gemfile.lock +0 -126
- data/Rakefile +0 -15
- data/benchmarks/Gemfile +0 -13
- data/benchmarks/Gemfile.lock +0 -144
- data/benchmarks/apps/committee.ru +0 -30
- data/benchmarks/apps/grape.ru +0 -21
- data/benchmarks/apps/hanami_router.ru +0 -14
- data/benchmarks/apps/openapi.yaml +0 -86
- data/benchmarks/apps/openapi_first.ru +0 -22
- data/benchmarks/apps/openapi_first_resolve_only.ru +0 -23
- data/benchmarks/apps/sinatra.ru +0 -26
- data/benchmarks/apps/syro.ru +0 -25
- data/benchmarks/benchmarks.rb +0 -46
- data/bin/console +0 -15
- data/bin/setup +0 -8
- data/examples/README.md +0 -13
- data/examples/app.rb +0 -12
- data/examples/config.ru +0 -7
- data/examples/openapi.yaml +0 -29
- data/lib/openapi_first/app.rb +0 -27
- data/lib/openapi_first/coverage.rb +0 -28
- data/lib/openapi_first/operation.rb +0 -48
- data/lib/openapi_first/operation_resolver.rb +0 -43
- data/lib/openapi_first/request_validation.rb +0 -144
- data/lib/openapi_first/utils.rb +0 -31
- data/lib/openapi_first/validation.rb +0 -15
- data/lib/openapi_first/validation_format.rb +0 -31
- data/openapi_first.gemspec +0 -46
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require 'committee'
|
|
4
|
-
require 'syro'
|
|
5
|
-
require 'multi_json'
|
|
6
|
-
|
|
7
|
-
app = Syro.new do
|
|
8
|
-
on 'hello' do
|
|
9
|
-
on :id do
|
|
10
|
-
get do
|
|
11
|
-
res.json MultiJson.dump(hello: 'world', id: inbox[:id])
|
|
12
|
-
end
|
|
13
|
-
end
|
|
14
|
-
|
|
15
|
-
get do
|
|
16
|
-
res.json [MultiJson.dump(hello: 'world')]
|
|
17
|
-
end
|
|
18
|
-
|
|
19
|
-
post do
|
|
20
|
-
res.status = 201
|
|
21
|
-
res.json MultiJson.dump(hello: 'world')
|
|
22
|
-
end
|
|
23
|
-
end
|
|
24
|
-
end
|
|
25
|
-
|
|
26
|
-
use Committee::Middleware::RequestValidation,
|
|
27
|
-
schema_path: './apps/openapi.yaml',
|
|
28
|
-
coerce_date_times: false
|
|
29
|
-
|
|
30
|
-
run app
|
data/benchmarks/apps/grape.ru
DELETED
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require 'grape'
|
|
4
|
-
|
|
5
|
-
class GrapeExample < Grape::API
|
|
6
|
-
format :json
|
|
7
|
-
|
|
8
|
-
get :hello do
|
|
9
|
-
[{ hello: 'world' }]
|
|
10
|
-
end
|
|
11
|
-
|
|
12
|
-
post :hello do
|
|
13
|
-
{ hello: 'world' }
|
|
14
|
-
end
|
|
15
|
-
|
|
16
|
-
get 'hello/:id' do
|
|
17
|
-
{ hello: 'world', id: params[:id] }
|
|
18
|
-
end
|
|
19
|
-
end
|
|
20
|
-
|
|
21
|
-
run GrapeExample
|
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require 'hanami/router'
|
|
4
|
-
require 'multi_json'
|
|
5
|
-
|
|
6
|
-
app = Hanami::Router.new do
|
|
7
|
-
get '/hello', to: ->(_env) { [200, {}, [MultiJson.dump(hello: 'world')]] }
|
|
8
|
-
get '/hello/:id', to: lambda { |env|
|
|
9
|
-
[200, {}, [MultiJson.dump(hello: 'world', id: env['router.params'][:id])]]
|
|
10
|
-
}
|
|
11
|
-
post '/hello', to: ->(_env) { [201, {}, [MultiJson.dump(hello: 'world')]] }
|
|
12
|
-
end
|
|
13
|
-
|
|
14
|
-
run app
|
|
@@ -1,86 +0,0 @@
|
|
|
1
|
-
openapi: 3.0.0
|
|
2
|
-
info:
|
|
3
|
-
title: "API"
|
|
4
|
-
version: "1.0.0"
|
|
5
|
-
contact:
|
|
6
|
-
name: Contact Name
|
|
7
|
-
email: contact@example.com
|
|
8
|
-
url: https://example.com/
|
|
9
|
-
tags:
|
|
10
|
-
- name: Metadata
|
|
11
|
-
description: Metadata related requests
|
|
12
|
-
paths:
|
|
13
|
-
/hello/{id}:
|
|
14
|
-
parameters:
|
|
15
|
-
- name: id
|
|
16
|
-
description: ID of the thing to get
|
|
17
|
-
in: path
|
|
18
|
-
required: true
|
|
19
|
-
schema:
|
|
20
|
-
type: string
|
|
21
|
-
get:
|
|
22
|
-
operationId: find_thing
|
|
23
|
-
description: Get one thing
|
|
24
|
-
tags: ["Metadata"]
|
|
25
|
-
responses:
|
|
26
|
-
"200":
|
|
27
|
-
description: OK
|
|
28
|
-
content:
|
|
29
|
-
application/json:
|
|
30
|
-
schema:
|
|
31
|
-
type: array
|
|
32
|
-
items:
|
|
33
|
-
type: object
|
|
34
|
-
required: [hello, id]
|
|
35
|
-
properties:
|
|
36
|
-
hello:
|
|
37
|
-
type: string
|
|
38
|
-
id:
|
|
39
|
-
type: string
|
|
40
|
-
/hello:
|
|
41
|
-
get:
|
|
42
|
-
operationId: find_things
|
|
43
|
-
description: Get multiple things
|
|
44
|
-
tags: ["Metadata"]
|
|
45
|
-
parameters:
|
|
46
|
-
- name: filter
|
|
47
|
-
description: filter things
|
|
48
|
-
in: query
|
|
49
|
-
required: false
|
|
50
|
-
schema:
|
|
51
|
-
type: object
|
|
52
|
-
required: [id]
|
|
53
|
-
properties:
|
|
54
|
-
id:
|
|
55
|
-
type: string
|
|
56
|
-
description: Comma separated list of thing-IDs
|
|
57
|
-
|
|
58
|
-
responses:
|
|
59
|
-
"200":
|
|
60
|
-
description: OK
|
|
61
|
-
content:
|
|
62
|
-
application/json:
|
|
63
|
-
schema:
|
|
64
|
-
type: object
|
|
65
|
-
required: [hello]
|
|
66
|
-
properties:
|
|
67
|
-
hello:
|
|
68
|
-
type: string
|
|
69
|
-
default:
|
|
70
|
-
description: Error response
|
|
71
|
-
|
|
72
|
-
post:
|
|
73
|
-
operationId: create_thing
|
|
74
|
-
description: Create a thing
|
|
75
|
-
tags: ["Metadata"]
|
|
76
|
-
responses:
|
|
77
|
-
"201":
|
|
78
|
-
description: OK
|
|
79
|
-
content:
|
|
80
|
-
application/json:
|
|
81
|
-
schema:
|
|
82
|
-
type: object
|
|
83
|
-
required: [hello]
|
|
84
|
-
properties:
|
|
85
|
-
hello:
|
|
86
|
-
type: string
|
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require 'multi_json'
|
|
4
|
-
require 'openapi_first'
|
|
5
|
-
|
|
6
|
-
namespace = Module.new do
|
|
7
|
-
def self.find_thing(params, _res)
|
|
8
|
-
{ hello: 'world', id: params.fetch('id') }
|
|
9
|
-
end
|
|
10
|
-
|
|
11
|
-
def self.find_things(_params, _res)
|
|
12
|
-
[{ hello: 'world' }]
|
|
13
|
-
end
|
|
14
|
-
|
|
15
|
-
def self.create_thing(_params, res)
|
|
16
|
-
res.status = 201
|
|
17
|
-
{ hello: 'world' }
|
|
18
|
-
end
|
|
19
|
-
end
|
|
20
|
-
|
|
21
|
-
oas_path = File.absolute_path('./openapi.yaml', __dir__)
|
|
22
|
-
run OpenapiFirst.app(oas_path, namespace: namespace)
|
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require 'multi_json'
|
|
4
|
-
require 'openapi_first'
|
|
5
|
-
|
|
6
|
-
namespace = Module.new do
|
|
7
|
-
def self.find_thing(params, _res)
|
|
8
|
-
{ hello: 'world', id: params.fetch('id') }
|
|
9
|
-
end
|
|
10
|
-
|
|
11
|
-
def self.find_things(_params, _res)
|
|
12
|
-
[{ hello: 'world' }]
|
|
13
|
-
end
|
|
14
|
-
|
|
15
|
-
def self.create_thing(_params, res)
|
|
16
|
-
res.status = 201
|
|
17
|
-
{ hello: 'world' }
|
|
18
|
-
end
|
|
19
|
-
end
|
|
20
|
-
|
|
21
|
-
spec = OpenapiFirst.load(File.absolute_path('./openapi.yaml', __dir__))
|
|
22
|
-
use OpenapiFirst::Router, spec: spec, namespace: namespace
|
|
23
|
-
run OpenapiFirst::OperationResolver.new
|
data/benchmarks/apps/sinatra.ru
DELETED
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require 'multi_json'
|
|
4
|
-
require 'sinatra/base'
|
|
5
|
-
|
|
6
|
-
class SinatraExample < Sinatra::Base
|
|
7
|
-
set :environment, :production
|
|
8
|
-
|
|
9
|
-
get '/hello/:id' do
|
|
10
|
-
content_type :json
|
|
11
|
-
MultiJson.dump(hello: 'world', id: params.fetch('id'))
|
|
12
|
-
end
|
|
13
|
-
|
|
14
|
-
get '/hello' do
|
|
15
|
-
content_type :json
|
|
16
|
-
[MultiJson.dump(hello: 'world')]
|
|
17
|
-
end
|
|
18
|
-
|
|
19
|
-
post '/hello' do
|
|
20
|
-
content_type :json
|
|
21
|
-
status 201
|
|
22
|
-
MultiJson.dump(hello: 'world')
|
|
23
|
-
end
|
|
24
|
-
end
|
|
25
|
-
|
|
26
|
-
run SinatraExample
|
data/benchmarks/apps/syro.ru
DELETED
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require 'syro'
|
|
4
|
-
require 'multi_json'
|
|
5
|
-
|
|
6
|
-
app = Syro.new do
|
|
7
|
-
on 'hello' do
|
|
8
|
-
on :id do
|
|
9
|
-
get do
|
|
10
|
-
res.json MultiJson.dump(hello: 'world', id: inbox[:id])
|
|
11
|
-
end
|
|
12
|
-
end
|
|
13
|
-
|
|
14
|
-
get do
|
|
15
|
-
res.json [MultiJson.dump(hello: 'world')]
|
|
16
|
-
end
|
|
17
|
-
|
|
18
|
-
post do
|
|
19
|
-
res.status = 201
|
|
20
|
-
res.json MultiJson.dump(hello: 'world')
|
|
21
|
-
end
|
|
22
|
-
end
|
|
23
|
-
end
|
|
24
|
-
|
|
25
|
-
run app
|
data/benchmarks/benchmarks.rb
DELETED
|
@@ -1,46 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require 'benchmark/ips'
|
|
4
|
-
require 'benchmark/memory'
|
|
5
|
-
require 'rack'
|
|
6
|
-
ENV['RACK_ENV'] = 'production'
|
|
7
|
-
|
|
8
|
-
examples = [
|
|
9
|
-
[Rack::MockRequest.env_for('/hello'), 200],
|
|
10
|
-
[Rack::MockRequest.env_for('/unknown'), 404],
|
|
11
|
-
[Rack::MockRequest.env_for('/hello', method: 'POST'), 201],
|
|
12
|
-
[Rack::MockRequest.env_for('/hello/1'), 200],
|
|
13
|
-
[Rack::MockRequest.env_for('/hello/123'), 200],
|
|
14
|
-
[Rack::MockRequest.env_for('/hello?filter[id]=1,2'), 200]
|
|
15
|
-
]
|
|
16
|
-
|
|
17
|
-
apps = Dir['./apps/*.ru'].each_with_object({}) do |config, hash|
|
|
18
|
-
hash[config] = Rack::Builder.parse_file(config).first
|
|
19
|
-
end
|
|
20
|
-
apps.freeze
|
|
21
|
-
|
|
22
|
-
Benchmark.ips do |x|
|
|
23
|
-
apps.each do |config, app|
|
|
24
|
-
x.report(config) do
|
|
25
|
-
examples.each do |example|
|
|
26
|
-
env, expected_status = example
|
|
27
|
-
response = app.call(env)
|
|
28
|
-
raise unless response[0] == expected_status
|
|
29
|
-
end
|
|
30
|
-
end
|
|
31
|
-
end
|
|
32
|
-
x.compare!
|
|
33
|
-
end
|
|
34
|
-
|
|
35
|
-
Benchmark.memory do |x|
|
|
36
|
-
apps.each do |config, app|
|
|
37
|
-
x.report(config) do
|
|
38
|
-
examples.each do |example|
|
|
39
|
-
env, expected_status = example
|
|
40
|
-
response = app.call(env)
|
|
41
|
-
raise unless response[0] == expected_status
|
|
42
|
-
end
|
|
43
|
-
end
|
|
44
|
-
end
|
|
45
|
-
x.compare!
|
|
46
|
-
end
|
data/bin/console
DELETED
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env ruby
|
|
2
|
-
# frozen_string_literal: true
|
|
3
|
-
|
|
4
|
-
require 'bundler/setup'
|
|
5
|
-
require 'openapi_first'
|
|
6
|
-
|
|
7
|
-
# You can add fixtures and/or initialization code here to make experimenting
|
|
8
|
-
# with your gem easier. You can also use a different console, if you like.
|
|
9
|
-
|
|
10
|
-
# (If you use this, don't forget to add pry to your Gemfile!)
|
|
11
|
-
# require "pry"
|
|
12
|
-
# Pry.start
|
|
13
|
-
|
|
14
|
-
require 'irb'
|
|
15
|
-
IRB.start(__FILE__)
|
data/bin/setup
DELETED
data/examples/README.md
DELETED
data/examples/app.rb
DELETED
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require 'openapi_first'
|
|
4
|
-
|
|
5
|
-
module Example
|
|
6
|
-
def self.find_thing(_params, _res)
|
|
7
|
-
{ hello: 'world' }
|
|
8
|
-
end
|
|
9
|
-
end
|
|
10
|
-
|
|
11
|
-
oas_path = File.absolute_path('./openapi.yaml', __dir__)
|
|
12
|
-
App = OpenapiFirst.app(oas_path, namespace: Example)
|
data/examples/config.ru
DELETED
data/examples/openapi.yaml
DELETED
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
openapi: 3.0.0
|
|
2
|
-
info:
|
|
3
|
-
title: "API"
|
|
4
|
-
version: "1.0.0"
|
|
5
|
-
contact:
|
|
6
|
-
name: Contact Name
|
|
7
|
-
email: contact@example.com
|
|
8
|
-
url: https://example.com/
|
|
9
|
-
tags:
|
|
10
|
-
- name: Metadata
|
|
11
|
-
description: Metadata related requests
|
|
12
|
-
paths:
|
|
13
|
-
/:
|
|
14
|
-
get:
|
|
15
|
-
operationId: find_thing
|
|
16
|
-
summary: Get metadata from the root of the API
|
|
17
|
-
tags: ["Metadata"]
|
|
18
|
-
responses:
|
|
19
|
-
"200":
|
|
20
|
-
description: OK
|
|
21
|
-
content:
|
|
22
|
-
application/json:
|
|
23
|
-
schema:
|
|
24
|
-
type: object
|
|
25
|
-
required: [hello]
|
|
26
|
-
properties:
|
|
27
|
-
hello:
|
|
28
|
-
type: string
|
|
29
|
-
|
data/lib/openapi_first/app.rb
DELETED
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require 'rack'
|
|
4
|
-
|
|
5
|
-
module OpenapiFirst
|
|
6
|
-
class App
|
|
7
|
-
def initialize(
|
|
8
|
-
parent_app,
|
|
9
|
-
spec,
|
|
10
|
-
namespace:
|
|
11
|
-
)
|
|
12
|
-
@stack = Rack::Builder.app do
|
|
13
|
-
freeze_app
|
|
14
|
-
use OpenapiFirst::Router,
|
|
15
|
-
spec: spec,
|
|
16
|
-
namespace: namespace,
|
|
17
|
-
parent_app: parent_app
|
|
18
|
-
use OpenapiFirst::RequestValidation
|
|
19
|
-
run OpenapiFirst::OperationResolver.new
|
|
20
|
-
end
|
|
21
|
-
end
|
|
22
|
-
|
|
23
|
-
def call(env)
|
|
24
|
-
@stack.call(env)
|
|
25
|
-
end
|
|
26
|
-
end
|
|
27
|
-
end
|
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module OpenapiFirst
|
|
4
|
-
class Coverage
|
|
5
|
-
attr_reader :to_be_called
|
|
6
|
-
|
|
7
|
-
def initialize(app, spec)
|
|
8
|
-
@app = app
|
|
9
|
-
@spec = spec
|
|
10
|
-
@to_be_called = spec.operations.map do |operation|
|
|
11
|
-
endpoint_id(operation)
|
|
12
|
-
end
|
|
13
|
-
end
|
|
14
|
-
|
|
15
|
-
def call(env)
|
|
16
|
-
response = @app.call(env)
|
|
17
|
-
operation = env[OPERATION]
|
|
18
|
-
@to_be_called.delete(endpoint_id(operation)) if operation
|
|
19
|
-
response
|
|
20
|
-
end
|
|
21
|
-
|
|
22
|
-
private
|
|
23
|
-
|
|
24
|
-
def endpoint_id(operation)
|
|
25
|
-
"#{operation.path}##{operation.method}"
|
|
26
|
-
end
|
|
27
|
-
end
|
|
28
|
-
end
|
|
@@ -1,48 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require 'forwardable'
|
|
4
|
-
|
|
5
|
-
module OpenapiFirst
|
|
6
|
-
class Operation
|
|
7
|
-
extend Forwardable
|
|
8
|
-
def_delegators :@operation,
|
|
9
|
-
:parameters,
|
|
10
|
-
:method,
|
|
11
|
-
:request_body,
|
|
12
|
-
:operation_id
|
|
13
|
-
|
|
14
|
-
def initialize(parsed)
|
|
15
|
-
@operation = parsed
|
|
16
|
-
end
|
|
17
|
-
|
|
18
|
-
def path
|
|
19
|
-
@operation.path.path
|
|
20
|
-
end
|
|
21
|
-
|
|
22
|
-
def parameters_json_schema
|
|
23
|
-
@parameters_json_schema ||= build_parameters_json_schema
|
|
24
|
-
end
|
|
25
|
-
|
|
26
|
-
def content_type_for(status)
|
|
27
|
-
content = @operation
|
|
28
|
-
.response_by_code(status.to_s, use_default: true)
|
|
29
|
-
.content
|
|
30
|
-
content.keys[0] if content
|
|
31
|
-
end
|
|
32
|
-
|
|
33
|
-
private
|
|
34
|
-
|
|
35
|
-
def build_parameters_json_schema
|
|
36
|
-
return unless @operation.parameters&.any?
|
|
37
|
-
|
|
38
|
-
@operation.parameters.each_with_object(
|
|
39
|
-
'type' => 'object',
|
|
40
|
-
'required' => [],
|
|
41
|
-
'properties' => {}
|
|
42
|
-
) do |parameter, schema|
|
|
43
|
-
schema['required'] << parameter.name if parameter.required
|
|
44
|
-
schema['properties'][parameter.name] = parameter.schema
|
|
45
|
-
end
|
|
46
|
-
end
|
|
47
|
-
end
|
|
48
|
-
end
|
|
@@ -1,43 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require 'rack'
|
|
4
|
-
|
|
5
|
-
module OpenapiFirst
|
|
6
|
-
class OperationResolver
|
|
7
|
-
def call(env)
|
|
8
|
-
operation = env[OpenapiFirst::OPERATION]
|
|
9
|
-
res = Rack::Response.new
|
|
10
|
-
params = build_params(env)
|
|
11
|
-
handler = env[HANDLER]
|
|
12
|
-
result = handler.call(params, res)
|
|
13
|
-
res.write serialize(result) if result && res.body.empty?
|
|
14
|
-
res[Rack::CONTENT_TYPE] ||= operation.content_type_for(res.status)
|
|
15
|
-
res.finish
|
|
16
|
-
end
|
|
17
|
-
|
|
18
|
-
private
|
|
19
|
-
|
|
20
|
-
def serialize(result)
|
|
21
|
-
return result if result.is_a?(String)
|
|
22
|
-
|
|
23
|
-
MultiJson.dump(result)
|
|
24
|
-
end
|
|
25
|
-
|
|
26
|
-
def build_params(env)
|
|
27
|
-
sources = [
|
|
28
|
-
env[PARAMS],
|
|
29
|
-
env[REQUEST_BODY]
|
|
30
|
-
].tap(&:compact!)
|
|
31
|
-
Params.new(env).merge!(*sources)
|
|
32
|
-
end
|
|
33
|
-
end
|
|
34
|
-
|
|
35
|
-
class Params < Hash
|
|
36
|
-
attr_reader :env
|
|
37
|
-
|
|
38
|
-
def initialize(env)
|
|
39
|
-
@env = env
|
|
40
|
-
super
|
|
41
|
-
end
|
|
42
|
-
end
|
|
43
|
-
end
|
|
@@ -1,144 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require 'rack'
|
|
4
|
-
require 'json_schemer'
|
|
5
|
-
require 'multi_json'
|
|
6
|
-
require_relative 'validation_format'
|
|
7
|
-
|
|
8
|
-
module OpenapiFirst
|
|
9
|
-
class RequestValidation # rubocop:disable Metrics/ClassLength
|
|
10
|
-
def initialize(app, _options = {})
|
|
11
|
-
@app = app
|
|
12
|
-
end
|
|
13
|
-
|
|
14
|
-
def call(env) # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
|
|
15
|
-
operation = env[OpenapiFirst::OPERATION]
|
|
16
|
-
return @app.call(env) unless operation
|
|
17
|
-
|
|
18
|
-
catch(:halt) do
|
|
19
|
-
validate_query_parameters!(env, operation, env[PARAMS])
|
|
20
|
-
req = Rack::Request.new(env)
|
|
21
|
-
content_type = req.content_type
|
|
22
|
-
return @app.call(env) unless operation.request_body
|
|
23
|
-
|
|
24
|
-
validate_request_content_type!(content_type, operation)
|
|
25
|
-
body = req.body.read
|
|
26
|
-
req.body.rewind
|
|
27
|
-
parse_and_validate_request_body!(env, content_type, body, operation)
|
|
28
|
-
@app.call(env)
|
|
29
|
-
end
|
|
30
|
-
end
|
|
31
|
-
|
|
32
|
-
private
|
|
33
|
-
|
|
34
|
-
def halt(response)
|
|
35
|
-
throw :halt, response
|
|
36
|
-
end
|
|
37
|
-
|
|
38
|
-
def parse_and_validate_request_body!(env, content_type, body, operation)
|
|
39
|
-
validate_request_body_presence!(body, operation)
|
|
40
|
-
return if body.empty?
|
|
41
|
-
|
|
42
|
-
schema = request_body_schema(content_type, operation)
|
|
43
|
-
return unless schema
|
|
44
|
-
|
|
45
|
-
parsed_request_body = MultiJson.load(body)
|
|
46
|
-
errors = validate_json_schema(schema, parsed_request_body)
|
|
47
|
-
if errors.any?
|
|
48
|
-
halt(error_response(400, serialize_request_body_errors(errors)))
|
|
49
|
-
end
|
|
50
|
-
env[OpenapiFirst::REQUEST_BODY] = parsed_request_body
|
|
51
|
-
end
|
|
52
|
-
|
|
53
|
-
def validate_request_content_type!(content_type, operation)
|
|
54
|
-
return if operation.request_body.content[content_type]
|
|
55
|
-
|
|
56
|
-
halt(error_response(415))
|
|
57
|
-
end
|
|
58
|
-
|
|
59
|
-
def validate_request_body_presence!(body, operation)
|
|
60
|
-
return unless operation.request_body.required && body.empty?
|
|
61
|
-
|
|
62
|
-
halt(error_response(415, 'Request body is required'))
|
|
63
|
-
end
|
|
64
|
-
|
|
65
|
-
def validate_json_schema(schema, object)
|
|
66
|
-
JSONSchemer.schema(schema).validate(object)
|
|
67
|
-
end
|
|
68
|
-
|
|
69
|
-
def default_error(status, title = Rack::Utils::HTTP_STATUS_CODES[status])
|
|
70
|
-
{
|
|
71
|
-
status: status.to_s,
|
|
72
|
-
title: title
|
|
73
|
-
}
|
|
74
|
-
end
|
|
75
|
-
|
|
76
|
-
def error_response(status, errors = [default_error(status)])
|
|
77
|
-
Rack::Response.new(
|
|
78
|
-
MultiJson.dump(errors: errors),
|
|
79
|
-
status,
|
|
80
|
-
Rack::CONTENT_TYPE => 'application/vnd.api+json'
|
|
81
|
-
).finish
|
|
82
|
-
end
|
|
83
|
-
|
|
84
|
-
def request_body_schema(content_type, operation)
|
|
85
|
-
return unless operation
|
|
86
|
-
|
|
87
|
-
operation.request_body.content[content_type]&.fetch('schema')
|
|
88
|
-
end
|
|
89
|
-
|
|
90
|
-
def serialize_request_body_errors(validation_errors)
|
|
91
|
-
validation_errors.map do |error|
|
|
92
|
-
{
|
|
93
|
-
source: {
|
|
94
|
-
pointer: error['data_pointer']
|
|
95
|
-
}
|
|
96
|
-
}.update(ValidationFormat.error_details(error))
|
|
97
|
-
end
|
|
98
|
-
end
|
|
99
|
-
|
|
100
|
-
def validate_query_parameters!(env, operation, params)
|
|
101
|
-
json_schema = operation.parameters_json_schema
|
|
102
|
-
return unless json_schema
|
|
103
|
-
|
|
104
|
-
params = filtered_params(json_schema, params)
|
|
105
|
-
errors = JSONSchemer.schema(json_schema).validate(params)
|
|
106
|
-
if errors.any?
|
|
107
|
-
halt error_response(400, serialize_query_parameter_errors(errors))
|
|
108
|
-
end
|
|
109
|
-
env[PARAMS] = params
|
|
110
|
-
end
|
|
111
|
-
|
|
112
|
-
def filtered_params(json_schema, params)
|
|
113
|
-
json_schema['properties']
|
|
114
|
-
.each_with_object({}) do |key_value, result|
|
|
115
|
-
parameter_name, schema = key_value
|
|
116
|
-
next unless params.key?(parameter_name)
|
|
117
|
-
|
|
118
|
-
value = params[parameter_name]
|
|
119
|
-
result[parameter_name] = parse_parameter(value, schema)
|
|
120
|
-
end
|
|
121
|
-
end
|
|
122
|
-
|
|
123
|
-
def serialize_query_parameter_errors(validation_errors)
|
|
124
|
-
validation_errors.map do |error|
|
|
125
|
-
{
|
|
126
|
-
source: { parameter: File.basename(error['data_pointer']) }
|
|
127
|
-
}.update(ValidationFormat.error_details(error))
|
|
128
|
-
end
|
|
129
|
-
end
|
|
130
|
-
|
|
131
|
-
def parse_parameter(value, schema)
|
|
132
|
-
return filtered_params(schema, value) if schema['properties']
|
|
133
|
-
|
|
134
|
-
begin
|
|
135
|
-
return Integer(value, 10) if schema['type'] == 'integer'
|
|
136
|
-
return Float(value) if schema['type'] == 'number'
|
|
137
|
-
rescue ArgumentError
|
|
138
|
-
value
|
|
139
|
-
end
|
|
140
|
-
|
|
141
|
-
value
|
|
142
|
-
end
|
|
143
|
-
end
|
|
144
|
-
end
|