openapi_first 0.6.10 → 0.7.0.alpha1

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: 07a70e89bc5624bdd59ce6b021bca7482f583a0b2a6b3b38e77f7241ec99b5b9
4
- data.tar.gz: cd4de92064ab68e06726e08bf7095b0f559686621ba09c5c18ce903e73941555
3
+ metadata.gz: 4885473ec6373b0fcfd9baace6175296f362099ba8ee73de0b8c1538b3420504
4
+ data.tar.gz: 5a72ae579e67bcb17c33debb3adbcae1cdaae09ad98ccc0e1158a59719d00115
5
5
  SHA512:
6
- metadata.gz: 757428e2000f46da3575d0e3b7614a5e4e23c93dde1dd178cda0266b7e065dd84d2acf4310b2567176e58f8fbfa74e9a8b3cf712001bac351f8a07671a2ab371
7
- data.tar.gz: c295ac507d28d0c30b6454c74ee5c74777d004cae60bbad0498345b775630260177cdcc86f586a9e16360eb1547fce1f52c7d1e0f148af4ce56d0a6eca65d0a8
6
+ metadata.gz: d2783c23798622b8bcc6c6f9520272d0477760d405f9879d4dc39e94b1d361b3873a87147ccef81e0ba3ec5c59e67941f645e41caf52dd221ddb915167140c6d
7
+ data.tar.gz: d540387f504454feca50ea746ef87745aed68e841bc5eb734c8d2e3419676e221a8dd93404601f010e3725152cd0e0e9a2775a1646690b8f4e5b62fbd8d7ca93
data/CHANGELOG.md CHANGED
@@ -2,6 +2,12 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ - Make use of hanami-router, because it's fast
6
+ - Remove OpenapiFirst::Coverage
7
+ - Remove option `allow_unknown_query_paramerters`
8
+ - Move the namespace option to Router
9
+ - Convert numeric parameters to `Integer` or `Float`
10
+
5
11
  ## 0.6.9
6
12
  - Removed radix tree, because of a bug (https://github.com/namusyaka/r2ree-ruby/issues/2)
7
13
 
data/Gemfile.lock CHANGED
@@ -1,10 +1,11 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- openapi_first (0.6.10)
4
+ openapi_first (0.7.0.alpha1)
5
+ hanami-router (~> 2.0.alpha2)
6
+ hanami-utils (~> 2.0.alpha1)
5
7
  json_schemer (~> 0.2)
6
8
  multi_json (~> 1.14)
7
- mustermann-contrib (~> 1.1.1)
8
9
  oas_parser (~> 0.24)
9
10
  rack (~> 2.2)
10
11
 
@@ -25,9 +26,16 @@ GEM
25
26
  concurrent-ruby (1.1.6)
26
27
  deep_merge (1.2.1)
27
28
  diff-lcs (1.3)
28
- ecma-re-validator (0.2.1)
29
+ ecma-re-validator (0.2.0)
29
30
  regexp_parser (~> 1.2)
30
31
  hana (1.3.5)
32
+ hanami-router (2.0.0.alpha2)
33
+ mustermann (~> 1.0)
34
+ mustermann-contrib (~> 1.0)
35
+ rack (~> 2.0)
36
+ hanami-utils (2.0.0.alpha1)
37
+ concurrent-ruby (~> 1.0)
38
+ transproc (~> 1.0)
31
39
  hansi (0.2.0)
32
40
  hash-deep-merge (0.1.1)
33
41
  i18n (1.8.2)
@@ -38,7 +46,7 @@ GEM
38
46
  hana (~> 1.3)
39
47
  regexp_parser (~> 1.5)
40
48
  uri_template (~> 0.7)
41
- method_source (0.9.2)
49
+ method_source (1.0.0)
42
50
  mini_portile2 (2.4.0)
43
51
  minitest (5.14.0)
44
52
  multi_json (1.14.1)
@@ -58,12 +66,12 @@ GEM
58
66
  mustermann-contrib (~> 1.1.1)
59
67
  nokogiri
60
68
  parallel (1.19.1)
61
- parser (2.7.0.4)
69
+ parser (2.7.0.5)
62
70
  ast (~> 2.4.0)
63
- pry (0.12.2)
64
- coderay (~> 1.1.0)
65
- method_source (~> 0.9.0)
66
- public_suffix (4.0.4)
71
+ pry (0.13.0)
72
+ coderay (~> 1.1)
73
+ method_source (~> 1.0)
74
+ public_suffix (4.0.3)
67
75
  rack (2.2.2)
68
76
  rack-test (1.1.0)
69
77
  rack (>= 1.0, < 3)
@@ -77,7 +85,7 @@ GEM
77
85
  rspec-mocks (~> 3.9.0)
78
86
  rspec-core (3.9.1)
79
87
  rspec-support (~> 3.9.1)
80
- rspec-expectations (3.9.0)
88
+ rspec-expectations (3.9.1)
81
89
  diff-lcs (>= 1.2.0, < 2.0)
82
90
  rspec-support (~> 3.9.0)
83
91
  rspec-mocks (3.9.1)
@@ -95,7 +103,8 @@ GEM
95
103
  ruby-progressbar (1.10.1)
96
104
  ruby2_keywords (0.0.2)
97
105
  thread_safe (0.3.6)
98
- tzinfo (1.2.7)
106
+ transproc (1.1.1)
107
+ tzinfo (1.2.6)
99
108
  thread_safe (~> 0.1)
100
109
  unicode-display_width (1.6.1)
101
110
  uri_template (0.7.0)
data/README.md CHANGED
@@ -36,8 +36,6 @@ Handler functions (`find_pet`) are called with two arguments:
36
36
  - `res` - Holds a Rack::Response that you can modify if needed
37
37
  If you want to access to plain Rack env you can call `params.env`.
38
38
 
39
- You can also use the provided Rack middlewares to auto-implement only certain aspects of the request-response flow like query parameter or request body parameter validation based on your OpenAPI file. Read on to learn how.
40
-
41
39
  ### Handling only certain paths
42
40
 
43
41
  You can filter the URIs that should be handled by pass ing `only` to `OpenapiFirst.load`:
@@ -57,9 +55,9 @@ run OpenapiFirst.middleware('./openapi/openapi.yaml', namespace: Pets)
57
55
 
58
56
  When using the middleware, all requests that are not part of the API description will be passed to the next app.
59
57
 
60
- ## Try it out
58
+ ### Try it out
61
59
 
62
- See [example](examples)
60
+ See [examples](examples).
63
61
 
64
62
 
65
63
  ## Installation
@@ -72,21 +70,31 @@ gem 'openapi_first'
72
70
 
73
71
  OpenapiFirst uses [`multi_json`](https://rubygems.org/gems/multi_json).
74
72
 
75
- ## How it works
73
+ ## Handlers
76
74
 
77
- OpenapiFirst offers Rack middlewares to auto-implement different aspects for request handling:
75
+ OpenapiFirst maps the HTTP request to a method call based on the [operationId](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#operation-object) in your API description.
78
76
 
79
- - Request validation
80
- - Mapping request to a function call
77
+ It works like this:
81
78
 
82
- It starts by adding a router middleware:
79
+ - "create_pet" or "createPet" or "create pet" calls `MyApi.create_pet(params, response)`
80
+ - "some_things.create" calls: `MyApi::SomeThings.create(params, response)`
81
+ - "pets#create" calls: `MyApi::Pets::Create.new.call(params, response)`
83
82
 
84
- ```ruby
85
- spec = OpenapiFirst.load('petstore.yaml')
86
- use OpenapiFirst::Router, spec: spec
87
- ```
83
+ These handler methods are called with two arguments:
84
+
85
+ - `params` - Holds the parsed request body, filtered query params and path parameters
86
+ - `res` - Holds a Rack::Response that you can modify if needed
87
+
88
+ You can call `params.env` to access the Rack env (just like in [Hanami actions](https://guides.hanamirb.org/actions/parameters/))
89
+
90
+ There are two ways to set the response body:
88
91
 
89
- 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`.
92
+ - Calling `res.write "things"` (see [Rack::Response](https://www.rubydoc.info/github/rack/rack/Rack/Response))
93
+ - Returning a value from the function (see example above) (this will always converted to JSON)
94
+
95
+ ## Request validation
96
+
97
+ If the request is not valid, these middlewares return a 400 status code with a body that describes the error.
90
98
 
91
99
  The error responses conform with [JSON:API](https://jsonapi.org).
92
100
 
@@ -107,18 +115,12 @@ content-type: "application/vnd.api+json"
107
115
  ]
108
116
  }
109
117
  ```
110
- ## Request validation
111
-
112
- ```ruby
113
- # Add the middleware:
114
- use OpenapiFirst::RequestValidation
115
- ```
116
118
 
117
- ## Query parameter validation
119
+ ### Parameter validation
118
120
 
119
- By default OpenapiFirst does not allow additional query parameters and will respond with 400 if additional parameters are sent. You can allow additional parameters with `allow_allow_unknown_query_parameters: true`:
121
+ The middleware filteres all top-level query parameters and paths parameters and tries to convert numeric values. Meaning, if you have an `:something_id` path with `type: integer`, it will try convert the value to an integer.
122
+ Note that is currently does not convert date, date-time or time formats and that conversion is currently on done for path and query parameters, but not for request bodies.
120
123
 
121
- The middleware filteres all top-level query parameters and adds these to the Rack env: `env[OpenapiFirst::QUERY_PARAMS]`.
122
124
  If you want to forbid _nested_ query parameters you will need to use [`additionalProperties: false`](https://json-schema.org/understanding-json-schema/reference/object.html#properties) in your query parameter JSON schema.
123
125
 
124
126
  _OpenapiFirst always treats query parameters like [`style: deepObject`](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#style-values), **but** it just works with nested objects (`filter[foo][bar]=baz`) (see [this discussion](https://github.com/OAI/OpenAPI-Specification/issues/1706))._
@@ -132,54 +134,6 @@ This will add the parsed request body to `env[OpenapiFirst::REQUEST_BODY]`.
132
134
 
133
135
  tbd.
134
136
 
135
- ## Mapping the request to a method call
136
-
137
- OpenapiFirst uses a `OperationResolver` middleware to map the HTTP request to a method call.
138
-
139
- The resolver function is found via the [`operationId`](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#operation-object) attribute in your API description like this:
140
-
141
- - `create_pet` will map to `MyApi.create_pet(params, response)`
142
- - `some_things.create` will map to `MyApi::SomeThings.create(params, response)`
143
- - `pets#create` will map to `MyApi::Pets::Create.new.call(params, response)` (like [Hanami::Router](https://github.com/hanami/router#controllers))
144
-
145
- These handler methods are called with two arguments:
146
-
147
- - `params` - Holds the parsed request body, filtered query params and path parameters
148
- - `res` - Holds a Rack::Response that you can modify if needed
149
-
150
- You can call `params.env` to access the Rack env (just like in [Hanami actions](https://guides.hanamirb.org/actions/parameters/))
151
-
152
- There are two ways to set the response body:
153
-
154
- - Calling `res.write "things"` (see [Rack::Response](https://www.rubydoc.info/github/rack/rack/Rack/Response))
155
- - Returning a value from the function (see example above) (this will always converted to JSON)
156
-
157
- ### Adding the middleware
158
-
159
- ```ruby
160
- # Define some methods
161
- module MyApi
162
- def self.create_pet(params, res)
163
- res.status = 201
164
- {
165
- id: '1',
166
- name: params['name']
167
- }
168
- end
169
- end
170
-
171
- # Add the middleware:
172
- use OpenapiFirst::OperationResolver, namespace: MyApi
173
- # If the operation was not found in the OAS file, the next app will be called
174
-
175
- # OR use it as a Rack app via `run`:
176
- run OpenapiFirst::OperationResolver, namespace: Pets
177
- # If the operation was not found, this will return 404
178
-
179
- # Now make a request like
180
- # POST /pets, { name: 'Oscar' }
181
- ```
182
-
183
137
  ## Response validation
184
138
 
185
139
  Response validation is useful 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).
data/benchmarks/Gemfile CHANGED
@@ -6,7 +6,7 @@ gem 'benchmark-ips'
6
6
  gem 'benchmark-memory'
7
7
  gem 'committee'
8
8
  gem 'grape'
9
- gem 'hanami-router', '~> 2.0.0.alpha1'
9
+ gem 'hanami-router', '~> 2.0.0.alpha2'
10
10
  gem 'multi_json'
11
11
  gem 'openapi_first', path: '../'
12
12
  gem 'sinatra'
@@ -1,10 +1,11 @@
1
1
  PATH
2
2
  remote: ..
3
3
  specs:
4
- openapi_first (0.6.9)
4
+ openapi_first (0.7.0.alpha1)
5
+ hanami-router (~> 2.0.alpha2)
6
+ hanami-utils (~> 2.0.alpha1)
5
7
  json_schemer (~> 0.2)
6
8
  multi_json (~> 1.14)
7
- mustermann-contrib (~> 1.1.1)
8
9
  oas_parser (~> 0.24)
9
10
  rack (~> 2.2)
10
11
 
@@ -44,7 +45,7 @@ GEM
44
45
  concurrent-ruby (~> 1.0)
45
46
  dry-core (~> 0.2)
46
47
  dry-equalizer (~> 0.2)
47
- dry-types (1.3.1)
48
+ dry-types (1.4.0)
48
49
  concurrent-ruby (~> 1.0)
49
50
  dry-container (~> 0.3)
50
51
  dry-core (~> 0.4, >= 0.4.4)
@@ -65,12 +66,15 @@ GEM
65
66
  mustermann (~> 1.0)
66
67
  mustermann-contrib (~> 1.0)
67
68
  rack (~> 2.0)
69
+ hanami-utils (2.0.0.alpha1)
70
+ concurrent-ruby (~> 1.0)
71
+ transproc (~> 1.0)
68
72
  hansi (0.2.0)
69
73
  hash-deep-merge (0.1.1)
70
74
  i18n (1.8.2)
71
75
  concurrent-ruby (~> 1.0)
72
76
  json_schema (0.20.8)
73
- json_schemer (0.2.10)
77
+ json_schemer (0.2.11)
74
78
  ecma-re-validator (~> 0.2)
75
79
  hana (~> 1.3)
76
80
  regexp_parser (~> 1.5)
@@ -116,6 +120,7 @@ GEM
116
120
  seg
117
121
  thread_safe (0.3.6)
118
122
  tilt (2.0.10)
123
+ transproc (1.1.1)
119
124
  tzinfo (1.2.6)
120
125
  thread_safe (~> 0.1)
121
126
  uri_template (0.7.0)
@@ -129,7 +134,7 @@ DEPENDENCIES
129
134
  benchmark-memory
130
135
  committee
131
136
  grape
132
- hanami-router (~> 2.0.0.alpha1)
137
+ hanami-router (~> 2.0.0.alpha2)
133
138
  multi_json
134
139
  openapi_first!
135
140
  sinatra
@@ -19,5 +19,5 @@ namespace = Module.new do
19
19
  end
20
20
 
21
21
  spec = OpenapiFirst.load(File.absolute_path('./openapi.yaml', __dir__))
22
- use OpenapiFirst::Router, spec: spec
23
- run OpenapiFirst::OperationResolver.new(namespace: namespace)
22
+ use OpenapiFirst::Router, spec: spec, namespace: namespace
23
+ run OpenapiFirst::OperationResolver.new
@@ -5,18 +5,18 @@ require 'rack'
5
5
  module OpenapiFirst
6
6
  class App
7
7
  def initialize(
8
- app,
8
+ parent_app,
9
9
  spec,
10
- namespace:,
11
- allow_unknown_operation: !app.nil?
10
+ namespace:
12
11
  )
13
12
  @stack = Rack::Builder.app do
14
13
  freeze_app
15
14
  use OpenapiFirst::Router,
16
15
  spec: spec,
17
- allow_unknown_operation: allow_unknown_operation
16
+ namespace: namespace,
17
+ parent_app: parent_app
18
18
  use OpenapiFirst::RequestValidation
19
- run OpenapiFirst::OperationResolver.new(app, namespace: namespace)
19
+ run OpenapiFirst::OperationResolver.new
20
20
  end
21
21
  end
22
22
 
@@ -13,15 +13,16 @@ module OpenapiFirst
13
13
  end
14
14
 
15
15
  def call(env)
16
- operation = @spec.find_operation(Rack::Request.new(env))
16
+ response = @app.call(env)
17
+ operation = env[OPERATION]
17
18
  @to_be_called.delete(endpoint_id(operation)) if operation
18
- @app.call(env)
19
+ response
19
20
  end
20
21
 
21
22
  private
22
23
 
23
24
  def endpoint_id(operation)
24
- "#{operation.path.path}##{operation.method}"
25
+ "#{operation.path}##{operation.method}"
25
26
  end
26
27
  end
27
28
  end
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative 'operation'
4
+
3
5
  module OpenapiFirst
4
6
  class Definition
5
7
  def initialize(parsed)
@@ -7,7 +9,7 @@ module OpenapiFirst
7
9
  end
8
10
 
9
11
  def operations
10
- @spec.endpoints
12
+ @spec.endpoints.map { |e| Operation.new(e) }
11
13
  end
12
14
 
13
15
  def find_operation!(request)
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'forwardable'
4
+
5
+ module OpenapiFirst
6
+ class Operation
7
+ extend Forwardable
8
+ def_delegators :@operation,
9
+ :parameters,
10
+ :method,
11
+ :request_body,
12
+ :operation_id
13
+
14
+ def initialize(parsed)
15
+ @operation = parsed
16
+ end
17
+
18
+ def path
19
+ @operation.path.path
20
+ end
21
+
22
+ def parameters_json_schema
23
+ @parameters_json_schema ||= build_parameters_json_schema
24
+ end
25
+
26
+ def content_type_for(status)
27
+ content = @operation
28
+ .response_by_code(status.to_s, use_default: true)
29
+ .content
30
+ content.keys[0] if content
31
+ end
32
+
33
+ private
34
+
35
+ def build_parameters_json_schema
36
+ return unless @operation.parameters&.any?
37
+
38
+ @operation.parameters.each_with_object(
39
+ 'type' => 'object',
40
+ 'required' => [],
41
+ 'properties' => {}
42
+ ) do |parameter, schema|
43
+ schema['required'] << parameter.name if parameter.required
44
+ schema['properties'][parameter.name] = parameter.schema
45
+ end
46
+ end
47
+ end
48
+ end
@@ -4,55 +4,28 @@ require 'rack'
4
4
 
5
5
  module OpenapiFirst
6
6
  class OperationResolver
7
- NOT_FOUND = Rack::Response.new('', 404).finish.freeze
8
- DEFAULT_APP = ->(_env) { NOT_FOUND }
9
-
10
- def initialize(app = DEFAULT_APP, options) # rubocop:disable Style/OptionalArguments
11
- @app = app
12
- @namespace = options.fetch(:namespace)
13
- end
14
-
15
- def call(env) # rubocop:disable Metrics/AbcSize
7
+ def call(env)
16
8
  operation = env[OpenapiFirst::OPERATION]
17
- return @app.call(env) unless operation
18
-
19
- operation_id = operation.operation_id
20
9
  res = Rack::Response.new
21
10
  params = build_params(env)
22
- handler = find_handler(operation_id)
11
+ handler = env[HANDLER]
23
12
  result = handler.call(params, res)
24
- res.write MultiJson.dump(result) if result && res.body.empty?
25
- res[Rack::CONTENT_TYPE] ||= find_content_type(operation, res.status)
13
+ res.write serialize(result) if result && res.body.empty?
14
+ res[Rack::CONTENT_TYPE] ||= operation.content_type_for(res.status)
26
15
  res.finish
27
16
  end
28
17
 
29
- def find_handler(operation_id)
30
- if operation_id.include?('.')
31
- module_name, method_name = operation_id.split('.')
32
- return @namespace.const_get(module_name.camelize).method(method_name)
33
- end
34
-
35
- if operation_id.include?('#')
36
- module_name, class_name = operation_id.split('#')
37
- return @namespace.const_get(module_name.camelize)
38
- .const_get(class_name.camelize).new
39
- end
40
- @namespace.method(operation_id)
41
- end
42
-
43
18
  private
44
19
 
45
- def find_content_type(operation, status)
46
- content = operation
47
- .response_by_code(status.to_s, use_default: true)
48
- .content
49
- content.keys[0] if content
20
+ def serialize(result)
21
+ return result if result.is_a?(String)
22
+
23
+ MultiJson.dump(result)
50
24
  end
51
25
 
52
26
  def build_params(env)
53
27
  sources = [
54
- env[PATH_PARAMS],
55
- env[QUERY_PARAMS],
28
+ env[PARAMS],
56
29
  env[REQUEST_BODY]
57
30
  ].tap(&:compact!)
58
31
  Params.new(env).merge!(*sources)
@@ -64,7 +37,7 @@ module OpenapiFirst
64
37
 
65
38
  def initialize(env)
66
39
  @env = env
67
- super()
40
+ super
68
41
  end
69
42
  end
70
43
  end
@@ -3,25 +3,21 @@
3
3
  require 'rack'
4
4
  require 'json_schemer'
5
5
  require 'multi_json'
6
- require_relative 'query_parameters'
7
6
  require_relative 'validation_format'
8
7
 
9
8
  module OpenapiFirst
10
- class RequestValidation
11
- def initialize(app, options = {})
9
+ class RequestValidation # rubocop:disable Metrics/ClassLength
10
+ def initialize(app, _options = {})
12
11
  @app = app
13
- @allow_unknown_query_parameters = options.fetch(
14
- :allow_unknown_query_parameters, false
15
- )
16
12
  end
17
13
 
18
14
  def call(env) # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
19
15
  operation = env[OpenapiFirst::OPERATION]
20
16
  return @app.call(env) unless operation
21
17
 
22
- req = Rack::Request.new(env)
23
18
  catch(:halt) do
24
- validate_query_parameters!(env, operation, req.params)
19
+ validate_query_parameters!(env, operation, env[PARAMS])
20
+ req = Rack::Request.new(env)
25
21
  content_type = req.content_type
26
22
  return @app.call(env) unless operation.request_body
27
23
 
@@ -33,6 +29,8 @@ module OpenapiFirst
33
29
  end
34
30
  end
35
31
 
32
+ private
33
+
36
34
  def halt(response)
37
35
  throw :halt, response
38
36
  end
@@ -83,10 +81,10 @@ module OpenapiFirst
83
81
  ).finish
84
82
  end
85
83
 
86
- def request_body_schema(content_type, endpoint)
87
- return unless endpoint
84
+ def request_body_schema(content_type, operation)
85
+ return unless operation
88
86
 
89
- endpoint.request_body.content[content_type]&.fetch('schema')
87
+ operation.request_body.content[content_type]&.fetch('schema')
90
88
  end
91
89
 
92
90
  def serialize_request_body_errors(validation_errors)
@@ -100,27 +98,25 @@ module OpenapiFirst
100
98
  end
101
99
 
102
100
  def validate_query_parameters!(env, operation, params)
103
- json_schema = QueryParameters.new(
104
- operation: operation,
105
- allow_unknown_parameters: @allow_unknown_query_parameters
106
- ).to_json_schema
107
-
101
+ json_schema = operation.parameters_json_schema
108
102
  return unless json_schema
109
103
 
104
+ params = filtered_params(json_schema, params)
110
105
  errors = JSONSchemer.schema(json_schema).validate(params)
111
106
  if errors.any?
112
107
  halt error_response(400, serialize_query_parameter_errors(errors))
113
108
  end
114
- env[QUERY_PARAMS] = allowed_params(json_schema, params)
109
+ env[PARAMS] = params
115
110
  end
116
111
 
117
- def allowed_params(json_schema, params)
112
+ def filtered_params(json_schema, params)
118
113
  json_schema['properties']
119
- .keys
120
- .each_with_object({}) do |parameter_name, filtered|
114
+ .each_with_object({}) do |key_value, result|
115
+ parameter_name, schema = key_value
121
116
  next unless params.key?(parameter_name)
122
117
 
123
- filtered[parameter_name] = params[parameter_name]
118
+ value = params[parameter_name]
119
+ result[parameter_name] = parse_parameter(value, schema)
124
120
  end
125
121
  end
126
122
 
@@ -131,5 +127,18 @@ module OpenapiFirst
131
127
  }.update(ValidationFormat.error_details(error))
132
128
  end
133
129
  end
130
+
131
+ def parse_parameter(value, schema)
132
+ return filtered_params(schema, value) if schema['properties']
133
+
134
+ begin
135
+ return Integer(value, 10) if schema['type'] == 'integer'
136
+ return Float(value) if schema['type'] == 'number'
137
+ rescue ArgumentError
138
+ value
139
+ end
140
+
141
+ value
142
+ end
134
143
  end
135
144
  end
@@ -1,9 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'rack'
4
- require 'json_schemer'
5
- require 'multi_json'
6
- require 'mustermann/template'
4
+ require 'hanami/router'
5
+ require_relative 'utils'
7
6
 
8
7
  module OpenapiFirst
9
8
  class Router
@@ -11,25 +10,75 @@ module OpenapiFirst
11
10
 
12
11
  def initialize(app, options)
13
12
  @app = app
14
- @spec = options.fetch(:spec)
15
- @allow_unknown_operation = options.fetch(:allow_unknown_operation, false)
13
+ @namespace = options.fetch(:namespace)
14
+ @parent_app = options.fetch(:parent_app, nil)
15
+ @router = build_router(options.fetch(:spec).operations)
16
16
  end
17
17
 
18
18
  def call(env)
19
- req = Rack::Request.new(env)
20
- operation = env[OPERATION] = @spec.find_operation(req)
21
- path_params = find_path_params(operation, req)
22
- env[PATH_PARAMS] = path_params if path_params
23
- return @app.call(env) if operation || @allow_unknown_operation
19
+ route = @router.recognize(env)
20
+ return route.endpoint.call(env) if route.routable?
21
+
22
+ return @parent_app.call(env) if @parent_app
24
23
 
25
24
  NOT_FOUND
26
25
  end
27
26
 
28
- def find_path_params(operation, req)
29
- return unless operation&.path_parameters&.any?
27
+ def find_handler(operation_id) # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
28
+ name = operation_id.match(/:*(.*)/)&.to_a&.at(1)
29
+ return if name.nil?
30
+
31
+ if name.include?('.')
32
+ module_name, method_name = name.split('.')
33
+ klass = find_const(@namespace, module_name)
34
+ return klass&.method(Utils.underscore(method_name))
35
+ end
36
+ if name.include?('#')
37
+ module_name, klass_name = name.split('#')
38
+ const = find_const(@namespace, module_name)
39
+ klass = find_const(const, klass_name)
40
+ return ->(params, res) { klass.new.call(params, res) }
41
+ end
42
+ method_name = Utils.underscore(name)
43
+ return unless @namespace.respond_to?(method_name)
44
+
45
+ @namespace.method(method_name)
46
+ end
47
+
48
+ private
49
+
50
+ def find_const(parent, name)
51
+ name = Utils.classify(name)
52
+ return unless parent.const_defined?(name, false)
53
+
54
+ parent.const_get(name, false)
55
+ end
30
56
 
31
- pattern = Mustermann::Template.new(operation.path.path)
32
- pattern.params(req.path)
57
+ def build_router(operations) # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
58
+ router = Hanami::Router.new {}
59
+ operations.each do |operation|
60
+ normalized_path = operation.path.gsub('{', ':').gsub('}', '')
61
+ if operation.operation_id.nil?
62
+ warn "operationId is missing in '#{operation.method} #{operation.path}'. I am ignoring this operation." # rubocop:disable Layout/LineLength
63
+ next
64
+ end
65
+ handler = find_handler(operation.operation_id)
66
+ if handler.nil?
67
+ warn "could not find handler for '#{operation.operation_id}' (#{operation.method} #{operation.path}). I am ignoring this operation." # rubocop:disable Layout/LineLength
68
+ next
69
+ end
70
+ router.public_send(
71
+ operation.method,
72
+ normalized_path,
73
+ to: lambda do |env|
74
+ env[OPERATION] = operation
75
+ env[PARAMS] = Utils.deep_stringify(env['router.params'])
76
+ env[HANDLER] = handler
77
+ @app.call(env)
78
+ end
79
+ )
80
+ end
81
+ router
33
82
  end
34
83
  end
35
84
  end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'hanami/utils/string'
4
+
5
+ module OpenapiFirst
6
+ module Utils
7
+ def self.underscore(string)
8
+ Hanami::Utils::String.underscore(string)
9
+ end
10
+
11
+ def self.classify(string)
12
+ Hanami::Utils::String.classify(string)
13
+ end
14
+
15
+ def self.deep_stringify(params) # rubocop:disable Metrics/MethodLength
16
+ params.each_with_object({}) do |(key, value), output|
17
+ output[key.to_s] =
18
+ case value
19
+ when ::Hash
20
+ deep_stringify(value)
21
+ when Array
22
+ value.map do |item|
23
+ item.is_a?(::Hash) ? deep_stringify(item) : item
24
+ end
25
+ else
26
+ value
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module OpenapiFirst
4
- VERSION = '0.6.10'
4
+ VERSION = '0.7.0.alpha1'
5
5
  end
data/lib/openapi_first.rb CHANGED
@@ -12,9 +12,9 @@ require 'openapi_first/app'
12
12
 
13
13
  module OpenapiFirst
14
14
  OPERATION = 'openapi_first.operation'
15
- PATH_PARAMS = 'openapi_first.path_params'
15
+ PARAMS = 'openapi_first.params'
16
16
  REQUEST_BODY = 'openapi_first.parsed_request_body'
17
- QUERY_PARAMS = 'openapi_first.query_params'
17
+ HANDLER = 'openapi_first.handler'
18
18
 
19
19
  def self.load(spec_path, only: nil)
20
20
  content = YAML.load_file(spec_path)
@@ -32,9 +32,10 @@ Gem::Specification.new do |spec|
32
32
  spec.bindir = 'exe'
33
33
  spec.require_paths = ['lib']
34
34
 
35
+ spec.add_dependency 'hanami-router', '~> 2.0.alpha2'
36
+ spec.add_dependency 'hanami-utils', '~> 2.0.alpha1'
35
37
  spec.add_dependency 'json_schemer', '~> 0.2'
36
38
  spec.add_dependency 'multi_json', '~> 1.14'
37
- spec.add_dependency 'mustermann-contrib', '~> 1.1.1'
38
39
  spec.add_dependency 'oas_parser', '~> 0.24'
39
40
  spec.add_dependency 'rack', '~> 2.2'
40
41
 
metadata CHANGED
@@ -1,57 +1,71 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: openapi_first
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.6.10
4
+ version: 0.7.0.alpha1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Andreas Haller
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2020-04-17 00:00:00.000000000 Z
11
+ date: 2020-03-23 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
- name: json_schemer
14
+ name: hanami-router
15
15
  requirement: !ruby/object:Gem::Requirement
16
16
  requirements:
17
17
  - - "~>"
18
18
  - !ruby/object:Gem::Version
19
- version: '0.2'
19
+ version: 2.0.alpha2
20
20
  type: :runtime
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
24
  - - "~>"
25
25
  - !ruby/object:Gem::Version
26
- version: '0.2'
26
+ version: 2.0.alpha2
27
27
  - !ruby/object:Gem::Dependency
28
- name: multi_json
28
+ name: hanami-utils
29
29
  requirement: !ruby/object:Gem::Requirement
30
30
  requirements:
31
31
  - - "~>"
32
32
  - !ruby/object:Gem::Version
33
- version: '1.14'
33
+ version: 2.0.alpha1
34
34
  type: :runtime
35
35
  prerelease: false
36
36
  version_requirements: !ruby/object:Gem::Requirement
37
37
  requirements:
38
38
  - - "~>"
39
39
  - !ruby/object:Gem::Version
40
- version: '1.14'
40
+ version: 2.0.alpha1
41
41
  - !ruby/object:Gem::Dependency
42
- name: mustermann-contrib
42
+ name: json_schemer
43
43
  requirement: !ruby/object:Gem::Requirement
44
44
  requirements:
45
45
  - - "~>"
46
46
  - !ruby/object:Gem::Version
47
- version: 1.1.1
47
+ version: '0.2'
48
48
  type: :runtime
49
49
  prerelease: false
50
50
  version_requirements: !ruby/object:Gem::Requirement
51
51
  requirements:
52
52
  - - "~>"
53
53
  - !ruby/object:Gem::Version
54
- version: 1.1.1
54
+ version: '0.2'
55
+ - !ruby/object:Gem::Dependency
56
+ name: multi_json
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '1.14'
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '1.14'
55
69
  - !ruby/object:Gem::Dependency
56
70
  name: oas_parser
57
71
  requirement: !ruby/object:Gem::Requirement
@@ -160,7 +174,6 @@ files:
160
174
  - benchmarks/apps/hanami_router.ru
161
175
  - benchmarks/apps/openapi.yaml
162
176
  - benchmarks/apps/openapi_first.ru
163
- - benchmarks/apps/openapi_first_request_validation_only.ru
164
177
  - benchmarks/apps/openapi_first_resolve_only.ru
165
178
  - benchmarks/apps/sinatra.ru
166
179
  - benchmarks/apps/syro.ru
@@ -175,11 +188,12 @@ files:
175
188
  - lib/openapi_first/app.rb
176
189
  - lib/openapi_first/coverage.rb
177
190
  - lib/openapi_first/definition.rb
191
+ - lib/openapi_first/operation.rb
178
192
  - lib/openapi_first/operation_resolver.rb
179
- - lib/openapi_first/query_parameters.rb
180
193
  - lib/openapi_first/request_validation.rb
181
194
  - lib/openapi_first/response_validator.rb
182
195
  - lib/openapi_first/router.rb
196
+ - lib/openapi_first/utils.rb
183
197
  - lib/openapi_first/validation.rb
184
198
  - lib/openapi_first/validation_format.rb
185
199
  - lib/openapi_first/version.rb
@@ -202,9 +216,9 @@ required_ruby_version: !ruby/object:Gem::Requirement
202
216
  version: '0'
203
217
  required_rubygems_version: !ruby/object:Gem::Requirement
204
218
  requirements:
205
- - - ">="
219
+ - - ">"
206
220
  - !ruby/object:Gem::Version
207
- version: '0'
221
+ version: 1.3.1
208
222
  requirements: []
209
223
  rubygems_version: 3.1.2
210
224
  signing_key:
@@ -1,30 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'openapi_first'
4
- require 'syro'
5
- require 'multi_json'
6
-
7
- app = Syro.new do
8
- on 'hello' do
9
- on :id do
10
- get do
11
- res.json MultiJson.dump(hello: 'world', id: inbox[:id])
12
- end
13
- end
14
-
15
- get do
16
- res.json [MultiJson.dump(hello: 'world')]
17
- end
18
-
19
- post do
20
- res.status = 201
21
- res.json MultiJson.dump(hello: 'world')
22
- end
23
- end
24
- end
25
-
26
- spec = OpenapiFirst.load(File.absolute_path('./openapi.yaml', __dir__))
27
- use OpenapiFirst::Router, spec: spec
28
- use OpenapiFirst::RequestValidation
29
-
30
- run app
@@ -1,24 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module OpenapiFirst
4
- class QueryParameters
5
- def initialize(operation:, allow_unknown_parameters: false)
6
- @operation = operation
7
- @allow_unknown_parameters = allow_unknown_parameters
8
- end
9
-
10
- def to_json_schema
11
- return unless @operation&.query_parameters&.any?
12
-
13
- @operation.query_parameters.each_with_object(
14
- 'type' => 'object',
15
- 'required' => [],
16
- 'additionalProperties' => @allow_unknown_parameters,
17
- 'properties' => {}
18
- ) do |parameter, schema|
19
- schema['required'] << parameter.name if parameter.required
20
- schema['properties'][parameter.name] = parameter.schema
21
- end
22
- end
23
- end
24
- end