ector-multi 0.0.1

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.
@@ -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