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.
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