aggro 0.0.1 → 0.0.2

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 (141) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +8 -0
  3. data/.travis.yml +15 -0
  4. data/Gemfile +9 -0
  5. data/README.md +5 -1
  6. data/Rakefile +10 -0
  7. data/aggro.gemspec +8 -1
  8. data/lib/aggro.rb +191 -7
  9. data/lib/aggro/abstract_store.rb +12 -0
  10. data/lib/aggro/aggregate.rb +98 -0
  11. data/lib/aggro/aggregate_ref.rb +68 -6
  12. data/lib/aggro/attribute_dsl.rb +96 -0
  13. data/lib/aggro/binding_dsl.rb +45 -0
  14. data/lib/aggro/block_helper.rb +14 -0
  15. data/lib/aggro/channel.rb +37 -0
  16. data/lib/aggro/client.rb +12 -0
  17. data/lib/aggro/cluster_config.rb +57 -0
  18. data/lib/aggro/command.rb +16 -0
  19. data/lib/aggro/concurrent_actor.rb +26 -0
  20. data/lib/aggro/event_bus.rb +94 -0
  21. data/lib/aggro/event_dsl.rb +53 -0
  22. data/lib/aggro/event_proxy.rb +23 -0
  23. data/lib/aggro/event_serializer.rb +14 -0
  24. data/lib/aggro/file_store.rb +97 -0
  25. data/lib/aggro/file_store/reader.rb +21 -0
  26. data/lib/aggro/file_store/writer.rb +27 -0
  27. data/lib/aggro/handler/command.rb +60 -0
  28. data/lib/aggro/handler/create_aggregate.rb +42 -0
  29. data/lib/aggro/handler/get_events.rb +30 -0
  30. data/lib/aggro/handler/query.rb +60 -0
  31. data/lib/aggro/handler/start_saga.rb +56 -0
  32. data/lib/aggro/local_node.rb +28 -0
  33. data/lib/aggro/locator.rb +32 -0
  34. data/lib/aggro/message/ask.rb +16 -0
  35. data/lib/aggro/message/command.rb +36 -0
  36. data/lib/aggro/message/create_aggregate.rb +16 -0
  37. data/lib/aggro/message/endpoint.rb +16 -0
  38. data/lib/aggro/message/events.rb +24 -0
  39. data/lib/aggro/message/get_events.rb +16 -0
  40. data/lib/aggro/message/heartbeat.rb +16 -0
  41. data/lib/aggro/message/invalid_target.rb +20 -0
  42. data/lib/aggro/message/ok.rb +20 -0
  43. data/lib/aggro/message/publisher_endpoint_inquiry.rb +16 -0
  44. data/lib/aggro/message/query.rb +36 -0
  45. data/lib/aggro/message/result.rb +16 -0
  46. data/lib/aggro/message/start_saga.rb +28 -0
  47. data/lib/aggro/message/unhandled_operation.rb +20 -0
  48. data/lib/aggro/message/unknown_operation.rb +20 -0
  49. data/lib/aggro/message_parser.rb +10 -0
  50. data/lib/aggro/message_router.rb +26 -0
  51. data/lib/aggro/nanomsg_transport.rb +31 -0
  52. data/lib/aggro/nanomsg_transport/client.rb +35 -0
  53. data/lib/aggro/nanomsg_transport/connection.rb +98 -0
  54. data/lib/aggro/nanomsg_transport/publish.rb +17 -0
  55. data/lib/aggro/nanomsg_transport/publisher.rb +37 -0
  56. data/lib/aggro/nanomsg_transport/raw_reply.rb +18 -0
  57. data/lib/aggro/nanomsg_transport/raw_request.rb +18 -0
  58. data/lib/aggro/nanomsg_transport/reply.rb +17 -0
  59. data/lib/aggro/nanomsg_transport/request.rb +17 -0
  60. data/lib/aggro/nanomsg_transport/server.rb +84 -0
  61. data/lib/aggro/nanomsg_transport/socket_error.rb +20 -0
  62. data/lib/aggro/nanomsg_transport/subscribe.rb +27 -0
  63. data/lib/aggro/nanomsg_transport/subscriber.rb +82 -0
  64. data/lib/aggro/node.rb +29 -0
  65. data/lib/aggro/node_list.rb +39 -0
  66. data/lib/aggro/projection.rb +13 -0
  67. data/lib/aggro/query.rb +11 -0
  68. data/lib/aggro/saga.rb +94 -0
  69. data/lib/aggro/saga_runner.rb +87 -0
  70. data/lib/aggro/saga_runner/start_saga.rb +12 -0
  71. data/lib/aggro/saga_status.rb +29 -0
  72. data/lib/aggro/server.rb +88 -0
  73. data/lib/aggro/subscriber.rb +48 -0
  74. data/lib/aggro/subscription.rb +41 -0
  75. data/lib/aggro/transform/boolean.rb +16 -0
  76. data/lib/aggro/transform/email.rb +26 -0
  77. data/lib/aggro/transform/id.rb +34 -0
  78. data/lib/aggro/transform/integer.rb +22 -0
  79. data/lib/aggro/transform/money.rb +22 -0
  80. data/lib/aggro/transform/noop.rb +16 -0
  81. data/lib/aggro/transform/string.rb +16 -0
  82. data/lib/aggro/transform/time_interval.rb +24 -0
  83. data/lib/aggro/version.rb +1 -1
  84. data/spec/lib/aggro/abstract_store_spec.rb +15 -0
  85. data/spec/lib/aggro/aggregate_ref_spec.rb +63 -12
  86. data/spec/lib/aggro/aggregate_spec.rb +207 -0
  87. data/spec/lib/aggro/channel_spec.rb +87 -0
  88. data/spec/lib/aggro/client_spec.rb +26 -0
  89. data/spec/lib/aggro/cluster_config_spec.rb +33 -0
  90. data/spec/lib/aggro/command_spec.rb +52 -0
  91. data/spec/lib/aggro/concurrent_actor_spec.rb +44 -0
  92. data/spec/lib/aggro/event_bus_spec.rb +20 -0
  93. data/spec/lib/aggro/event_serializer_spec.rb +28 -0
  94. data/spec/lib/aggro/file_store/reader_spec.rb +32 -0
  95. data/spec/lib/aggro/file_store/writer_spec.rb +67 -0
  96. data/spec/lib/aggro/file_store_spec.rb +51 -0
  97. data/spec/lib/aggro/handler/command_spec.rb +78 -0
  98. data/spec/lib/aggro/handler/create_aggregate_spec.rb +64 -0
  99. data/spec/lib/aggro/handler/get_events_handler_spec.rb +45 -0
  100. data/spec/lib/aggro/handler/query_spec.rb +78 -0
  101. data/spec/lib/aggro/handler/start_saga_spec.rb +64 -0
  102. data/spec/lib/aggro/local_node_spec.rb +52 -0
  103. data/spec/lib/aggro/locator_spec.rb +61 -0
  104. data/spec/lib/aggro/message/ask_spec.rb +23 -0
  105. data/spec/lib/aggro/message/command_spec.rb +50 -0
  106. data/spec/lib/aggro/message/create_aggregate_spec.rb +28 -0
  107. data/spec/lib/aggro/message/endpoint_spec.rb +23 -0
  108. data/spec/lib/aggro/message/events_spec.rb +37 -0
  109. data/spec/lib/aggro/message/get_events_spec.rb +33 -0
  110. data/spec/lib/aggro/message/heartbeat_spec.rb +23 -0
  111. data/spec/lib/aggro/message/invalid_target_spec.rb +28 -0
  112. data/spec/lib/aggro/message/ok_spec.rb +27 -0
  113. data/spec/lib/aggro/message/publisher_endpoint_inquiry_spec.rb +23 -0
  114. data/spec/lib/aggro/message/query_spec.rb +50 -0
  115. data/spec/lib/aggro/message/start_saga_spec.rb +37 -0
  116. data/spec/lib/aggro/message/unhandled_operation_spec.rb +28 -0
  117. data/spec/lib/aggro/message/unknown_operation_spec.rb +28 -0
  118. data/spec/lib/aggro/message_parser_spec.rb +16 -0
  119. data/spec/lib/aggro/message_router_spec.rb +35 -0
  120. data/spec/lib/aggro/nanomsg_transport/socket_error_spec.rb +21 -0
  121. data/spec/lib/aggro/nanomsg_transport_spec.rb +37 -0
  122. data/spec/lib/aggro/node_list_spec.rb +38 -0
  123. data/spec/lib/aggro/node_spec.rb +44 -0
  124. data/spec/lib/aggro/projection_spec.rb +22 -0
  125. data/spec/lib/aggro/query_spec.rb +47 -0
  126. data/spec/lib/aggro/saga_runner_spec.rb +84 -0
  127. data/spec/lib/aggro/saga_spec.rb +126 -0
  128. data/spec/lib/aggro/saga_status_spec.rb +56 -0
  129. data/spec/lib/aggro/server_spec.rb +118 -0
  130. data/spec/lib/aggro/subscriber_spec.rb +59 -0
  131. data/spec/lib/aggro/subscription_spec.rb +50 -0
  132. data/spec/lib/aggro/transform/boolean_spec.rb +23 -0
  133. data/spec/lib/aggro/transform/email_spec.rb +13 -0
  134. data/spec/lib/aggro/transform/id_spec.rb +70 -0
  135. data/spec/lib/aggro/transform/integer_spec.rb +30 -0
  136. data/spec/lib/aggro/transform/money_spec.rb +34 -0
  137. data/spec/lib/aggro/transform/string_spec.rb +15 -0
  138. data/spec/lib/aggro/transform/time_interval_spec.rb +29 -0
  139. data/spec/lib/aggro_spec.rb +63 -19
  140. data/spec/spec_helper.rb +21 -2
  141. metadata +283 -3
@@ -0,0 +1,78 @@
1
+ RSpec.describe Handler::Command do
2
+ subject(:handler) { Handler::Command.new message, server }
3
+
4
+ let(:command) { double }
5
+ let(:commandee_id) { SecureRandom.uuid }
6
+ let(:message) { double to_command: command, commandee_id: commandee_id }
7
+ let(:server) { double }
8
+
9
+ let(:node) { double }
10
+ let(:local) { true }
11
+ let(:fake_locator) { double local?: local, primary_node: node }
12
+ let(:locator_class) { double new: fake_locator }
13
+
14
+ before do
15
+ stub_const 'Aggro::Locator', locator_class
16
+ end
17
+
18
+ describe '#call' do
19
+ context 'comandee is not handled by the server' do
20
+ let(:node_id) { SecureRandom.uuid }
21
+ let(:client) { spy post: Message::OK.new }
22
+ let(:node) { double id: node_id, client: client }
23
+ let(:local) { false }
24
+
25
+ it 'should forward the request to the correct node and return reply' do
26
+ expect(handler.call).to be_a Message::OK
27
+ expect(client).to have_received(:post)
28
+ end
29
+ end
30
+
31
+ context 'local system knows the command' do
32
+ context 'commandee exists on system' do
33
+ let(:channel) { spy handles_command?: handles }
34
+
35
+ before do
36
+ fake_channels = { commandee_id => channel }
37
+ stub_const 'Aggro', double(channels: fake_channels)
38
+ end
39
+
40
+ context 'channel understands command type' do
41
+ let(:handles) { true }
42
+
43
+ it 'should return OK' do
44
+ expect(handler.call).to be_a Message::OK
45
+ end
46
+
47
+ it 'should forward command to the channel' do
48
+ handler.call
49
+
50
+ expect(channel).to have_received(:forward_command).with command
51
+ end
52
+ end
53
+
54
+ context 'channel does not understand command type' do
55
+ let(:handles) { false }
56
+
57
+ it 'should return UnhandledOperation' do
58
+ expect(handler.call).to be_a Message::UnhandledOperation
59
+ end
60
+ end
61
+ end
62
+
63
+ context 'commandee does not exist on system' do
64
+ it 'should return InvalidTarget' do
65
+ expect(handler.call).to be_a Message::InvalidTarget
66
+ end
67
+ end
68
+ end
69
+
70
+ context 'local system does not know the command' do
71
+ it 'should return UnknownOperation' do
72
+ allow(message).to receive(:to_command).and_return(nil)
73
+
74
+ expect(handler.call).to be_a Message::UnknownOperation
75
+ end
76
+ end
77
+ end
78
+ end
@@ -0,0 +1,64 @@
1
+ RSpec.describe Handler::CreateAggregate do
2
+ subject(:handler) { Handler::CreateAggregate.new message, server }
3
+
4
+ let(:type) { 'Test' }
5
+ let(:id) { SecureRandom.uuid }
6
+ let(:message) { double id: id, type: type }
7
+ let(:server) { double }
8
+
9
+ let(:node) { double }
10
+ let(:local) { true }
11
+ let(:fake_locator) { double local?: local, primary_node: node }
12
+ let(:locator_class) { double new: fake_locator }
13
+
14
+ let(:fake_store) { spy(create: true) }
15
+
16
+ before do
17
+ stub_const 'Aggro::Locator', locator_class
18
+
19
+ allow(Aggro).to receive(:store).and_return fake_store
20
+ end
21
+
22
+ describe '#call' do
23
+ context 'id is handled by the server' do
24
+ it 'should return an OK message' do
25
+ expect(handler.call).to be_a Message::OK
26
+ end
27
+
28
+ it 'should create the aggregate in the current store' do
29
+ handler.call
30
+
31
+ expect(fake_store).to have_received(:create).with(id, type)
32
+ end
33
+
34
+ context 'does not exist in the channel list' do
35
+ let(:fake_channels) { {} }
36
+
37
+ let(:fake_channel) { double }
38
+ let(:channel_class) { double new: fake_channel }
39
+
40
+ before do
41
+ allow(Aggro).to receive(:channels).and_return fake_channels
42
+ stub_const 'Aggro::Channel', channel_class
43
+ end
44
+
45
+ it 'should add a channel for the aggregate to the channels' do
46
+ handler.call
47
+
48
+ expect(fake_channels[id]).to eq fake_channel
49
+ end
50
+ end
51
+ end
52
+
53
+ context 'id is not handled by the server' do
54
+ let(:node_id) { SecureRandom.uuid }
55
+ let(:node) { double id: node_id }
56
+ let(:local) { false }
57
+
58
+ it 'should return Ask with another node to try' do
59
+ expect(handler.call).to be_a Message::Ask
60
+ expect(handler.call.node_id).to eq node_id
61
+ end
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,45 @@
1
+ RSpec.describe Handler::GetEvents do
2
+ subject(:handler) { Handler::GetEvents.new message, server }
3
+
4
+ let(:type) { 'Test' }
5
+ let(:id) { SecureRandom.uuid }
6
+ let(:message) { double id: id, from_version: 0 }
7
+ let(:server) { double }
8
+
9
+ let(:node) { double }
10
+ let(:local) { true }
11
+ let(:fake_locator) { double local?: local, primary_node: node }
12
+ let(:locator_class) { double new: fake_locator }
13
+
14
+ let(:stream) { double id: id, type: type, events: [] }
15
+ let(:fake_store) { spy(read: [stream]) }
16
+
17
+ before do
18
+ stub_const 'Aggro::Locator', locator_class
19
+
20
+ allow(Aggro).to receive(:store).and_return fake_store
21
+ end
22
+
23
+ describe '#call' do
24
+ context 'id is handled by the server' do
25
+ it 'should return an Events message' do
26
+ response = handler.call
27
+
28
+ expect(response).to be_a Message::Events
29
+ expect(response.id).to eq id
30
+ expect(response.events).to eq []
31
+ end
32
+ end
33
+
34
+ context 'id is not handled by the server' do
35
+ let(:node_id) { SecureRandom.uuid }
36
+ let(:node) { double id: node_id }
37
+ let(:local) { false }
38
+
39
+ it 'should return Ask with another node to try' do
40
+ expect(handler.call).to be_a Message::Ask
41
+ expect(handler.call.node_id).to eq node_id
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,78 @@
1
+ RSpec.describe Handler::Query do
2
+ subject(:handler) { Handler::Query.new message, server }
3
+
4
+ let(:query) { double }
5
+ let(:queryable_id) { SecureRandom.uuid }
6
+ let(:message) { double to_query: query, queryable_id: queryable_id }
7
+ let(:server) { double }
8
+
9
+ let(:node) { double }
10
+ let(:local) { true }
11
+ let(:fake_locator) { double local?: local, primary_node: node }
12
+ let(:locator_class) { double new: fake_locator }
13
+
14
+ before do
15
+ stub_const 'Aggro::Locator', locator_class
16
+ end
17
+
18
+ describe '#call' do
19
+ context 'comandee is not handled by the server' do
20
+ let(:node_id) { SecureRandom.uuid }
21
+ let(:client) { spy post: Message::Result.new }
22
+ let(:node) { double id: node_id, client: client }
23
+ let(:local) { false }
24
+
25
+ it 'should forward the request to the correct node and return reply' do
26
+ expect(handler.call).to be_a Message::Result
27
+ expect(client).to have_received(:post)
28
+ end
29
+ end
30
+
31
+ context 'local system knows the query' do
32
+ context 'queryee exists on system' do
33
+ let(:channel) { spy handles_query?: handles }
34
+
35
+ before do
36
+ fake_channels = { queryable_id => channel }
37
+ stub_const 'Aggro', double(channels: fake_channels)
38
+ end
39
+
40
+ context 'channel understands query type' do
41
+ let(:handles) { true }
42
+
43
+ it 'should return Result' do
44
+ expect(handler.call).to be_a Message::Result
45
+ end
46
+
47
+ it 'should forward query to the channel' do
48
+ handler.call
49
+
50
+ expect(channel).to have_received(:run_query).with query
51
+ end
52
+ end
53
+
54
+ context 'channel does not understand query type' do
55
+ let(:handles) { false }
56
+
57
+ it 'should return UnhandledOperation' do
58
+ expect(handler.call).to be_a Message::UnhandledOperation
59
+ end
60
+ end
61
+ end
62
+
63
+ context 'queryee does not exist on system' do
64
+ it 'should return InvalidTarget' do
65
+ expect(handler.call).to be_a Message::InvalidTarget
66
+ end
67
+ end
68
+ end
69
+
70
+ context 'local system does not know the query' do
71
+ it 'should return UnknownOperation' do
72
+ allow(message).to receive(:to_query).and_return(nil)
73
+
74
+ expect(handler.call).to be_a Message::UnknownOperation
75
+ end
76
+ end
77
+ end
78
+ end
@@ -0,0 +1,64 @@
1
+ RSpec.describe Handler::StartSaga do
2
+ subject(:handler) { Handler::StartSaga.new message, server }
3
+
4
+ class TestSaga
5
+ include Saga
6
+ end
7
+
8
+ let(:id) { SecureRandom.uuid }
9
+ let(:args) { { test: 'foo' } }
10
+ let(:message) { double name: 'TestSaga', id: id, args: args }
11
+ let(:server) { double }
12
+
13
+ let(:node) { double }
14
+ let(:local) { true }
15
+ let(:fake_locator) { double local?: local, primary_node: node }
16
+ let(:locator_class) { double new: fake_locator }
17
+
18
+ before do
19
+ stub_const 'Aggro::Locator', locator_class
20
+ end
21
+
22
+ describe '#call' do
23
+ context 'saga is handled by the server' do
24
+ let(:channel) { spy }
25
+
26
+ before do
27
+ stub_const 'Aggro::Channel', double(new: channel)
28
+ end
29
+
30
+ context 'channel understands command type' do
31
+ it 'should return OK' do
32
+ expect(handler.call).to be_a Message::OK
33
+ end
34
+
35
+ it 'should send :start to the channel' do
36
+ handler.call
37
+
38
+ expect(channel).to have_received(:forward_command).with \
39
+ SagaRunner::StartSaga
40
+ end
41
+ end
42
+
43
+ context 'saga is not handled by the server' do
44
+ let(:node_id) { SecureRandom.uuid }
45
+ let(:client) { spy post: Message::OK.new }
46
+ let(:node) { double id: node_id, client: client }
47
+ let(:local) { false }
48
+
49
+ it 'should forward the request to the correct node and return reply' do
50
+ expect(handler.call).to be_a Message::OK
51
+ expect(client).to have_received(:post)
52
+ end
53
+ end
54
+ end
55
+
56
+ context 'local system does not know the command' do
57
+ it 'should return an UnknownOperation' do
58
+ allow(message).to receive(:name).and_return('NotReal')
59
+
60
+ expect(handler.call).to be_a Message::UnknownOperation
61
+ end
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,52 @@
1
+ RSpec.describe LocalNode do
2
+ subject(:node) { LocalNode.new('flashing-sparkle') }
3
+
4
+ let(:fake_server) { spy(handle_message: 'OK') }
5
+
6
+ describe '#client' do
7
+ before do
8
+ allow(Aggro).to receive(:server).and_return(fake_server)
9
+ end
10
+
11
+ it 'should return a Client-like object which locally routes messages' do
12
+ node.client.post 'MSG'
13
+ expect(fake_server).to have_received(:handle_message).with('MSG')
14
+ end
15
+ end
16
+
17
+ describe '#endpoint' do
18
+ before do
19
+ allow(Aggro).to receive(:port).and_return 6000
20
+ end
21
+
22
+ it 'should have a local TCP endpoint with the correct port' do
23
+ expect(node.endpoint).to eq 'tcp://127.0.0.1:6000'
24
+ end
25
+ end
26
+
27
+ describe '#publisher_endpoint' do
28
+ before do
29
+ allow(Aggro).to receive(:publisher_port).and_return 7000
30
+ end
31
+
32
+ it 'should have a local TCP endpoint with the correct port' do
33
+ expect(node.publisher_endpoint).to eq 'tcp://127.0.0.1:7000'
34
+ end
35
+ end
36
+
37
+ describe '#to_s' do
38
+ let(:moved_node) { LocalNode.new('flashing-sparkle') }
39
+ let(:other_node) { LocalNode.new('dancing-sparkle') }
40
+
41
+ let(:ring) { ConsistentHashing::Ring.new }
42
+ let(:hasher) { ring.method(:hash_key) }
43
+
44
+ it 'should be consistently hashed the same if id matches' do
45
+ expect(hasher.call(node)).to eq hasher.call(moved_node)
46
+ end
47
+
48
+ it 'should be consistently hashed differently if id differs' do
49
+ expect(hasher.call(node)).to_not eq hasher.call(other_node)
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,61 @@
1
+ RSpec.describe Locator do
2
+ subject(:locator) { Locator.new id }
3
+
4
+ let(:id) { SecureRandom.uuid }
5
+ let(:node) { Node.new('flashing-sparkle', '10.0.0.70') }
6
+ let(:other_node) { Node.new('winking-tiger', '10.0.0.90') }
7
+ let(:nodes) { [node, other_node] }
8
+ let(:node_list) { spy(nodes_for: nodes, state: 'initial') }
9
+
10
+ before do
11
+ allow(Aggro).to receive(:node_list).and_return node_list
12
+ end
13
+
14
+ describe '#local?' do
15
+ context 'primary node is a LocalNode' do
16
+ let(:node) { LocalNode.new('flashing-sparkle') }
17
+
18
+ it 'should return true' do
19
+ expect(locator.local?).to be_truthy
20
+ end
21
+ end
22
+
23
+ context 'primary node is not a LocalNode' do
24
+ it 'should return true' do
25
+ expect(locator.local?).to be_falsey
26
+ end
27
+ end
28
+ end
29
+
30
+ describe '#nodes' do
31
+ it 'should return the nodes on which the aggregate should persist' do
32
+ expect(locator.nodes.first.endpoint).to eq '10.0.0.70'
33
+ end
34
+
35
+ it 'should memorize the lookup to reduce hashing' do
36
+ 5.times { locator.nodes }
37
+
38
+ expect(node_list).to have_received(:nodes_for).once
39
+ end
40
+
41
+ it 'should forget memorized servers if ring state changes' do
42
+ locator.nodes
43
+ allow(node_list).to receive(:state).and_return('changed')
44
+ locator.nodes
45
+
46
+ expect(node_list).to have_received(:nodes_for).twice
47
+ end
48
+ end
49
+
50
+ describe '#primary_node' do
51
+ it 'should return the first associated node' do
52
+ expect(locator.primary_node).to eq node
53
+ end
54
+ end
55
+
56
+ describe '#secondary_nodes' do
57
+ it 'should return the rest of the associated nodes' do
58
+ expect(locator.secondary_nodes).to eq [other_node]
59
+ end
60
+ end
61
+ end