stub_requests 0.1.9 → 0.1.10
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.reek.yml +5 -9
- data/.rubocop.yml +1 -0
- data/CHANGELOG.md +23 -0
- data/README.md +9 -9
- data/Rakefile +7 -5
- data/lib/stub_requests.rb +30 -16
- data/lib/stub_requests/api.rb +45 -26
- data/lib/stub_requests/callback.rb +3 -1
- data/lib/stub_requests/callback_registry.rb +9 -55
- data/lib/stub_requests/concerns/argument_validation.rb +47 -0
- data/lib/stub_requests/concerns/property.rb +114 -0
- data/lib/stub_requests/concerns/property/validator.rb +137 -0
- data/lib/stub_requests/concerns/register_verb.rb +110 -0
- data/lib/stub_requests/configuration.rb +19 -2
- data/lib/stub_requests/dsl.rb +5 -6
- data/lib/stub_requests/dsl/method_definition.rb +2 -8
- data/lib/stub_requests/endpoint.rb +22 -23
- data/lib/stub_requests/endpoint_registry.rb +157 -0
- data/lib/stub_requests/exceptions.rb +28 -10
- data/lib/stub_requests/request_stub.rb +29 -14
- data/lib/stub_requests/service.rb +55 -7
- data/lib/stub_requests/service_registry.rb +30 -79
- data/lib/stub_requests/stub_registry.rb +22 -80
- data/lib/stub_requests/stub_requests.rb +8 -5
- data/lib/stub_requests/uri.rb +0 -17
- data/lib/stub_requests/utils/fuzzy.rb +70 -0
- data/lib/stub_requests/version.rb +1 -1
- data/lib/stub_requests/webmock/builder.rb +9 -51
- data/lib/stub_requests/webmock/stub_registry_extension.rb +1 -1
- data/lib/tasks/changelog.rake +1 -7
- data/stub_requests.gemspec +1 -0
- data/update_docs.sh +2 -2
- metadata +27 -8
- data/lib/stub_requests/argument_validation.rb +0 -48
- data/lib/stub_requests/endpoint_stub.rb +0 -89
- data/lib/stub_requests/endpoints.rb +0 -246
- data/lib/stub_requests/hash_util.rb +0 -32
- data/lib/stub_requests/observable.rb +0 -18
- data/lib/stub_requests/property.rb +0 -112
- data/lib/stub_requests/property/validator.rb +0 -135
@@ -13,79 +13,42 @@ module StubRequests
|
|
13
13
|
# @author Mikael Henriksson <mikael@zoolutions.se>
|
14
14
|
#
|
15
15
|
class ServiceRegistry
|
16
|
+
# extend "Forwardable"
|
17
|
+
# @!parse extend Forwardable
|
18
|
+
extend Forwardable
|
19
|
+
|
20
|
+
# includes "Singleton"
|
21
|
+
# @!parse include Singleton
|
16
22
|
include Singleton
|
23
|
+
# includes "Enumerable"
|
24
|
+
# @!parse include Enumerable
|
17
25
|
include Enumerable
|
18
26
|
|
19
|
-
|
20
|
-
|
27
|
+
delegate [:each, :[], :[]=, :keys, :delete] => :services
|
28
|
+
|
21
29
|
#
|
22
|
-
#
|
23
|
-
#
|
30
|
+
# @!attribute [rw] services
|
31
|
+
# @return [Concurrent::Map<Symbol, Service>] a map with services
|
32
|
+
attr_reader :services
|
33
|
+
|
24
34
|
#
|
25
|
-
#
|
26
|
-
# register_service(:documents, "https://company.com/api/v1") do
|
27
|
-
# get "documents/:id", as: :show
|
28
|
-
# get "documents", as: :index
|
29
|
-
# post "documents", as: :create
|
30
|
-
# patch "documents/:id", as: :update
|
31
|
-
# delete "documents/:id", as: :destroy
|
32
|
-
# end
|
35
|
+
# Initialize a new instance (used by Singleton)
|
33
36
|
#
|
34
|
-
# @return [Service] a new service or a previously registered service
|
35
37
|
#
|
36
|
-
def
|
37
|
-
|
38
|
-
Docile.dsl_eval(service.endpoints, &block) if block.present?
|
39
|
-
service
|
38
|
+
def initialize
|
39
|
+
@services = Concurrent::Map.new
|
40
40
|
end
|
41
41
|
|
42
42
|
#
|
43
|
-
#
|
43
|
+
# Returns the size of the registry
|
44
44
|
#
|
45
45
|
#
|
46
|
-
# @
|
47
|
-
# @param [Symbol] endpoint_id the id of a registered endpoint
|
48
|
-
# @param [Hash<Symbol>] route_params a map with route parameters
|
46
|
+
# @return [Integer]
|
49
47
|
#
|
50
|
-
|
51
|
-
|
52
|
-
# @example Stub a request to a registered service endpoint
|
53
|
-
# stub_endpoint(:google_api, :get_map_location)
|
54
|
-
# .to_return(body: "No content", status: 204)
|
55
|
-
#
|
56
|
-
# @example Stub a request to a registered service endpoint using block
|
57
|
-
# stub_endpoint(:documents, :index) do
|
58
|
-
# with(headers: { "Accept" => "application/json" }}})
|
59
|
-
# to_return(body: "No content", status: 204)
|
60
|
-
# end
|
61
|
-
#
|
62
|
-
# @return [WebMock::RequestStub] a mocked request
|
63
|
-
#
|
64
|
-
def self.stub_endpoint(service_id, endpoint_id, route_params = {}, &callback)
|
65
|
-
service, endpoint, uri = StubRequests::URI.for_service_endpoint(service_id, endpoint_id, route_params)
|
66
|
-
webmock_stub = WebMock::Builder.build(endpoint.verb, uri, {}, &callback)
|
67
|
-
|
68
|
-
StubRegistry.record(service, endpoint, webmock_stub)
|
69
|
-
::WebMock::StubRegistry.instance.register_request_stub(webmock_stub)
|
70
|
-
end
|
71
|
-
|
72
|
-
# @api private
|
73
|
-
# Used only for testing purposes
|
74
|
-
def self.__stub_endpoint(service_id, endpoint_id, route_params = {})
|
75
|
-
_service, endpoint, uri = StubRequests::URI.for_service_endpoint(service_id, endpoint_id, route_params)
|
76
|
-
endpoint_stub = WebMock::Builder.build(endpoint.verb, uri)
|
77
|
-
|
78
|
-
::WebMock::StubRegistry.instance.register_request_stub(endpoint_stub)
|
79
|
-
end
|
80
|
-
|
81
|
-
#
|
82
|
-
# @!attribute [rw] services
|
83
|
-
# @return [Concurrent::Map<Symbol, Service>] a map with services
|
84
|
-
attr_reader :services
|
85
|
-
|
86
|
-
def initialize
|
87
|
-
@services = Concurrent::Map.new
|
48
|
+
def size
|
49
|
+
keys.size
|
88
50
|
end
|
51
|
+
alias count size
|
89
52
|
|
90
53
|
#
|
91
54
|
# Resets the map with registered services
|
@@ -96,18 +59,6 @@ module StubRequests
|
|
96
59
|
services.clear
|
97
60
|
end
|
98
61
|
|
99
|
-
#
|
100
|
-
# Required by Enumerable
|
101
|
-
#
|
102
|
-
#
|
103
|
-
# @return [Concurrent::Map<Symbol, Service>] an map with services
|
104
|
-
#
|
105
|
-
# @yield used by Enumerable
|
106
|
-
#
|
107
|
-
def each(&block)
|
108
|
-
services.each(&block)
|
109
|
-
end
|
110
|
-
|
111
62
|
#
|
112
63
|
# Registers a service in the registry
|
113
64
|
#
|
@@ -118,11 +69,11 @@ module StubRequests
|
|
118
69
|
# @return [Service] the service that was just registered
|
119
70
|
#
|
120
71
|
def register(service_id, service_uri)
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
72
|
+
service = Service.new(service_id, service_uri)
|
73
|
+
StubRequests.logger.warn("Service already registered #{service}") if self[service_id]
|
74
|
+
|
75
|
+
self[service_id] = service
|
76
|
+
service
|
126
77
|
end
|
127
78
|
|
128
79
|
#
|
@@ -134,7 +85,7 @@ module StubRequests
|
|
134
85
|
# @raise [ServiceNotFound] when the service was not removed
|
135
86
|
#
|
136
87
|
def remove(service_id)
|
137
|
-
|
88
|
+
delete(service_id) || raise(ServiceNotFound, service_id)
|
138
89
|
end
|
139
90
|
|
140
91
|
#
|
@@ -146,7 +97,7 @@ module StubRequests
|
|
146
97
|
# @return [Service] the found service
|
147
98
|
#
|
148
99
|
def find(service_id)
|
149
|
-
|
100
|
+
self[service_id]
|
150
101
|
end
|
151
102
|
|
152
103
|
#
|
@@ -160,7 +111,7 @@ module StubRequests
|
|
160
111
|
# @return [Service]
|
161
112
|
#
|
162
113
|
def find!(service_id)
|
163
|
-
|
114
|
+
self[service_id] || raise(ServiceNotFound, service_id)
|
164
115
|
end
|
165
116
|
end
|
166
117
|
end
|
@@ -15,6 +15,10 @@ module StubRequests
|
|
15
15
|
# @since 0.1.2
|
16
16
|
#
|
17
17
|
class StubRegistry
|
18
|
+
# extend "Forwardable"
|
19
|
+
# @!parse extend Forwardable
|
20
|
+
extend Forwardable
|
21
|
+
|
18
22
|
# includes "Singleton"
|
19
23
|
# @!parse include Singleton
|
20
24
|
include Singleton
|
@@ -22,50 +26,19 @@ module StubRequests
|
|
22
26
|
# @!parse include Enumerable
|
23
27
|
include Enumerable
|
24
28
|
|
25
|
-
|
26
|
-
# Records metrics about stubbed endpoints
|
27
|
-
#
|
28
|
-
#
|
29
|
-
# @param [Service] service a Service
|
30
|
-
# @param [Endpoint] endpoint an Endpoint
|
31
|
-
# @param [WebMock::RequestStub] webmock_stub the stubbed webmock request
|
32
|
-
#
|
33
|
-
# @note the class method of record validates that
|
34
|
-
# configuration option :collect_metrics is true.
|
35
|
-
#
|
36
|
-
# @return [EndpointStub] the stub that was recorded
|
37
|
-
#
|
38
|
-
def self.record(service, endpoint, webmock_stub)
|
39
|
-
# Note: The class method v
|
40
|
-
return unless StubRequests.config.record_metrics?
|
41
|
-
|
42
|
-
instance.record(service, endpoint, webmock_stub)
|
43
|
-
end
|
29
|
+
delegate [:each, :concat] => :stubs
|
44
30
|
|
45
31
|
#
|
46
|
-
#
|
47
|
-
#
|
48
|
-
|
49
|
-
#
|
50
|
-
# @param [WebMock::RequestStub] webmock_stub the stubbed webmock request
|
51
|
-
#
|
52
|
-
# @return [void]
|
53
|
-
#
|
54
|
-
def self.mark_as_responded(webmock_stub)
|
55
|
-
instance.mark_as_responded(webmock_stub)
|
56
|
-
end
|
57
|
-
|
58
|
-
#
|
59
|
-
# @!attribute [rw] services
|
60
|
-
# @return [Concurrent::Array<Endpoint>] a map with stubbed endpoints
|
61
|
-
attr_reader :endpoints
|
32
|
+
# @!attribute [r] stubs
|
33
|
+
# @return [Concurrent::Array] a collection of {RequestStub}
|
34
|
+
attr_reader :stubs
|
62
35
|
|
63
36
|
#
|
64
37
|
# Initialize a new registry
|
65
38
|
#
|
66
39
|
#
|
67
40
|
def initialize
|
68
|
-
|
41
|
+
reset
|
69
42
|
end
|
70
43
|
|
71
44
|
#
|
@@ -74,37 +47,22 @@ module StubRequests
|
|
74
47
|
#
|
75
48
|
# @api private
|
76
49
|
def reset
|
77
|
-
|
78
|
-
end
|
79
|
-
|
80
|
-
#
|
81
|
-
# Required by Enumerable
|
82
|
-
#
|
83
|
-
#
|
84
|
-
# @return [Concurrent::Array<Endpoint>] an array with stubbed endpoints
|
85
|
-
#
|
86
|
-
# @yield used by Enumerable
|
87
|
-
#
|
88
|
-
def each(&block)
|
89
|
-
endpoints.each(&block)
|
50
|
+
@stubs = Concurrent::Array.new
|
90
51
|
end
|
91
52
|
|
92
53
|
#
|
93
|
-
# Records
|
54
|
+
# Records a WebMock::RequestStub as stubbed
|
94
55
|
#
|
56
|
+
# @param [WebMock::RequestStub] webmock_stub <description>
|
95
57
|
#
|
96
|
-
# @
|
97
|
-
# @param [Endpoint] endpoint a string with a base_uri to the service
|
98
|
-
# @param [WebMock::RequestStub] webmock_stub the stubbed request
|
58
|
+
# @return [RequestStub]
|
99
59
|
#
|
100
|
-
|
101
|
-
|
102
|
-
def record(service, endpoint, webmock_stub)
|
103
|
-
endpoint_stub = find_or_initialize_endpoint_stub(service, endpoint)
|
104
|
-
endpoint_stub.record(webmock_stub)
|
60
|
+
def record(endpoint_id, webmock_stub)
|
61
|
+
return unless StubRequests.config.record_stubs?
|
105
62
|
|
106
|
-
|
107
|
-
|
63
|
+
request_stub = RequestStub.new(endpoint_id, webmock_stub)
|
64
|
+
concat([request_stub])
|
65
|
+
request_stub
|
108
66
|
end
|
109
67
|
|
110
68
|
#
|
@@ -117,10 +75,10 @@ module StubRequests
|
|
117
75
|
# @return [void]
|
118
76
|
#
|
119
77
|
def mark_as_responded(webmock_stub)
|
120
|
-
return unless (request_stub =
|
78
|
+
return unless (request_stub = find_by_webmock_stub(webmock_stub))
|
121
79
|
|
122
80
|
request_stub.mark_as_responded
|
123
|
-
CallbackRegistry.invoke_callbacks(request_stub)
|
81
|
+
CallbackRegistry.instance.invoke_callbacks(request_stub)
|
124
82
|
request_stub
|
125
83
|
end
|
126
84
|
|
@@ -132,24 +90,8 @@ module StubRequests
|
|
132
90
|
#
|
133
91
|
# @return [RequestStub] the request_stubbed matching the request stub
|
134
92
|
#
|
135
|
-
def
|
136
|
-
|
137
|
-
endpoint.find_by(attribute: :request_stub, value: webmock_stub)
|
138
|
-
end.compact.first
|
139
|
-
end
|
140
|
-
|
141
|
-
private
|
142
|
-
|
143
|
-
def find_or_initialize_endpoint_stub(service, endpoint)
|
144
|
-
find_endpoint_stub(service, endpoint) || initialize_endpoint_stub(service, endpoint)
|
145
|
-
end
|
146
|
-
|
147
|
-
def find_endpoint_stub(service, endpoint)
|
148
|
-
find { |ep| ep.service_id == service.id && ep.endpoint_id == endpoint.id }
|
149
|
-
end
|
150
|
-
|
151
|
-
def initialize_endpoint_stub(service, endpoint)
|
152
|
-
EndpointStub.new(service, endpoint)
|
93
|
+
def find_by_webmock_stub(webmock_stub)
|
94
|
+
find { |stub| stub.webmock_stub == webmock_stub }
|
153
95
|
end
|
154
96
|
end
|
155
97
|
end
|
@@ -22,11 +22,6 @@ module StubRequests
|
|
22
22
|
# @!parse extend API
|
23
23
|
include API
|
24
24
|
|
25
|
-
#
|
26
|
-
# @!attribute [rw] logger
|
27
|
-
# @return [Logger] the logger to use in the gem
|
28
|
-
attr_accessor :logger
|
29
|
-
|
30
25
|
#
|
31
26
|
# Allows the gem to be configured
|
32
27
|
#
|
@@ -50,6 +45,14 @@ module StubRequests
|
|
50
45
|
@config ||= Configuration.new
|
51
46
|
end
|
52
47
|
|
48
|
+
def logger
|
49
|
+
config.logger
|
50
|
+
end
|
51
|
+
|
52
|
+
def logger=(obj)
|
53
|
+
config.logger = obj
|
54
|
+
end
|
55
|
+
|
53
56
|
#
|
54
57
|
# The current version of the gem
|
55
58
|
#
|
data/lib/stub_requests/uri.rb
CHANGED
@@ -40,22 +40,5 @@ module StubRequests
|
|
40
40
|
def self.safe_join(host, path)
|
41
41
|
[host.chomp("/"), path.sub(%r{\A/}, "")].join("/")
|
42
42
|
end
|
43
|
-
|
44
|
-
#
|
45
|
-
# UtilityFunction to construct the full URI for a service endpoint
|
46
|
-
#
|
47
|
-
# @param [Symbol] service_id the id of a service
|
48
|
-
# @param [Symbol] endpoint_id the id of an endpoint
|
49
|
-
# @param [Hash<Symbol>] route_params hash with route_params
|
50
|
-
#
|
51
|
-
# @return [Array<Service, Endpoint, String] the service, endpoint and full URI
|
52
|
-
#
|
53
|
-
def self.for_service_endpoint(service_id, endpoint_id, route_params)
|
54
|
-
service = ServiceRegistry.instance.find!(service_id)
|
55
|
-
endpoint = service.endpoints.find!(endpoint_id)
|
56
|
-
uri = URI::Builder.build(service.uri, endpoint.path, route_params)
|
57
|
-
|
58
|
-
[service, endpoint, uri]
|
59
|
-
end
|
60
43
|
end
|
61
44
|
end
|
@@ -0,0 +1,70 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
#
|
4
|
+
# Abstraction over WebMock to reduce duplication
|
5
|
+
#
|
6
|
+
# @author Mikael Henriksson <mikael@zoolutions.se>
|
7
|
+
# @since 0.1.0
|
8
|
+
#
|
9
|
+
module StubRequests
|
10
|
+
#
|
11
|
+
# Module Utils provides a namespace for utility module
|
12
|
+
#
|
13
|
+
# @author Mikael Henriksson <mikael@zoolutions.se>
|
14
|
+
#
|
15
|
+
module Utils
|
16
|
+
#
|
17
|
+
# Provides convenience methods for hashes
|
18
|
+
#
|
19
|
+
# @author Mikael Henriksson <mikael@zoolutions.se>
|
20
|
+
#
|
21
|
+
module Fuzzy
|
22
|
+
#
|
23
|
+
# @return [Regexp] a pattern excluding all except alphanumeric
|
24
|
+
FILTER_REGEX = /(^\w\d)/.freeze
|
25
|
+
#
|
26
|
+
# Find strings that are similar
|
27
|
+
#
|
28
|
+
#
|
29
|
+
# @param [String] original a string to match
|
30
|
+
# @param [Array<String>] others an array of string to search
|
31
|
+
#
|
32
|
+
# @return [Array] Returns
|
33
|
+
#
|
34
|
+
def self.match(original, others)
|
35
|
+
matches = compute_distances(original, others).sort.reverse
|
36
|
+
filter_matches(matches.to_h)
|
37
|
+
end
|
38
|
+
|
39
|
+
# :nodoc:
|
40
|
+
def self.filter_matches(matches)
|
41
|
+
suggestions = matches.values
|
42
|
+
return suggestions if suggestions.size <= 3
|
43
|
+
|
44
|
+
matches.select { |distance, _| distance >= 0.7 }
|
45
|
+
.values
|
46
|
+
end
|
47
|
+
|
48
|
+
# :nodoc:
|
49
|
+
def self.compute_distances(original, others)
|
50
|
+
others.each_with_object([]) do |other, memo|
|
51
|
+
memo << [jaro_distance(original, other), other]
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
# :nodoc:
|
56
|
+
def self.jaro_distance(original, other)
|
57
|
+
JaroWinkler.jaro_distance(
|
58
|
+
normalize_string(original),
|
59
|
+
normalize_string(other),
|
60
|
+
StubRequests.config.jaro_options,
|
61
|
+
)
|
62
|
+
end
|
63
|
+
|
64
|
+
# :nodoc:
|
65
|
+
def self.normalize_string(value)
|
66
|
+
value.to_s.gsub(FILTER_REGEX, "")
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
@@ -21,32 +21,25 @@ module StubRequests
|
|
21
21
|
# @since 0.1.2
|
22
22
|
#
|
23
23
|
class Builder
|
24
|
-
include HashUtil
|
25
|
-
|
26
24
|
#
|
27
25
|
# Builds and registers a WebMock::RequestStub
|
28
26
|
#
|
29
27
|
#
|
30
28
|
# @param [Symbol] verb a HTTP verb/method
|
31
29
|
# @param [String] uri a URI to call
|
32
|
-
# @param [Hash<Symbol>] options request/response options for Webmock::RequestStub
|
33
30
|
#
|
34
31
|
# @yield a callback to eventually yield to the caller
|
35
32
|
#
|
36
|
-
# @return [WebMock::RequestStub]
|
33
|
+
# @return [WebMock::RequestStub]
|
37
34
|
#
|
38
|
-
def self.build(verb, uri,
|
39
|
-
new(verb, uri,
|
35
|
+
def self.build(verb, uri, &callback)
|
36
|
+
new(verb, uri, &callback).build
|
40
37
|
end
|
41
38
|
|
42
39
|
#
|
43
|
-
# @!attribute [r]
|
40
|
+
# @!attribute [r] webmock_stub
|
44
41
|
# @return [WebMock::RequestStub] a stubbed webmock request
|
45
|
-
attr_reader :
|
46
|
-
#
|
47
|
-
# @!attribute [r] options
|
48
|
-
# @return [Hash<Symbol>] options for the stub_request
|
49
|
-
attr_reader :options
|
42
|
+
attr_reader :webmock_stub
|
50
43
|
#
|
51
44
|
# @!attribute [r] callback
|
52
45
|
# @return [Block] call back when given a block
|
@@ -58,13 +51,11 @@ module StubRequests
|
|
58
51
|
#
|
59
52
|
# @param [Symbol] verb a HTTP verb/method
|
60
53
|
# @param [String] uri a URI to call
|
61
|
-
# @param [Hash<Symbol>] options request/response options for Webmock::RequestStub
|
62
54
|
#
|
63
55
|
# @yield a block to eventually yield to the caller
|
64
56
|
#
|
65
|
-
def initialize(verb, uri,
|
66
|
-
@
|
67
|
-
@options = options
|
57
|
+
def initialize(verb, uri, &callback)
|
58
|
+
@webmock_stub = ::WebMock::RequestStub.new(verb, uri)
|
68
59
|
@callback = callback
|
69
60
|
end
|
70
61
|
|
@@ -75,41 +66,8 @@ module StubRequests
|
|
75
66
|
# @return [WebMock::RequestStub] the registered stub
|
76
67
|
#
|
77
68
|
def build
|
78
|
-
if callback.present?
|
79
|
-
|
80
|
-
else
|
81
|
-
prepare_mock_request
|
82
|
-
end
|
83
|
-
|
84
|
-
request_stub
|
85
|
-
end
|
86
|
-
|
87
|
-
private
|
88
|
-
|
89
|
-
def prepare_mock_request
|
90
|
-
prepare_with
|
91
|
-
prepare_to_return
|
92
|
-
prepare_to_raise
|
93
|
-
request_stub.to_timeout if options[:timeout]
|
94
|
-
request_stub
|
95
|
-
end
|
96
|
-
|
97
|
-
def prepare_with
|
98
|
-
HashUtil.compact(options[:request]) do |request_options|
|
99
|
-
request_stub.with(request_options)
|
100
|
-
end
|
101
|
-
end
|
102
|
-
|
103
|
-
def prepare_to_return
|
104
|
-
HashUtil.compact(options[:response]) do |response_options|
|
105
|
-
request_stub.to_return(response_options)
|
106
|
-
end
|
107
|
-
end
|
108
|
-
|
109
|
-
def prepare_to_raise
|
110
|
-
return unless (error = options[:error])
|
111
|
-
|
112
|
-
request_stub.to_raise(*Array(error))
|
69
|
+
Docile.dsl_eval(webmock_stub, &callback) if callback.present?
|
70
|
+
webmock_stub
|
113
71
|
end
|
114
72
|
end
|
115
73
|
end
|