openapi_first 1.0.0.beta5 → 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.
- checksums.yaml +4 -4
- data/.github/workflows/ruby.yml +2 -1
- data/CHANGELOG.md +8 -0
- data/Gemfile +2 -1
- data/Gemfile.lock +6 -9
- data/Gemfile.rack2 +15 -0
- data/lib/openapi_first/{body_parser_middleware.rb → body_parser.rb} +3 -15
- data/lib/openapi_first/definition/cookie_parameters.rb +12 -0
- data/lib/openapi_first/definition/has_content.rb +37 -0
- data/lib/openapi_first/definition/header_parameters.rb +12 -0
- data/lib/openapi_first/definition/operation.rb +103 -0
- data/lib/openapi_first/definition/parameters.rb +47 -0
- data/lib/openapi_first/definition/path_item.rb +23 -0
- data/lib/openapi_first/definition/path_parameters.rb +13 -0
- data/lib/openapi_first/definition/query_parameters.rb +12 -0
- data/lib/openapi_first/definition/request_body.rb +32 -0
- data/lib/openapi_first/definition/response.rb +37 -0
- data/lib/openapi_first/{json_schema → definition/schema}/result.rb +1 -1
- data/lib/openapi_first/{json_schema.rb → definition/schema.rb} +2 -2
- data/lib/openapi_first/definition.rb +26 -6
- data/lib/openapi_first/error_response.rb +2 -0
- data/lib/openapi_first/request_body_validator.rb +17 -21
- data/lib/openapi_first/request_validation.rb +34 -30
- data/lib/openapi_first/response_validation.rb +31 -11
- data/lib/openapi_first/router.rb +19 -53
- data/lib/openapi_first/version.rb +1 -1
- data/openapi_first.gemspec +7 -4
- metadata +32 -52
- data/.rspec +0 -3
- data/.rubocop.yml +0 -14
- data/Rakefile +0 -15
- data/benchmarks/Gemfile +0 -16
- data/benchmarks/Gemfile.lock +0 -142
- data/benchmarks/README.md +0 -29
- data/benchmarks/apps/committee_with_hanami_api.ru +0 -26
- data/benchmarks/apps/committee_with_response_validation.ru +0 -29
- data/benchmarks/apps/committee_with_sinatra.ru +0 -31
- data/benchmarks/apps/grape.ru +0 -21
- data/benchmarks/apps/hanami_api.ru +0 -21
- data/benchmarks/apps/hanami_router.ru +0 -14
- data/benchmarks/apps/openapi.yaml +0 -268
- data/benchmarks/apps/openapi_first_with_hanami_api.ru +0 -24
- data/benchmarks/apps/openapi_first_with_plain_rack.ru +0 -32
- data/benchmarks/apps/openapi_first_with_response_validation.ru +0 -25
- data/benchmarks/apps/openapi_first_with_sinatra.ru +0 -29
- data/benchmarks/apps/roda.ru +0 -27
- data/benchmarks/apps/sinatra.ru +0 -26
- data/benchmarks/apps/syro.ru +0 -25
- data/benchmarks/benchmark-wrk.sh +0 -3
- data/benchmarks/benchmarks.rb +0 -48
- data/benchmarks/post.lua +0 -3
- data/bin/console +0 -15
- data/bin/setup +0 -8
- data/examples/README.md +0 -13
- data/examples/app.rb +0 -18
- data/examples/config.ru +0 -7
- data/examples/openapi.yaml +0 -29
- data/lib/openapi_first/operation.rb +0 -170
- data/lib/openapi_first/string_keyed_hash.rb +0 -20
data/benchmarks/post.lua
DELETED
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,18 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
require 'openapi_first'
|
4
|
-
require 'rack'
|
5
|
-
|
6
|
-
# This example is a bit contrived, but it shows what you could do with the middlewares
|
7
|
-
|
8
|
-
App = Rack::Builder.new do
|
9
|
-
use OpenapiFirst::RequestValidation, raise_error: true, spec: File.expand_path('./openapi.yaml', __dir__)
|
10
|
-
use OpenapiFirst::ResponseValidation
|
11
|
-
|
12
|
-
handlers = {
|
13
|
-
'things#index' => ->(_env) { [200, { 'Content-Type' => 'application/json' }, ['{"hello": "world"}']] }
|
14
|
-
}
|
15
|
-
not_found = ->(_env) { [404, {}, []] }
|
16
|
-
|
17
|
-
run ->(env) { handlers.fetch(env[OpenapiFirst::OPERATION].operation_id, not_found).call(env) }
|
18
|
-
end
|
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: things#index
|
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
|
-
|
@@ -1,170 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
require 'forwardable'
|
4
|
-
require 'set'
|
5
|
-
require_relative 'json_schema'
|
6
|
-
|
7
|
-
module OpenapiFirst
|
8
|
-
class Operation # rubocop:disable Metrics/ClassLength
|
9
|
-
extend Forwardable
|
10
|
-
def_delegators :operation_object,
|
11
|
-
:[],
|
12
|
-
:dig
|
13
|
-
|
14
|
-
WRITE_METHODS = Set.new(%w[post put patch delete]).freeze
|
15
|
-
private_constant :WRITE_METHODS
|
16
|
-
|
17
|
-
attr_reader :path, :method, :openapi_version
|
18
|
-
|
19
|
-
def initialize(path, request_method, path_item_object, openapi_version:)
|
20
|
-
@path = path
|
21
|
-
@method = request_method
|
22
|
-
@path_item_object = path_item_object
|
23
|
-
@openapi_version = openapi_version
|
24
|
-
end
|
25
|
-
|
26
|
-
def operation_id
|
27
|
-
operation_object['operationId']
|
28
|
-
end
|
29
|
-
|
30
|
-
def read?
|
31
|
-
!write?
|
32
|
-
end
|
33
|
-
|
34
|
-
def write?
|
35
|
-
WRITE_METHODS.include?(method)
|
36
|
-
end
|
37
|
-
|
38
|
-
def request_body
|
39
|
-
operation_object['requestBody']
|
40
|
-
end
|
41
|
-
|
42
|
-
def response_body_schema(status, content_type)
|
43
|
-
content = response_for(status)['content']
|
44
|
-
return if content.nil? || content.empty?
|
45
|
-
|
46
|
-
raise ResponseInvalid, "Response has no content-type for '#{name}'" unless content_type
|
47
|
-
|
48
|
-
media_type = find_content_for_content_type(content, content_type)
|
49
|
-
|
50
|
-
unless media_type
|
51
|
-
message = "Response content type not found '#{content_type}' for '#{name}'"
|
52
|
-
raise ResponseContentTypeNotFoundError, message
|
53
|
-
end
|
54
|
-
schema = media_type['schema']
|
55
|
-
return unless schema
|
56
|
-
|
57
|
-
JsonSchema.new(schema, write: false, openapi_version:)
|
58
|
-
end
|
59
|
-
|
60
|
-
def request_body_schema(request_content_type)
|
61
|
-
(@request_body_schema ||= {})[request_content_type] ||= begin
|
62
|
-
content = operation_object.dig('requestBody', 'content')
|
63
|
-
media_type = find_content_for_content_type(content, request_content_type)
|
64
|
-
schema = media_type&.fetch('schema', nil)
|
65
|
-
JsonSchema.new(schema, write: write?, openapi_version:) if schema
|
66
|
-
end
|
67
|
-
end
|
68
|
-
|
69
|
-
def response_for(status)
|
70
|
-
response_content = response_by_code(status)
|
71
|
-
return response_content if response_content
|
72
|
-
|
73
|
-
message = "Response status code or default not found: #{status} for '#{name}'"
|
74
|
-
raise OpenapiFirst::ResponseCodeNotFoundError, message
|
75
|
-
end
|
76
|
-
|
77
|
-
def name
|
78
|
-
@name ||= "#{method.upcase} #{path} (#{operation_id})"
|
79
|
-
end
|
80
|
-
|
81
|
-
def valid_request_content_type?(request_content_type)
|
82
|
-
content = operation_object.dig('requestBody', 'content')
|
83
|
-
return false unless content
|
84
|
-
|
85
|
-
!!find_content_for_content_type(content, request_content_type)
|
86
|
-
end
|
87
|
-
|
88
|
-
def query_parameters
|
89
|
-
@query_parameters ||= all_parameters.filter { |p| p['in'] == 'query' }
|
90
|
-
end
|
91
|
-
|
92
|
-
def path_parameters
|
93
|
-
@path_parameters ||= all_parameters.filter { |p| p['in'] == 'path' }
|
94
|
-
end
|
95
|
-
|
96
|
-
IGNORED_HEADERS = Set['Content-Type', 'Accept', 'Authorization'].freeze
|
97
|
-
private_constant :IGNORED_HEADERS
|
98
|
-
|
99
|
-
def header_parameters
|
100
|
-
@header_parameters ||= all_parameters.filter { |p| p['in'] == 'header' && !IGNORED_HEADERS.include?(p['name']) }
|
101
|
-
end
|
102
|
-
|
103
|
-
def cookie_parameters
|
104
|
-
@cookie_parameters ||= all_parameters.filter { |p| p['in'] == 'cookie' }
|
105
|
-
end
|
106
|
-
|
107
|
-
def all_parameters
|
108
|
-
@all_parameters ||= begin
|
109
|
-
parameters = @path_item_object['parameters']&.dup || []
|
110
|
-
parameters_on_operation = operation_object['parameters']
|
111
|
-
parameters.concat(parameters_on_operation) if parameters_on_operation
|
112
|
-
parameters
|
113
|
-
end
|
114
|
-
end
|
115
|
-
|
116
|
-
# Return JSON Schema of for all query parameters
|
117
|
-
def query_parameters_schema
|
118
|
-
@query_parameters_schema ||= build_json_schema(query_parameters)
|
119
|
-
end
|
120
|
-
|
121
|
-
# Return JSON Schema of for all path parameters
|
122
|
-
def path_parameters_schema
|
123
|
-
@path_parameters_schema ||= build_json_schema(path_parameters)
|
124
|
-
end
|
125
|
-
|
126
|
-
def header_parameters_schema
|
127
|
-
@header_parameters_schema ||= build_json_schema(header_parameters)
|
128
|
-
end
|
129
|
-
|
130
|
-
def cookie_parameters_schema
|
131
|
-
@cookie_parameters_schema ||= build_json_schema(cookie_parameters)
|
132
|
-
end
|
133
|
-
|
134
|
-
private
|
135
|
-
|
136
|
-
# Build JSON Schema for given parameter definitions
|
137
|
-
# @parameter_defs [Array<Hash>] Parameter definitions
|
138
|
-
def build_json_schema(parameter_defs)
|
139
|
-
init_schema = {
|
140
|
-
'type' => 'object',
|
141
|
-
'properties' => {},
|
142
|
-
'required' => []
|
143
|
-
}
|
144
|
-
schema = parameter_defs.each_with_object(init_schema) do |parameter_def, result|
|
145
|
-
parameter = OpenapiParameters::Parameter.new(parameter_def)
|
146
|
-
result['properties'][parameter.name] = parameter.schema if parameter.schema
|
147
|
-
result['required'] << parameter.name if parameter.required?
|
148
|
-
end
|
149
|
-
JsonSchema.new(schema, openapi_version:)
|
150
|
-
end
|
151
|
-
|
152
|
-
def response_by_code(status)
|
153
|
-
operation_object.dig('responses', status.to_s) ||
|
154
|
-
operation_object.dig('responses', "#{status / 100}XX") ||
|
155
|
-
operation_object.dig('responses', "#{status / 100}xx") ||
|
156
|
-
operation_object.dig('responses', 'default')
|
157
|
-
end
|
158
|
-
|
159
|
-
def operation_object
|
160
|
-
@path_item_object[method]
|
161
|
-
end
|
162
|
-
|
163
|
-
def find_content_for_content_type(content, request_content_type)
|
164
|
-
content.fetch(request_content_type) do |_|
|
165
|
-
type = request_content_type.split(';')[0]
|
166
|
-
content[type] || content["#{type.split('/')[0]}/*"] || content['*/*']
|
167
|
-
end
|
168
|
-
end
|
169
|
-
end
|
170
|
-
end
|
@@ -1,20 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module OpenapiFirst
|
4
|
-
class StringKeyedHash
|
5
|
-
extend Forwardable
|
6
|
-
def_delegators :@orig, :empty?
|
7
|
-
|
8
|
-
def initialize(original)
|
9
|
-
@orig = original
|
10
|
-
end
|
11
|
-
|
12
|
-
def key?(key)
|
13
|
-
@orig.key?(key.to_sym)
|
14
|
-
end
|
15
|
-
|
16
|
-
def [](key)
|
17
|
-
@orig[key.to_sym]
|
18
|
-
end
|
19
|
-
end
|
20
|
-
end
|