undestroy 0.0.1 → 0.0.2
Sign up to get free protection for your applications and to get access to all the features.
- data/ARCH.md +84 -0
- data/Gemfile +2 -1
- data/LICENSE +1 -1
- data/README.md +58 -48
- data/lib/undestroy.rb +3 -4
- data/lib/undestroy/archive.rb +36 -0
- data/lib/undestroy/binding.rb +8 -0
- data/lib/undestroy/binding/active_record.rb +63 -0
- data/lib/undestroy/config.rb +54 -0
- data/lib/undestroy/transfer.rb +22 -0
- data/lib/undestroy/version.rb +2 -1
- data/lib/undestroy/with_binding.rb +5 -0
- data/lib/undestroy/without_binding.rb +10 -0
- data/test/fixtures/active_record_models.rb +42 -0
- data/test/fixtures/ar.rb +41 -0
- data/test/fixtures/archive.rb +21 -0
- data/test/helper.rb +42 -1
- data/test/integration/active_record_test.rb +180 -0
- data/test/unit/archive_test.rb +115 -0
- data/test/unit/binding/active_record_test.rb +188 -0
- data/test/unit/config_test.rb +146 -0
- data/test/unit/transfer_test.rb +65 -0
- data/undestroy.gemspec +2 -1
- metadata +29 -6
- data/test/unit/undestroy_test.rb +0 -10
data/ARCH.md
ADDED
@@ -0,0 +1,84 @@
|
|
1
|
+
# Undestroy Model Structure
|
2
|
+
|
3
|
+
This is the basic class structure of this gem. It was designed to be
|
4
|
+
modular and easy to tailor to your specific needs.
|
5
|
+
|
6
|
+
### `Config`
|
7
|
+
|
8
|
+
Holds configuration information for Undestroy. An instance is created
|
9
|
+
globally and serves as defaults for each model using Undestroy. Each
|
10
|
+
model also creates its own instance of Config allowing any model to
|
11
|
+
override any of the globally configurable options.
|
12
|
+
|
13
|
+
To change global defaults use this configuration DSL:
|
14
|
+
|
15
|
+
```ruby
|
16
|
+
Undestroy::Config.configure do |config|
|
17
|
+
config.abstract_class = ArchiveModel
|
18
|
+
config.fields = {
|
19
|
+
:deleted_at => proc { Time.now },
|
20
|
+
:deleted_by_id => proc { User.current.id if User.current }
|
21
|
+
}
|
22
|
+
end
|
23
|
+
```
|
24
|
+
|
25
|
+
This changes the default abstract class from ActiveRecord::Base to a
|
26
|
+
model called ArchiveModel. This also sets the default fields to include
|
27
|
+
a deleted_by_id which automatically sets the current user as the deleter
|
28
|
+
of the record.
|
29
|
+
|
30
|
+
Possible configuration options are listed in the _Usage_ section above.
|
31
|
+
|
32
|
+
### `Archive`
|
33
|
+
|
34
|
+
Map the source model's schema to the archive model's and initiate the
|
35
|
+
transfer through `Transfer`. When `run` is called the Transfer is
|
36
|
+
initialized with a primitive hash mapping the schema to the archive
|
37
|
+
table.
|
38
|
+
|
39
|
+
Initialized with:
|
40
|
+
|
41
|
+
* `:config`: Instance of Undestroy::Config for this model
|
42
|
+
* `:source`: Instance of the source model
|
43
|
+
|
44
|
+
### `Restore`
|
45
|
+
|
46
|
+
Map the archive model's schema to the source model's and initiate the
|
47
|
+
transfer through `Transfer`
|
48
|
+
|
49
|
+
Initialized with:
|
50
|
+
|
51
|
+
* `:config`: Instance of Undestroy::Config for this model
|
52
|
+
* `:archive`: Instance of the archived model
|
53
|
+
|
54
|
+
### `Transfer`
|
55
|
+
|
56
|
+
Handles the actual movement of data from one table to another. This
|
57
|
+
class simply uses the AR interface to create and delete the appropriate
|
58
|
+
records. This can be subclassed to provide enhanced performance or
|
59
|
+
customized behavior for your situation.
|
60
|
+
|
61
|
+
Initialized with:
|
62
|
+
|
63
|
+
* `:fields`: Hash of field names to values to be stored in this table
|
64
|
+
* `:klass`: Target AR model which will be created with these attributes
|
65
|
+
|
66
|
+
### `Binding::ActiveRecord`
|
67
|
+
|
68
|
+
Binds the base functionality to ActiveRecord. It is initialized by the
|
69
|
+
parameters to the `undestroy` class method and contains the method that
|
70
|
+
is bound to the `before_destroy` callback that performs the archiving
|
71
|
+
functions. Any of the code that handles ActiveRecord specific logic
|
72
|
+
lives in here.
|
73
|
+
|
74
|
+
Initialized with: *Config options from above*
|
75
|
+
|
76
|
+
Attributes:
|
77
|
+
|
78
|
+
* `config`: Returns this model's config object
|
79
|
+
* `model`: The AR model this instnace was created for
|
80
|
+
|
81
|
+
Methods:
|
82
|
+
|
83
|
+
* `before_destroy`: Perform the archive process
|
84
|
+
|
data/Gemfile
CHANGED
data/LICENSE
CHANGED
data/README.md
CHANGED
@@ -3,9 +3,9 @@
|
|
3
3
|
Allow copying records to alternate table before destroying an
|
4
4
|
ActiveRecord model for archiving purposes. Data will be mapped
|
5
5
|
one-to-one to the archive table schema. Additional fields can also be
|
6
|
-
configured for additional tracking information. Archive table schema
|
6
|
+
configured for additional tracking information. -Archive table schema
|
7
7
|
will automatically be updated when the parent model's table is migrated
|
8
|
-
through Rails
|
8
|
+
through Rails.- (not yet)
|
9
9
|
|
10
10
|
## Installation
|
11
11
|
|
@@ -21,21 +21,34 @@ Or install it yourself as:
|
|
21
21
|
|
22
22
|
$ gem install undestroy
|
23
23
|
|
24
|
+
You can also tell Undestroy to not extend ActiveRecord when required by
|
25
|
+
using this line in your Gemfile instead:
|
26
|
+
|
27
|
+
gem 'undestroy', :require => 'undestroy/without_binding'
|
28
|
+
|
29
|
+
If you do this you must call
|
30
|
+
`Undestroy::Binding::ActiveRecord.add(MyARSubclass)` where
|
31
|
+
`MYARSubclass` is the class you want Undestroy to extend instead.
|
32
|
+
|
24
33
|
## Usage
|
25
34
|
|
26
|
-
To activate Undestroy on a model, simply call the `
|
35
|
+
To activate Undestroy on a model, simply call the `undestroy` method
|
27
36
|
on the class like so:
|
28
37
|
|
29
|
-
|
30
|
-
|
31
|
-
|
38
|
+
```ruby
|
39
|
+
class Person < ActiveRecord::Base
|
40
|
+
undestroy
|
41
|
+
end
|
42
|
+
```
|
32
43
|
|
33
|
-
This method also
|
44
|
+
This method can also accept an options hash to further customize
|
34
45
|
Undestroy to your needs.
|
35
46
|
|
36
|
-
* `:table_name`: use this table for archiving
|
37
|
-
|
38
|
-
* `:
|
47
|
+
* `:table_name`: use this table for archiving (Defaults to the
|
48
|
+
source class's table_name prefixed with "archive_").
|
49
|
+
* `:abstract_class`: use this as the base class for the target_class
|
50
|
+
specify an alternate for custom extensions / DB connections (defaults
|
51
|
+
to ActiveRecord::Base)
|
39
52
|
* `:fields`: Specify a hash of fields to values for additional fields
|
40
53
|
you would like to include on the archive table -- lambdas will be
|
41
54
|
called with the instance being destroyed and returned value will be
|
@@ -43,44 +56,41 @@ Undestroy to your needs.
|
|
43
56
|
* `:migrate`: Should Undestroy migrate the archive table together with
|
44
57
|
this model's table (default: true)
|
45
58
|
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
class simply uses the AR interface to create and delete the appropriate
|
82
|
-
records. This can be subclassed to provide enhanced performance or
|
83
|
-
customized behavior for your situation.
|
59
|
+
Internal Options (for advanced users):
|
60
|
+
|
61
|
+
* `:source_class`: the AR model of the originating data. Set
|
62
|
+
automatically to class `undestroy` method is called on.
|
63
|
+
* `:target_class`: use this AR model for archiving. Set automatically
|
64
|
+
to dynamically generated class based on `archive_*` options.
|
65
|
+
* `internals`: internal classes to use for archival process. Possible
|
66
|
+
keys are `:archive`, `:transfer` and `:restore`. Defaults to
|
67
|
+
corresponding internal classes. Customize to your heart's content.
|
68
|
+
|
69
|
+
```
|
70
|
+
$ person = Person.find(1)
|
71
|
+
$ person.destroy
|
72
|
+
# => Inserts person data into archive_people table
|
73
|
+
# => Deletes person data from people table
|
74
|
+
```
|
75
|
+
|
76
|
+
## Configuring
|
77
|
+
|
78
|
+
You can specify custom global configurations for Undestroy through a
|
79
|
+
configuration block in your application initializer:
|
80
|
+
|
81
|
+
```ruby
|
82
|
+
Undestroy::Config.configure do |config|
|
83
|
+
config.abstract_class = ArchiveModelBase
|
84
|
+
config.fields = {
|
85
|
+
:deleted_at => proc { Time.now },
|
86
|
+
:deleted_by_id => proc { User.current.id if User.current }
|
87
|
+
}
|
88
|
+
end
|
89
|
+
```
|
90
|
+
|
91
|
+
Options set in this block will be the default for all models with
|
92
|
+
undestroy activated. They can be overriden with options passed to the
|
93
|
+
`undestroy` method
|
84
94
|
|
85
95
|
## Contributing
|
86
96
|
|
data/lib/undestroy.rb
CHANGED
@@ -0,0 +1,36 @@
|
|
1
|
+
|
2
|
+
class Undestroy::Archive
|
3
|
+
attr_accessor :source, :config, :transfer
|
4
|
+
|
5
|
+
def initialize(args={})
|
6
|
+
validate_arguments(args)
|
7
|
+
|
8
|
+
self.source = args[:source]
|
9
|
+
self.config = args[:config]
|
10
|
+
self.transfer = args[:transfer]
|
11
|
+
end
|
12
|
+
|
13
|
+
def transfer
|
14
|
+
@transfer ||= self.config.internals[:transfer].new(
|
15
|
+
:klass => self.config.target_class,
|
16
|
+
:fields => archive_fields
|
17
|
+
)
|
18
|
+
end
|
19
|
+
|
20
|
+
def run
|
21
|
+
transfer.run
|
22
|
+
end
|
23
|
+
|
24
|
+
protected
|
25
|
+
|
26
|
+
def archive_fields
|
27
|
+
self.config.primitive_fields(self.source).merge(self.source.attributes)
|
28
|
+
end
|
29
|
+
|
30
|
+
def validate_arguments(args)
|
31
|
+
unless (args.keys & [:source, :config]).size == 2
|
32
|
+
raise ArgumentError, ":source and :config are required keys"
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
@@ -0,0 +1,63 @@
|
|
1
|
+
require 'active_record'
|
2
|
+
|
3
|
+
class Undestroy::Binding::ActiveRecord
|
4
|
+
attr_accessor :config, :model
|
5
|
+
|
6
|
+
def initialize(model, options={})
|
7
|
+
ensure_is_ar! model
|
8
|
+
|
9
|
+
self.model = model
|
10
|
+
self.config = Undestroy::Config.config.merge(options)
|
11
|
+
|
12
|
+
set_defaults
|
13
|
+
end
|
14
|
+
|
15
|
+
def before_destroy(instance)
|
16
|
+
config.internals[:archive].new(:config => config, :source => instance).run
|
17
|
+
end
|
18
|
+
|
19
|
+
protected
|
20
|
+
|
21
|
+
def set_defaults
|
22
|
+
self.config.source_class = self.model
|
23
|
+
self.config.table_name ||= table_prefix + self.model.table_name if self.model.respond_to?(:table_name)
|
24
|
+
self.config.target_class ||= create_target_class
|
25
|
+
ensure_is_ar! self.config.target_class
|
26
|
+
end
|
27
|
+
|
28
|
+
# Builds a dynamic AR class representing the archival table
|
29
|
+
def create_target_class
|
30
|
+
Class.new(self.config.abstract_class || ActiveRecord::Base).tap do |target_class|
|
31
|
+
target_class.table_name = self.config.table_name
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def table_prefix
|
36
|
+
"archive_"
|
37
|
+
end
|
38
|
+
|
39
|
+
def ensure_is_ar!(klass)
|
40
|
+
raise ArgumentError, "#{klass.inspect} must be an ActiveRecord model" unless is_ar?(klass)
|
41
|
+
end
|
42
|
+
|
43
|
+
def is_ar?(klass)
|
44
|
+
klass.is_a?(Class) && klass.ancestors.include?(ActiveRecord::Base)
|
45
|
+
end
|
46
|
+
|
47
|
+
# Add binding to the given class if it doesn't already have it
|
48
|
+
def self.add(klass=ActiveRecord::Base)
|
49
|
+
klass.class_eval do
|
50
|
+
class_attribute :undestroy_model_binding, :instance_writer => false
|
51
|
+
|
52
|
+
def self.undestroy(options={})
|
53
|
+
before_destroy do
|
54
|
+
self.undestroy_model_binding.before_destroy(self) if undestroy_model_binding
|
55
|
+
end unless self.undestroy_model_binding
|
56
|
+
self.undestroy_model_binding = Undestroy::Binding::ActiveRecord.new(self, options)
|
57
|
+
end
|
58
|
+
|
59
|
+
end unless klass.respond_to?(:undestroy_model_binding)
|
60
|
+
end
|
61
|
+
|
62
|
+
end
|
63
|
+
|
@@ -0,0 +1,54 @@
|
|
1
|
+
class Undestroy::Config
|
2
|
+
OPTIONS = [
|
3
|
+
:table_name, :abstract_class, :fields, :migrate,
|
4
|
+
:source_class, :target_class, :internals
|
5
|
+
]
|
6
|
+
attr_accessor *OPTIONS
|
7
|
+
|
8
|
+
def initialize(options={})
|
9
|
+
self.migrate = true
|
10
|
+
self.fields = {
|
11
|
+
:deleted_at => proc { Time.now }
|
12
|
+
}
|
13
|
+
self.internals = {
|
14
|
+
:archive => Undestroy::Archive,
|
15
|
+
:transfer => Undestroy::Transfer,
|
16
|
+
}
|
17
|
+
|
18
|
+
options.each do |key, value|
|
19
|
+
self[key] = value
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def [](key)
|
24
|
+
self.send(key) if OPTIONS.include?(key)
|
25
|
+
end
|
26
|
+
|
27
|
+
def []=(key, value)
|
28
|
+
self.send("#{key}=", value) if OPTIONS.include?(key)
|
29
|
+
end
|
30
|
+
|
31
|
+
def to_hash
|
32
|
+
OPTIONS.inject({}) { |hash, key| hash.merge(key => self[key]) }
|
33
|
+
end
|
34
|
+
|
35
|
+
def merge(object)
|
36
|
+
self.class.new(self.to_hash.merge(object.to_hash))
|
37
|
+
end
|
38
|
+
|
39
|
+
def primitive_fields(object)
|
40
|
+
self.fields.inject({}) do |hash, (key, val)|
|
41
|
+
hash.merge(key => val.is_a?(Proc) ? val.call(object) : val)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def self.configure
|
46
|
+
yield(config) if block_given?
|
47
|
+
end
|
48
|
+
|
49
|
+
def self.config
|
50
|
+
@config ||= self.new
|
51
|
+
end
|
52
|
+
|
53
|
+
end
|
54
|
+
|
@@ -0,0 +1,22 @@
|
|
1
|
+
|
2
|
+
class Undestroy::Transfer
|
3
|
+
attr_accessor :target
|
4
|
+
|
5
|
+
def initialize(args={})
|
6
|
+
raise ArgumentError, ":klass option required" unless args[:klass]
|
7
|
+
args[:fields] ||= {}
|
8
|
+
|
9
|
+
self.target = args[:klass].new
|
10
|
+
|
11
|
+
# Set instance values directly to avoid AR's filtering of protected fields
|
12
|
+
args[:fields].each do |field, value|
|
13
|
+
self.target[field] = value
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
def run
|
18
|
+
self.target.save
|
19
|
+
end
|
20
|
+
|
21
|
+
end
|
22
|
+
|
data/lib/undestroy/version.rb
CHANGED