acfs 1.3.0 → 1.4.0
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 +50 -0
- data/README.md +3 -4
- data/acfs.gemspec +19 -10
- data/lib/acfs.rb +2 -0
- data/lib/acfs/adapter/base.rb +6 -8
- data/lib/acfs/adapter/typhoeus.rb +26 -7
- data/lib/acfs/collection.rb +2 -1
- data/lib/acfs/collections/paginatable.rb +2 -1
- data/lib/acfs/configuration.rb +14 -7
- data/lib/acfs/errors.rb +33 -12
- data/lib/acfs/global.rb +12 -2
- data/lib/acfs/location.rb +9 -5
- data/lib/acfs/middleware/base.rb +5 -1
- data/lib/acfs/middleware/json.rb +2 -0
- data/lib/acfs/middleware/logger.rb +2 -0
- data/lib/acfs/middleware/msgpack.rb +2 -0
- data/lib/acfs/middleware/print.rb +2 -0
- data/lib/acfs/middleware/serializer.rb +2 -0
- data/lib/acfs/operation.rb +5 -3
- data/lib/acfs/request.rb +3 -0
- data/lib/acfs/request/callbacks.rb +5 -1
- data/lib/acfs/resource.rb +2 -0
- data/lib/acfs/resource/attributes.rb +3 -2
- data/lib/acfs/resource/attributes/base.rb +2 -1
- data/lib/acfs/resource/attributes/boolean.rb +2 -0
- data/lib/acfs/resource/attributes/date_time.rb +2 -1
- data/lib/acfs/resource/attributes/dict.rb +2 -0
- data/lib/acfs/resource/attributes/float.rb +5 -3
- data/lib/acfs/resource/attributes/integer.rb +2 -0
- data/lib/acfs/resource/attributes/list.rb +2 -0
- data/lib/acfs/resource/attributes/string.rb +2 -0
- data/lib/acfs/resource/attributes/uuid.rb +4 -3
- data/lib/acfs/resource/dirty.rb +2 -0
- data/lib/acfs/resource/initialization.rb +2 -0
- data/lib/acfs/resource/loadable.rb +2 -0
- data/lib/acfs/resource/locatable.rb +10 -6
- data/lib/acfs/resource/operational.rb +2 -1
- data/lib/acfs/resource/persistence.rb +7 -4
- data/lib/acfs/resource/query_methods.rb +5 -3
- data/lib/acfs/resource/service.rb +3 -1
- data/lib/acfs/resource/validation.rb +3 -1
- data/lib/acfs/response.rb +2 -0
- data/lib/acfs/response/formats.rb +2 -0
- data/lib/acfs/response/status.rb +3 -1
- data/lib/acfs/rspec.rb +2 -0
- data/lib/acfs/runner.rb +6 -1
- data/lib/acfs/service.rb +8 -2
- data/lib/acfs/service/middleware.rb +2 -0
- data/lib/acfs/service/middleware/stack.rb +5 -3
- data/lib/acfs/singleton_resource.rb +4 -2
- data/lib/acfs/stub.rb +33 -11
- data/lib/acfs/util.rb +2 -0
- data/lib/acfs/version.rb +3 -1
- data/lib/acfs/yard.rb +1 -0
- data/spec/acfs/adapter/typhoeus_spec.rb +30 -3
- data/spec/acfs/collection_spec.rb +7 -5
- data/spec/acfs/configuration_spec.rb +2 -0
- data/spec/acfs/global_spec.rb +48 -1
- data/spec/acfs/location_spec.rb +25 -0
- data/spec/acfs/middleware/json_spec.rb +2 -0
- data/spec/acfs/middleware/msgpack_spec.rb +2 -0
- data/spec/acfs/operation_spec.rb +2 -0
- data/spec/acfs/request/callbacks_spec.rb +2 -0
- data/spec/acfs/request_spec.rb +3 -1
- data/spec/acfs/resource/attributes/boolean_spec.rb +2 -0
- data/spec/acfs/resource/attributes/date_time_spec.rb +2 -0
- data/spec/acfs/resource/attributes/dict_spec.rb +4 -2
- data/spec/acfs/resource/attributes/float_spec.rb +2 -0
- data/spec/acfs/resource/attributes/integer_spec.rb +2 -0
- data/spec/acfs/resource/attributes/list_spec.rb +5 -3
- data/spec/acfs/resource/attributes/uuid_spec.rb +2 -0
- data/spec/acfs/resource/attributes_spec.rb +6 -4
- data/spec/acfs/resource/dirty_spec.rb +2 -0
- data/spec/acfs/resource/initialization_spec.rb +8 -2
- data/spec/acfs/resource/loadable_spec.rb +2 -0
- data/spec/acfs/resource/locatable_spec.rb +2 -0
- data/spec/acfs/resource/persistance_spec.rb +10 -4
- data/spec/acfs/resource/query_methods_spec.rb +25 -18
- data/spec/acfs/resource/validation_spec.rb +2 -0
- data/spec/acfs/response/formats_spec.rb +3 -1
- data/spec/acfs/response/status_spec.rb +2 -0
- data/spec/acfs/runner_spec.rb +6 -8
- data/spec/acfs/service/middleware_spec.rb +2 -0
- data/spec/acfs/service_spec.rb +3 -1
- data/spec/acfs/singleton_resource_spec.rb +2 -0
- data/spec/acfs/stub_spec.rb +2 -0
- data/spec/acfs_spec.rb +2 -0
- data/spec/spec_helper.rb +11 -6
- data/spec/support/hash.rb +2 -0
- data/spec/support/response.rb +2 -0
- data/spec/support/service.rb +1 -0
- data/spec/support/shared/find_callbacks.rb +2 -0
- metadata +12 -11
data/lib/acfs/response/status.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module Acfs
|
2
4
|
class Response
|
3
5
|
# Method to fetch information about response status.
|
@@ -11,7 +13,7 @@ module Acfs
|
|
11
13
|
# return response.response_code unless response.nil?
|
12
14
|
# 0
|
13
15
|
end
|
14
|
-
|
16
|
+
alias code status_code
|
15
17
|
|
16
18
|
# Return true if response was successful indicated by
|
17
19
|
# response status code.
|
data/lib/acfs/rspec.rb
CHANGED
data/lib/acfs/runner.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'acfs/service/middleware'
|
2
4
|
|
3
5
|
module Acfs
|
@@ -59,7 +61,7 @@ module Acfs
|
|
59
61
|
|
60
62
|
enqueue_operations
|
61
63
|
start_all
|
62
|
-
rescue
|
64
|
+
rescue StandardError
|
63
65
|
queue.clear
|
64
66
|
raise
|
65
67
|
end
|
@@ -87,10 +89,13 @@ module Acfs
|
|
87
89
|
|
88
90
|
def op_request(op)
|
89
91
|
return if Acfs::Stub.enabled? && Acfs::Stub.stubbed(op)
|
92
|
+
|
90
93
|
req = op.service.prepare op.request
|
91
94
|
return unless req.is_a? Acfs::Request
|
95
|
+
|
92
96
|
req = prepare req
|
93
97
|
return unless req.is_a? Acfs::Request
|
98
|
+
|
94
99
|
yield req
|
95
100
|
end
|
96
101
|
end
|
data/lib/acfs/service.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'acfs/service/middleware'
|
2
4
|
|
3
5
|
module Acfs
|
@@ -47,12 +49,16 @@ module Acfs
|
|
47
49
|
opts[:path].to_s
|
48
50
|
end
|
49
51
|
|
50
|
-
|
52
|
+
if path.blank?
|
53
|
+
path = (resource_class.name || 'class').pluralize.underscore
|
54
|
+
end
|
51
55
|
|
52
56
|
resource_class.location_default_path(action, path.strip)
|
53
57
|
end
|
54
58
|
|
55
|
-
|
59
|
+
if path.nil?
|
60
|
+
raise ArgumentError.new "Location for `#{action}' explicit disabled by set to nil."
|
61
|
+
end
|
56
62
|
|
57
63
|
Location.new [self.class.base_url.to_s, path.to_s].join('/')
|
58
64
|
end
|
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module Acfs
|
2
4
|
class Service
|
3
5
|
module Middleware
|
@@ -5,7 +7,7 @@ module Acfs
|
|
5
7
|
include Enumerable
|
6
8
|
|
7
9
|
MUTEX = Mutex.new
|
8
|
-
IDENTITY = ->
|
10
|
+
IDENTITY = ->(i) { i }
|
9
11
|
|
10
12
|
attr_reader :middlewares
|
11
13
|
|
@@ -41,7 +43,7 @@ module Acfs
|
|
41
43
|
next_middleware.call(klass.call(env, *args))
|
42
44
|
end
|
43
45
|
else
|
44
|
-
|
46
|
+
raise "Invalid middleware, doesn't respond to `call`: #{klass.inspect}"
|
45
47
|
end
|
46
48
|
end
|
47
49
|
end
|
@@ -51,7 +53,7 @@ module Acfs
|
|
51
53
|
end
|
52
54
|
|
53
55
|
def each
|
54
|
-
middlewares.each {
|
56
|
+
middlewares.each {|x| yield x.first }
|
55
57
|
end
|
56
58
|
|
57
59
|
def clear
|
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module Acfs
|
2
4
|
# Acfs SingletonResources
|
3
5
|
#
|
@@ -71,8 +73,8 @@ module Acfs
|
|
71
73
|
def all
|
72
74
|
raise ::Acfs::UnsupportedOperation.new
|
73
75
|
end
|
74
|
-
|
75
|
-
|
76
|
+
alias find_by all
|
77
|
+
alias find_by! all
|
76
78
|
|
77
79
|
# @api private
|
78
80
|
def location_default_path(_, path)
|
data/lib/acfs/stub.rb
CHANGED
@@ -1,10 +1,12 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'rack/utils'
|
2
4
|
|
3
5
|
module Acfs
|
4
6
|
# Global handler for stubbing resources.
|
5
7
|
#
|
6
8
|
class Stub
|
7
|
-
ACTIONS = [
|
9
|
+
ACTIONS = %i[read create update delete list].freeze
|
8
10
|
|
9
11
|
attr_reader :opts
|
10
12
|
|
@@ -13,7 +15,10 @@ module Acfs
|
|
13
15
|
|
14
16
|
@opts[:with].stringify_keys! if @opts[:with].is_a? Hash
|
15
17
|
@opts[:return].stringify_keys! if @opts[:return].is_a? Hash
|
16
|
-
|
18
|
+
|
19
|
+
if @opts[:return].is_a? Array
|
20
|
+
@opts[:return].map! {|h| h.stringify_keys! if h.is_a? Hash }
|
21
|
+
end
|
17
22
|
end
|
18
23
|
|
19
24
|
def accept?(op)
|
@@ -28,8 +33,13 @@ module Acfs
|
|
28
33
|
case opts.fetch(:match, :inclusion)
|
29
34
|
when :legacy
|
30
35
|
return true if with.empty? && params.empty? && data.empty?
|
31
|
-
|
32
|
-
|
36
|
+
if with.reject {|_, v| v.nil? } == params.reject {|_, v| v.nil? }
|
37
|
+
return true
|
38
|
+
end
|
39
|
+
if with.reject {|_, v| v.nil? } == data.reject {|_, v| v.nil? }
|
40
|
+
return true
|
41
|
+
end
|
42
|
+
|
33
43
|
false
|
34
44
|
when :inclusion
|
35
45
|
with.each_pair.all? do |k, v|
|
@@ -43,7 +53,9 @@ module Acfs
|
|
43
53
|
end
|
44
54
|
|
45
55
|
def called?(count = nil)
|
46
|
-
|
56
|
+
if count.respond_to? :count
|
57
|
+
count = count.count
|
58
|
+
end # For `5.times` Enumerators
|
47
59
|
count.nil? ? calls.any? : calls.size == count
|
48
60
|
end
|
49
61
|
|
@@ -60,8 +72,8 @@ module Acfs
|
|
60
72
|
|
61
73
|
response = Acfs::Response.new op.request,
|
62
74
|
headers: opts[:headers] || {},
|
63
|
-
status:
|
64
|
-
data:
|
75
|
+
status: opts[:status] || 200,
|
76
|
+
data: data || {}
|
65
77
|
op.call data, response
|
66
78
|
else
|
67
79
|
raise ArgumentError.new 'Unsupported stub.'
|
@@ -72,6 +84,7 @@ module Acfs
|
|
72
84
|
|
73
85
|
def raise_error(op, name, data)
|
74
86
|
raise name if name.is_a? Class
|
87
|
+
|
75
88
|
data.stringify_keys! if data.respond_to? :stringify_keys!
|
76
89
|
|
77
90
|
op.handle_failure ::Acfs::Response.new op.request, status: Rack::Utils.status_code(name), data: data
|
@@ -83,7 +96,9 @@ module Acfs
|
|
83
96
|
#
|
84
97
|
def resource(klass, action, opts = {}, &_block)
|
85
98
|
action = action.to_sym
|
86
|
-
|
99
|
+
unless ACTIONS.include? action
|
100
|
+
raise ArgumentError.new "Unknown action `#{action}`."
|
101
|
+
end
|
87
102
|
|
88
103
|
Stub.new(opts).tap do |stub|
|
89
104
|
stubs[klass] ||= {}
|
@@ -128,7 +143,9 @@ module Acfs
|
|
128
143
|
|
129
144
|
accepted_stubs = stubs.select {|stub| stub.accept? op }
|
130
145
|
|
131
|
-
|
146
|
+
if accepted_stubs.size > 1
|
147
|
+
raise AmbiguousStubError.new stubs: accepted_stubs, operation: op
|
148
|
+
end
|
132
149
|
|
133
150
|
accepted_stubs.first
|
134
151
|
end
|
@@ -137,6 +154,7 @@ module Acfs
|
|
137
154
|
stub = stub_for op
|
138
155
|
unless stub
|
139
156
|
return false if allow_requests?
|
157
|
+
|
140
158
|
raise RealRequestsNotAllowedError.new <<-MSG.strip.gsub(/^[ ]{12}/, '')
|
141
159
|
No stub found for `#{op.action}' on `#{op.resource.name}' with params `#{op.full_params.inspect}', data `#{op.data.inspect}' and id `#{op.id}'.
|
142
160
|
|
@@ -159,8 +177,12 @@ module Acfs
|
|
159
177
|
stubs.each do |stub|
|
160
178
|
out << " #{action}"
|
161
179
|
out << " with #{stub.opts[:with].inspect}" if stub.opts[:with]
|
162
|
-
|
163
|
-
|
180
|
+
if stub.opts[:return]
|
181
|
+
out << " and return #{stub.opts[:return].inspect}"
|
182
|
+
end
|
183
|
+
if stub.opts[:raise]
|
184
|
+
out << " and raise #{stub.opts[:raise].inspect}"
|
185
|
+
end
|
164
186
|
out << "\n"
|
165
187
|
end
|
166
188
|
end
|
data/lib/acfs/util.rb
CHANGED
data/lib/acfs/version.rb
CHANGED
data/lib/acfs/yard.rb
CHANGED
@@ -1,14 +1,19 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'spec_helper'
|
2
4
|
|
3
5
|
describe Acfs::Adapter::Typhoeus do
|
4
6
|
let(:adapter) { described_class.new }
|
5
|
-
|
7
|
+
|
8
|
+
before do
|
9
|
+
stub_request(:any, 'http://example.org').to_return status: 200
|
10
|
+
end
|
6
11
|
|
7
12
|
it 'raises an error' do
|
8
|
-
request1 = Acfs::Request.new 'http://
|
13
|
+
request1 = Acfs::Request.new 'http://example.org' do |_rsp|
|
9
14
|
raise '404-1'
|
10
15
|
end
|
11
|
-
request2 = Acfs::Request.new 'http://
|
16
|
+
request2 = Acfs::Request.new 'http://example.org' do |_rsp|
|
12
17
|
raise '404-2'
|
13
18
|
end
|
14
19
|
adapter.queue request1
|
@@ -18,6 +23,28 @@ describe Acfs::Adapter::Typhoeus do
|
|
18
23
|
expect { adapter.start }.to_not raise_error
|
19
24
|
end
|
20
25
|
|
26
|
+
it 'raises timeout' do
|
27
|
+
stub_request(:any, 'http://example.org').to_timeout
|
28
|
+
|
29
|
+
request = Acfs::Request.new 'http://example.org'
|
30
|
+
adapter.queue request
|
31
|
+
|
32
|
+
expect { adapter.run(request) }.to raise_error(::Acfs::TimeoutError) do |err|
|
33
|
+
expect(err.message).to eq 'Timeout reached: GET http://example.org'
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
it 'raises connection errors' do
|
38
|
+
WebMock.allow_net_connect!
|
39
|
+
|
40
|
+
request = Acfs::Request.new 'http://should-never-exists.example.org'
|
41
|
+
adapter.queue request
|
42
|
+
|
43
|
+
expect { adapter.run(request) }.to raise_error(::Acfs::RequestError) do |err|
|
44
|
+
expect(err.message).to eq 'Couldn\'t resolve host name: GET http://should-never-exists.example.org'
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
21
48
|
it 'passes arguments to typhoeus hydra' do
|
22
49
|
value = {key: 1, key2: 2}
|
23
50
|
|
@@ -1,10 +1,12 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'spec_helper'
|
2
4
|
|
3
5
|
describe Acfs::Collection do
|
4
6
|
let(:model) { MyUser }
|
5
7
|
|
6
8
|
describe 'Pagination' do
|
7
|
-
let(:params) {
|
9
|
+
let(:params) { {} }
|
8
10
|
let!(:collection) { model.all params }
|
9
11
|
|
10
12
|
subject { Acfs.run; collection }
|
@@ -62,7 +64,7 @@ describe Acfs::Collection do
|
|
62
64
|
.to_return response([{id: 1, name: 'Anon', age: 12, born_at: 'Berlin'}],
|
63
65
|
headers: {
|
64
66
|
'X-Total-Pages' => '2',
|
65
|
-
'Link'
|
67
|
+
'Link' => '<http://users.example.org/users?page=2>; rel="next"'
|
66
68
|
})
|
67
69
|
end
|
68
70
|
let!(:req) do
|
@@ -86,7 +88,7 @@ describe Acfs::Collection do
|
|
86
88
|
.to_return response([{id: 2, name: 'Anno', age: 1604, born_at: 'Santa Maria'}],
|
87
89
|
headers: {
|
88
90
|
'X-Total-Pages' => '2',
|
89
|
-
'Link'
|
91
|
+
'Link' => '<http://users.example.org/users>; rel="prev"'
|
90
92
|
})
|
91
93
|
end
|
92
94
|
let!(:req) do
|
@@ -110,7 +112,7 @@ describe Acfs::Collection do
|
|
110
112
|
.to_return response([{id: 2, name: 'Anno', age: 1604, born_at: 'Santa Maria'}],
|
111
113
|
headers: {
|
112
114
|
'X-Total-Pages' => '2',
|
113
|
-
'Link'
|
115
|
+
'Link' => '<http://users.example.org/users>; rel="first"'
|
114
116
|
})
|
115
117
|
end
|
116
118
|
let!(:req) do
|
@@ -134,7 +136,7 @@ describe Acfs::Collection do
|
|
134
136
|
.to_return response([{id: 2, name: 'Anno', age: 1604, born_at: 'Santa Maria'}],
|
135
137
|
headers: {
|
136
138
|
'X-Total-Pages' => '2',
|
137
|
-
'Link'
|
139
|
+
'Link' => '<http://users.example.org/users?page=12>; rel="last"'
|
138
140
|
})
|
139
141
|
end
|
140
142
|
let!(:req) do
|
data/spec/acfs/global_spec.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'spec_helper'
|
2
4
|
|
3
5
|
class NotificationCollector
|
@@ -48,7 +50,8 @@ describe ::Acfs::Global do
|
|
48
50
|
stub_request(:get, %r{http://users.example.org/users/\d+}).to_return(
|
49
51
|
status: 200,
|
50
52
|
body: '{}',
|
51
|
-
headers: {'Content-Type' => 'application/json'}
|
53
|
+
headers: {'Content-Type' => 'application/json'}
|
54
|
+
)
|
52
55
|
end
|
53
56
|
|
54
57
|
it 'should invoke when both resources' do
|
@@ -71,6 +74,50 @@ describe ::Acfs::Global do
|
|
71
74
|
end
|
72
75
|
Acfs.run
|
73
76
|
end
|
77
|
+
|
78
|
+
context 'with an empty result for a find_by call' do
|
79
|
+
before do
|
80
|
+
stub_request(:get, %r{http://users.example.org/users})
|
81
|
+
.with(query: {id: '2'})
|
82
|
+
.to_return(
|
83
|
+
status: 200,
|
84
|
+
body: '{}',
|
85
|
+
headers: {'Content-Type' => 'application/json'}
|
86
|
+
)
|
87
|
+
end
|
88
|
+
|
89
|
+
it 'invokes once both requests are finished' do
|
90
|
+
user1 = MyUser.find 1
|
91
|
+
user2 = MyUser.find_by id: 2
|
92
|
+
|
93
|
+
expect do |cb|
|
94
|
+
Acfs.on(user1, user2, &cb)
|
95
|
+
Acfs.run
|
96
|
+
end.to yield_with_args(user1, be_nil)
|
97
|
+
end
|
98
|
+
|
99
|
+
it 'invokes once remaining requests are finished' do
|
100
|
+
user1 = MyUser.find 1
|
101
|
+
Acfs.run # Finish the first request
|
102
|
+
|
103
|
+
user2 = MyUser.find_by id: 2
|
104
|
+
|
105
|
+
expect do |cb|
|
106
|
+
Acfs.on(user1, user2, &cb)
|
107
|
+
Acfs.run
|
108
|
+
end.to yield_with_args(user1, be_nil)
|
109
|
+
end
|
110
|
+
|
111
|
+
it 'invokes immediately when all requests have already been finished' do
|
112
|
+
user1 = MyUser.find 1
|
113
|
+
user2 = MyUser.find_by id: 2
|
114
|
+
Acfs.run
|
115
|
+
|
116
|
+
expect do |cb|
|
117
|
+
Acfs.on(user1, user2, &cb)
|
118
|
+
end.to yield_with_args(user1, be_nil)
|
119
|
+
end
|
120
|
+
end
|
74
121
|
end
|
75
122
|
|
76
123
|
describe '#runner' do
|
@@ -0,0 +1,25 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'spec_helper'
|
4
|
+
|
5
|
+
describe ::Acfs::Location do
|
6
|
+
let(:location) { described_class.new(uri, args) }
|
7
|
+
let(:uri) { 'http://localhost/users/:id' }
|
8
|
+
let(:args) { {id: 4} }
|
9
|
+
|
10
|
+
describe '#str' do
|
11
|
+
subject(:str) { location.str }
|
12
|
+
|
13
|
+
it 'replaces variables with values' do
|
14
|
+
expect(str).to eq 'http://localhost/users/4'
|
15
|
+
end
|
16
|
+
|
17
|
+
context 'with special characters' do
|
18
|
+
let(:args) { {id: '4 [@(\/!^$'} }
|
19
|
+
|
20
|
+
it 'escapes special characters' do
|
21
|
+
expect(str).to eq 'http://localhost/users/4+%5B%40%28%5C%2F%21%5E%24'
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|