clone_kit 0.4.1 → 0.4.2
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.
- checksums.yaml +5 -5
- data/.circleci/config.yml +49 -0
- data/.circleci/wait_for_mongo.sh +6 -0
- data/CHANGELOG.md +8 -0
- data/README.md +109 -18
- data/clone_kit.gemspec +1 -0
- data/lib/clone_kit/cloners/mongoid_ruleset_cloner.rb +33 -3
- data/lib/clone_kit/rules/allow_only_mongoid_fields.rb +4 -2
- data/lib/clone_kit/rules/except.rb +7 -4
- data/lib/clone_kit/rules/remap.rb +15 -0
- data/lib/clone_kit/rules/safe_remap.rb +4 -0
- data/lib/clone_kit/version.rb +1 -1
- metadata +20 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
|
-
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: c6c302894250fb88b745cb86ae6d329c24c8f601
|
4
|
+
data.tar.gz: 691b3d3bbcff0db46980266e8818a644da0f8626
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 6442ecee0507caa2d84382a296272f955340aac5e16837183e0c9f4adf8513b535afbf5384a87fe905bcc1c0c467973fce81f5ac5ae207c185805d696a91e5ee
|
7
|
+
data.tar.gz: b38a06f98cf9b42a0005a7cc6b5a0cbd3d0ec7113b2d207e2508acfa9cb64786b2b148abfdb4392b1325090179402f22d59621be1e213e7b58d6ae19012c2049
|
@@ -0,0 +1,49 @@
|
|
1
|
+
version: 2
|
2
|
+
|
3
|
+
jobs:
|
4
|
+
build:
|
5
|
+
docker:
|
6
|
+
- image: kapost/ruby:2.3.1-node-6.11.5
|
7
|
+
environment:
|
8
|
+
DATABASE_URL: "postgres://circleci@localhost/clone_kit_test"
|
9
|
+
- image: circleci/postgres:9.6-alpine
|
10
|
+
environment:
|
11
|
+
POSTGRES_USER: circleci
|
12
|
+
POSTGRES_DB: clone_kit_test
|
13
|
+
- image: kapost/mongo:3.2
|
14
|
+
|
15
|
+
working_directory: ~/clone_kit
|
16
|
+
|
17
|
+
steps:
|
18
|
+
- checkout
|
19
|
+
- run:
|
20
|
+
name: Install deps
|
21
|
+
command: |
|
22
|
+
apt-get update
|
23
|
+
apt-get -y install netcat
|
24
|
+
- restore_cache:
|
25
|
+
keys:
|
26
|
+
- bundle-v1-{{ arch }}-{{ .Branch }}-{{ checksum "clone_kit.gemspec" }}
|
27
|
+
- bundle-v1-{{ arch }}-{{ .Branch }}
|
28
|
+
- bundle-v1-{{ arch }}
|
29
|
+
- run:
|
30
|
+
name: Bundle Install
|
31
|
+
command: |
|
32
|
+
bundle check --path=vendor/bundle || bundle install --path=vendor/bundle --jobs=4 --retry=3
|
33
|
+
- save_cache:
|
34
|
+
key: bundle-v1-{{ arch }}-{{ .Branch }}-{{ checksum "clone_kit.gemspec" }}
|
35
|
+
paths:
|
36
|
+
- vendor/bundle
|
37
|
+
- run:
|
38
|
+
name: Wait for mongo
|
39
|
+
command: ./.circleci/wait_for_mongo.sh
|
40
|
+
- run:
|
41
|
+
name: Run Specs
|
42
|
+
command: |
|
43
|
+
bundle exec rspec --profile 10 \
|
44
|
+
--format RspecJunitFormatter \
|
45
|
+
--out test_results/rspec.xml \
|
46
|
+
--format progress \
|
47
|
+
$(circleci tests glob "spec/**/*_spec.rb" | circleci tests split --split-by=timings)
|
48
|
+
- store_test_results:
|
49
|
+
path: test_results
|
data/CHANGELOG.md
ADDED
@@ -0,0 +1,8 @@
|
|
1
|
+
# Changelog
|
2
|
+
All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
|
3
|
+
and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).
|
4
|
+
|
5
|
+
## [0.4.2](https://github.com/kapost/clone_kit/compare/v0.4.1...v0.4.2) - 2018-11-14
|
6
|
+
### Added
|
7
|
+
- CHANGELOG
|
8
|
+
- Validation errors on embedded models when cloning
|
data/README.md
CHANGED
@@ -26,31 +26,118 @@ You can specify the dependency order of cloning, the scope of the operation, and
|
|
26
26
|
|
27
27
|
```ruby
|
28
28
|
CloneKit::Specification.new(BlogPost) do |spec|
|
29
|
-
spec.dependencies = %w(Account BlogType) # Helps derive the cloning order
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
29
|
+
spec.dependencies = %w(Account BlogType) # Helps derive the cloning order
|
30
|
+
spec.emitter = TenantEmitter.new(BlogPost) # The scope of the operation for this collection
|
31
|
+
spec.cloner = CloneKit::Cloners::MongoidRulesetCloner.new( # The cloning behavior
|
32
|
+
BlogPost,
|
33
|
+
rules: [
|
34
|
+
ReTenantRule.new,
|
35
|
+
CloneKit::Rules::Remap.new("BlogPost", "Account" => "account_id", "BlogType" => "blog_type_id")
|
36
|
+
]
|
37
|
+
)
|
38
|
+
spec.after_operation do |operation|
|
39
|
+
...
|
40
|
+
end
|
38
41
|
end
|
39
42
|
```
|
40
43
|
|
41
44
|
## Writing an Emitter
|
42
45
|
|
43
|
-
|
46
|
+
By default, CloneKit specifications utilize an empty emitter, making all clones no-ops. Emitters are expected to make db calls using logic defined in the emitter.
|
44
47
|
|
45
|
-
|
48
|
+
#### Emitter rules
|
49
|
+
- Emitters must respond to `#emit_all` and `#scope`.
|
50
|
+
- `emit_all` must return an object that responds to `#pluck`.
|
46
51
|
|
47
|
-
|
52
|
+
```ruby
|
53
|
+
CloneKit::ActiveRecordSpecification.new(BlogPost) do |spec|
|
54
|
+
...
|
55
|
+
spec.emitter = ActiveRecordEmitter.new(BlogPost)
|
56
|
+
...
|
57
|
+
end
|
58
|
+
|
59
|
+
class ActiveRecordEmitter
|
60
|
+
def initialize(klass)
|
61
|
+
self.klass = klass
|
62
|
+
end
|
63
|
+
|
64
|
+
def scope(*)
|
65
|
+
klass.all # add any scope restrictions here
|
66
|
+
end
|
67
|
+
|
68
|
+
def emit_all # the method that will be used to pluck the record ids
|
69
|
+
scope
|
70
|
+
end
|
71
|
+
|
72
|
+
private
|
73
|
+
|
74
|
+
attr_accessor :klass
|
75
|
+
end
|
76
|
+
```
|
77
|
+
|
78
|
+
## Custom Cloners
|
79
|
+
|
80
|
+
Cloners are the classes that determine what model class is cloned and how. There are several built-in cloners that can be extended. See `lib/clone_kit/cloners` for a list.
|
81
|
+
|
82
|
+
Custom cloners will need to define:
|
83
|
+
|
84
|
+
1. The Mongoid or ActiveRecord model class, which will be used to make db calls
|
85
|
+
2. Rules, which are executed in the defined order and determine how the ids are mapped from source to destination records. See more in next section.
|
86
|
+
3. Merge fields, which allow two records to be merged into one provided all listed fields are equal.
|
48
87
|
|
49
|
-
|
88
|
+
Optionally, if you are merging records you will probably want to override the `compare` and `merge` methods with custom logic, though basic logic comes for free.
|
50
89
|
|
51
|
-
|
90
|
+
```ruby
|
91
|
+
CloneKit::ActiveRecordSpecification.new(self) do |spec|
|
92
|
+
...
|
93
|
+
spec.cloner = BlogPostCloner.new
|
94
|
+
...
|
95
|
+
end
|
96
|
+
|
97
|
+
class BlogPostCloner < ActiveRecordRulesetCloner
|
98
|
+
OMIT_ATTRIBUTES = [:created_at, :updated_at]
|
99
|
+
|
100
|
+
def initialize
|
101
|
+
super(
|
102
|
+
BlogPost, # model class
|
103
|
+
rules: [ # rules
|
104
|
+
CloneKit::Rules::Except.new(*OMIT_ATTRIBUTES),
|
105
|
+
CloneKit::Rules::Remap.new(BlogPost)
|
106
|
+
],
|
107
|
+
merge_fields: []) # merge fields
|
108
|
+
end
|
109
|
+
|
110
|
+
def compare(first, second)
|
111
|
+
# returns a boolean to determine if two records are mergeable
|
112
|
+
end
|
113
|
+
|
114
|
+
def merge(records)
|
115
|
+
# returns a single record that is the merged result
|
116
|
+
# of all argument `records`,
|
117
|
+
# e.g. [{ a: 1, b: 1 }, { a: 2, b: 1}] => { a: 2, b: 1 }
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
```
|
122
|
+
|
123
|
+
## Writing a Cloner rule
|
52
124
|
|
53
|
-
|
125
|
+
Rules respond to a single `#fix` method. `#fix` mutates a record's attributes, allowing the same `attributes` object to be passed down a pipeline of rules.
|
126
|
+
|
127
|
+
```ruby
|
128
|
+
# given the following rules
|
129
|
+
rules: [
|
130
|
+
CloneKit::Rules::Except.new(:title),
|
131
|
+
CloneKit::Rules::Remap.new(BlogPost, "Author" => "author_id" )
|
132
|
+
]
|
133
|
+
|
134
|
+
# a blog post's attributes will be changed
|
135
|
+
{ title: "Title", content: "Content", author_id: 5 }
|
136
|
+
{ content: "Content", author_id: 5 } # CloneKit::Rules::Except
|
137
|
+
{ content: "Content", author_id: 6 } # CloneKit::Rules::Remap
|
138
|
+
```
|
139
|
+
|
140
|
+
See `lib/clone_kit/rules` for examples with documentation.
|
54
141
|
|
55
142
|
## Installation
|
56
143
|
|
@@ -62,11 +149,15 @@ gem 'clone_kit'
|
|
62
149
|
|
63
150
|
And then execute:
|
64
151
|
|
65
|
-
|
152
|
+
```bash
|
153
|
+
$ bundle
|
154
|
+
```
|
66
155
|
|
67
156
|
Or install it yourself as:
|
68
157
|
|
69
|
-
|
158
|
+
```bash
|
159
|
+
$ gem install clone_kit
|
160
|
+
```
|
70
161
|
|
71
162
|
## Development
|
72
163
|
|
@@ -76,4 +167,4 @@ To install this gem onto your local machine, run `bundle exec rake install`. To
|
|
76
167
|
|
77
168
|
## Contributing
|
78
169
|
|
79
|
-
Bug reports and pull requests are welcome on GitHub at https://github.com/
|
170
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/kapost/clone_kit.
|
data/clone_kit.gemspec
CHANGED
@@ -36,6 +36,7 @@ Gem::Specification.new do |spec|
|
|
36
36
|
spec.add_development_dependency "pry-byebug"
|
37
37
|
spec.add_development_dependency "rake", "~> 11.0"
|
38
38
|
spec.add_development_dependency "rspec-collection_matchers"
|
39
|
+
spec.add_development_dependency "rspec_junit_formatter", "~> 0.3"
|
39
40
|
spec.add_development_dependency "rspec-rails", "~> 3.4"
|
40
41
|
spec.add_development_dependency "rubocop"
|
41
42
|
spec.add_development_dependency "simplecov", "~> 0.12.0"
|
@@ -107,12 +107,42 @@ module CloneKit
|
|
107
107
|
if model_that_we_wont_save.valid?
|
108
108
|
model_klass.collection.insert(attributes)
|
109
109
|
else
|
110
|
-
|
111
|
-
id = attributes["_id"]
|
112
|
-
current_operation.error("#{model_klass} #{id} failed model validation and was not cloned: #{details}")
|
110
|
+
report_errors(attributes, model_that_we_wont_save)
|
113
111
|
end
|
114
112
|
end
|
115
113
|
|
114
|
+
def report_errors(attributes, model)
|
115
|
+
model_error = model.errors.full_messages.to_sentence
|
116
|
+
embedded_errors = collect_embedded_errors(model)
|
117
|
+
id = attributes["_id"]
|
118
|
+
|
119
|
+
current_operation.error("#{model_klass} #{id} failed model validation and was not cloned: #{model_error}")
|
120
|
+
|
121
|
+
embedded_errors.each do |e|
|
122
|
+
current_operation.error("[#{model_klass} #{id}]: #{e}")
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
def collect_embedded_errors(model)
|
127
|
+
embedded_documents(model).each_with_object([]) do |doc, accum|
|
128
|
+
next if doc.blank? || doc.valid?
|
129
|
+
|
130
|
+
klass = doc.class
|
131
|
+
id = doc._id.to_s
|
132
|
+
error = doc.errors.full_messages.to_sentence
|
133
|
+
|
134
|
+
accum << "Embedded #{klass} #{id} failed model validation: #{error}"
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|
138
|
+
def embedded_documents(model)
|
139
|
+
model.associations
|
140
|
+
.select { |_key, assoc| assoc.embedded? }
|
141
|
+
.keys
|
142
|
+
.map { |field| model.send(field) }
|
143
|
+
.flatten
|
144
|
+
end
|
145
|
+
|
116
146
|
def each_existing_record(ids)
|
117
147
|
ids.each do |id|
|
118
148
|
record = model_klass.collection.find(_id: id).one
|
@@ -2,8 +2,10 @@
|
|
2
2
|
|
3
3
|
module CloneKit
|
4
4
|
module Rules
|
5
|
-
#
|
6
|
-
#
|
5
|
+
#
|
6
|
+
# The purpose of this rule is to only include attributes that are
|
7
|
+
# presently defined on the model (and its embedded models)
|
8
|
+
|
7
9
|
class AllowOnlyMongoidFields < CloneKit::Rule
|
8
10
|
def initialize(model_klass)
|
9
11
|
self.model_klass = model_klass
|
@@ -2,20 +2,23 @@
|
|
2
2
|
|
3
3
|
module CloneKit
|
4
4
|
module Rules
|
5
|
+
#
|
6
|
+
# Removes attributes defined by an array of keys
|
7
|
+
|
5
8
|
class Except < CloneKit::Rule
|
6
|
-
def initialize(*
|
7
|
-
self.
|
9
|
+
def initialize(*excepted_attributes)
|
10
|
+
self.excepted_attributes = excepted_attributes
|
8
11
|
end
|
9
12
|
|
10
13
|
def fix(_old_id, attributes)
|
11
|
-
|
14
|
+
excepted_attributes.each do |key|
|
12
15
|
attributes.delete(key)
|
13
16
|
end
|
14
17
|
end
|
15
18
|
|
16
19
|
private
|
17
20
|
|
18
|
-
attr_accessor :
|
21
|
+
attr_accessor :excepted_attributes
|
19
22
|
end
|
20
23
|
end
|
21
24
|
end
|
@@ -2,6 +2,21 @@
|
|
2
2
|
|
3
3
|
module CloneKit
|
4
4
|
module Rules
|
5
|
+
#
|
6
|
+
# Utilizes the SharedIdMap stored in Redis to remap original
|
7
|
+
# ids to their new cloned values.
|
8
|
+
#
|
9
|
+
# Given an original blog post being cloned:
|
10
|
+
# { title: "Title", author_id: 5 }
|
11
|
+
# And a blog post rule:
|
12
|
+
# Remap.new(BlogPost, "Author" => "author_id")
|
13
|
+
# And an author record that was cloned from => to:
|
14
|
+
# { id: 5, name: "Pat" } => { id: 6, name: "Pat" }
|
15
|
+
# The cloned blog post will show the remapped author id:
|
16
|
+
# { title: "Title", author_id: 6 }
|
17
|
+
#
|
18
|
+
# When a remapped id is missing, an error is added to the operation.
|
19
|
+
|
5
20
|
class Remap < CloneKit::Rule
|
6
21
|
def initialize(model_name, remap_hash = {}, id_generator: nil)
|
7
22
|
super(id_generator: id_generator)
|
@@ -4,6 +4,10 @@ require "clone_kit/rules/remap"
|
|
4
4
|
|
5
5
|
module CloneKit
|
6
6
|
module Rules
|
7
|
+
#
|
8
|
+
# Operates like Remap, but returns a default value instead of `nil`.
|
9
|
+
# When the default is used, the event outlet receives a #warn message.
|
10
|
+
|
7
11
|
class SafeRemap < Remap
|
8
12
|
def initialize(model_name, remap_hash = {}, safe_value = nil, id_generator: nil)
|
9
13
|
super(model_name, remap_hash, id_generator: id_generator)
|
data/lib/clone_kit/version.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: clone_kit
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.4.
|
4
|
+
version: 0.4.2
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Brandon Croft
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2018-
|
11
|
+
date: 2018-11-14 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activesupport
|
@@ -192,6 +192,20 @@ dependencies:
|
|
192
192
|
- - ">="
|
193
193
|
- !ruby/object:Gem::Version
|
194
194
|
version: '0'
|
195
|
+
- !ruby/object:Gem::Dependency
|
196
|
+
name: rspec_junit_formatter
|
197
|
+
requirement: !ruby/object:Gem::Requirement
|
198
|
+
requirements:
|
199
|
+
- - "~>"
|
200
|
+
- !ruby/object:Gem::Version
|
201
|
+
version: '0.3'
|
202
|
+
type: :development
|
203
|
+
prerelease: false
|
204
|
+
version_requirements: !ruby/object:Gem::Requirement
|
205
|
+
requirements:
|
206
|
+
- - "~>"
|
207
|
+
- !ruby/object:Gem::Version
|
208
|
+
version: '0.3'
|
195
209
|
- !ruby/object:Gem::Dependency
|
196
210
|
name: rspec-rails
|
197
211
|
requirement: !ruby/object:Gem::Requirement
|
@@ -241,11 +255,14 @@ executables: []
|
|
241
255
|
extensions: []
|
242
256
|
extra_rdoc_files: []
|
243
257
|
files:
|
258
|
+
- ".circleci/config.yml"
|
259
|
+
- ".circleci/wait_for_mongo.sh"
|
244
260
|
- ".gitignore"
|
245
261
|
- ".rspec"
|
246
262
|
- ".rubocop.kapost.yml"
|
247
263
|
- ".rubocop.yml"
|
248
264
|
- ".ruby-version"
|
265
|
+
- CHANGELOG.md
|
249
266
|
- Gemfile
|
250
267
|
- README.md
|
251
268
|
- Rakefile
|
@@ -297,7 +314,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
297
314
|
version: '0'
|
298
315
|
requirements: []
|
299
316
|
rubyforge_project:
|
300
|
-
rubygems_version: 2.
|
317
|
+
rubygems_version: 2.5.2.2
|
301
318
|
signing_key:
|
302
319
|
specification_version: 4
|
303
320
|
summary: A toolkit to assist in complex cloning operations
|