clone_kit 0.3.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.
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CloneKit
4
+ class Rule
5
+ def current_operation=(operation)
6
+ @shared_id_map = nil
7
+ @current_operation = operation
8
+ end
9
+
10
+ protected
11
+
12
+ attr_reader :current_operation
13
+
14
+ def operation_arguments
15
+ current_operation.arguments
16
+ end
17
+
18
+ def shared_id_map
19
+ @shared_id_map ||= CloneKit::SharedIdMap.new(current_operation.id)
20
+ end
21
+
22
+ def warn_event(message)
23
+ current_operation.warn(message)
24
+ end
25
+
26
+ def error_event(message)
27
+ current_operation.error(message)
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CloneKit
4
+ module Rules
5
+ # The purpose of this rule is to only include attributes that are presently defined on the model
6
+ # (and its embedded models)
7
+ class AllowOnlyMongoidFields < CloneKit::Rule
8
+ def initialize(model_klass)
9
+ self.model_klass = model_klass
10
+ end
11
+
12
+ def fix(_old_id, attributes)
13
+ slice_allowed!(polymorphic_class(model_klass.to_s, attributes), attributes)
14
+ end
15
+
16
+ private
17
+
18
+ attr_accessor :model_klass
19
+
20
+ def slice_allowed!(klass, attributes)
21
+ return if attributes.nil?
22
+
23
+ attributes.slice!(*(klass.attribute_names + klass.embedded_relations.keys))
24
+
25
+ klass.embedded_relations.each do |name, metadata|
26
+ if metadata.macro == :embeds_many
27
+ Array.wrap(attributes[name]).each do |item|
28
+ slice_allowed!(polymorphic_class(metadata.class_name, item), item)
29
+ end
30
+ elsif !attributes[name].nil?
31
+ slice_allowed!(polymorphic_class(metadata.class_name, attributes[name]), attributes[name])
32
+ end
33
+ end
34
+ end
35
+
36
+ def polymorphic_class(class_name, item)
37
+ if item.key?("_type")
38
+ item["_type"]
39
+ else
40
+ class_name
41
+ end.constantize
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CloneKit
4
+ module Rules
5
+ class Except < CloneKit::Rule
6
+ def initialize(*attributes)
7
+ self.except_attributes = attributes
8
+ end
9
+
10
+ def fix(_old_id, attributes)
11
+ except_attributes.each do |key|
12
+ attributes.delete(key)
13
+ end
14
+ end
15
+
16
+ private
17
+
18
+ attr_accessor :except_attributes
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CloneKit
4
+ module Rules
5
+ class Remap < CloneKit::Rule
6
+ def initialize(model_name, remap_hash = {})
7
+ self.remap_hash = remap_hash
8
+ self.model_name = model_name
9
+ end
10
+
11
+ def fix(_old_id, attributes)
12
+ remap_hash.each do |klass, remap_attributes|
13
+ Array.wrap(remap_attributes).each do |att|
14
+ next unless try?(attributes, att)
15
+
16
+ attributes[att] = if attributes[att].is_a?(Array)
17
+ attributes[att].map { |id| remap(klass, id) unless id.blank? }.compact
18
+ else
19
+ remap(klass, attributes[att])
20
+ end
21
+ end
22
+ end
23
+ end
24
+
25
+ protected
26
+
27
+ def remap(klass, old_id)
28
+ shared_id_map.lookup(klass, old_id)
29
+ rescue ArgumentError
30
+ error_event("#{model_name} missing remapped id for #{klass} #{old_id}")
31
+ nil
32
+ end
33
+
34
+ def try?(attributes, key)
35
+ attributes.key?(key) && !attributes[key].blank?
36
+ end
37
+
38
+ private
39
+
40
+ attr_accessor :remap_hash,
41
+ :model_name
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "clone_kit/rules/remap"
4
+
5
+ module CloneKit
6
+ module Rules
7
+ class SafeRemap < Remap
8
+ def initialize(model_name, remap_hash = {}, safe_value = nil)
9
+ self.safe_value = safe_value
10
+ super(model_name, remap_hash)
11
+ end
12
+
13
+ protected
14
+
15
+ def remap(klass, old_id)
16
+ result = shared_id_map.lookup_safe(klass, old_id, safe_value)
17
+ warn_event("#{model_name} missing remapped id for #{klass}/#{old_id}") if result == safe_value
18
+ result
19
+ end
20
+
21
+ private
22
+
23
+ attr_accessor :safe_value
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "redis"
4
+
5
+ module CloneKit
6
+ class SharedIdMap
7
+ attr_reader :namespace
8
+
9
+ def initialize(namespace, redis: Redis.new)
10
+ self.namespace = namespace
11
+ self.redis = redis
12
+ end
13
+
14
+ def lookup(klass, original_id)
15
+ BSON::ObjectId.from_string(redis.hget(hash_key(klass), original_id.to_s))
16
+ rescue BSON::ObjectId::Invalid
17
+ raise ArgumentError, "No mapping found for #{klass}. This usually indicates a dependency has not be specified"
18
+ end
19
+
20
+ def lookup_safe(klass, original_id, default = nil)
21
+ val = redis.hget(hash_key(klass), original_id.to_s)
22
+ if val.blank?
23
+ default
24
+ else
25
+ BSON::ObjectId.from_string(val)
26
+ end
27
+ end
28
+
29
+ def insert(klass, original_id, new_id)
30
+ redis.hset(hash_key(klass), original_id.to_s, new_id.to_s)
31
+ end
32
+
33
+ def insert_many(klass, hash)
34
+ redis.pipelined do
35
+ hash.each do |k, v|
36
+ insert(klass, k, v)
37
+ end
38
+ end
39
+ end
40
+
41
+ def mapping(klass)
42
+ Hash[redis.hgetall(hash_key(klass)).map { |k, v| [k, v] }]
43
+ end
44
+
45
+ def hash_key(klass)
46
+ klass = klass.name if klass.is_a?(Class)
47
+ "clone_kit_id_map/#{namespace}/#{klass}"
48
+ end
49
+
50
+ private
51
+
52
+ attr_writer :namespace
53
+ attr_accessor :redis
54
+ end
55
+ end
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "clone_kit/emitters/empty"
4
+ require "clone_kit/cloners/no_op"
5
+
6
+ module CloneKit
7
+ class SpecificationError < StandardError; end
8
+
9
+ class Specification
10
+ attr_accessor :model,
11
+ :emitter,
12
+ :cloner,
13
+ :dependencies,
14
+ :after_operation_block
15
+
16
+ EMPTY_EMITTER = Emitters::Empty.new
17
+ NO_OP_CLONER = Cloners::NoOp.new
18
+
19
+ def initialize(model, &block)
20
+ self.model = model
21
+ self.emitter = EMPTY_EMITTER
22
+ self.cloner = NO_OP_CLONER
23
+ self.dependencies = []
24
+ self.after_operation_block = -> (_op) {}
25
+
26
+ validate!
27
+
28
+ model.instance_exec(self, &block)
29
+
30
+ CloneKit.add_specification(self)
31
+ end
32
+
33
+ def after_operation(&block)
34
+ self.after_operation_block = block
35
+ end
36
+
37
+ private
38
+
39
+ def validate!
40
+ fail SpecificationError, "Model type not supported" unless mongoid_document?
41
+ fail SpecificationError, "Cannot clone embedded documents" if mongoid_embedded_document?
42
+ end
43
+
44
+ def mongoid_document?
45
+ defined?(Mongoid) && model < Mongoid::Document
46
+ end
47
+
48
+ def mongoid_embedded_document?
49
+ mongoid_document? && model.embedded?
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CloneKit
4
+ module Strategies
5
+ class Synchronous
6
+ def initialize(operation)
7
+ self.operation = operation
8
+ end
9
+
10
+ def all_batches_complete
11
+ # NOP
12
+ end
13
+
14
+ def clone_next_batch(model_specs, complete_handler)
15
+ model_specs.each do |spec|
16
+ spec.cloner.clone_ids(spec.emitter.scope(operation.arguments).pluck(:id), operation)
17
+ end
18
+
19
+ complete_handler.new.complete(
20
+ true,
21
+ "operation" => {
22
+ already_cloned: operation.already_cloned + model_specs.map(&:model).map(&:to_s),
23
+ id: operation.id,
24
+ arguments: operation.arguments,
25
+ strategy: CloneKit::Strategies::Synchronous
26
+ }
27
+ )
28
+ end
29
+
30
+ attr_accessor :operation
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CloneKit
4
+ VERSION = "0.3.0"
5
+ end
metadata ADDED
@@ -0,0 +1,227 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: clone_kit
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.3.0
5
+ platform: ruby
6
+ authors:
7
+ - Brandon Croft
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2017-03-24 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: redis
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: activesupport
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">"
32
+ - !ruby/object:Gem::Version
33
+ version: 3.0.0
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">"
39
+ - !ruby/object:Gem::Version
40
+ version: 3.0.0
41
+ - !ruby/object:Gem::Dependency
42
+ name: bundler
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '1.13'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '1.13'
55
+ - !ruby/object:Gem::Dependency
56
+ name: mongoid
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: 4.0.2
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: 4.0.2
69
+ - !ruby/object:Gem::Dependency
70
+ name: database_cleaner
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - '='
74
+ - !ruby/object:Gem::Version
75
+ version: 1.5.3
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - '='
81
+ - !ruby/object:Gem::Version
82
+ version: 1.5.3
83
+ - !ruby/object:Gem::Dependency
84
+ name: rake
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - "~>"
88
+ - !ruby/object:Gem::Version
89
+ version: '11.0'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - "~>"
95
+ - !ruby/object:Gem::Version
96
+ version: '11.0'
97
+ - !ruby/object:Gem::Dependency
98
+ name: rspec
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - "~>"
102
+ - !ruby/object:Gem::Version
103
+ version: '3.4'
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - "~>"
109
+ - !ruby/object:Gem::Version
110
+ version: '3.4'
111
+ - !ruby/object:Gem::Dependency
112
+ name: rspec-collection_matchers
113
+ requirement: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - ">="
116
+ - !ruby/object:Gem::Version
117
+ version: '0'
118
+ type: :development
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - ">="
123
+ - !ruby/object:Gem::Version
124
+ version: '0'
125
+ - !ruby/object:Gem::Dependency
126
+ name: pry-byebug
127
+ requirement: !ruby/object:Gem::Requirement
128
+ requirements:
129
+ - - ">="
130
+ - !ruby/object:Gem::Version
131
+ version: '0'
132
+ type: :development
133
+ prerelease: false
134
+ version_requirements: !ruby/object:Gem::Requirement
135
+ requirements:
136
+ - - ">="
137
+ - !ruby/object:Gem::Version
138
+ version: '0'
139
+ - !ruby/object:Gem::Dependency
140
+ name: pry
141
+ requirement: !ruby/object:Gem::Requirement
142
+ requirements:
143
+ - - ">="
144
+ - !ruby/object:Gem::Version
145
+ version: '0'
146
+ type: :development
147
+ prerelease: false
148
+ version_requirements: !ruby/object:Gem::Requirement
149
+ requirements:
150
+ - - ">="
151
+ - !ruby/object:Gem::Version
152
+ version: '0'
153
+ - !ruby/object:Gem::Dependency
154
+ name: fakeredis
155
+ requirement: !ruby/object:Gem::Requirement
156
+ requirements:
157
+ - - ">="
158
+ - !ruby/object:Gem::Version
159
+ version: '0'
160
+ type: :development
161
+ prerelease: false
162
+ version_requirements: !ruby/object:Gem::Requirement
163
+ requirements:
164
+ - - ">="
165
+ - !ruby/object:Gem::Version
166
+ version: '0'
167
+ description: Supports rules-based cloning, Mongoid, and distributed operations
168
+ email:
169
+ - brandon@kapost.com
170
+ executables: []
171
+ extensions: []
172
+ extra_rdoc_files: []
173
+ files:
174
+ - ".gitignore"
175
+ - ".rspec"
176
+ - ".rubocop.kapost.yml"
177
+ - ".rubocop.yml"
178
+ - ".ruby-version"
179
+ - Gemfile
180
+ - README.md
181
+ - Rakefile
182
+ - bin/console
183
+ - bin/setup
184
+ - clone_kit.gemspec
185
+ - lib/clone_kit.rb
186
+ - lib/clone_kit/cloners/mongoid_merging_ruleset_cloner.rb
187
+ - lib/clone_kit/cloners/mongoid_ruleset_cloner.rb
188
+ - lib/clone_kit/cloners/no_op.rb
189
+ - lib/clone_kit/decorators/embedded_cloner_decorator.rb
190
+ - lib/clone_kit/emitters/empty.rb
191
+ - lib/clone_kit/event_outlet.rb
192
+ - lib/clone_kit/graph.rb
193
+ - lib/clone_kit/merge_attributes_tool.rb
194
+ - lib/clone_kit/operation.rb
195
+ - lib/clone_kit/rule.rb
196
+ - lib/clone_kit/rules/allow_only_mongoid_fields.rb
197
+ - lib/clone_kit/rules/except.rb
198
+ - lib/clone_kit/rules/remap.rb
199
+ - lib/clone_kit/rules/safe_remap.rb
200
+ - lib/clone_kit/shared_id_map.rb
201
+ - lib/clone_kit/specification.rb
202
+ - lib/clone_kit/strategies/synchronous.rb
203
+ - lib/clone_kit/version.rb
204
+ homepage: https://github.com/kapost/clone_kit
205
+ licenses: []
206
+ metadata: {}
207
+ post_install_message:
208
+ rdoc_options: []
209
+ require_paths:
210
+ - lib
211
+ required_ruby_version: !ruby/object:Gem::Requirement
212
+ requirements:
213
+ - - ">="
214
+ - !ruby/object:Gem::Version
215
+ version: '0'
216
+ required_rubygems_version: !ruby/object:Gem::Requirement
217
+ requirements:
218
+ - - ">="
219
+ - !ruby/object:Gem::Version
220
+ version: '0'
221
+ requirements: []
222
+ rubyforge_project:
223
+ rubygems_version: 2.5.1
224
+ signing_key:
225
+ specification_version: 4
226
+ summary: A toolkit to assist in complex cloning operations
227
+ test_files: []