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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- SHA256:
3
- metadata.gz: 44c1a6651a51d141af62a548478d3434d1ade17975090d3cd0a3c574c774fa0a
4
- data.tar.gz: 8ac5a6548b0612eca38f70cc915c65f6fcbb3f74fd52e4ce1fe69fc244fe0de7
2
+ SHA1:
3
+ metadata.gz: c6c302894250fb88b745cb86ae6d329c24c8f601
4
+ data.tar.gz: 691b3d3bbcff0db46980266e8818a644da0f8626
5
5
  SHA512:
6
- metadata.gz: 3b0869c41f2031c89c9703d10a32f18e53bde40781d17ee90316706419e3826164bd475888a4906365c4f13feb5815a32b95b6f207adaa7a85d7e85526c9c04d
7
- data.tar.gz: d534b0614f37999c26607c3e4a3b5c6e015e69442d609a7c6af690e7904237cdccf1a30eb4875ce8dd8db441ae24daa14b1048a340158010ac5fb69419823a2b
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
@@ -0,0 +1,6 @@
1
+ #!/bin/bash
2
+
3
+ until nc -z localhost 27017
4
+ do
5
+ sleep 1
6
+ done
@@ -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
- 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
- )
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
- You have to write some emitters for your app. By default, CloneKit specifications utilize and empty emitter, making all clones no-operations.
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
- TODO
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
- ## Writing a Cloner
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
- TODO
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
- ## Extending the built-in Cloners
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
- TODO
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
- $ bundle
152
+ ```bash
153
+ $ bundle
154
+ ```
66
155
 
67
156
  Or install it yourself as:
68
157
 
69
- $ gem install clone_kit
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/[USERNAME]/clone_kit.
170
+ Bug reports and pull requests are welcome on GitHub at https://github.com/kapost/clone_kit.
@@ -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
- details = model_that_we_wont_save.errors.full_messages.to_sentence
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
- # The purpose of this rule is to only include attributes that are presently defined on the model
6
- # (and its embedded models)
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(*attributes)
7
- self.except_attributes = attributes
9
+ def initialize(*excepted_attributes)
10
+ self.excepted_attributes = excepted_attributes
8
11
  end
9
12
 
10
13
  def fix(_old_id, attributes)
11
- except_attributes.each do |key|
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 :except_attributes
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)
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module CloneKit
4
- VERSION = "0.4.1"
4
+ VERSION = "0.4.2"
5
5
  end
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.1
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-04-27 00:00:00.000000000 Z
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.7.6
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