openapi_first 1.0.0.beta5 → 1.0.0.beta6

Sign up to get free protection for your applications and to get access to all the features.
Files changed (59) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ruby.yml +2 -1
  3. data/CHANGELOG.md +8 -0
  4. data/Gemfile +2 -1
  5. data/Gemfile.lock +6 -9
  6. data/Gemfile.rack2 +15 -0
  7. data/lib/openapi_first/{body_parser_middleware.rb → body_parser.rb} +3 -15
  8. data/lib/openapi_first/definition/cookie_parameters.rb +12 -0
  9. data/lib/openapi_first/definition/has_content.rb +37 -0
  10. data/lib/openapi_first/definition/header_parameters.rb +12 -0
  11. data/lib/openapi_first/definition/operation.rb +103 -0
  12. data/lib/openapi_first/definition/parameters.rb +47 -0
  13. data/lib/openapi_first/definition/path_item.rb +23 -0
  14. data/lib/openapi_first/definition/path_parameters.rb +13 -0
  15. data/lib/openapi_first/definition/query_parameters.rb +12 -0
  16. data/lib/openapi_first/definition/request_body.rb +32 -0
  17. data/lib/openapi_first/definition/response.rb +37 -0
  18. data/lib/openapi_first/{json_schema → definition/schema}/result.rb +1 -1
  19. data/lib/openapi_first/{json_schema.rb → definition/schema.rb} +2 -2
  20. data/lib/openapi_first/definition.rb +26 -6
  21. data/lib/openapi_first/error_response.rb +2 -0
  22. data/lib/openapi_first/request_body_validator.rb +17 -21
  23. data/lib/openapi_first/request_validation.rb +34 -30
  24. data/lib/openapi_first/response_validation.rb +31 -11
  25. data/lib/openapi_first/router.rb +19 -53
  26. data/lib/openapi_first/version.rb +1 -1
  27. data/openapi_first.gemspec +7 -4
  28. metadata +32 -52
  29. data/.rspec +0 -3
  30. data/.rubocop.yml +0 -14
  31. data/Rakefile +0 -15
  32. data/benchmarks/Gemfile +0 -16
  33. data/benchmarks/Gemfile.lock +0 -142
  34. data/benchmarks/README.md +0 -29
  35. data/benchmarks/apps/committee_with_hanami_api.ru +0 -26
  36. data/benchmarks/apps/committee_with_response_validation.ru +0 -29
  37. data/benchmarks/apps/committee_with_sinatra.ru +0 -31
  38. data/benchmarks/apps/grape.ru +0 -21
  39. data/benchmarks/apps/hanami_api.ru +0 -21
  40. data/benchmarks/apps/hanami_router.ru +0 -14
  41. data/benchmarks/apps/openapi.yaml +0 -268
  42. data/benchmarks/apps/openapi_first_with_hanami_api.ru +0 -24
  43. data/benchmarks/apps/openapi_first_with_plain_rack.ru +0 -32
  44. data/benchmarks/apps/openapi_first_with_response_validation.ru +0 -25
  45. data/benchmarks/apps/openapi_first_with_sinatra.ru +0 -29
  46. data/benchmarks/apps/roda.ru +0 -27
  47. data/benchmarks/apps/sinatra.ru +0 -26
  48. data/benchmarks/apps/syro.ru +0 -25
  49. data/benchmarks/benchmark-wrk.sh +0 -3
  50. data/benchmarks/benchmarks.rb +0 -48
  51. data/benchmarks/post.lua +0 -3
  52. data/bin/console +0 -15
  53. data/bin/setup +0 -8
  54. data/examples/README.md +0 -13
  55. data/examples/app.rb +0 -18
  56. data/examples/config.ru +0 -7
  57. data/examples/openapi.yaml +0 -29
  58. data/lib/openapi_first/operation.rb +0 -170
  59. data/lib/openapi_first/string_keyed_hash.rb +0 -20
data/benchmarks/post.lua DELETED
@@ -1,3 +0,0 @@
1
- wrk.method = "POST"
2
- wrk.body = "{\"say\":\"hi!\"}"
3
- wrk.headers["Content-Type"] = "application/json"
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
@@ -1,8 +0,0 @@
1
- #!/usr/bin/env bash
2
- set -euo pipefail
3
- IFS=$'\n\t'
4
- set -vx
5
-
6
- bundle install
7
-
8
- # Do any other automated setup that you need to do here
data/examples/README.md DELETED
@@ -1,13 +0,0 @@
1
- # Example
2
-
3
- How to run the example:
4
-
5
- ```bash
6
- cd examples
7
- bundle install
8
- bundle exec rackup
9
- ```
10
-
11
- open http://localhost:9292/
12
-
13
- 🎉
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
@@ -1,7 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- lib = File.expand_path('../lib', __dir__)
4
- $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
5
-
6
- require_relative 'app'
7
- run App
@@ -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