ector-multi 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: a20f20adbf811c03f487888057135de8267b17e5484792d7ab5a9096379321d4
4
+ data.tar.gz: f3011de86fd858e580b782059ffc5e36399183252a90c11936f8ffbc114b94a1
5
+ SHA512:
6
+ metadata.gz: 3897ed4e2e54db0fbf30a9a5c63a9ab5265c1c7fd2f810893d913ade99e8f8e8207ac58680efe0d4b334b4f7ca780178010cbff770a9c7936f4033466f4f10cb
7
+ data.tar.gz: 79472b2e945fe5bb44597a68d042a099cff5019600f066244df1f9d123edad5df2623d181cbdc9bd519bb769cd7e335fb13629246b03a9ba27e3950e6fc85793
@@ -0,0 +1,2 @@
1
+ # multi
2
+ `Multi` is an object for grouping multiple DB operations that should be performed in a single database transaction.
@@ -0,0 +1,28 @@
1
+ require_relative 'lib/ector-multi'
2
+
3
+ Gem::Specification.new do |s|
4
+ s.name = 'ector-multi'
5
+ s.version = ::Ector::VERSION
6
+ s.date = Time.now.strftime('%Y-%m-%d')
7
+ s.summary = 'Grouping multiple DB operations in a single transaction.'
8
+ s.description = 'Grouping multiple DB operations in a single transaction. 100% Inspired by Ecto'
9
+ s.authors = ['Emiliano Mancuso']
10
+ s.email = ['emiliano.mancuso@gmail.com']
11
+ s.homepage = 'http://github.com/emancu/ector-multi'
12
+ s.license = 'MIT'
13
+
14
+ s.files = Dir[
15
+ 'README.md',
16
+ 'rakefile',
17
+ 'lib/**/*.rb',
18
+ '*.gemspec'
19
+ ]
20
+
21
+ s.test_files = Dir['test/*.*']
22
+
23
+ s.required_ruby_version = '>= 2.2'
24
+
25
+ s.add_dependency 'activerecord', '~> 5.2'
26
+ s.add_development_dependency 'protest', '~> 0'
27
+ s.add_development_dependency 'sqlite3', '~> 0'
28
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'active_record'
4
+
5
+ require_relative 'ector-multi/errors'
6
+ require_relative 'ector-multi/result'
7
+ require_relative 'ector-multi/operations'
8
+ require_relative 'ector-multi/multi'
9
+
10
+ module Ector
11
+ VERSION = '0.0.1'
12
+ end
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ector
4
+ class Multi
5
+ # Parent class for all Multi errors
6
+ Error = Class.new(StandardError)
7
+
8
+ # Parent class for all "Rollback-able" errors
9
+ Rollback = Class.new(Error)
10
+
11
+ class OperationFailure < Rollback
12
+ attr_reader :operation, :arguments, :caused_by
13
+ alias_method :value, :arguments
14
+
15
+ def initialize(operation, arguments, caused_by)
16
+ @operation = operation
17
+ @arguments = arguments
18
+ @caused_by = caused_by
19
+
20
+ super("Rollback fired by #{operation.name}")
21
+ end
22
+
23
+ def inspect
24
+ "#<#{self.class.name} #{@operation.name} @caused_by=#{@caused_by.class} @arguments=#{@arguments}>"
25
+ end
26
+ end
27
+
28
+ class ControlledFailure < OperationFailure
29
+ def initialize(operation, value)
30
+ super(operation, value, nil)
31
+ end
32
+
33
+ def inspect
34
+ "#<#{self.class.name} #{@operation.name} value=#{@arguments}>"
35
+ end
36
+ end
37
+
38
+ class UniqueOperationError < Error
39
+ attr_reader :name
40
+
41
+ def initialize(name)
42
+ @name = name
43
+
44
+ super("Operation name '#{name}' is not unique!")
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,105 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ector
4
+ class Multi
5
+ attr_reader :failure, :results, :operations
6
+
7
+ def initialize
8
+ @operations = []
9
+ end
10
+
11
+ def append(multi)
12
+ check_operation_uniqueness!(multi.to_list)
13
+
14
+ @operations.push *multi.operations
15
+
16
+ self
17
+ end
18
+
19
+ def create(name, model, attributes = {}, &block)
20
+ add_operation Operation::Create.new(name, model, operation_block(attributes, block))
21
+ end
22
+
23
+ def destroy(name, object = nil, &block)
24
+ add_operation Operation::Destroy.new(name, operation_block(object, block))
25
+ end
26
+
27
+ def destroy_all(name, dataset = nil, &block)
28
+ add_operation Operation::DestroyAll.new(name, operation_block(dataset, block))
29
+ end
30
+
31
+ def error(name, value)
32
+ add_operation Operation::Error.new(name, operation_block(value, nil))
33
+ end
34
+
35
+ def merge(multi)
36
+ raise NotImplemented
37
+ end
38
+
39
+ def prepend(multi)
40
+ check_operation_uniqueness!(multi.to_list)
41
+
42
+ @operations.prepend *multi.operations
43
+
44
+ self
45
+ end
46
+
47
+ def run(name, &procedure)
48
+ add_operation Operation::Lambda.new(name, procedure)
49
+ end
50
+
51
+ def update(name, object, new_values, &block)
52
+ add_operation Operation::Update.new(name, object, operation_block(new_values, block))
53
+ end
54
+
55
+ def update_all(name, dataset, new_values, &block)
56
+ add_operation Operation::UpdateAll.new(name, dataset, operation_block(new_values, block))
57
+ end
58
+
59
+ def commit
60
+ results = {}
61
+
62
+ operations.find(&:fail_fast?)&.run(results)
63
+
64
+ ::ActiveRecord::Base.transaction(joinable: false, requires_new: true) do
65
+ operations.each do |operation|
66
+ ::ActiveRecord::Base.transaction(joinable: false, requires_new: true) do
67
+ results[operation.name] = operation.run(::OpenStruct.new(results))
68
+ end
69
+ end
70
+ end
71
+
72
+ Ector::Multi::Result.new(results)
73
+ rescue Ector::Multi::Rollback => error
74
+ Ector::Multi::Result.new(results, error)
75
+ end
76
+
77
+ def to_list
78
+ operations.map(&:name)
79
+ end
80
+
81
+ private
82
+
83
+ def add_operation(operation)
84
+ check_operation_uniqueness!(operation.name)
85
+
86
+ @operations << operation
87
+
88
+ self
89
+ end
90
+
91
+ def check_operation_uniqueness!(names)
92
+ repeated_names = to_list & Array(names)
93
+
94
+ fail UniqueOperationError.new(repeated_names) if repeated_names.any?
95
+ end
96
+
97
+ def operation_block(args, block)
98
+ if block
99
+ Proc.new { |results| block.(results, args) }
100
+ else
101
+ Proc.new { |_| args }
102
+ end
103
+ end
104
+ end
105
+ end
@@ -0,0 +1,111 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ector
4
+ class Multi
5
+ module Operation
6
+ class Base
7
+ attr_reader :name
8
+
9
+ def initialize(name, block)
10
+ @name = name
11
+ @block = block
12
+ end
13
+
14
+ def inspect
15
+ "#<#{self.class.name} #{@name}>"
16
+ end
17
+
18
+ def run(results)
19
+ output = @block.(results)
20
+
21
+ perform(output)
22
+ rescue Ector::Multi::Rollback => error
23
+ raise Ector::Multi::OperationFailure.new(self, output, error)
24
+ end
25
+
26
+ def fail_fast?
27
+ self.is_a?(Ector::Multi::Operation::Error)
28
+ end
29
+
30
+ private
31
+
32
+ def perform(_output)
33
+ raise NotImplemented
34
+ end
35
+ end
36
+
37
+ class Create < Base
38
+ def initialize(name, model, attributes_block)
39
+ @model = model
40
+ super(name, attributes_block)
41
+ end
42
+
43
+ private
44
+
45
+ def perform(attributes)
46
+ @model.create!(attributes)
47
+ end
48
+ end
49
+
50
+ class Update < Base
51
+ def initialize(name, instance, attributes_block)
52
+ @instance = instance
53
+ super(name, attributes_block)
54
+ end
55
+
56
+ private
57
+
58
+ def perform(attributes)
59
+ @instance.update!(attributes)
60
+
61
+ @instance
62
+ end
63
+ end
64
+
65
+ class UpdateAll < Base
66
+ def initialize(name, dataset, attributes_block)
67
+ @dataset = dataset
68
+ super(name, attributes_block)
69
+ end
70
+
71
+ private
72
+
73
+ def perform(attributes)
74
+ @dataset.update_all(attributes)
75
+ end
76
+ end
77
+
78
+ class Destroy < Base
79
+ private
80
+
81
+ def perform(model)
82
+ model.destroy
83
+ end
84
+ end
85
+
86
+ class DestroyAll < Base
87
+ private
88
+
89
+ def perform(dataset)
90
+ dataset.destroy_all
91
+ end
92
+ end
93
+
94
+ class Lambda < Base
95
+ private
96
+
97
+ def perform(block_output)
98
+ block_output
99
+ end
100
+ end
101
+
102
+ class Error < Base
103
+ def run(results)
104
+ output = @block.(results)
105
+
106
+ raise Ector::Multi::ControlledFailure.new(self, output)
107
+ end
108
+ end
109
+ end
110
+ end
111
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ector
4
+ class Multi
5
+ class Result
6
+ attr_reader :results, :error
7
+
8
+ def initialize(results, error = nil)
9
+ @results = results
10
+ @error = error
11
+ end
12
+
13
+ def success?
14
+ !@error
15
+ end
16
+
17
+ def failure?
18
+ !success?
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,8 @@
1
+ task :default => :test
2
+
3
+ desc 'Run tests'
4
+ task :test do
5
+ require File.expand_path("./test/helper", File.dirname(__FILE__))
6
+
7
+ Dir["test/**/*_test.rb"].each { |file| load file }
8
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Protest
4
+ require 'protest'
5
+
6
+ Protest.report_with(:progress)
7
+
8
+ def refute(condition, message="Expected condition to be unsatisfied")
9
+ assert !condition, message
10
+ end
11
+
12
+ # ActiveRecord
13
+ require 'active_record'
14
+
15
+ ActiveRecord::Base.establish_connection(adapter: 'sqlite3', database: ':memory:')
16
+ ActiveRecord::Schema.verbose = false
17
+ # ActiveRecord::Base.logger = Logger.new(STDOUT)
18
+
19
+ ActiveRecord::Migration.create_table :dummies, force: true do |t|
20
+ t.integer :user_id
21
+ t.string :name
22
+ end
23
+
24
+ class Dummy < ActiveRecord::Base; end
25
+
26
+ # Load lib
27
+
28
+ require_relative '../lib/ector-multi.rb'
@@ -0,0 +1,399 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'helper'
4
+
5
+ Protest.describe 'Ector::Multi' do
6
+ setup do
7
+ Dummy.delete_all
8
+ @multi = Ector::Multi.new
9
+ end
10
+
11
+ context '#append' do
12
+ setup do
13
+ @left_multi = @multi.run(:left) { |_| }
14
+ end
15
+
16
+ test 'adds operations to the end of the queue' do
17
+ right_multi =
18
+ Ector::Multi.new
19
+ .run(:right) { |_| }
20
+ .run(:far_right) { |_| }
21
+
22
+ @left_multi.append(right_multi)
23
+
24
+ assert_equal @left_multi.to_list, [:left, :right, :far_right]
25
+ end
26
+
27
+ test 'nothing changes when appends an empty multi' do
28
+ @left_multi.append(Ector::Multi.new)
29
+
30
+ assert_equal @left_multi.to_list, [:left]
31
+ end
32
+
33
+ test 'fails when there are duplicated operations' do
34
+ duplicated_multi =
35
+ Ector::Multi.new
36
+ .run(:should_fail) { |_| }
37
+ .run(:left) { |_| }
38
+
39
+ assert_raise(Ector::Multi::UniqueOperationError) { @left_multi.append(duplicated_multi) }
40
+ assert_equal @left_multi.to_list, [:left]
41
+ end
42
+
43
+ test 'returns self' do
44
+ right_multi = Ector::Multi.new.run(:right) { |_| }
45
+
46
+ returned = @left_multi.append(right_multi)
47
+
48
+ assert_equal @left_multi, returned
49
+ end
50
+ end
51
+
52
+ context '#create' do
53
+ test 'adds a Create operation to the queue' do
54
+ @multi.create(:dummy, Dummy, id: 1)
55
+
56
+ assert_equal @multi.to_list, [:dummy]
57
+ assert_equal @multi.operations.first.class, Ector::Multi::Operation::Create
58
+ end
59
+
60
+ test 'fails when the operation name exists' do
61
+ @multi.run(:operation) { |_| }
62
+
63
+ assert_raise(Ector::Multi::UniqueOperationError) { @multi.create(:operation, Dummy, id: 1) }
64
+ end
65
+
66
+ test 'does not create an instance yet' do
67
+ @multi.create(:dummy, Dummy, name: 'New one')
68
+
69
+ assert_equal Dummy.count, 0
70
+ end
71
+
72
+ test 'returns self' do
73
+ assert_equal @multi, @multi.create(:dummy, Dummy, id: 1)
74
+ end
75
+ end
76
+
77
+ context '#destroy' do
78
+ setup do
79
+ @instance = Dummy.create(name: 'Destroyable')
80
+ end
81
+
82
+ test 'adds a Delete operation to the queue' do
83
+ @multi.destroy(:destroy_object, @instance)
84
+
85
+ assert_equal @multi.to_list, [:destroy_object]
86
+ assert_equal @multi.operations.first.class, Ector::Multi::Operation::Destroy
87
+ end
88
+
89
+ test 'fails when the operation name exists' do
90
+ @multi.run(:operation) { |_| }
91
+
92
+ assert_raise(Ector::Multi::UniqueOperationError) { @multi.destroy(:operation, @instance) }
93
+ end
94
+
95
+ test 'does not delete the instance yet' do
96
+ @multi.destroy(:destroy_object, @instance)
97
+
98
+ assert Dummy.find(@instance.id)
99
+ end
100
+
101
+ test 'returns self' do
102
+ assert_equal @multi, @multi.destroy(:dummy, @instance)
103
+ end
104
+ end
105
+
106
+ context '#destroy_all' do
107
+ setup do
108
+ Dummy.create(name: 'Destroyable')
109
+ end
110
+
111
+ test 'adds a Delete operation to the queue' do
112
+ @multi.destroy_all(:destroy_object, Dummy)
113
+
114
+ assert_equal @multi.to_list, [:destroy_object]
115
+ assert_equal @multi.operations.first.class, Ector::Multi::Operation::DestroyAll
116
+ end
117
+
118
+ test 'fails when the operation name exists' do
119
+ @multi.run(:operation) { |_| }
120
+
121
+ assert_raise(Ector::Multi::UniqueOperationError) { @multi.destroy(:operation, @instance) }
122
+ end
123
+
124
+ test 'does not delete the instance yet' do
125
+ @multi.destroy(:destroy_object, @instance)
126
+
127
+ assert Dummy.count, 1
128
+ end
129
+
130
+ test 'returns self' do
131
+ assert_equal @multi, @multi.destroy_all(:dummy, Dummy)
132
+ end
133
+ end
134
+
135
+ context '#error' do
136
+ test 'adds an Error operation to the queue' do
137
+ @multi.error(:fail_fast, 1)
138
+
139
+ assert_equal @multi.to_list, [:fail_fast]
140
+ assert_equal @multi.operations.first.class, Ector::Multi::Operation::Error
141
+ end
142
+
143
+ test 'fails when the operation name exists' do
144
+ @multi.run(:operation) { |_| }
145
+
146
+ assert_raise(Ector::Multi::UniqueOperationError) { @multi.error(:operation, 1) }
147
+ end
148
+
149
+ test 'returns self' do
150
+ assert_equal @multi, @multi.error(:dummy, 1)
151
+ end
152
+ end
153
+
154
+ context '#merge'
155
+
156
+ # test 'returns self' do
157
+ # assert_equal @multi, @multi.merge(:dummy, ???)
158
+ # end
159
+ # end
160
+
161
+ context '#prepend' do
162
+ setup do
163
+ @left_multi = @multi.run(:left) { }
164
+ end
165
+
166
+ test 'adds operations to the beginning of the queue' do
167
+ right_multi =
168
+ Ector::Multi.new
169
+ .run(:right) { }
170
+ .run(:far_right) { }
171
+
172
+ @left_multi.prepend(right_multi)
173
+
174
+ assert_equal @left_multi.to_list, [:right, :far_right, :left]
175
+ end
176
+
177
+ test 'nothing changes when prepends an empty multi' do
178
+ @left_multi.prepend(Ector::Multi.new)
179
+
180
+ assert_equal @left_multi.to_list, [:left]
181
+ end
182
+
183
+ test 'fails when there are duplicated operations' do
184
+ duplicated_multi =
185
+ Ector::Multi.new
186
+ .run(:should_fail) { }
187
+ .run(:left) { }
188
+
189
+ assert_raise(Ector::Multi::UniqueOperationError) { @left_multi.prepend(duplicated_multi) }
190
+ assert_equal @left_multi.to_list, [:left]
191
+ end
192
+
193
+ test 'returns self' do
194
+ right_multi = Ector::Multi.new.run(:right) { }
195
+
196
+ returned = @left_multi.prepend(right_multi)
197
+
198
+ assert_equal @left_multi, returned
199
+ end
200
+ end
201
+
202
+ context '#run' do
203
+ test 'adds an Lambda operation to the queue' do
204
+ @multi.run(:procedure) { |results| results }
205
+
206
+ operation = @multi.operations.first
207
+ assert_equal @multi.to_list, [:procedure]
208
+ assert_equal operation.class, Ector::Multi::Operation::Lambda
209
+ end
210
+
211
+ test 'fails when the operation name exists' do
212
+ @multi.run(:operation) { }
213
+
214
+ assert_raise(Ector::Multi::UniqueOperationError) { @multi.run(:operation) { |_| 1 } }
215
+ end
216
+
217
+ test 'does not run the block yet' do
218
+ call_counter = 0
219
+
220
+ @multi.run(:procedure) { call_counter += 1 }
221
+
222
+ assert_equal call_counter, 0
223
+ end
224
+
225
+ test 'returns self' do
226
+ assert_equal(@multi, @multi.run(:dummy ) { })
227
+ end
228
+ end
229
+
230
+ context '#update' do
231
+ setup do
232
+ @instance = Dummy.create(name: 'Original')
233
+ end
234
+
235
+ test 'adds an Update operation to the queue' do
236
+ @multi.update(:change_name, @instance, name: 'Updated')
237
+
238
+ operation = @multi.operations.first
239
+ assert_equal @multi.to_list, [:change_name]
240
+ assert_equal operation.class, Ector::Multi::Operation::Update
241
+ end
242
+
243
+ test 'fails when the operation name exists' do
244
+ @multi.run(:operation) { |_| }
245
+
246
+ assert_raise(Ector::Multi::UniqueOperationError) { @multi.update(:operation, @instance, id: 1) }
247
+ end
248
+
249
+ test 'does not update the instance yet' do
250
+ @multi.update(:change_name, @instance, name: 'Updated')
251
+
252
+ assert_equal @instance.reload.name, 'Original'
253
+ end
254
+
255
+ test 'returns self' do
256
+ assert_equal @multi, @multi.update(:dummy, @instance, name: 'asd')
257
+ end
258
+ end
259
+
260
+ context '#update_all' do
261
+ setup do
262
+ @instance = Dummy.create(name: 'Original')
263
+ end
264
+
265
+ test 'adds an UpdateAll operation to the queue' do
266
+ @multi.update_all(:change_name, Dummy, name: 'Updated')
267
+
268
+ operation = @multi.operations.first
269
+ assert_equal @multi.to_list, [:change_name]
270
+ assert_equal operation.class, Ector::Multi::Operation::UpdateAll
271
+ end
272
+
273
+ test 'fails when the operation name exists' do
274
+ @multi.run(:operation) { |_| }
275
+
276
+ assert_raise(Ector::Multi::UniqueOperationError) { @multi.update(:operation, @instance, id: 1) }
277
+ end
278
+
279
+ test 'does not update the instance yet' do
280
+ @multi.update_all(:change_name, Dummy, name: 'Updated')
281
+
282
+ assert_equal @instance.reload.name, 'Original'
283
+ end
284
+
285
+ test 'returns self' do
286
+ assert_equal @multi, @multi.update_all(:dummy, Dummy, name: 'asd')
287
+ end
288
+ end
289
+
290
+ context '#commit' do
291
+ test 'trap Ector::Multi::Rollback exceptions' do
292
+ @multi.run(:fail_gracefully) { fail Ector::Multi::Rollback.new 'handled' }
293
+
294
+ result = @multi.commit
295
+
296
+ assert result.failure?
297
+ assert result.error.caused_by.is_a?(Ector::Multi::Rollback)
298
+ end
299
+
300
+ test 'raises any exception that is not a Ector::Multi::Rollback' do
301
+ @multi.run(:fail_violently) { fail 'unhandled' }
302
+
303
+ assert_raise(StandardError) { @multi.commit }
304
+ end
305
+
306
+ test 'stops execution immediately when an exception occurs' do
307
+ @multi
308
+ .run(:op1) { :should_pass }
309
+ .run(:op2) { fail Ector::Multi::Rollback.new 'sorry' }
310
+ .run(:op3) { :never_gets_called }
311
+
312
+ result = @multi.commit
313
+
314
+ assert result.failure?
315
+ assert_equal result.results.keys, [:op1]
316
+ assert_equal result.error.operation.name, :op2
317
+ end
318
+
319
+ test 'fails immediately when an Error Operation is enqueued' do
320
+ @multi
321
+ .run(:op1) { :never_gets_called }
322
+ .run(:op2) { :never_gets_called }
323
+ .run(:op3) { :never_gets_called }
324
+ .error(:fail_fast, 1)
325
+
326
+ result = @multi.commit
327
+
328
+ assert result.failure?
329
+ assert result.results.empty?
330
+ assert_equal result.error.value, 1
331
+ assert_equal result.error.operation.name, :fail_fast
332
+ end
333
+
334
+ test 'creates a transaction for each operation' do
335
+ transaction_ids =
336
+ @multi
337
+ .run(:op1) { ActiveRecord::Base.connection.current_transaction.__id__ }
338
+ .run(:op2) { ActiveRecord::Base.connection.current_transaction.__id__ }
339
+ .run(:op3) { ActiveRecord::Base.connection.current_transaction.__id__ }
340
+ .commit
341
+ .results
342
+ .values
343
+ .uniq
344
+
345
+ assert_equal transaction_ids.size, 3
346
+ end
347
+
348
+ test 'changes are persisted on success' do
349
+ instance = Dummy.create(name: 'Original')
350
+ deletable = Dummy.create(name: 'Deletable')
351
+
352
+ result =
353
+ @multi
354
+ .create(:new_dummy, Dummy, name: 'From multi')
355
+ .update(:rename, instance, name: 'Updated')
356
+ .destroy(:remove, deletable)
357
+ .commit
358
+
359
+ assert result.success?
360
+ assert_equal Dummy.all.map(&:name), ['Updated', 'From multi']
361
+ assert_equal result.results.keys, [:new_dummy, :rename, :remove]
362
+ end
363
+
364
+ test 'changes are rolledback on failure' do
365
+ instance = Dummy.create(name: 'Original')
366
+ deletable = Dummy.create(name: 'Deletable')
367
+
368
+ result =
369
+ @multi
370
+ .create(:new_dummy, Dummy, name: 'From multi')
371
+ .update(:rename, instance, name: 'Updated')
372
+ .destroy(:remove, deletable)
373
+ .run(:force_failure) { fail Ector::Multi::Rollback.new('abort')}
374
+ .commit
375
+
376
+ assert result.failure?
377
+ assert_equal Dummy.all.map(&:name), ['Original', 'Deletable']
378
+ assert_equal result.results.keys, [:new_dummy, :rename, :remove]
379
+ assert_equal result.error.operation.name, :force_failure
380
+ end
381
+
382
+ test 'returns a Ector::Multi::Result' do
383
+ assert @multi.commit.is_a?(Ector::Multi::Result)
384
+ end
385
+ end
386
+
387
+ context '#to_list' do
388
+ test 'returns an ordered list with the name of the operations' do
389
+ nop = -> { }
390
+ empty = @multi
391
+ single = Ector::Multi.new.run(:lambda, &nop)
392
+ multi = Ector::Multi.new.run(:annonymous, &nop).run(:operations, &nop)
393
+
394
+ assert_equal empty.to_list, []
395
+ assert_equal single.to_list, [:lambda]
396
+ assert_equal multi.to_list, [:annonymous, :operations]
397
+ end
398
+ end
399
+ end
@@ -0,0 +1,182 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'helper'
4
+
5
+ Protest.describe 'Operations' do
6
+ setup do
7
+ Dummy.delete_all
8
+ end
9
+
10
+ context 'Create' do
11
+ setup do
12
+ @op = Ector::Multi::Operation::Create.new(:t, Dummy, lambda { |results| { name: 'Emi' }.merge(results) } )
13
+ end
14
+
15
+ test 'it does not fail fast' do
16
+ refute @op.fail_fast?
17
+ end
18
+
19
+ test 'raises an exception when creation fails' do
20
+ assert_raise(StandardError) do
21
+ @op.run(unknown_attribute: 123)
22
+ end
23
+
24
+ assert_equal Dummy.count, 0
25
+ end
26
+
27
+ test 'returns the object created' do
28
+ res = @op.run(name: 'Created')
29
+
30
+ assert_equal res.class, Dummy
31
+ assert_equal res.name, 'Created'
32
+ assert_equal res.id, Dummy.last.id
33
+ end
34
+ end
35
+
36
+ context 'Update' do
37
+ setup do
38
+ @instance = Dummy.create(name: 'Original')
39
+ @op = Ector::Multi::Operation::Update.new(:t, @instance, lambda { |results| { name: 'Updated' }.merge(results) } )
40
+ end
41
+
42
+ test 'it does not fail fast' do
43
+ refute @op.fail_fast?
44
+ end
45
+
46
+ test 'raises an exception when update fails' do
47
+ assert_raise(StandardError) do
48
+ @op.run(unknown_attribute: 123)
49
+ end
50
+
51
+ assert_equal @instance.reload.name, 'Original'
52
+ end
53
+
54
+ test 'returns the object created' do
55
+ res = @op.run(name: 'Updated baby')
56
+
57
+ assert_equal res.class, Dummy
58
+ assert_equal res.name, 'Updated baby'
59
+ assert_equal res.id, @instance.id
60
+ end
61
+ end
62
+
63
+ context 'UpdateAll' do
64
+ setup do
65
+ @instance = Dummy.create(name: 'Original')
66
+ @op = Ector::Multi::Operation::UpdateAll.new(:t, Dummy, lambda { |results| { name: 'Updated' }.merge(results) } )
67
+ end
68
+
69
+ test 'it does not fail fast' do
70
+ refute @op.fail_fast?
71
+ end
72
+
73
+ test 'raises an exception when update fails' do
74
+ assert_raise(StandardError) do
75
+ @op.run(unknown_attribute: 123)
76
+ end
77
+
78
+ assert_equal @instance.reload.name, 'Original'
79
+ end
80
+
81
+ test 'returns how many records were updated' do
82
+ res = @op.run(name: 'Mass update')
83
+
84
+ assert_equal res, 1
85
+ assert_equal @instance.reload.name, 'Mass update'
86
+ end
87
+
88
+ test 'accepts a dataset' do
89
+ Dummy.create(name: 'original 2')
90
+ Dummy.create(name: 'original 3')
91
+
92
+ @op = Ector::Multi::Operation::UpdateAll.new(:t, Dummy.where.not(name: 'original 2'), lambda { |results| results } )
93
+ res = @op.run(name: 'MassUpdated')
94
+
95
+ assert_equal res, 2
96
+ assert_equal Dummy.count, 3
97
+ assert_equal Dummy.all.map(&:name), ['MassUpdated', 'original 2', 'MassUpdated']
98
+ end
99
+ end
100
+
101
+ context 'Destroy' do
102
+ setup do
103
+ @instance = Dummy.create(name: 'Original')
104
+ @op = Ector::Multi::Operation::Destroy.new(:t, lambda { |_| @instance } )
105
+ end
106
+
107
+ test 'it does not fail fast' do
108
+ refute @op.fail_fast?
109
+ end
110
+
111
+ test 'returns the object destroyed' do
112
+ res = @op.run({})
113
+
114
+ assert res.frozen?
115
+ assert_equal res.class, Dummy
116
+ assert_equal res.id, @instance.id
117
+ end
118
+ end
119
+
120
+ context 'DestroyAll' do
121
+ setup do
122
+ @instance = Dummy.create(name: 'Original')
123
+ @op = Ector::Multi::Operation::DestroyAll.new(:t, lambda { |_| Dummy })
124
+ end
125
+
126
+ test 'it does not fail fast' do
127
+ refute @op.fail_fast?
128
+ end
129
+
130
+ test 'returns a list of instances destroyed' do
131
+ res = @op.run(destroy: 'all')
132
+
133
+ assert_equal res, [@instance]
134
+ assert_equal Dummy.count, 0
135
+ assert_raise(ActiveRecord::RecordNotFound) { @instance.reload }
136
+ end
137
+
138
+ test 'accepts a dataset' do
139
+ Dummy.create(name: 'original 2')
140
+ Dummy.create(name: 'original 3')
141
+
142
+ @op = Ector::Multi::Operation::DestroyAll.new(:t, lambda { |_| Dummy.where.not(name: 'original 2') } )
143
+ res = @op.run(destroy: 'with where')
144
+
145
+ assert_equal res.map(&:name), ['Original', 'original 3']
146
+ assert_equal Dummy.count, 1
147
+ assert_equal Dummy.all.map(&:name), ['original 2']
148
+ end
149
+ end
150
+
151
+ context 'Lambda' do
152
+ setup do
153
+ @lambda = Proc.new { [true, 1] }
154
+ @op = Ector::Multi::Operation::Lambda.new(:t, @lambda )
155
+ end
156
+
157
+ test 'it does not fail fast' do
158
+ refute @op.fail_fast?
159
+ end
160
+
161
+ test 'returns the return value of the block' do
162
+ res = @op.run({})
163
+
164
+ assert_equal res, [true, 1]
165
+ assert_equal res, @lambda.call
166
+ end
167
+ end
168
+
169
+ context 'Error' do
170
+ setup do
171
+ @op = Ector::Multi::Operation::Error.new(:t, lambda { |r| 1} )
172
+ end
173
+
174
+ test 'it does not fail fast' do
175
+ assert @op.fail_fast?
176
+ end
177
+
178
+ test 'returns the return value of the block' do
179
+ assert_raise(Ector::Multi::ControlledFailure) { @op.run({}) }
180
+ end
181
+ end
182
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'helper'
4
+
5
+ Protest.describe 'Ector::Multi::Result' do
6
+ test 'attribute readers' do
7
+ results = { operation: 1, procedure: 2 }
8
+ error = Ector::Multi::Rollback.new
9
+ result = Ector::Multi::Result.new(results, error)
10
+
11
+ assert_equal result.results, results
12
+ assert_equal result.error, error
13
+ end
14
+
15
+ context 'succcess?' do
16
+ test 'returns `true` when it succeeded' do
17
+ result = Ector::Multi::Result.new({})
18
+
19
+ assert result.success?
20
+ end
21
+
22
+ test 'returns `false` when there is an error' do
23
+ result = Ector::Multi::Result.new({}, Ector::Multi::Rollback.new)
24
+
25
+ refute result.success?
26
+ end
27
+ end
28
+
29
+ context 'failure?' do
30
+ test 'returns `true` when it failed' do
31
+ result = Ector::Multi::Result.new({}, Ector::Multi::Rollback.new)
32
+
33
+ assert result.failure?
34
+ end
35
+
36
+ test 'returns `false` when there is no error' do
37
+ result = Ector::Multi::Result.new({})
38
+
39
+ refute result.failure?
40
+ end
41
+ end
42
+ end
metadata ADDED
@@ -0,0 +1,103 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: ector-multi
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Emiliano Mancuso
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2018-12-28 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: activerecord
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '5.2'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '5.2'
27
+ - !ruby/object:Gem::Dependency
28
+ name: protest
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: sqlite3
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ description: Grouping multiple DB operations in a single transaction. 100% Inspired
56
+ by Ecto
57
+ email:
58
+ - emiliano.mancuso@gmail.com
59
+ executables: []
60
+ extensions: []
61
+ extra_rdoc_files: []
62
+ files:
63
+ - README.md
64
+ - ector-multi.gemspec
65
+ - lib/ector-multi.rb
66
+ - lib/ector-multi/errors.rb
67
+ - lib/ector-multi/multi.rb
68
+ - lib/ector-multi/operations.rb
69
+ - lib/ector-multi/result.rb
70
+ - rakefile
71
+ - test/helper.rb
72
+ - test/multi_test.rb
73
+ - test/operations_test.rb
74
+ - test/result_test.rb
75
+ homepage: http://github.com/emancu/ector-multi
76
+ licenses:
77
+ - MIT
78
+ metadata: {}
79
+ post_install_message:
80
+ rdoc_options: []
81
+ require_paths:
82
+ - lib
83
+ required_ruby_version: !ruby/object:Gem::Requirement
84
+ requirements:
85
+ - - ">="
86
+ - !ruby/object:Gem::Version
87
+ version: '2.2'
88
+ required_rubygems_version: !ruby/object:Gem::Requirement
89
+ requirements:
90
+ - - ">="
91
+ - !ruby/object:Gem::Version
92
+ version: '0'
93
+ requirements: []
94
+ rubyforge_project:
95
+ rubygems_version: 2.7.8
96
+ signing_key:
97
+ specification_version: 4
98
+ summary: Grouping multiple DB operations in a single transaction.
99
+ test_files:
100
+ - test/helper.rb
101
+ - test/multi_test.rb
102
+ - test/operations_test.rb
103
+ - test/result_test.rb