active_merge 1.0.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.
data/MIT-LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright 2014 YOURNAME
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.rdoc ADDED
@@ -0,0 +1,26 @@
1
+ = ActiveMerge
2
+
3
+ Declares the <tt>ActiveMerge</tt> module for extending ActiveRecord models.
4
+
5
+ The module contains the <tt>merge</tt> class method for merging class instances
6
+ into the first one.
7
+
8
+ When merging a list of instances:
9
+ * all "has_many" relatives are reattached to the instance with the lowest id
10
+ * all the instances except for the first one (with the lowest id) are deleted
11
+
12
+ == Example
13
+
14
+ class Post < ActiveRecord
15
+ extend ActiveMerge
16
+ has_many :comments
17
+ end
18
+
19
+ Post.all.merge!
20
+ # This will merge all the posts into the first one.
21
+ # The other posts will be deleted after their comment are reattached
22
+ # to the first post.
23
+
24
+ == License
25
+
26
+ This project rocks and uses link:MIT-LICENSE.
data/Rakefile ADDED
@@ -0,0 +1,17 @@
1
+ begin
2
+ require 'bundler/setup'
3
+ rescue LoadError
4
+ puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
5
+ end
6
+
7
+ require 'rdoc/task'
8
+
9
+ RDoc::Task.new(:rdoc) do |rdoc|
10
+ rdoc.rdoc_dir = 'rdoc'
11
+ rdoc.title = 'ActiveMerge'
12
+ rdoc.options << '--line-numbers'
13
+ rdoc.rdoc_files.include('README.rdoc')
14
+ rdoc.rdoc_files.include('lib/**/*.rb')
15
+ end
16
+
17
+ Bundler::GemHelper.install_tasks
@@ -0,0 +1,166 @@
1
+ # encoding: utf-8
2
+ module ActiveMerge
3
+
4
+ # Сервисный объект (паттерн Service Object), отвечающий за объединение
5
+ # всех записей ActiveRecord, переданных в аргументе.
6
+ #
7
+ # При инициализации указывается ActiveRecord::Association или массив объектов.
8
+ # Из массива выбираются только сохраненные объекты ActiveRecord, причем все
9
+ # они должны быть объектами одного класса.
10
+ #
11
+ # При объединении:
12
+ # * Сохраняется первый объект (с наименьшим id)
13
+ # * У всех прочих объектов ассоциации has_many перепривязываются к первому объекту
14
+ # * После перепривязки все объекты, кроме первого, удаляются.
15
+ #
16
+ # В результате остается только один объект, к которому привязаны все подобъекты,
17
+ # ранее относившиеся к объединяемым записям.
18
+ #
19
+ # == Пример:
20
+ #
21
+ # # Рассмотрим модель королевства, имеющего много графств и жителей
22
+ # class Kingdom < ActiveRecord::Base
23
+ # has_many :shires
24
+ # has_many :men
25
+ # end
26
+ #
27
+ # class Shire < ActiveRecord::Base
28
+ # belongs_to :kingdom
29
+ # end
30
+ #
31
+ # class Man < ActiveRecord::Base
32
+ # belongs_to :kingdom
33
+ # end
34
+ #
35
+ # # У Британии есть 100 тыс. жителей и 10 графств
36
+ # britain = Kingdom.create!
37
+ # 1000000.times.each{ britain.men.create! }
38
+ # 10.times.each{ britain.shires.create! }
39
+ #
40
+ # # У Шотландии есть 30 тыс. жителей и 5 графств
41
+ # scotland = Kingdom.create!
42
+ # 300000.times.each{ britain.men.create! }
43
+ # 5.times.each{ britain.shires.create! }
44
+ #
45
+ # # Объединяем королевства
46
+ # Service.new(Kingdom.all).submit!
47
+ #
48
+ # # К Британии (добавлена ранее) присоединены шотландские люди и графства
49
+ # britain.reload.men.count # => 130000
50
+ # britain.reload.shires.count # => 15
51
+ #
52
+ # # Королевство Шотландия удалено
53
+ # Kingdom.find_by(scotland.id) # => nil
54
+ #
55
+ # == Обратите внимание!
56
+ #
57
+ # Объединение происходит единой транзакцией. При любой ошибке все изменения
58
+ # отменяются.
59
+ #
60
+ # Если в примере выше графство не может быть перепривязано к новому
61
+ # королевству (незыблемость суверенитета):
62
+ #
63
+ # class Shire
64
+ # attr_readonly :kingdom_id
65
+ # end
66
+ #
67
+ # то объединения не произойдет!
68
+ #
69
+ class Service
70
+
71
+ def initialize(list = [])
72
+ list = _extract_from list
73
+ @item, @items = list.first, Array(list[1..-1])
74
+ end
75
+
76
+ attr_reader :item, :items, :klass, :klasses
77
+
78
+ # Возвращает класс объединяемых объектов
79
+ def klass
80
+ @klass ||= item.class
81
+ end
82
+
83
+ # Возвращает хэш, в котором ключами выступают связи has_many,
84
+ # а значениями - foreign keys
85
+ def klasses
86
+ @klasses ||= klass.reflect_on_all_associations(:has_many).
87
+ inject({}){ |hash, item| hash.merge(item.name => item.foreign_key) }
88
+ end
89
+
90
+ # Объединяет все записи из переменной #items с записью в переменной #item
91
+ #
92
+ # При этом:
93
+ # * все ссылки на записи из массива #items (связь has_many) перепривязываются к #item
94
+ # * все записи #items удаляются
95
+ #
96
+ # При любой ошибке все сделанные изменения отменяются.
97
+ #
98
+ def provide
99
+ ActiveRecord::Base.transaction requires_new: true do
100
+ _raise :blank if items.blank?
101
+ items.each{ |item| _merge_one(item) }
102
+ end
103
+ end
104
+
105
+ private
106
+
107
+ # Вызывает исключение указанного типа с переданными парамертами
108
+ def _raise(type, item = nil)
109
+ options = item ? { type: item.class.name.underscore, id: item.id } : {}
110
+ options.merge! scope: %w(active_merge errors service)
111
+ raise I18n.t(type, options)
112
+ end
113
+
114
+ # Извлекает из списка только сохраненные объекты ActiveRecord
115
+ # Если список содержит элементы ActiveRecord разных классов, возвращает
116
+ # пустой массив.
117
+ def _extract_from(list)
118
+ begin
119
+ list = list.select{ |item| _valid? item }
120
+ klass = list.first.class
121
+ list.each { |item| raise unless item.class == klass }
122
+ list.sort_by { |item| item.id }
123
+ rescue
124
+ []
125
+ end
126
+ end
127
+
128
+ # Проверяет, что объект является сохраненным объектом ActiveRecord
129
+ def _valid?(item)
130
+ begin
131
+ item.class.ancestors.include?(ActiveRecord::Base) && item.persisted?
132
+ rescue
133
+ false
134
+ end
135
+ end
136
+
137
+ # Объединяет указанный объект с объектом из переменной #item
138
+ # * Перепривязывает ссылки на объект #item
139
+ # * Удаляет объект, переданный в аргументе
140
+ def _merge_one(item)
141
+ klasses.each do |list, foreign_key|
142
+ item.send(list).each { |ref| _rebind! ref, foreign_key }
143
+ end
144
+ _destroy! item
145
+ end
146
+
147
+ # Перепривязывает запись к первому объединяемому объекту
148
+ def _rebind!(item, foreign_key)
149
+ begin
150
+ item.send "#{ foreign_key }=", self.item.id
151
+ item.save!
152
+ rescue
153
+ _raise :rebind, item
154
+ end
155
+ end
156
+
157
+ # Удаляет запись, сохраняя ошибки
158
+ def _destroy!(item)
159
+ begin
160
+ item.destroy!
161
+ rescue
162
+ _raise :destroy, item
163
+ end
164
+ end
165
+ end
166
+ end
@@ -0,0 +1,4 @@
1
+ module ActiveMerge
2
+ # Текущая версия плагина
3
+ VERSION = "1.0.0"
4
+ end
@@ -0,0 +1,26 @@
1
+ # encoding: utf-8
2
+
3
+ # Модуль содержит методы объединения записей ActiveRecord
4
+ #
5
+ # После расширения класса, унаследованного от <tt>ActiveRecord::Base</tt>
6
+ # становится доступен метод класса <tt>::merge_all</tt>, объединяющий записи.
7
+ #
8
+ module ActiveMerge
9
+ extend ActiveSupport::Autoload
10
+ autoload :Service
11
+
12
+ # Объединение указанных записей.
13
+ #
14
+ # class Lord < ActiveRecord::Base
15
+ # extend ActiveMerge
16
+ # end
17
+ #
18
+ # Lord.all.merge_all # => объединяет все записи
19
+ # Lord.where(id > 100) # => объединяет все записи с id > 100
20
+ #
21
+ # Детали см. в описании метода <tt>ActiveMerge::Service#provide</tt>
22
+ #
23
+ def merge_all
24
+ ActiveMerge::Service.new(self).provide
25
+ end
26
+ end
metadata ADDED
@@ -0,0 +1,133 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: active_merge
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Andrew Kozin
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2014-03-08 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: rails
16
+ requirement: !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ~>
20
+ - !ruby/object:Gem::Version
21
+ version: 4.0.3
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ~>
28
+ - !ruby/object:Gem::Version
29
+ version: 4.0.3
30
+ - !ruby/object:Gem::Dependency
31
+ name: sqlite3
32
+ requirement: !ruby/object:Gem::Requirement
33
+ none: false
34
+ requirements:
35
+ - - ! '>='
36
+ - !ruby/object:Gem::Version
37
+ version: '0'
38
+ type: :development
39
+ prerelease: false
40
+ version_requirements: !ruby/object:Gem::Requirement
41
+ none: false
42
+ requirements:
43
+ - - ! '>='
44
+ - !ruby/object:Gem::Version
45
+ version: '0'
46
+ - !ruby/object:Gem::Dependency
47
+ name: rspec
48
+ requirement: !ruby/object:Gem::Requirement
49
+ none: false
50
+ requirements:
51
+ - - ! '>='
52
+ - !ruby/object:Gem::Version
53
+ version: '0'
54
+ type: :development
55
+ prerelease: false
56
+ version_requirements: !ruby/object:Gem::Requirement
57
+ none: false
58
+ requirements:
59
+ - - ! '>='
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ - !ruby/object:Gem::Dependency
63
+ name: yard
64
+ requirement: !ruby/object:Gem::Requirement
65
+ none: false
66
+ requirements:
67
+ - - ! '>='
68
+ - !ruby/object:Gem::Version
69
+ version: '0'
70
+ type: :development
71
+ prerelease: false
72
+ version_requirements: !ruby/object:Gem::Requirement
73
+ none: false
74
+ requirements:
75
+ - - ! '>='
76
+ - !ruby/object:Gem::Version
77
+ version: '0'
78
+ - !ruby/object:Gem::Dependency
79
+ name: database_cleaner
80
+ requirement: !ruby/object:Gem::Requirement
81
+ none: false
82
+ requirements:
83
+ - - ! '>='
84
+ - !ruby/object:Gem::Version
85
+ version: '0'
86
+ type: :development
87
+ prerelease: false
88
+ version_requirements: !ruby/object:Gem::Requirement
89
+ none: false
90
+ requirements:
91
+ - - ! '>='
92
+ - !ruby/object:Gem::Version
93
+ version: '0'
94
+ description: Declares the ActiveMerge module with the 'merge!' class method.
95
+ email:
96
+ - andrew.kozin@gmail.com
97
+ executables: []
98
+ extensions: []
99
+ extra_rdoc_files: []
100
+ files:
101
+ - lib/active_merge/service.rb
102
+ - lib/active_merge/version.rb
103
+ - lib/active_merge.rb
104
+ - MIT-LICENSE
105
+ - Rakefile
106
+ - README.rdoc
107
+ homepage: https://github.com/nepalez/active_merge
108
+ licenses:
109
+ - MIT
110
+ post_install_message:
111
+ rdoc_options: []
112
+ require_paths:
113
+ - lib
114
+ required_ruby_version: !ruby/object:Gem::Requirement
115
+ none: false
116
+ requirements:
117
+ - - ! '>='
118
+ - !ruby/object:Gem::Version
119
+ version: '0'
120
+ required_rubygems_version: !ruby/object:Gem::Requirement
121
+ none: false
122
+ requirements:
123
+ - - ! '>='
124
+ - !ruby/object:Gem::Version
125
+ version: '0'
126
+ requirements: []
127
+ rubyforge_project:
128
+ rubygems_version: 1.8.24
129
+ signing_key:
130
+ specification_version: 3
131
+ summary: Merges instances of an ActiveRecord class.
132
+ test_files: []
133
+ has_rdoc: