grosser-ar_merge 0.1.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.
- data/CHANGELOG +1 -0
- data/README.markdown +52 -0
- data/Rakefile +34 -0
- data/ar_merge.gemspec +34 -0
- data/init.rb +1 -0
- data/lib/ar_merge.rb +55 -0
- data/spec/ar_merge_spec.rb +98 -0
- data/spec/setup_test_model.rb +37 -0
- data/spec/spec_helper.rb +39 -0
- metadata +77 -0
data/CHANGELOG
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
v 0.1.1 -- added activerecord dependency
|
data/README.markdown
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
PROBLEM
|
|
2
|
+
=======
|
|
3
|
+
- Merging 2 records is often needed
|
|
4
|
+
- Merging has many hidden & problematic aspects
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
SOLUTION
|
|
8
|
+
==========
|
|
9
|
+
ActiveRecord extension that introduces a simple merging API.
|
|
10
|
+
|
|
11
|
+
- Specify associations/attributes you want to merge
|
|
12
|
+
- Protects from self-merges
|
|
13
|
+
- Keeps counters valid
|
|
14
|
+
- Removes merged record
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
INSTALL
|
|
18
|
+
=======
|
|
19
|
+
|
|
20
|
+
Rails plugin
|
|
21
|
+
|
|
22
|
+
script/plugin install git://github.com/grosser/ar_merge.git
|
|
23
|
+
|
|
24
|
+
OR Gem
|
|
25
|
+
|
|
26
|
+
sudo gem install grosser-ar_merge
|
|
27
|
+
#require ar_merge after activerecord
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
USAGE
|
|
31
|
+
=====
|
|
32
|
+
Merge from outside the model:
|
|
33
|
+
|
|
34
|
+
user.merge!(other,:attributes=>user.attributes.keys,:associations=>%w[movies friends])`
|
|
35
|
+
|
|
36
|
+
Merge from inside the model
|
|
37
|
+
|
|
38
|
+
User < ActiveRecord::Base
|
|
39
|
+
def merge!(other)
|
|
40
|
+
super(other,:attributes=>%w[email website])
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
Merge duplicates
|
|
45
|
+
|
|
46
|
+
#merge all new users, that have the same email
|
|
47
|
+
User.merge_duplicates!(User.find_all_by_status('new')) , :compare=>:email)
|
|
48
|
+
|
|
49
|
+
AUTHOR
|
|
50
|
+
======
|
|
51
|
+
Michael Grosser
|
|
52
|
+
grosser dot michael ät gmail dot com
|
data/Rakefile
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
require 'rubygems'
|
|
2
|
+
require 'spec'
|
|
3
|
+
|
|
4
|
+
desc 'Default: run spec.'
|
|
5
|
+
task :default => :spec
|
|
6
|
+
|
|
7
|
+
desc "Run all specs in spec directory"
|
|
8
|
+
task :spec do |t|
|
|
9
|
+
options = "--colour --format progress --loadby --reverse"
|
|
10
|
+
files = FileList['spec/**/*_spec.rb']
|
|
11
|
+
system("spec #{options} #{files}")
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
require 'rubygems'
|
|
15
|
+
require 'rake'
|
|
16
|
+
require 'echoe'
|
|
17
|
+
|
|
18
|
+
Echoe.new('ar_merge', '0.1.1') do |p|
|
|
19
|
+
p.description = "Simply and securely merge AciveRecord`s."
|
|
20
|
+
p.url = "http://github.com/grosser/ar_merge"
|
|
21
|
+
p.author = "Michael Grosser"
|
|
22
|
+
p.email = "grosser.michael@gmail.com"
|
|
23
|
+
p.ignore_pattern = ["tmp/*", "script/*"]
|
|
24
|
+
p.dependencies = %w[activerecord]
|
|
25
|
+
p.development_dependencies = []
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
Dir["#{File.dirname(__FILE__)}/tasks/*.rake"].sort.each { |ext| load ext }
|
|
29
|
+
|
|
30
|
+
task :update_gemspec do
|
|
31
|
+
puts "updating..."
|
|
32
|
+
`rake manifest`
|
|
33
|
+
`rake build_gemspec`
|
|
34
|
+
end
|
data/ar_merge.gemspec
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
|
2
|
+
|
|
3
|
+
Gem::Specification.new do |s|
|
|
4
|
+
s.name = %q{ar_merge}
|
|
5
|
+
s.version = "0.1.1"
|
|
6
|
+
|
|
7
|
+
s.required_rubygems_version = Gem::Requirement.new(">= 1.2") if s.respond_to? :required_rubygems_version=
|
|
8
|
+
s.authors = ["Michael Grosser"]
|
|
9
|
+
s.date = %q{2008-12-23}
|
|
10
|
+
s.description = %q{Simply and securely merge AciveRecord`s.}
|
|
11
|
+
s.email = %q{grosser.michael@gmail.com}
|
|
12
|
+
s.extra_rdoc_files = ["CHANGELOG", "lib/ar_merge.rb", "README.markdown"]
|
|
13
|
+
s.files = ["Manifest", "CHANGELOG", "lib/ar_merge.rb", "spec/setup_test_model.rb", "spec/spec_helper.rb", "spec/ar_merge_spec.rb", "init.rb", "Rakefile", "README.markdown", "ar_merge.gemspec"]
|
|
14
|
+
s.has_rdoc = true
|
|
15
|
+
s.homepage = %q{http://github.com/grosser/ar_merge}
|
|
16
|
+
s.rdoc_options = ["--line-numbers", "--inline-source", "--title", "Ar_merge", "--main", "README.markdown"]
|
|
17
|
+
s.require_paths = ["lib"]
|
|
18
|
+
s.rubyforge_project = %q{ar_merge}
|
|
19
|
+
s.rubygems_version = %q{1.3.1}
|
|
20
|
+
s.summary = %q{Simply and securely merge AciveRecord`s.}
|
|
21
|
+
|
|
22
|
+
if s.respond_to? :specification_version then
|
|
23
|
+
current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
|
|
24
|
+
s.specification_version = 2
|
|
25
|
+
|
|
26
|
+
if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then
|
|
27
|
+
s.add_runtime_dependency(%q<activerecord>, [">= 0"])
|
|
28
|
+
else
|
|
29
|
+
s.add_dependency(%q<activerecord>, [">= 0"])
|
|
30
|
+
end
|
|
31
|
+
else
|
|
32
|
+
s.add_dependency(%q<activerecord>, [">= 0"])
|
|
33
|
+
end
|
|
34
|
+
end
|
data/init.rb
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
require 'ar_merge'
|
data/lib/ar_merge.rb
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
module AR_Merge
|
|
2
|
+
def self.included(base) #:nodoc:
|
|
3
|
+
base.extend ClassMethods
|
|
4
|
+
base.send(:include,InstanceMethods)
|
|
5
|
+
end
|
|
6
|
+
|
|
7
|
+
module InstanceMethods
|
|
8
|
+
def merge!(other,options={})
|
|
9
|
+
raise "cannot merge wit a new record" if other.new_record?
|
|
10
|
+
raise "cannot merge with myself" if other == self
|
|
11
|
+
|
|
12
|
+
#merge associations
|
|
13
|
+
(options[:associations]||[]).each do |association_name|
|
|
14
|
+
other.send(association_name).each do |object|
|
|
15
|
+
send(association_name).concat object
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
#update counters, this is very basic/hacky/not secure for customized counters...
|
|
19
|
+
counter = "#{association_name}_count"
|
|
20
|
+
if other.respond_to?(counter)
|
|
21
|
+
other.send(counter).times{self.class.increment_counter(counter, id)}
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
#merge attributes
|
|
26
|
+
(options[:attributes]||[]).each do |attr|
|
|
27
|
+
send("#{attr}=",other.send(attr)) if send(attr).blank?
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
#cleanup
|
|
31
|
+
other.reload.destroy
|
|
32
|
+
save!
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
module ClassMethods
|
|
37
|
+
def merge_duplicates!(records,options)
|
|
38
|
+
records.each do |record|
|
|
39
|
+
next if record.nil?
|
|
40
|
+
records.each do |other|
|
|
41
|
+
next if other.nil?
|
|
42
|
+
next if other == record
|
|
43
|
+
is_comparable = other.send(options[:compare]) == record.send(options[:compare])
|
|
44
|
+
next unless is_comparable
|
|
45
|
+
|
|
46
|
+
#merge and remove the other
|
|
47
|
+
records[records.index(other)]=nil
|
|
48
|
+
record.merge!(other)
|
|
49
|
+
end
|
|
50
|
+
end.reject(&:nil?)
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
ActiveRecord::Base.send(:include,AR_Merge)
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
require File.expand_path("spec_helper", File.dirname(__FILE__))
|
|
2
|
+
|
|
3
|
+
describe "AR_Merge" do
|
|
4
|
+
describe :merge! do
|
|
5
|
+
before do
|
|
6
|
+
@user = User.create!(:name=>'x')
|
|
7
|
+
@u2 = User.create!(:name=>'y')
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
describe :removal do
|
|
11
|
+
it "removes the merged user" do
|
|
12
|
+
@user.merge!(@u2)
|
|
13
|
+
lambda{@u2.reload}.should raise_error(ActiveRecord::RecordNotFound)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
it "saves the merging user" do
|
|
17
|
+
@user.expects(:save!)
|
|
18
|
+
@user.merge!(@u2)
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
describe :attributes do
|
|
23
|
+
it "merges and overtakes attributes" do
|
|
24
|
+
@user.name = ''
|
|
25
|
+
@user.merge!(@u2,:attributes=>['name'])
|
|
26
|
+
@user.name.should == 'y'
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
it "does not overtake attributes when current is not blank" do
|
|
30
|
+
@user.merge!(@u2,:attributes=>['name'])
|
|
31
|
+
@user.name.should == 'x'
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
describe :associations do
|
|
36
|
+
before do
|
|
37
|
+
Movie.delete_all
|
|
38
|
+
@user.movies << Movie.new
|
|
39
|
+
@user.save!
|
|
40
|
+
@user.movies.size.should == Movie.count
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
it "overtakes asociated objects" do
|
|
44
|
+
@u2.merge!(@user,:associations=>[:movies])
|
|
45
|
+
@u2.reload.should have(Movie.count).movies
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
it "does not create new objects" do
|
|
49
|
+
lambda{@u2.merge!(@user,:associations=>['movies'])}.should_not change(Movie,:count)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
it "keeps counters in sync" do
|
|
53
|
+
user, merged_user = CountingUser.create!, CountingUser.create!
|
|
54
|
+
user.movies_count.should == 0
|
|
55
|
+
merged_user.movies << Movie.new
|
|
56
|
+
merged_user.save!
|
|
57
|
+
merged_user.reload.movies_count.should == 1
|
|
58
|
+
|
|
59
|
+
user.merge!(merged_user,:associations=>[:movies])
|
|
60
|
+
user.reload.movies_count.should == 1
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
it "does no merge with new" do
|
|
65
|
+
lambda{@user.merge!(User.new)}.should raise_error
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
it "does no merge with self" do
|
|
69
|
+
lambda{@user.merge!(User.find(@user.id))}.should raise_error
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
describe :merge_duplicates! do
|
|
74
|
+
before do
|
|
75
|
+
@u1 = User.create!(:name=>'a')
|
|
76
|
+
@u2 = User.create!(:name=>'b')
|
|
77
|
+
@u3 = User.create!(:name=>'a')
|
|
78
|
+
@u4 = User.create!(:name=>'a')
|
|
79
|
+
@users=[@u1,@u2,@u3]
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
it "merges all records that have the same attributes" do
|
|
83
|
+
User.merge_duplicates!(@users,:compare=>:name).should == [@u1,@u2]
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
it "destroys the duplicates" do
|
|
87
|
+
User.merge_duplicates!(@users,:compare=>:name)
|
|
88
|
+
lambda{@u3.reload}.should raise_error(ActiveRecord::RecordNotFound)
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
it "merges the first with each of its duplicates" do
|
|
92
|
+
@users = [@u2,@u3,@u1,@u4]
|
|
93
|
+
@u3.expects(:merge!).with(@u1)
|
|
94
|
+
@u3.expects(:merge!).with(@u4)
|
|
95
|
+
User.merge_duplicates!(@users,:compare=>:name).should == [@u2,@u3]
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
require 'rubygems'
|
|
2
|
+
require 'active_record'
|
|
3
|
+
require 'active_record/fixtures'
|
|
4
|
+
|
|
5
|
+
#create model table
|
|
6
|
+
ActiveRecord::Schema.define(:version => 1) do
|
|
7
|
+
create_table :users do |t|
|
|
8
|
+
t.string :name
|
|
9
|
+
t.timestamps
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
create_table :counting_users do |t|
|
|
13
|
+
t.integer :movies_count, :default=>0, :null=>false
|
|
14
|
+
t.string :name
|
|
15
|
+
t.timestamps
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
create_table :movies do |t|
|
|
19
|
+
t.integer :user_id
|
|
20
|
+
t.integer :counting_user_id
|
|
21
|
+
t.timestamps
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
#create model
|
|
26
|
+
class User < ActiveRecord::Base
|
|
27
|
+
has_many :movies
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
class CountingUser < ActiveRecord::Base
|
|
31
|
+
has_many :movies
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
class Movie < ActiveRecord::Base
|
|
35
|
+
belongs_to :user
|
|
36
|
+
belongs_to :counting_user, :counter_cache => true
|
|
37
|
+
end
|
data/spec/spec_helper.rb
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# ---- requirements
|
|
2
|
+
require 'rubygems'
|
|
3
|
+
require 'spec'
|
|
4
|
+
require 'mocha'
|
|
5
|
+
require 'activerecord'
|
|
6
|
+
|
|
7
|
+
$LOAD_PATH << File.expand_path("../lib", File.dirname(__FILE__))
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
# ---- rspec
|
|
11
|
+
Spec::Runner.configure do |config|
|
|
12
|
+
config.mock_with :mocha
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
# ---- load database
|
|
17
|
+
RAILS_ENV = "test"
|
|
18
|
+
ActiveRecord::Base.configurations = {"test" => {
|
|
19
|
+
:adapter => "sqlite3",
|
|
20
|
+
:database => ":memory:",
|
|
21
|
+
}.with_indifferent_access}
|
|
22
|
+
|
|
23
|
+
ActiveRecord::Base.logger = Logger.new(File.directory?("log") ? "log/#{RAILS_ENV}.log" : "/dev/null")
|
|
24
|
+
ActiveRecord::Base.establish_connection(:test)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
# ---- setup environment/plugin
|
|
28
|
+
require 'ar_merge'
|
|
29
|
+
require File.expand_path("../init", File.dirname(__FILE__))
|
|
30
|
+
load File.expand_path("setup_test_model.rb", File.dirname(__FILE__))
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
# ---- fixtures
|
|
34
|
+
Spec::Example::ExampleGroupMethods.module_eval do
|
|
35
|
+
def fixtures(*tables)
|
|
36
|
+
dir = File.expand_path("fixtures", File.dirname(__FILE__))
|
|
37
|
+
tables.each{|table| Fixtures.create_fixtures(dir, table.to_s) }
|
|
38
|
+
end
|
|
39
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: grosser-ar_merge
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.1
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Michael Grosser
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: bin
|
|
10
|
+
cert_chain: []
|
|
11
|
+
|
|
12
|
+
date: 2008-12-23 00:00:00 -08:00
|
|
13
|
+
default_executable:
|
|
14
|
+
dependencies:
|
|
15
|
+
- !ruby/object:Gem::Dependency
|
|
16
|
+
name: activerecord
|
|
17
|
+
version_requirement:
|
|
18
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
19
|
+
requirements:
|
|
20
|
+
- - ">="
|
|
21
|
+
- !ruby/object:Gem::Version
|
|
22
|
+
version: "0"
|
|
23
|
+
version:
|
|
24
|
+
description: Simply and securely merge AciveRecord`s.
|
|
25
|
+
email: grosser.michael@gmail.com
|
|
26
|
+
executables: []
|
|
27
|
+
|
|
28
|
+
extensions: []
|
|
29
|
+
|
|
30
|
+
extra_rdoc_files:
|
|
31
|
+
- CHANGELOG
|
|
32
|
+
- lib/ar_merge.rb
|
|
33
|
+
- README.markdown
|
|
34
|
+
files:
|
|
35
|
+
- Manifest
|
|
36
|
+
- CHANGELOG
|
|
37
|
+
- lib/ar_merge.rb
|
|
38
|
+
- spec/setup_test_model.rb
|
|
39
|
+
- spec/spec_helper.rb
|
|
40
|
+
- spec/ar_merge_spec.rb
|
|
41
|
+
- init.rb
|
|
42
|
+
- Rakefile
|
|
43
|
+
- README.markdown
|
|
44
|
+
- ar_merge.gemspec
|
|
45
|
+
has_rdoc: true
|
|
46
|
+
homepage: http://github.com/grosser/ar_merge
|
|
47
|
+
post_install_message:
|
|
48
|
+
rdoc_options:
|
|
49
|
+
- --line-numbers
|
|
50
|
+
- --inline-source
|
|
51
|
+
- --title
|
|
52
|
+
- Ar_merge
|
|
53
|
+
- --main
|
|
54
|
+
- README.markdown
|
|
55
|
+
require_paths:
|
|
56
|
+
- lib
|
|
57
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
58
|
+
requirements:
|
|
59
|
+
- - ">="
|
|
60
|
+
- !ruby/object:Gem::Version
|
|
61
|
+
version: "0"
|
|
62
|
+
version:
|
|
63
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
64
|
+
requirements:
|
|
65
|
+
- - ">="
|
|
66
|
+
- !ruby/object:Gem::Version
|
|
67
|
+
version: "1.2"
|
|
68
|
+
version:
|
|
69
|
+
requirements: []
|
|
70
|
+
|
|
71
|
+
rubyforge_project: ar_merge
|
|
72
|
+
rubygems_version: 1.2.0
|
|
73
|
+
signing_key:
|
|
74
|
+
specification_version: 2
|
|
75
|
+
summary: Simply and securely merge AciveRecord`s.
|
|
76
|
+
test_files: []
|
|
77
|
+
|