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.
@@ -0,0 +1 @@
1
+ v 0.1.1 -- added activerecord dependency
@@ -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
@@ -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
@@ -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'
@@ -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
@@ -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
+