openapi_first 0.6.10 → 0.7.0.alpha1

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