undestroy 0.0.2 → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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
+