openapi_first 0.6.0 → 0.6.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 55b0bba2e51dd0517e3306e34d345ec34d2165a2a3d8536c140941b74818d945
4
- data.tar.gz: 8ba6d8d04a3abb38e60a2716f7515df945b870e0b8a874b6d217773894e4642c
3
+ metadata.gz: 6888d00f37a7dce1f2b22ad9da04f4ecb69236ea18221173fc77dac1b5edba6c
4
+ data.tar.gz: 8eca9351e46942b1acf6fa47d3394dbfa9a85c4f0dc3dc1f77c075d2818bc3ed
5
5
  SHA512:
6
- metadata.gz: d072794a009783f7cb4ea4141ffadfaea699701954f1c994dc6db69faf18e36ed554bdeb979d81d5b31d7abb8a668e17113156e96ccf311b99a0187a7d48dc06
7
- data.tar.gz: 608a19940d52d6e554c6d53a95ea1942731d2a8df9118a3c91202d7b8220c7eb0899bffccbcd9a3ff6213ad520851ab446909bd6e2b4d17950937017179f264b
6
+ metadata.gz: 967dd87e43808f328a5ab72e99cf04c4b37eb756b2a4ef06925e3e419b038583e014cf3c11074168f02fd07b16db4ba266f743a884a608f37e6c1bfeed0ce911
7
+ data.tar.gz: 9c2a62d290c09417c0c75eaec88427b847f43d787e3cb548dcef543749a98f7252d5915de48fe64c9eb34eca8f579590f1d94bec9455c1ba08932b1a5de5c7f4
data/CHANGELOG.md CHANGED
@@ -1,5 +1,9 @@
1
1
  # Unreleased
2
2
 
3
+ # 0.6.1
4
+
5
+ - Make ResponseValidator errors easier to read
6
+
3
7
  # 0.6.0
4
8
 
5
9
  - Set the content-type based on the OpenAPI description [#29](https://github.com/ahx/openapi-first/pull/29)
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- openapi_first (0.6.0)
4
+ openapi_first (0.6.1)
5
5
  json_schemer (~> 0.2)
6
6
  multi_json (~> 1.13)
7
7
  oas_parser (~> 0.19)
@@ -29,7 +29,7 @@ GEM
29
29
  hansi (0.2.0)
30
30
  i18n (1.6.0)
31
31
  concurrent-ruby (~> 1.0)
32
- jaro_winkler (1.5.2)
32
+ jaro_winkler (1.5.3)
33
33
  json_schemer (0.2.0)
34
34
  ecma-re-validator (~> 0.2.0)
35
35
  hana (~> 1.3.3)
@@ -69,23 +69,23 @@ GEM
69
69
  rspec-core (~> 3.8.0)
70
70
  rspec-expectations (~> 3.8.0)
71
71
  rspec-mocks (~> 3.8.0)
72
- rspec-core (3.8.0)
72
+ rspec-core (3.8.2)
73
73
  rspec-support (~> 3.8.0)
74
- rspec-expectations (3.8.3)
74
+ rspec-expectations (3.8.4)
75
75
  diff-lcs (>= 1.2.0, < 2.0)
76
76
  rspec-support (~> 3.8.0)
77
- rspec-mocks (3.8.0)
77
+ rspec-mocks (3.8.1)
78
78
  diff-lcs (>= 1.2.0, < 2.0)
79
79
  rspec-support (~> 3.8.0)
80
- rspec-support (3.8.0)
81
- rubocop (0.69.0)
80
+ rspec-support (3.8.2)
81
+ rubocop (0.72.0)
82
82
  jaro_winkler (~> 1.5.1)
83
83
  parallel (~> 1.10)
84
84
  parser (>= 2.6)
85
85
  rainbow (>= 2.2.2, < 4.0)
86
86
  ruby-progressbar (~> 1.7)
87
87
  unicode-display_width (>= 1.4.0, < 1.7)
88
- ruby-progressbar (1.10.0)
88
+ ruby-progressbar (1.10.1)
89
89
  thread_safe (0.3.6)
90
90
  tzinfo (1.2.5)
91
91
  thread_safe (~> 0.1)
data/README.md CHANGED
@@ -1,12 +1,12 @@
1
1
  # OpenapiFirst
2
2
 
3
- OpenapiFirst helps to implement Rack based HTTP APIs based on an [OpenApi](https://www.openapis.org/) API description. The idea is that you create an API description first, then add minimal code about your business logic (some call this "Resolver") and be done.
3
+ OpenapiFirst helps to implement HTTP APIs based on an [OpenApi](https://www.openapis.org/) API description. The idea is that you create an API description first, then add minimal code about your business logic (some call this "Resolver") and be done.
4
4
 
5
5
  ## TL;DR
6
6
 
7
7
  ```ruby
8
8
  module Pets
9
- def self.find_pet(params, _res) # "find_pet" is an operationId from your OpenApi file
9
+ def self.find_pet(params, res)
10
10
  {
11
11
  id: params['id'],
12
12
  name: 'Oscar'
@@ -22,10 +22,16 @@ run OpenapiFirst.app('./openapi/openapi.yaml', namespace: Pets)
22
22
  The above will:
23
23
 
24
24
  - Validate the request and respond with 400 if the request does not match against your spec
25
- - Map the request (for example `GET /pet/1`) to the method call `Pets.find_pet`
25
+ - Map the request to a method call `Pets.find_pet` based on the `operationId` in the API description
26
26
  - Set the response content type according to your spec (here with the default status code `200`)
27
27
 
28
- ### Usage as Rack middlware
28
+ Resolver functions (`find_pet`) are called with two arguments:
29
+
30
+ - `params` - Holds the parsed request body, filtered query params and path parameters
31
+ - `res` - Holds a Rack::Response that you can modify if needed
32
+ If you want to access to plain Rack env you can call `params.env`.
33
+
34
+ ### Usage as Rack middleware
29
35
 
30
36
  ```ruby
31
37
  # Just like the above, except the last line
@@ -40,6 +46,7 @@ When using the middleware, all requests that are not part of the API description
40
46
  See [example](examples)
41
47
 
42
48
  ## Missing features
49
+
43
50
  See [issues](https://github.com/ahx/openapi_first/issues).
44
51
 
45
52
  ## Start
@@ -58,6 +65,67 @@ gem 'openapi_first'
58
65
 
59
66
  OpenapiFirst uses [`multi_json`](https://rubygems.org/gems/multi_json).
60
67
 
68
+ ## Testing
69
+
70
+ OpenapiFirst offers tools to help testing your app against your API description.
71
+
72
+ ### Response validation
73
+
74
+ Response validation is to make sure your app responds as described in your API description. You usually do this in your tests using [rack-test](https://github.com/rack-test/rack-test).
75
+
76
+ ```ruby
77
+ # In your test:
78
+ require 'openapi_first/response_validator'
79
+ spec = OpenapiFirst.load('petstore.yaml')
80
+ validator = OpenapiFirst::ResponseValidator.new(spec)
81
+ validator.validate(last_request, last_response).errors? # => true or false
82
+ ```
83
+
84
+ TODO: Add RSpec matcher (via extra rubygem)
85
+
86
+ ### Coverage
87
+
88
+ (This is a bit experimental. Please try it out and give feedback.)
89
+
90
+ `OpenapiFirst::Coverage` helps you make sure, that you have called all endpoints of your OAS file when running tests via `rack-test`.
91
+
92
+ ```ruby
93
+ # In your test (rspec example):
94
+ require 'openapi_first/coverage'
95
+
96
+ describe MyApp do
97
+ include Rack::Test::Methods
98
+
99
+ before(:all) do
100
+ spec = OpenapiFirst.load('petstore.yaml')
101
+ @app_wrapper = OpenapiFirst::Coverage.new(MyApp, spec)
102
+ end
103
+
104
+ after(:all) do
105
+ message = "The following paths have not been called yet: #{@app_wrapper.to_be_called}"
106
+ expect(@app_wrapper.to_be_called).to be_empty
107
+ end
108
+
109
+ # Overwrite `#app` to make rack-test call the wrapped app
110
+ def app
111
+ @app_wrapper
112
+ end
113
+
114
+ it 'does things' do
115
+ get '/i/my/stuff'
116
+ # …
117
+ end
118
+ end
119
+ ```
120
+
121
+ ## Mocking
122
+
123
+ Mocking is currently out of scope. Try https://github.com/JustinFeng/fakeit or something else.
124
+
125
+ ## Alternatives
126
+
127
+ This gem is inspired by [committee](https://github.com/interagent/committee), which has much more features like response stubs or support for Hyper-Schema or OpenAPI 2.
128
+
61
129
  ## How it works
62
130
 
63
131
  OpenapiFirst offers Rack middlewares to auto-implement different aspects of request validation:
@@ -73,7 +141,7 @@ spec = OpenapiFirst.load('petstore.yaml')
73
141
  use OpenapiFirst::Router, spec: spec
74
142
  ```
75
143
 
76
- If the request is not valid, these middlewares return a 400 status code with a body that describes the error. If unkwon routes in your application exist, which are not specified in the openapi spec file, set `:allow_unknown_operation` to `true`.
144
+ If the request is not valid, these middlewares return a 400 status code with a body that describes the error. If unkwon routes in your application exist, which are not specified in the API description, set `:allow_unknown_operation` to `true`.
77
145
 
78
146
  The error responses conform with [JSON:API](https://jsonapi.org).
79
147
 
@@ -184,7 +252,8 @@ Response validation is to make sure your app responds as described in your OpenA
184
252
  require 'openapi_first/response_validator'
185
253
  spec = OpenapiFirst.load('petstore.yaml')
186
254
  validator = OpenapiFirst::ResponseValidator.new(spec)
187
- validator.validate(last_request, last_response).errors? # => true or false
255
+
256
+ expect(validator.validate(last_request, last_response).errors).to be_empty
188
257
  ```
189
258
 
190
259
  TODO: Add RSpec matcher (via extra rubygem)
@@ -226,7 +295,7 @@ end
226
295
 
227
296
  ## Mocking
228
297
 
229
- Currently out of scope. Use https://github.com/JustinFeng/fakeit or something else.
298
+ Currently out of scope.
230
299
 
231
300
  ## Alternatives
232
301
 
data/lib/openapi_first.rb CHANGED
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'oas_parser'
4
+ require 'openapi_first/definition'
4
5
  require 'openapi_first/version'
5
6
  require 'openapi_first/router'
6
7
  require 'openapi_first/query_parameter_validation'
@@ -15,7 +16,7 @@ module OpenapiFirst
15
16
  QUERY_PARAMS = 'openapi_first.query_params'
16
17
 
17
18
  def self.load(spec_path)
18
- OasParser::Definition.resolve(spec_path)
19
+ Definition.new(OasParser::Definition.resolve(spec_path))
19
20
  end
20
21
 
21
22
  def self.app(spec, namespace:)
@@ -7,29 +7,21 @@ module OpenapiFirst
7
7
  def initialize(app, spec)
8
8
  @app = app
9
9
  @spec = spec
10
- @to_be_called = spec.endpoints.map do |endpoint|
11
- endpoint_id(endpoint)
10
+ @to_be_called = spec.operations.map do |operation|
11
+ endpoint_id(operation)
12
12
  end
13
13
  end
14
14
 
15
15
  def call(env)
16
- endpoint = endpoint_for_request(Rack::Request.new(env))
17
- @to_be_called.delete(endpoint_id(endpoint)) if endpoint
16
+ operation = @spec.find_operation(Rack::Request.new(env))
17
+ @to_be_called.delete(endpoint_id(operation)) if operation
18
18
  @app.call(env)
19
19
  end
20
20
 
21
21
  private
22
22
 
23
- def endpoint_id(endpoint)
24
- "#{endpoint.path.path}##{endpoint.method}"
25
- end
26
-
27
- def endpoint_for_request(request)
28
- @spec
29
- .path_by_path(request.path)
30
- .endpoint_by_method(request.request_method.downcase)
31
- rescue OasParser::PathNotFound
32
- nil
23
+ def endpoint_id(operation)
24
+ "#{operation.path.path}##{operation.method}"
33
25
  end
34
26
  end
35
27
  end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OpenapiFirst
4
+ class Definition
5
+ def initialize(parsed)
6
+ @spec = parsed
7
+ end
8
+
9
+ def operations
10
+ @spec.endpoints
11
+ end
12
+
13
+ def find_operation!(request)
14
+ @spec
15
+ .path_by_path(request.path)
16
+ .endpoint_by_method(request.request_method.downcase)
17
+ end
18
+
19
+ def find_operation(request)
20
+ find_operation!(request)
21
+ rescue OasParser::PathNotFound, OasParser::MethodNotFound
22
+ nil
23
+ end
24
+ end
25
+ end
@@ -6,8 +6,8 @@ require_relative 'validation'
6
6
 
7
7
  module OpenapiFirst
8
8
  class ResponseValidator
9
- def initialize(schema)
10
- @schema = schema
9
+ def initialize(spec)
10
+ @spec = spec
11
11
  end
12
12
 
13
13
  def validate(request, response)
@@ -20,7 +20,7 @@ module OpenapiFirst
20
20
  private
21
21
 
22
22
  def validation_errors(request, response)
23
- content = response_for(request, response).content
23
+ content = response_for(request, response)&.content
24
24
  return unless content
25
25
 
26
26
  content_type = content[response.content_type]
@@ -37,16 +37,23 @@ module OpenapiFirst
37
37
 
38
38
  def validate_json_schema(schema, data)
39
39
  JSONSchemer.schema(schema).validate(data).to_a.map do |error|
40
- error.delete('root_schema')
41
- error
40
+ format_error(error)
41
+ end
42
+ end
43
+
44
+ def format_error(error)
45
+ ValidationFormat.error_details(error)
46
+ .merge!(
47
+ data_pointer: error['data_pointer'],
48
+ schema_pointer: error['schema_pointer']
49
+ ).tap do |formatted|
42
50
  end
43
51
  end
44
52
 
45
53
  def response_for(request, response)
46
- @schema
47
- .path_by_path(request.path)
48
- .endpoint_by_method(request.request_method.downcase)
49
- .response_by_code(response.status.to_s, use_default: true)
54
+ @spec
55
+ .find_operation!(request)
56
+ &.response_by_code(response.status.to_s, use_default: true)
50
57
  end
51
58
  end
52
59
  end
@@ -15,7 +15,7 @@ module OpenapiFirst
15
15
 
16
16
  def call(env)
17
17
  req = Rack::Request.new(env)
18
- operation = env[OPERATION] = find_operation(req)
18
+ operation = env[OPERATION] = @spec.find_operation(req)
19
19
  path_params = find_path_params(operation, req)
20
20
  env[PATH_PARAMS] = path_params if path_params
21
21
  return @app.call(env) if operation || @allow_unknown_operation
@@ -23,13 +23,6 @@ module OpenapiFirst
23
23
  Rack::Response.new('', 404)
24
24
  end
25
25
 
26
- def find_operation(req)
27
- path = @spec.path_by_path(req.path)
28
- path.endpoint_by_method(req.request_method.downcase)
29
- rescue OasParser::PathNotFound, OasParser::MethodNotFound
30
- nil
31
- end
32
-
33
26
  def find_path_params(operation, req)
34
27
  return unless operation&.path_parameters&.any?
35
28
 
@@ -2,6 +2,8 @@
2
2
 
3
3
  module OpenapiFirst
4
4
  module ValidationFormat
5
+ SIMPLE_TYPES = %w[string integer].freeze
6
+
5
7
  # rubocop:disable Metrics/MethodLength
6
8
  def self.error_details(error)
7
9
  if error['type'] == 'pattern'
@@ -14,6 +16,10 @@ module OpenapiFirst
14
16
  {
15
17
  title: "is missing required properties: #{missing_keys.join(', ')}"
16
18
  }
19
+ elsif SIMPLE_TYPES.include?(error['type'])
20
+ {
21
+ title: "should be a #{error['type']}"
22
+ }
17
23
  elsif error['schema'] == false
18
24
  { title: 'unknown fields are not allowed' }
19
25
  else
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module OpenapiFirst
4
- VERSION = '0.6.0'
4
+ VERSION = '0.6.1'
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: openapi_first
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.6.0
4
+ version: 0.6.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Andreas Haller
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2019-07-07 00:00:00.000000000 Z
11
+ date: 2019-07-10 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: json_schemer
@@ -148,6 +148,7 @@ files:
148
148
  - lib/openapi_first.rb
149
149
  - lib/openapi_first/app.rb
150
150
  - lib/openapi_first/coverage.rb
151
+ - lib/openapi_first/definition.rb
151
152
  - lib/openapi_first/error_response_method.rb
152
153
  - lib/openapi_first/operation_resolver.rb
153
154
  - lib/openapi_first/query_parameter_validation.rb