openapi_first 0.6.0 → 0.6.1

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