openapi_first 0.7.0.alpha1 → 3.1.0.beta1

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.
Files changed (96) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +585 -2
  3. data/LICENSE.txt +21 -0
  4. data/README.md +360 -114
  5. data/lib/openapi_first/builder.rb +214 -0
  6. data/lib/openapi_first/child_configuration.rb +26 -0
  7. data/lib/openapi_first/configuration.rb +76 -0
  8. data/lib/openapi_first/definition.rb +98 -13
  9. data/lib/openapi_first/error_response.rb +40 -0
  10. data/lib/openapi_first/error_responses/default.rb +74 -0
  11. data/lib/openapi_first/error_responses/jsonapi.rb +60 -0
  12. data/lib/openapi_first/errors.rb +40 -0
  13. data/lib/openapi_first/failure.rb +77 -0
  14. data/lib/openapi_first/file_loader.rb +22 -0
  15. data/lib/openapi_first/header.rb +9 -0
  16. data/lib/openapi_first/json.rb +25 -0
  17. data/lib/openapi_first/middlewares/request_validation.rb +58 -0
  18. data/lib/openapi_first/middlewares/response_validation.rb +41 -0
  19. data/lib/openapi_first/parsed_request.rb +5 -0
  20. data/lib/openapi_first/ref_resolver.rb +193 -0
  21. data/lib/openapi_first/registry.rb +44 -0
  22. data/lib/openapi_first/request.rb +79 -0
  23. data/lib/openapi_first/request_body_parsers.rb +64 -0
  24. data/lib/openapi_first/request_validator.rb +32 -0
  25. data/lib/openapi_first/response.rb +43 -0
  26. data/lib/openapi_first/response_body_parsers.rb +31 -0
  27. data/lib/openapi_first/response_parser.rb +50 -0
  28. data/lib/openapi_first/response_validator.rb +16 -48
  29. data/lib/openapi_first/router/find_content.rb +17 -0
  30. data/lib/openapi_first/router/find_response.rb +53 -0
  31. data/lib/openapi_first/router/path_template.rb +48 -0
  32. data/lib/openapi_first/router.rb +84 -57
  33. data/lib/openapi_first/schema/hash.rb +39 -0
  34. data/lib/openapi_first/schema/validation_error.rb +45 -0
  35. data/lib/openapi_first/schema/validation_result.rb +30 -0
  36. data/lib/openapi_first/test/app.rb +30 -0
  37. data/lib/openapi_first/test/callable.rb +25 -0
  38. data/lib/openapi_first/test/configuration.rb +76 -0
  39. data/lib/openapi_first/test/coverage/covered_request.rb +11 -0
  40. data/lib/openapi_first/test/coverage/covered_response.rb +11 -0
  41. data/lib/openapi_first/test/coverage/plan.rb +77 -0
  42. data/lib/openapi_first/test/coverage/request_task.rb +42 -0
  43. data/lib/openapi_first/test/coverage/response_task.rb +42 -0
  44. data/lib/openapi_first/test/coverage/route_task.rb +13 -0
  45. data/lib/openapi_first/test/coverage/terminal_formatter.rb +115 -0
  46. data/lib/openapi_first/test/coverage/tracker.rb +32 -0
  47. data/lib/openapi_first/test/coverage.rb +102 -0
  48. data/lib/openapi_first/test/methods.rb +62 -0
  49. data/lib/openapi_first/test/minitest_helpers.rb +28 -0
  50. data/lib/openapi_first/test/observe.rb +37 -0
  51. data/lib/openapi_first/test/observer_middleware.rb +23 -0
  52. data/lib/openapi_first/test/plain_helpers.rb +31 -0
  53. data/lib/openapi_first/test.rb +171 -0
  54. data/lib/openapi_first/validated_request.rb +79 -0
  55. data/lib/openapi_first/validated_response.rb +53 -0
  56. data/lib/openapi_first/validators/request_body.rb +25 -0
  57. data/lib/openapi_first/validators/request_parameters.rb +49 -0
  58. data/lib/openapi_first/validators/response_body.rb +27 -0
  59. data/lib/openapi_first/validators/response_headers.rb +40 -0
  60. data/lib/openapi_first/version.rb +1 -1
  61. data/lib/openapi_first.rb +67 -35
  62. metadata +99 -133
  63. data/.github/CODEOWNERS +0 -1
  64. data/.gitignore +0 -11
  65. data/.rspec +0 -3
  66. data/.rubocop.yml +0 -14
  67. data/.travis.yml +0 -8
  68. data/Gemfile +0 -11
  69. data/Gemfile.lock +0 -126
  70. data/Rakefile +0 -15
  71. data/benchmarks/Gemfile +0 -13
  72. data/benchmarks/Gemfile.lock +0 -144
  73. data/benchmarks/apps/committee.ru +0 -30
  74. data/benchmarks/apps/grape.ru +0 -21
  75. data/benchmarks/apps/hanami_router.ru +0 -14
  76. data/benchmarks/apps/openapi.yaml +0 -86
  77. data/benchmarks/apps/openapi_first.ru +0 -22
  78. data/benchmarks/apps/openapi_first_resolve_only.ru +0 -23
  79. data/benchmarks/apps/sinatra.ru +0 -26
  80. data/benchmarks/apps/syro.ru +0 -25
  81. data/benchmarks/benchmarks.rb +0 -46
  82. data/bin/console +0 -15
  83. data/bin/setup +0 -8
  84. data/examples/README.md +0 -13
  85. data/examples/app.rb +0 -12
  86. data/examples/config.ru +0 -7
  87. data/examples/openapi.yaml +0 -29
  88. data/lib/openapi_first/app.rb +0 -27
  89. data/lib/openapi_first/coverage.rb +0 -28
  90. data/lib/openapi_first/operation.rb +0 -48
  91. data/lib/openapi_first/operation_resolver.rb +0 -43
  92. data/lib/openapi_first/request_validation.rb +0 -144
  93. data/lib/openapi_first/utils.rb +0 -31
  94. data/lib/openapi_first/validation.rb +0 -15
  95. data/lib/openapi_first/validation_format.rb +0 -31
  96. data/openapi_first.gemspec +0 -46
@@ -1,30 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'committee'
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
- use Committee::Middleware::RequestValidation,
27
- schema_path: './apps/openapi.yaml',
28
- coerce_date_times: false
29
-
30
- run app
@@ -1,21 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'grape'
4
-
5
- class GrapeExample < Grape::API
6
- format :json
7
-
8
- get :hello do
9
- [{ hello: 'world' }]
10
- end
11
-
12
- post :hello do
13
- { hello: 'world' }
14
- end
15
-
16
- get 'hello/:id' do
17
- { hello: 'world', id: params[:id] }
18
- end
19
- end
20
-
21
- run GrapeExample
@@ -1,14 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'hanami/router'
4
- require 'multi_json'
5
-
6
- app = Hanami::Router.new do
7
- get '/hello', to: ->(_env) { [200, {}, [MultiJson.dump(hello: 'world')]] }
8
- get '/hello/:id', to: lambda { |env|
9
- [200, {}, [MultiJson.dump(hello: 'world', id: env['router.params'][:id])]]
10
- }
11
- post '/hello', to: ->(_env) { [201, {}, [MultiJson.dump(hello: 'world')]] }
12
- end
13
-
14
- run app
@@ -1,86 +0,0 @@
1
- openapi: 3.0.0
2
- info:
3
- title: "API"
4
- version: "1.0.0"
5
- contact:
6
- name: Contact Name
7
- email: contact@example.com
8
- url: https://example.com/
9
- tags:
10
- - name: Metadata
11
- description: Metadata related requests
12
- paths:
13
- /hello/{id}:
14
- parameters:
15
- - name: id
16
- description: ID of the thing to get
17
- in: path
18
- required: true
19
- schema:
20
- type: string
21
- get:
22
- operationId: find_thing
23
- description: Get one thing
24
- tags: ["Metadata"]
25
- responses:
26
- "200":
27
- description: OK
28
- content:
29
- application/json:
30
- schema:
31
- type: array
32
- items:
33
- type: object
34
- required: [hello, id]
35
- properties:
36
- hello:
37
- type: string
38
- id:
39
- type: string
40
- /hello:
41
- get:
42
- operationId: find_things
43
- description: Get multiple things
44
- tags: ["Metadata"]
45
- parameters:
46
- - name: filter
47
- description: filter things
48
- in: query
49
- required: false
50
- schema:
51
- type: object
52
- required: [id]
53
- properties:
54
- id:
55
- type: string
56
- description: Comma separated list of thing-IDs
57
-
58
- responses:
59
- "200":
60
- description: OK
61
- content:
62
- application/json:
63
- schema:
64
- type: object
65
- required: [hello]
66
- properties:
67
- hello:
68
- type: string
69
- default:
70
- description: Error response
71
-
72
- post:
73
- operationId: create_thing
74
- description: Create a thing
75
- tags: ["Metadata"]
76
- responses:
77
- "201":
78
- description: OK
79
- content:
80
- application/json:
81
- schema:
82
- type: object
83
- required: [hello]
84
- properties:
85
- hello:
86
- type: string
@@ -1,22 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'multi_json'
4
- require 'openapi_first'
5
-
6
- namespace = Module.new do
7
- def self.find_thing(params, _res)
8
- { hello: 'world', id: params.fetch('id') }
9
- end
10
-
11
- def self.find_things(_params, _res)
12
- [{ hello: 'world' }]
13
- end
14
-
15
- def self.create_thing(_params, res)
16
- res.status = 201
17
- { hello: 'world' }
18
- end
19
- end
20
-
21
- oas_path = File.absolute_path('./openapi.yaml', __dir__)
22
- run OpenapiFirst.app(oas_path, namespace: namespace)
@@ -1,23 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'multi_json'
4
- require 'openapi_first'
5
-
6
- namespace = Module.new do
7
- def self.find_thing(params, _res)
8
- { hello: 'world', id: params.fetch('id') }
9
- end
10
-
11
- def self.find_things(_params, _res)
12
- [{ hello: 'world' }]
13
- end
14
-
15
- def self.create_thing(_params, res)
16
- res.status = 201
17
- { hello: 'world' }
18
- end
19
- end
20
-
21
- spec = OpenapiFirst.load(File.absolute_path('./openapi.yaml', __dir__))
22
- use OpenapiFirst::Router, spec: spec, namespace: namespace
23
- run OpenapiFirst::OperationResolver.new
@@ -1,26 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'multi_json'
4
- require 'sinatra/base'
5
-
6
- class SinatraExample < Sinatra::Base
7
- set :environment, :production
8
-
9
- get '/hello/:id' do
10
- content_type :json
11
- MultiJson.dump(hello: 'world', id: params.fetch('id'))
12
- end
13
-
14
- get '/hello' do
15
- content_type :json
16
- [MultiJson.dump(hello: 'world')]
17
- end
18
-
19
- post '/hello' do
20
- content_type :json
21
- status 201
22
- MultiJson.dump(hello: 'world')
23
- end
24
- end
25
-
26
- run SinatraExample
@@ -1,25 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'syro'
4
- require 'multi_json'
5
-
6
- app = Syro.new do
7
- on 'hello' do
8
- on :id do
9
- get do
10
- res.json MultiJson.dump(hello: 'world', id: inbox[:id])
11
- end
12
- end
13
-
14
- get do
15
- res.json [MultiJson.dump(hello: 'world')]
16
- end
17
-
18
- post do
19
- res.status = 201
20
- res.json MultiJson.dump(hello: 'world')
21
- end
22
- end
23
- end
24
-
25
- run app
@@ -1,46 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'benchmark/ips'
4
- require 'benchmark/memory'
5
- require 'rack'
6
- ENV['RACK_ENV'] = 'production'
7
-
8
- examples = [
9
- [Rack::MockRequest.env_for('/hello'), 200],
10
- [Rack::MockRequest.env_for('/unknown'), 404],
11
- [Rack::MockRequest.env_for('/hello', method: 'POST'), 201],
12
- [Rack::MockRequest.env_for('/hello/1'), 200],
13
- [Rack::MockRequest.env_for('/hello/123'), 200],
14
- [Rack::MockRequest.env_for('/hello?filter[id]=1,2'), 200]
15
- ]
16
-
17
- apps = Dir['./apps/*.ru'].each_with_object({}) do |config, hash|
18
- hash[config] = Rack::Builder.parse_file(config).first
19
- end
20
- apps.freeze
21
-
22
- Benchmark.ips do |x|
23
- apps.each do |config, app|
24
- x.report(config) do
25
- examples.each do |example|
26
- env, expected_status = example
27
- response = app.call(env)
28
- raise unless response[0] == expected_status
29
- end
30
- end
31
- end
32
- x.compare!
33
- end
34
-
35
- Benchmark.memory do |x|
36
- apps.each do |config, app|
37
- x.report(config) do
38
- examples.each do |example|
39
- env, expected_status = example
40
- response = app.call(env)
41
- raise unless response[0] == expected_status
42
- end
43
- end
44
- end
45
- x.compare!
46
- end
data/bin/console DELETED
@@ -1,15 +0,0 @@
1
- #!/usr/bin/env ruby
2
- # frozen_string_literal: true
3
-
4
- require 'bundler/setup'
5
- require 'openapi_first'
6
-
7
- # You can add fixtures and/or initialization code here to make experimenting
8
- # with your gem easier. You can also use a different console, if you like.
9
-
10
- # (If you use this, don't forget to add pry to your Gemfile!)
11
- # require "pry"
12
- # Pry.start
13
-
14
- require 'irb'
15
- IRB.start(__FILE__)
data/bin/setup DELETED
@@ -1,8 +0,0 @@
1
- #!/usr/bin/env bash
2
- set -euo pipefail
3
- IFS=$'\n\t'
4
- set -vx
5
-
6
- bundle install
7
-
8
- # Do any other automated setup that you need to do here
data/examples/README.md DELETED
@@ -1,13 +0,0 @@
1
- # Example
2
-
3
- How to run the example:
4
-
5
- ```bash
6
- cd examples
7
- bundle install
8
- bundle exec rackup
9
- ```
10
-
11
- open http://localhost:9292/
12
-
13
- 🎉
data/examples/app.rb DELETED
@@ -1,12 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'openapi_first'
4
-
5
- module Example
6
- def self.find_thing(_params, _res)
7
- { hello: 'world' }
8
- end
9
- end
10
-
11
- oas_path = File.absolute_path('./openapi.yaml', __dir__)
12
- App = OpenapiFirst.app(oas_path, namespace: Example)
data/examples/config.ru DELETED
@@ -1,7 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- lib = File.expand_path('../lib', __dir__)
4
- $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
5
-
6
- require_relative 'app'
7
- run App
@@ -1,29 +0,0 @@
1
- openapi: 3.0.0
2
- info:
3
- title: "API"
4
- version: "1.0.0"
5
- contact:
6
- name: Contact Name
7
- email: contact@example.com
8
- url: https://example.com/
9
- tags:
10
- - name: Metadata
11
- description: Metadata related requests
12
- paths:
13
- /:
14
- get:
15
- operationId: find_thing
16
- summary: Get metadata from the root of the API
17
- tags: ["Metadata"]
18
- responses:
19
- "200":
20
- description: OK
21
- content:
22
- application/json:
23
- schema:
24
- type: object
25
- required: [hello]
26
- properties:
27
- hello:
28
- type: string
29
-
@@ -1,27 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'rack'
4
-
5
- module OpenapiFirst
6
- class App
7
- def initialize(
8
- parent_app,
9
- spec,
10
- namespace:
11
- )
12
- @stack = Rack::Builder.app do
13
- freeze_app
14
- use OpenapiFirst::Router,
15
- spec: spec,
16
- namespace: namespace,
17
- parent_app: parent_app
18
- use OpenapiFirst::RequestValidation
19
- run OpenapiFirst::OperationResolver.new
20
- end
21
- end
22
-
23
- def call(env)
24
- @stack.call(env)
25
- end
26
- end
27
- end
@@ -1,28 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module OpenapiFirst
4
- class Coverage
5
- attr_reader :to_be_called
6
-
7
- def initialize(app, spec)
8
- @app = app
9
- @spec = spec
10
- @to_be_called = spec.operations.map do |operation|
11
- endpoint_id(operation)
12
- end
13
- end
14
-
15
- def call(env)
16
- response = @app.call(env)
17
- operation = env[OPERATION]
18
- @to_be_called.delete(endpoint_id(operation)) if operation
19
- response
20
- end
21
-
22
- private
23
-
24
- def endpoint_id(operation)
25
- "#{operation.path}##{operation.method}"
26
- end
27
- end
28
- end
@@ -1,48 +0,0 @@
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
@@ -1,43 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'rack'
4
-
5
- module OpenapiFirst
6
- class OperationResolver
7
- def call(env)
8
- operation = env[OpenapiFirst::OPERATION]
9
- res = Rack::Response.new
10
- params = build_params(env)
11
- handler = env[HANDLER]
12
- result = handler.call(params, res)
13
- res.write serialize(result) if result && res.body.empty?
14
- res[Rack::CONTENT_TYPE] ||= operation.content_type_for(res.status)
15
- res.finish
16
- end
17
-
18
- private
19
-
20
- def serialize(result)
21
- return result if result.is_a?(String)
22
-
23
- MultiJson.dump(result)
24
- end
25
-
26
- def build_params(env)
27
- sources = [
28
- env[PARAMS],
29
- env[REQUEST_BODY]
30
- ].tap(&:compact!)
31
- Params.new(env).merge!(*sources)
32
- end
33
- end
34
-
35
- class Params < Hash
36
- attr_reader :env
37
-
38
- def initialize(env)
39
- @env = env
40
- super
41
- end
42
- end
43
- end
@@ -1,144 +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
-
8
- module OpenapiFirst
9
- class RequestValidation # rubocop:disable Metrics/ClassLength
10
- def initialize(app, _options = {})
11
- @app = app
12
- end
13
-
14
- def call(env) # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
15
- operation = env[OpenapiFirst::OPERATION]
16
- return @app.call(env) unless operation
17
-
18
- catch(:halt) do
19
- validate_query_parameters!(env, operation, env[PARAMS])
20
- req = Rack::Request.new(env)
21
- content_type = req.content_type
22
- return @app.call(env) unless operation.request_body
23
-
24
- validate_request_content_type!(content_type, operation)
25
- body = req.body.read
26
- req.body.rewind
27
- parse_and_validate_request_body!(env, content_type, body, operation)
28
- @app.call(env)
29
- end
30
- end
31
-
32
- private
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, operation)
85
- return unless operation
86
-
87
- operation.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 = operation.parameters_json_schema
102
- return unless json_schema
103
-
104
- params = filtered_params(json_schema, params)
105
- errors = JSONSchemer.schema(json_schema).validate(params)
106
- if errors.any?
107
- halt error_response(400, serialize_query_parameter_errors(errors))
108
- end
109
- env[PARAMS] = params
110
- end
111
-
112
- def filtered_params(json_schema, params)
113
- json_schema['properties']
114
- .each_with_object({}) do |key_value, result|
115
- parameter_name, schema = key_value
116
- next unless params.key?(parameter_name)
117
-
118
- value = params[parameter_name]
119
- result[parameter_name] = parse_parameter(value, schema)
120
- end
121
- end
122
-
123
- def serialize_query_parameter_errors(validation_errors)
124
- validation_errors.map do |error|
125
- {
126
- source: { parameter: File.basename(error['data_pointer']) }
127
- }.update(ValidationFormat.error_details(error))
128
- end
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
143
- end
144
- end