openapi_first 0.6.4 → 0.6.5
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 +4 -4
- data/CHANGELOG.md +4 -0
- data/Gemfile.lock +2 -2
- data/README.md +13 -24
- data/benchmarks/Gemfile.lock +1 -1
- data/benchmarks/apps/openapi_first_request_validation_only.ru +1 -2
- data/lib/openapi_first.rb +1 -2
- data/lib/openapi_first/app.rb +1 -2
- data/lib/openapi_first/query_parameters.rb +24 -0
- data/lib/openapi_first/request_validation.rb +135 -0
- data/lib/openapi_first/version.rb +1 -1
- metadata +4 -6
- data/lib/openapi_first/error_response_method.rb +0 -20
- data/lib/openapi_first/query_parameter_schemas.rb +0 -29
- data/lib/openapi_first/query_parameter_validation.rb +0 -56
- data/lib/openapi_first/request_body_validation.rb +0 -93
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 035e8a92e6ae6b8817bc3660141c33807d2b813ea79ccdaf045056947b37ed2b
|
4
|
+
data.tar.gz: 7e776d7b8d66a5c36a2d70fc73043b648c1d8fb034563064b35963e95aa8732d
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 8d196918a2091123c0ad50da1a631da01a2b4923117400dedf01047cb12094375595bde9297de6887cd188258f6b2cb1acdea6a5d2b6af2551077a5788fc9ee2
|
7
|
+
data.tar.gz: 8289e602befcfa2d7e8aef2adf0c4d3663d624b68d002b2d3fd8ceb9266b20213cfeeca8191f8c84b76ce8d2b3dfe9fefe911d3673ad5b25a3263d5ddd377b5f
|
data/CHANGELOG.md
CHANGED
data/Gemfile.lock
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
PATH
|
2
2
|
remote: .
|
3
3
|
specs:
|
4
|
-
openapi_first (0.6.
|
4
|
+
openapi_first (0.6.5)
|
5
5
|
json_schemer (~> 0.2)
|
6
6
|
multi_json (~> 1.13)
|
7
7
|
oas_parser (~> 0.19)
|
@@ -32,7 +32,7 @@ GEM
|
|
32
32
|
i18n (1.7.0)
|
33
33
|
concurrent-ruby (~> 1.0)
|
34
34
|
jaro_winkler (1.5.3)
|
35
|
-
json_schemer (0.2.
|
35
|
+
json_schemer (0.2.8)
|
36
36
|
ecma-re-validator (~> 0.2)
|
37
37
|
hana (~> 1.3)
|
38
38
|
regexp_parser (~> 1.5)
|
data/README.md
CHANGED
@@ -77,8 +77,7 @@ OpenapiFirst uses [`multi_json`](https://rubygems.org/gems/multi_json).
|
|
77
77
|
|
78
78
|
OpenapiFirst offers Rack middlewares to auto-implement different aspects for request handling:
|
79
79
|
|
80
|
-
-
|
81
|
-
- Request body validation
|
80
|
+
- Request validation
|
82
81
|
- Mapping request to a function call
|
83
82
|
|
84
83
|
It starts by adding a router middleware:
|
@@ -109,40 +108,30 @@ content-type: "application/vnd.api+json"
|
|
109
108
|
]
|
110
109
|
}
|
111
110
|
```
|
112
|
-
|
113
|
-
## Query parameter validation
|
111
|
+
## Request validation
|
114
112
|
|
115
113
|
```ruby
|
116
|
-
|
114
|
+
# Add the middleware:
|
115
|
+
use OpenapiFirst::RequestValidation
|
117
116
|
```
|
118
117
|
|
119
|
-
|
118
|
+
## Query parameter validation
|
120
119
|
|
121
|
-
|
122
|
-
use OpenapiFirst::QueryParameterValidation,
|
123
|
-
allow_additional_parameters: true
|
124
|
-
```
|
120
|
+
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`:
|
125
121
|
|
126
122
|
The middleware filteres all top-level query parameters and adds these to the Rack env: `env[OpenapiFirst::QUERY_PARAMS]`.
|
127
|
-
If you want to forbid
|
123
|
+
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.
|
128
124
|
|
129
|
-
|
125
|
+
_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))._
|
130
126
|
|
131
|
-
|
132
|
-
|
133
|
-
tbd.
|
127
|
+
### Request body validation
|
134
128
|
|
135
|
-
|
136
|
-
|
137
|
-
```ruby
|
138
|
-
# Add the middleware:
|
139
|
-
use OpenapiFirst::RequestBodyValidation
|
140
|
-
```
|
141
|
-
|
142
|
-
This will return a `415` if the requests content type does not match or `400` if the request body is invalid.
|
129
|
+
The middleware will return a `415` if the requests content type does not match or `400` if the request body is invalid.
|
143
130
|
This will add the parsed request body to `env[OpenapiFirst::REQUEST_BODY]`.
|
144
131
|
|
145
|
-
|
132
|
+
### Header, Cookie, Path parameter validation
|
133
|
+
|
134
|
+
tbd.
|
146
135
|
|
147
136
|
## Mapping the request to a method call
|
148
137
|
|
data/benchmarks/Gemfile.lock
CHANGED
@@ -25,7 +25,6 @@ end
|
|
25
25
|
|
26
26
|
spec = OpenapiFirst.load(File.absolute_path('./openapi.yaml', __dir__))
|
27
27
|
use OpenapiFirst::Router, spec: spec
|
28
|
-
use OpenapiFirst::
|
29
|
-
use OpenapiFirst::RequestBodyValidation
|
28
|
+
use OpenapiFirst::RequestValidation
|
30
29
|
|
31
30
|
run app
|
data/lib/openapi_first.rb
CHANGED
@@ -5,8 +5,7 @@ require 'oas_parser'
|
|
5
5
|
require 'openapi_first/definition'
|
6
6
|
require 'openapi_first/version'
|
7
7
|
require 'openapi_first/router'
|
8
|
-
require 'openapi_first/
|
9
|
-
require 'openapi_first/request_body_validation'
|
8
|
+
require 'openapi_first/request_validation'
|
10
9
|
require 'openapi_first/response_validator'
|
11
10
|
require 'openapi_first/operation_resolver'
|
12
11
|
require 'openapi_first/app'
|
data/lib/openapi_first/app.rb
CHANGED
@@ -14,8 +14,7 @@ module OpenapiFirst
|
|
14
14
|
use OpenapiFirst::Router,
|
15
15
|
spec: spec,
|
16
16
|
allow_unknown_operation: allow_unknown_operation
|
17
|
-
use OpenapiFirst::
|
18
|
-
use OpenapiFirst::RequestBodyValidation
|
17
|
+
use OpenapiFirst::RequestValidation
|
19
18
|
run OpenapiFirst::OperationResolver.new(app, namespace: namespace)
|
20
19
|
end
|
21
20
|
end
|
@@ -0,0 +1,24 @@
|
|
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
|
@@ -0,0 +1,135 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'rack'
|
4
|
+
require 'json_schemer'
|
5
|
+
require 'multi_json'
|
6
|
+
require_relative 'query_parameters'
|
7
|
+
require_relative 'validation_format'
|
8
|
+
|
9
|
+
module OpenapiFirst
|
10
|
+
class RequestValidation
|
11
|
+
def initialize(app, allow_unknown_query_parameters: false)
|
12
|
+
@app = app
|
13
|
+
@allow_unknown_query_parameters = allow_unknown_query_parameters
|
14
|
+
end
|
15
|
+
|
16
|
+
def call(env) # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
|
17
|
+
operation = env[OpenapiFirst::OPERATION]
|
18
|
+
return @app.call(env) unless operation
|
19
|
+
|
20
|
+
req = Rack::Request.new(env)
|
21
|
+
catch(:halt) do
|
22
|
+
validate_query_parameters!(env, operation, req.params)
|
23
|
+
content_type = req.content_type
|
24
|
+
return @app.call(env) unless operation.request_body
|
25
|
+
|
26
|
+
validate_request_content_type!(content_type, operation)
|
27
|
+
body = req.body.read
|
28
|
+
req.body.rewind
|
29
|
+
parse_and_validate_request_body!(env, content_type, body, operation)
|
30
|
+
@app.call(env)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def halt(response)
|
35
|
+
throw :halt, response
|
36
|
+
end
|
37
|
+
|
38
|
+
def parse_and_validate_request_body!(env, content_type, body, operation)
|
39
|
+
validate_request_body_presence!(body, operation)
|
40
|
+
return if body.empty?
|
41
|
+
|
42
|
+
schema = request_body_schema(content_type, operation)
|
43
|
+
return unless schema
|
44
|
+
|
45
|
+
parsed_request_body = MultiJson.load(body)
|
46
|
+
errors = validate_json_schema(schema, parsed_request_body)
|
47
|
+
if errors.any?
|
48
|
+
halt(error_response(400, serialize_request_body_errors(errors)))
|
49
|
+
end
|
50
|
+
env[OpenapiFirst::REQUEST_BODY] = parsed_request_body
|
51
|
+
end
|
52
|
+
|
53
|
+
def validate_request_content_type!(content_type, operation)
|
54
|
+
return if operation.request_body.content[content_type]
|
55
|
+
|
56
|
+
halt(error_response(415))
|
57
|
+
end
|
58
|
+
|
59
|
+
def validate_request_body_presence!(body, operation)
|
60
|
+
return unless operation.request_body.required && body.empty?
|
61
|
+
|
62
|
+
halt(error_response(415, 'Request body is required'))
|
63
|
+
end
|
64
|
+
|
65
|
+
def validate_json_schema(schema, object)
|
66
|
+
JSONSchemer.schema(schema).validate(object)
|
67
|
+
end
|
68
|
+
|
69
|
+
def default_error(status, title = Rack::Utils::HTTP_STATUS_CODES[status])
|
70
|
+
{
|
71
|
+
status: status.to_s,
|
72
|
+
title: title
|
73
|
+
}
|
74
|
+
end
|
75
|
+
|
76
|
+
def error_response(status, errors = [default_error(status)])
|
77
|
+
Rack::Response.new(
|
78
|
+
MultiJson.dump(errors: errors),
|
79
|
+
status,
|
80
|
+
Rack::CONTENT_TYPE => 'application/vnd.api+json'
|
81
|
+
).finish
|
82
|
+
end
|
83
|
+
|
84
|
+
def request_body_schema(content_type, endpoint)
|
85
|
+
return unless endpoint
|
86
|
+
|
87
|
+
endpoint.request_body.content[content_type]&.fetch('schema')
|
88
|
+
end
|
89
|
+
|
90
|
+
def serialize_request_body_errors(validation_errors)
|
91
|
+
validation_errors.map do |error|
|
92
|
+
{
|
93
|
+
source: {
|
94
|
+
pointer: error['data_pointer']
|
95
|
+
}
|
96
|
+
}.update(ValidationFormat.error_details(error))
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
def validate_query_parameters!(env, operation, params)
|
101
|
+
json_schema = QueryParameters.new(
|
102
|
+
operation: operation,
|
103
|
+
allow_unknown_parameters: @allow_unknown_query_parameters
|
104
|
+
).to_json_schema
|
105
|
+
|
106
|
+
return unless json_schema
|
107
|
+
|
108
|
+
errors = JSONSchemer.schema(json_schema).validate(params)
|
109
|
+
if errors.any?
|
110
|
+
halt error_response(400, serialize_query_parameter_errors(errors))
|
111
|
+
end
|
112
|
+
env[QUERY_PARAMS] = allowed_params(json_schema, params)
|
113
|
+
end
|
114
|
+
|
115
|
+
def allowed_params(json_schema, params)
|
116
|
+
json_schema['properties']
|
117
|
+
.keys
|
118
|
+
.each_with_object({}) do |parameter_name, filtered|
|
119
|
+
next unless params.key?(parameter_name)
|
120
|
+
|
121
|
+
filtered[parameter_name] = params[parameter_name]
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
def serialize_query_parameter_errors(validation_errors)
|
126
|
+
validation_errors.map do |error|
|
127
|
+
{
|
128
|
+
source: {
|
129
|
+
parameter: File.basename(error['data_pointer'])
|
130
|
+
}
|
131
|
+
}.update(ValidationFormat.error_details(error))
|
132
|
+
end
|
133
|
+
end
|
134
|
+
end
|
135
|
+
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.
|
4
|
+
version: 0.6.5
|
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-10-
|
11
|
+
date: 2019-10-30 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: json_schemer
|
@@ -161,11 +161,9 @@ files:
|
|
161
161
|
- lib/openapi_first/app.rb
|
162
162
|
- lib/openapi_first/coverage.rb
|
163
163
|
- lib/openapi_first/definition.rb
|
164
|
-
- lib/openapi_first/error_response_method.rb
|
165
164
|
- lib/openapi_first/operation_resolver.rb
|
166
|
-
- lib/openapi_first/
|
167
|
-
- lib/openapi_first/
|
168
|
-
- lib/openapi_first/request_body_validation.rb
|
165
|
+
- lib/openapi_first/query_parameters.rb
|
166
|
+
- lib/openapi_first/request_validation.rb
|
169
167
|
- lib/openapi_first/response_validator.rb
|
170
168
|
- lib/openapi_first/router.rb
|
171
169
|
- lib/openapi_first/validation.rb
|
@@ -1,20 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module OpenapiFirst
|
4
|
-
module ErrorResponseMethod
|
5
|
-
def default_error(status, title = Rack::Utils::HTTP_STATUS_CODES[status])
|
6
|
-
{
|
7
|
-
status: status.to_s,
|
8
|
-
title: title
|
9
|
-
}
|
10
|
-
end
|
11
|
-
|
12
|
-
def error_response(status, errors = [default_error(status)])
|
13
|
-
Rack::Response.new(
|
14
|
-
MultiJson.dump(errors: errors),
|
15
|
-
status,
|
16
|
-
Rack::CONTENT_TYPE => 'application/vnd.api+json'
|
17
|
-
).finish
|
18
|
-
end
|
19
|
-
end
|
20
|
-
end
|
@@ -1,29 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module OpenapiFirst
|
4
|
-
class QueryParameterSchemas
|
5
|
-
def initialize(allow_additional_parameters:)
|
6
|
-
@additional_properties = allow_additional_parameters
|
7
|
-
end
|
8
|
-
|
9
|
-
def find(operation)
|
10
|
-
build_parameter_schema(operation)
|
11
|
-
end
|
12
|
-
|
13
|
-
private
|
14
|
-
|
15
|
-
def build_parameter_schema(operation)
|
16
|
-
return unless operation&.query_parameters&.any?
|
17
|
-
|
18
|
-
operation.query_parameters.each_with_object(
|
19
|
-
'type' => 'object',
|
20
|
-
'required' => [],
|
21
|
-
'additionalProperties' => @additional_properties,
|
22
|
-
'properties' => {}
|
23
|
-
) do |parameter, schema|
|
24
|
-
schema['required'] << parameter.name if parameter.required
|
25
|
-
schema['properties'][parameter.name] = parameter.schema
|
26
|
-
end
|
27
|
-
end
|
28
|
-
end
|
29
|
-
end
|
@@ -1,56 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
require 'rack'
|
4
|
-
require 'json_schemer'
|
5
|
-
require 'multi_json'
|
6
|
-
require_relative 'validation_format'
|
7
|
-
require_relative 'error_response_method'
|
8
|
-
require_relative 'query_parameter_schemas'
|
9
|
-
|
10
|
-
module OpenapiFirst
|
11
|
-
class QueryParameterValidation
|
12
|
-
include ErrorResponseMethod
|
13
|
-
|
14
|
-
def initialize(app, allow_additional_parameters: false)
|
15
|
-
@app = app
|
16
|
-
@schemas = QueryParameterSchemas.new(
|
17
|
-
allow_additional_parameters: allow_additional_parameters
|
18
|
-
)
|
19
|
-
@additional_properties = allow_additional_parameters
|
20
|
-
end
|
21
|
-
|
22
|
-
def call(env)
|
23
|
-
req = Rack::Request.new(env)
|
24
|
-
operation = env[OpenapiFirst::OPERATION]
|
25
|
-
schema = operation && @schemas.find(operation)
|
26
|
-
if schema
|
27
|
-
params = req.params
|
28
|
-
errors = schema && JSONSchemer.schema(schema).validate(params)
|
29
|
-
return error_response(400, serialize_errors(errors)) if errors&.any?
|
30
|
-
|
31
|
-
req.env[QUERY_PARAMS] = allowed_query_parameters(schema, params)
|
32
|
-
end
|
33
|
-
|
34
|
-
@app.call(env)
|
35
|
-
end
|
36
|
-
|
37
|
-
def allowed_query_parameters(params_schema, query_params)
|
38
|
-
params_schema['properties']
|
39
|
-
.keys
|
40
|
-
.each_with_object({}) do |parameter_name, filtered|
|
41
|
-
value = query_params[parameter_name]
|
42
|
-
filtered[parameter_name] = value if value
|
43
|
-
end
|
44
|
-
end
|
45
|
-
|
46
|
-
def serialize_errors(validation_errors)
|
47
|
-
validation_errors.map do |error|
|
48
|
-
{
|
49
|
-
source: {
|
50
|
-
parameter: File.basename(error['data_pointer'])
|
51
|
-
}
|
52
|
-
}.update(ValidationFormat.error_details(error))
|
53
|
-
end
|
54
|
-
end
|
55
|
-
end
|
56
|
-
end
|
@@ -1,93 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
require 'rack'
|
4
|
-
require 'json_schemer'
|
5
|
-
require 'multi_json'
|
6
|
-
require_relative 'error_response_method'
|
7
|
-
require_relative 'validation_format'
|
8
|
-
|
9
|
-
module OpenapiFirst
|
10
|
-
class RequestBodyValidation
|
11
|
-
include ErrorResponseMethod
|
12
|
-
|
13
|
-
def initialize(app)
|
14
|
-
@app = app
|
15
|
-
end
|
16
|
-
|
17
|
-
def call(env) # rubocop:disable Metrics/MethodLength
|
18
|
-
operation = env[OpenapiFirst::OPERATION]
|
19
|
-
return @app.call(env) unless operation&.request_body
|
20
|
-
|
21
|
-
req = Rack::Request.new(env)
|
22
|
-
body = req.body.read
|
23
|
-
req.body.rewind
|
24
|
-
content_type = req.content_type
|
25
|
-
catch(:halt) do
|
26
|
-
validate_request_content_type!(content_type, operation)
|
27
|
-
validate_request_body_presence!(env, body, operation)
|
28
|
-
parse_and_validate_request_body!(env, content_type, body, operation)
|
29
|
-
@app.call(env)
|
30
|
-
end
|
31
|
-
end
|
32
|
-
|
33
|
-
def halt(response)
|
34
|
-
throw :halt, response
|
35
|
-
end
|
36
|
-
|
37
|
-
def validate_request_content_type!(content_type, operation)
|
38
|
-
return if content_type_valid?(content_type, operation)
|
39
|
-
|
40
|
-
halt(error_response(415))
|
41
|
-
end
|
42
|
-
|
43
|
-
def validate_request_body_presence!(env, body, operation)
|
44
|
-
return unless body.size.zero?
|
45
|
-
|
46
|
-
if operation.request_body.required
|
47
|
-
halt(error_response(415, 'Request body is required'))
|
48
|
-
end
|
49
|
-
halt(@app.call(env))
|
50
|
-
end
|
51
|
-
|
52
|
-
def parse_and_validate_request_body!(env, content_type, body, operation)
|
53
|
-
schema = request_body_schema(content_type, operation)
|
54
|
-
return unless schema
|
55
|
-
|
56
|
-
parsed_request_body = MultiJson.load(body)
|
57
|
-
errors = validate_json_schema(schema, parsed_request_body)
|
58
|
-
halt(error_response(400, serialize_errors(errors))) if errors&.any?
|
59
|
-
env[OpenapiFirst::REQUEST_BODY] = parsed_request_body
|
60
|
-
end
|
61
|
-
|
62
|
-
def validate_json_schema(schema, object)
|
63
|
-
JSONSchemer.schema(schema).validate(object)
|
64
|
-
end
|
65
|
-
|
66
|
-
def default_error(status, title = Rack::Utils::HTTP_STATUS_CODES[status])
|
67
|
-
{
|
68
|
-
status: status.to_s,
|
69
|
-
title: title
|
70
|
-
}
|
71
|
-
end
|
72
|
-
|
73
|
-
def content_type_valid?(content_type, endpoint)
|
74
|
-
endpoint.request_body.content[content_type]
|
75
|
-
end
|
76
|
-
|
77
|
-
def request_body_schema(content_type, endpoint)
|
78
|
-
return unless endpoint
|
79
|
-
|
80
|
-
endpoint.request_body.content[content_type]&.fetch('schema')
|
81
|
-
end
|
82
|
-
|
83
|
-
def serialize_errors(validation_errors)
|
84
|
-
validation_errors.map do |error|
|
85
|
-
{
|
86
|
-
source: {
|
87
|
-
pointer: error['data_pointer']
|
88
|
-
}
|
89
|
-
}.update(ValidationFormat.error_details(error))
|
90
|
-
end
|
91
|
-
end
|
92
|
-
end
|
93
|
-
end
|