undestroy 0.0.2 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (32) hide show
  1. data/ARCH.md +13 -24
  2. data/README.md +72 -27
  3. data/lib/undestroy/binding.rb +1 -0
  4. data/lib/undestroy/binding/active_record.rb +53 -11
  5. data/lib/undestroy/binding/active_record/migration_statement.rb +118 -0
  6. data/lib/undestroy/binding/active_record/restorable.rb +28 -0
  7. data/lib/undestroy/config.rb +32 -8
  8. data/lib/undestroy/config/field.rb +20 -0
  9. data/lib/undestroy/restore.rb +44 -0
  10. data/lib/undestroy/version.rb +1 -1
  11. data/lib/undestroy/without_binding.rb +1 -0
  12. data/test/fixtures/ar.rb +8 -4
  13. data/test/fixtures/load_test/models1/test_model001.rb +2 -0
  14. data/test/fixtures/load_test/models1/test_model002.rb +2 -0
  15. data/test/fixtures/load_test/models1/test_module001.rb +2 -0
  16. data/test/fixtures/load_test/models1/test_module001/test_model003.rb +2 -0
  17. data/test/fixtures/load_test/models2/test_model001.rb +2 -0
  18. data/test/fixtures/load_test/models2/test_model002.rb +2 -0
  19. data/test/fixtures/load_test/models2/test_module001.rb +2 -0
  20. data/test/fixtures/load_test/models2/test_module001/test_model003.rb +2 -0
  21. data/test/helper.rb +12 -0
  22. data/test/helpers/model_loading.rb +20 -0
  23. data/test/integration/active_record_test.rb +50 -1
  24. data/test/irb.rb +2 -0
  25. data/test/unit/archive_test.rb +1 -1
  26. data/test/unit/binding/active_record/migration_statement_test.rb +306 -0
  27. data/test/unit/binding/active_record/restorable_test.rb +132 -0
  28. data/test/unit/binding/active_record_test.rb +187 -19
  29. data/test/unit/config/field_test.rb +86 -0
  30. data/test/unit/config_test.rb +114 -8
  31. data/test/unit/restore_test.rb +95 -0
  32. metadata +35 -3
@@ -1,23 +1,34 @@
1
1
  class Undestroy::Config
2
2
  OPTIONS = [
3
- :table_name, :abstract_class, :fields, :migrate,
4
- :source_class, :target_class, :internals
3
+ :table_name, :abstract_class, :fields, :migrate, :indexes, :prefix,
4
+ :source_class, :target_class, :internals, :model_paths
5
5
  ]
6
6
  attr_accessor *OPTIONS
7
7
 
8
8
  def initialize(options={})
9
+ self.indexes = false
9
10
  self.migrate = true
10
- self.fields = {
11
- :deleted_at => proc { Time.now }
12
- }
11
+ self.prefix = "archive_"
12
+ self.fields = {}
13
+ self.model_paths = []
13
14
  self.internals = {
14
15
  :archive => Undestroy::Archive,
15
16
  :transfer => Undestroy::Transfer,
17
+ :restore => Undestroy::Restore
16
18
  }
17
19
 
20
+ add_field :deleted_at, :datetime do |instance|
21
+ Time.now
22
+ end
23
+
24
+ # Default for Rails apps
25
+ self.model_paths << Rails.root.join('app', 'models') if defined?(Rails)
26
+
18
27
  options.each do |key, value|
19
- self[key] = value
28
+ self[key] = value.duplicable? ? value.dup : value
20
29
  end
30
+
31
+ self.class.catalog << self
21
32
  end
22
33
 
23
34
  def [](key)
@@ -37,11 +48,15 @@ class Undestroy::Config
37
48
  end
38
49
 
39
50
  def primitive_fields(object)
40
- self.fields.inject({}) do |hash, (key, val)|
41
- hash.merge(key => val.is_a?(Proc) ? val.call(object) : val)
51
+ self.fields.inject({}) do |hash, (key, field)|
52
+ hash.merge(key => field.value(object))
42
53
  end
43
54
  end
44
55
 
56
+ def add_field(name, *args, &block)
57
+ self.fields[name.to_sym] = Field.new(name, *args, &block)
58
+ end
59
+
45
60
  def self.configure
46
61
  yield(config) if block_given?
47
62
  end
@@ -50,5 +65,14 @@ class Undestroy::Config
50
65
  @config ||= self.new
51
66
  end
52
67
 
68
+ def self.catalog
69
+ @@catalog ||= []
70
+ end
71
+
72
+ def self.reset_catalog
73
+ @@catalog = []
74
+ end
75
+
53
76
  end
54
77
 
78
+ require 'undestroy/config/field'
@@ -0,0 +1,20 @@
1
+ class Undestroy::Config::Field
2
+
3
+ attr_accessor :name, :type, :raw_value
4
+
5
+ def <=>(b)
6
+ name.to_s <=> b.name.to_s
7
+ end
8
+
9
+ def initialize(name, type, value=nil, &block)
10
+ self.name = name
11
+ self.type = type
12
+ self.raw_value = block || value || raise(ArgumentError, "Must pass a value or block")
13
+ end
14
+
15
+ def value(*args)
16
+ raw_value.is_a?(Proc) ? raw_value.call(*args) : raw_value
17
+ end
18
+
19
+ end
20
+
@@ -0,0 +1,44 @@
1
+
2
+ class Undestroy::Restore
3
+
4
+ attr_accessor :target, :config, :transfer
5
+
6
+ def initialize(args={})
7
+ validate_arguments(args)
8
+
9
+ self.target = args[:target]
10
+ self.config = args[:config]
11
+ self.transfer = args[:transfer]
12
+ end
13
+
14
+ def transfer
15
+ @transfer ||= config.internals[:transfer].new(
16
+ :klass => config.source_class,
17
+ :fields => transfer_fields
18
+ )
19
+ end
20
+
21
+ def run
22
+ transfer.run
23
+ end
24
+
25
+ protected
26
+
27
+ def validate_arguments(args)
28
+ unless (args.keys & [:target, :config]).size == 2
29
+ raise ArgumentError, ":target and :config are required keys"
30
+ end
31
+ end
32
+
33
+ def transfer_fields
34
+ self.target.attributes.inject({}) do |hash, (key, value)|
35
+ if config.fields.keys.include?(key.to_sym)
36
+ hash
37
+ else
38
+ hash.merge(key => value)
39
+ end
40
+ end
41
+ end
42
+
43
+ end
44
+
@@ -1,4 +1,4 @@
1
1
  module Undestroy
2
- VERSION = "0.0.2"
2
+ VERSION = "0.1.0"
3
3
  end
4
4
 
@@ -2,6 +2,7 @@ require "undestroy/version"
2
2
  require "undestroy/config"
3
3
  require "undestroy/transfer"
4
4
  require "undestroy/archive"
5
+ require "undestroy/restore"
5
6
  require "undestroy/binding"
6
7
 
7
8
  module Undestroy
@@ -7,21 +7,25 @@ class Undestroy::Test::Fixtures::ARFixture
7
7
 
8
8
  def initialize(attributes={})
9
9
  @saved = false
10
- self.attributes = attributes.dup
10
+ self.attributes = HashWithIndifferentAccess.new(attributes)
11
11
  self.attributes.delete(:id)
12
12
  end
13
13
 
14
14
  def [](key)
15
- self.attributes[key.to_sym]
15
+ @attributes[key]
16
16
  end
17
17
 
18
18
  def []=(key, val)
19
- self.attributes[key.to_sym] = val
19
+ @attributes[key] = val
20
20
  end
21
21
 
22
22
  # Method missing won't catch this one
23
23
  def id
24
- self.attributes[:id]
24
+ self[:id]
25
+ end
26
+
27
+ def attributes
28
+ @attributes.stringify_keys
25
29
  end
26
30
 
27
31
  def save
@@ -0,0 +1,2 @@
1
+ class TestModel001
2
+ end
@@ -0,0 +1,2 @@
1
+ class TestModel002
2
+ end
@@ -0,0 +1,2 @@
1
+ module TestModule001
2
+ end
@@ -0,0 +1,2 @@
1
+ class TestModule001::TestModel003
2
+ end
@@ -0,0 +1,2 @@
1
+ class TestModel001
2
+ end
@@ -0,0 +1,2 @@
1
+ class TestModel002
2
+ end
@@ -0,0 +1,2 @@
1
+ module TestModule001
2
+ end
@@ -0,0 +1,2 @@
1
+ class TestModule001::TestModel003
2
+ end
@@ -15,6 +15,10 @@ module Undestroy::Test
15
15
 
16
16
  class Base < Assert::Context
17
17
 
18
+ teardown do
19
+ Undestroy::Config.reset_catalog
20
+ end
21
+
18
22
  teardown_once do
19
23
  `rm -f tmp/*.db`
20
24
  end
@@ -31,6 +35,10 @@ module Undestroy::Test
31
35
  establish_connection 'alt'
32
36
  end
33
37
 
38
+ module Helpers
39
+ autoload :ModelLoading, 'test/helpers/model_loading'
40
+ end
41
+
34
42
  module Integration
35
43
  end
36
44
 
@@ -39,5 +47,9 @@ module Undestroy::Test
39
47
  autoload :ARFixture, 'test/fixtures/ar'
40
48
  autoload :Archive, 'test/fixtures/archive'
41
49
  end
50
+
51
+ def self.fixtures_path(*paths)
52
+ File.join(File.expand_path(File.join(File.dirname(__FILE__), 'fixtures')), *paths)
53
+ end
42
54
  end
43
55
 
@@ -0,0 +1,20 @@
1
+ module Undestroy::Test::Helpers::ModelLoading
2
+
3
+ def assert_loads_models(path, &block)
4
+ require 'active_support/dependencies'
5
+ ActiveSupport::Dependencies.hook!
6
+ ActiveSupport::Dependencies.autoload_paths += [path]
7
+
8
+ block.call
9
+
10
+ assert defined?(TestModel001), "TestModel001 did not load"
11
+ assert defined?(TestModel002), "TestModel002 did not load"
12
+ assert defined?(TestModule001), "TestModule001 did not load"
13
+ assert defined?(TestModule001::TestModel003), "Testmodule001::TestModel003 did not load"
14
+ ensure
15
+ ActiveSupport::Dependencies.autoload_paths -= [path]
16
+ ActiveSupport::Dependencies.remove_unloadable_constants!
17
+ ActiveSupport::Dependencies.unhook!
18
+ end
19
+
20
+ end
@@ -16,7 +16,12 @@ module Undestroy::Test::Integration::ActiveRecordTest
16
16
  desc 'extensions'
17
17
 
18
18
  should "add extensions to AR" do
19
- assert_respond_to :undestroy_model_binding, ActiveRecord::Base
19
+ assert_respond_to :undestroy, ActiveRecord::Base
20
+ end
21
+
22
+ should "add alias method chain in method_missing on AR::Migration" do
23
+ assert_respond_to :method_missing_with_undestroy, ActiveRecord::Migration.new
24
+ assert ActiveRecord::Migration.new.method(:method_missing_without_undestroy)
20
25
  end
21
26
  end
22
27
 
@@ -79,6 +84,50 @@ module Undestroy::Test::Integration::ActiveRecordTest
79
84
  assert (Time.now - archive.deleted_at) < 1.second
80
85
  assert_equal 0, @model.all.size
81
86
  end
87
+
88
+ should "restore an archived record removing the archive" do
89
+ @model.undestroy
90
+ @model.create(:name => "Fart")
91
+ original = @model.first
92
+ original.destroy
93
+
94
+ assert_equal 0, @model.count
95
+ @model.restore(1)
96
+
97
+ assert_equal 0, @model.archived.count
98
+ assert_equal 1, @model.count
99
+ assert_equal 1, @model.first.id
100
+ assert_equal "Fart", @model.first.name
101
+ end
102
+
103
+ should "restore an archived record and leave the archive when restore_copy called" do
104
+ @model.undestroy
105
+ @model.create(:name => "Fart")
106
+ original = @model.first
107
+ original.destroy
108
+
109
+ assert_equal 0, @model.count
110
+ @model.archived.first.restore_copy
111
+
112
+ assert_equal 1, @model.archived.count
113
+ assert_equal 1, @model.count
114
+ end
115
+
116
+ should "restore a relation of items when restore_all called" do
117
+ @model.undestroy
118
+ @model.create(:name => "Bobby")
119
+ @model.create(:name => "Billy")
120
+ @model.create(:name => "Jan")
121
+
122
+ assert_equal 3, @model.count
123
+ @model.destroy_all
124
+ assert_equal 0, @model.count
125
+
126
+ @model.archived.where("name LIKE ?", "B%").restore_all
127
+
128
+ assert_equal 2, @model.count
129
+ assert_equal 1, @model.archived.count
130
+ end
82
131
  end
83
132
 
84
133
  class BasicModelWithDifferentBase < Base
@@ -0,0 +1,2 @@
1
+ require 'assert/setup'
2
+
@@ -75,7 +75,7 @@ module Undestroy::Archive::Test
75
75
 
76
76
  should "eval lambdas with source instance as argument" do
77
77
  val = nil
78
- @archive.config.fields[:test] = proc { |arg| val = arg; "FOO" }
78
+ @archive.config.add_field(:test, :string) { |arg| val = arg; "FOO" }
79
79
  target = @archive.transfer.target
80
80
  assert_equal @archive.source, val
81
81
  assert_equal "FOO", target.attributes[:test]
@@ -0,0 +1,306 @@
1
+ require 'assert'
2
+
3
+ module Undestroy::Binding::ActiveRecord::MigrationStatement::Test
4
+
5
+ class Base < Undestroy::Test::Base
6
+ desc 'Binding::ActiveRecord::MigrationStatement class'
7
+ subject { @subject_class }
8
+
9
+ setup do
10
+ @subject_class = Undestroy::Binding::ActiveRecord::MigrationStatement
11
+ @catalog = Undestroy::Config.catalog
12
+
13
+ @source_class = Class.new(Undestroy::Test::ARMain)
14
+ end
15
+
16
+ def config
17
+ @source_class.undestroy_model_binding.config
18
+ end
19
+
20
+ end
21
+
22
+ class BasicInstance < Base
23
+ desc 'basic instance'
24
+ subject { @subject_class.new :foo }
25
+
26
+ should have_accessor :method_name, :arguments, :block
27
+ end
28
+
29
+ class AddClassMethod < Base
30
+ desc 'add class method'
31
+
32
+ setup do
33
+ @klass = Class.new do
34
+ @@method_missing_calls = []
35
+
36
+ def method_missing(*args, &block)
37
+ @@method_missing_calls << [args, block]
38
+ end
39
+
40
+ def self.method_missing_calls
41
+ @@method_missing_calls
42
+ end
43
+ end
44
+ end
45
+
46
+ should "add method method_missing_with_undestroy" do
47
+ subject.add(@klass)
48
+ assert_respond_to :method_missing_with_undestroy, @klass.new
49
+ end
50
+
51
+ should "alias original method_missing as method_missing_without_undestroy" do
52
+ subject.add(@klass)
53
+ assert @klass.new.method(:method_missing_without_undestroy)
54
+ end
55
+
56
+ should "always call original method_missing" do
57
+ subject.add(@klass)
58
+ @klass.new.foo(:bar) { }
59
+ assert_equal 1, @klass.method_missing_calls.size
60
+ assert_equal [:foo, :bar], @klass.method_missing_calls[0].first
61
+ assert_instance_of Proc, @klass.method_missing_calls[0].last
62
+ end
63
+
64
+ should "always create an instance of MethodStatement and call run! if run?" do
65
+ @source_class.table_name = 'source'
66
+ @source_class.undestroy
67
+ subject.add(@klass)
68
+ @klass.new.add_column :source, :foo, :string
69
+ assert_equal 2, @klass.method_missing_calls.size
70
+ assert_equal [:add_column, 'archive_source', :foo, :string], @klass.method_missing_calls[1].first
71
+ end
72
+ end
73
+
74
+ class InitMethod < Base
75
+ desc 'init method'
76
+ include Undestroy::Test::Helpers::ModelLoading
77
+
78
+ should "set required arg1 to method_name attr" do
79
+ obj = subject.new :method
80
+ assert_equal :method, obj.method_name
81
+ end
82
+
83
+ should "set remaining args to arguments attr as Array" do
84
+ obj = subject.new :method, :arg1, 'arg2', 3, :four => :val1, :five => :val2
85
+ assert_equal [:arg1, 'arg2', 3, { :four => :val1, :five => :val2 }], obj.arguments
86
+ end
87
+
88
+ should "set optional block to block attr" do
89
+ block = proc { "FOOO" }
90
+ obj = subject.new :method, &block
91
+ assert_equal block, obj.block
92
+ assert_equal "FOOO", obj.block.call
93
+ end
94
+
95
+ should "call load_models on all Config.config.model_paths if not loaded yet" do
96
+ path = Undestroy::Test.fixtures_path('load_test', 'models2')
97
+ Undestroy::Config.config.model_paths = [path]
98
+ assert_loads_models(path) do
99
+ subject.new :method
100
+ end
101
+ Undestroy::Config.config.model_paths = []
102
+ end
103
+ end
104
+
105
+ class SourceTableNameMethod < Base
106
+ desc 'source_table_name method'
107
+
108
+ should "return arguments[0]" do
109
+ obj = subject.new :method, 'table_name', 1
110
+ assert_equal 'table_name', obj.source_table_name
111
+ end
112
+ end
113
+
114
+ class TargetTableNameMethod < Base
115
+ desc 'target_table_name method'
116
+
117
+ should 'return "archive_#{original_table_name}" when :table_name config not set' do
118
+ @source_class.table_name = 'poop'
119
+ @source_class.undestroy
120
+ assert_equal 'archive_poop', subject.new(:method, 'poop').target_table_name
121
+ end
122
+
123
+ should 'return :table_name config value when set' do
124
+ @source_class.table_name = 'poop'
125
+ @source_class.undestroy :table_name => 'old_poop_table'
126
+ assert_equal 'old_poop_table', subject.new(:method, 'poop').target_table_name
127
+ end
128
+ end
129
+
130
+ class ConfigMethod < Base
131
+ desc 'config method'
132
+
133
+ should "fetch config object for the given table name from Undestroy::Config.catalog" do
134
+ @source_class.table_name = 'foobar'
135
+ @source_class.undestroy
136
+ assert_equal config, subject.new(:method, 'foobar').config
137
+ end
138
+ end
139
+
140
+ class SchemaActionMethod < Base
141
+ desc 'schema_action? method'
142
+
143
+ should "return true if method_name is a schema modification method" do
144
+ [
145
+ :create_table, :drop_table, :rename_table,
146
+ :add_column, :rename_column, :change_column, :remove_column
147
+ ].each do |method|
148
+ obj = subject.new method
149
+ assert obj.schema_action?
150
+ end
151
+ end
152
+
153
+ should "return false otherwise" do
154
+ obj = subject.new :method
155
+ assert_not obj.schema_action?
156
+ end
157
+ end
158
+
159
+ class IndexActionMethod < Base
160
+ desc 'index_action? method'
161
+
162
+ should "return true if method_name is an index modification method" do
163
+ [
164
+ :add_index, :remove_index
165
+ ].each do |method|
166
+ obj = subject.new method
167
+ assert obj.index_action?
168
+ end
169
+ end
170
+
171
+ should "return false otherwise" do
172
+ obj = subject.new :method
173
+ assert_not obj.index_action?
174
+ end
175
+ end
176
+
177
+ class RunQueryMethod < Base
178
+ desc 'run? method'
179
+
180
+ should "always return false if arguments are empty" do
181
+ obj = subject.new :method
182
+ assert_not obj.run?
183
+ end
184
+
185
+ should "always return false if no configuration is present for this *source* table name" do
186
+ @source_class.table_name = 'bar'
187
+ @source_class.undestroy
188
+ obj = subject.new :method, 'foo'
189
+ assert_not obj.run?
190
+ end
191
+
192
+ should "return true if :migrate is configured and schema_action? is true" do
193
+ @source_class.table_name = 'bar'
194
+ @source_class.undestroy
195
+ obj = subject.new :add_column, :bar, :name, :string
196
+ assert obj.schema_action?
197
+ assert obj.run?
198
+ end
199
+
200
+ should "return false if :migrate is configured and schema_action? is false" do
201
+ @source_class.table_name = 'bar'
202
+ @source_class.undestroy
203
+ obj = subject.new :method, :bar, :name, :string
204
+ assert_not obj.run?
205
+ end
206
+
207
+ should "return false if :migrate not configured for this table" do
208
+ @source_class.table_name = 'bar'
209
+ @source_class.undestroy :migrate => false
210
+ obj = subject.new :add_column, :bar, :name, :string
211
+ assert obj.schema_action?
212
+ assert_not obj.run?
213
+ end
214
+
215
+ should "return true if :index is configured and index_action? is true" do
216
+ @source_class.table_name = 'bar'
217
+ @source_class.undestroy :indexes => true
218
+ obj = subject.new :add_index, :bar
219
+ assert obj.index_action?
220
+ assert obj.run?
221
+ end
222
+
223
+ should "return false if :index is configured and index_action? is false" do
224
+ @source_class.table_name = 'bar'
225
+ @source_class.undestroy :indexes => true
226
+ obj = subject.new :method, :bar
227
+ assert_not obj.index_action?
228
+ assert_not obj.run?
229
+ end
230
+
231
+ should "return false if :index is not configured for this table" do
232
+ @source_class.table_name = 'bar'
233
+ @source_class.undestroy
234
+ obj = subject.new :add_index, :bar
235
+ assert obj.index_action?
236
+ assert_not obj.run?
237
+ end
238
+
239
+ # We will not rename a table that has been configured to a specific name
240
+ should "return false if :method_name is rename_table and :table_name configuration is set explicitly" do
241
+ @source_class.table_name = 'bar'
242
+ @source_class.undestroy :table_name => 'old_bar'
243
+ obj = subject.new :rename_table, :bar, :baz
244
+ assert_not obj.run?
245
+ end
246
+ end
247
+
248
+ class TargetArgsMethod < Base
249
+ desc 'target_arguments method'
250
+
251
+ setup do
252
+ @source_class.table_name = 'source'
253
+ @source_class.undestroy
254
+ end
255
+
256
+ should "leave original arguments alone" do
257
+ obj = subject.new :add_column, :source, :foo, :string
258
+ args = obj.arguments.dup
259
+ obj.target_arguments
260
+ assert_equal args, obj.arguments
261
+ end
262
+
263
+ should "substitute source table_name for target table_name" do
264
+ obj = subject.new :add_column, :source, :foo, :string
265
+ assert_equal ['archive_source', :foo, :string], obj.target_arguments
266
+ end
267
+
268
+ should "substitute arg[1] for target table_name on rename_table method" do
269
+ obj = subject.new :rename_table, :source, :new_source
270
+ assert_equal ['archive_source', 'archive_new_source'], obj.target_arguments
271
+ end
272
+ end
273
+
274
+ class RunBangMethod < Base
275
+ desc 'run! method'
276
+
277
+ setup do
278
+ @source_class.table_name = 'source'
279
+ @source_class.undestroy
280
+ end
281
+
282
+ should "accept callable argument to run on and call with (method_name, target_args, block)" do
283
+ called = false
284
+ callable = proc { |*args, &block| called = [args, block] }
285
+ block = proc { }
286
+ obj = subject.new :add_column, :source, :foo, :string, &block
287
+ obj.run!(callable)
288
+ assert_equal [obj.method_name, obj.target_arguments, block].flatten, called.flatten
289
+ end
290
+
291
+ should "run additional add_column calls for all config.fields on :create_table method" do
292
+ config.add_field :deleted_by_id, :integer, 1
293
+ calls = []
294
+ callable = proc { |*args, &block| calls << [args, block] }
295
+ obj = subject.new :create_table, :source
296
+ obj.run!(callable)
297
+ assert_equal 3, calls.size
298
+ assert_equal [:create_table, 'archive_source', nil], calls[0].flatten
299
+ assert_equal [:add_column, 'archive_source', :deleted_at, :datetime, nil], calls[1].flatten
300
+ assert_equal [:add_column, 'archive_source', :deleted_by_id, :integer, nil], calls[2].flatten
301
+ end
302
+
303
+ end
304
+
305
+ end
306
+