clone_kit 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: b62cef9da193908351d660a1089e8778553a963e
4
+ data.tar.gz: ff375e6bce0abff22230fb94ec0be5bd3e31adc0
5
+ SHA512:
6
+ metadata.gz: 3417281d9a7f0c7d6f701f9185f187165d766263215b4faf5f8b9ca06916f29f8eadd61ae34724c83c6e75e28564aa62153d65dca0e940dafb7f3c2b6964109b
7
+ data.tar.gz: 9d34d0962ce7a93b60eb5e1688be2b8e07e39d7b0aa695eb70e2a974a57b4ed4a541cdb0151bf1017dd122298933d732076b0b5be1db142419d04ac3fb6f16fa
@@ -0,0 +1,2 @@
1
+ Gemfile.lock
2
+ vendor/ruby
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --format documentation
2
+ --color
@@ -0,0 +1,99 @@
1
+ # Please don't modify this file, without first checking if it makes sense to
2
+ # adopt for most other Kapost projects. Ideally, we would keep these files as
3
+ # similar as possible to ease maintainability. Instead, open a PR in the
4
+ # codeclimate-common repo at https://github.com/kapost/codeclimate-common
5
+
6
+ Rails:
7
+ Enabled: true
8
+
9
+ AllCops:
10
+ DisplayCopNames: true
11
+ DisplayStyleGuide: true
12
+ Include:
13
+ - "**/Rakefile"
14
+ - "**/config.ru"
15
+ Exclude:
16
+ - "vendor/**/*"
17
+ - "spec/fixtures/**/*"
18
+ - "bin/**/*"
19
+ - "script/**/*"
20
+
21
+ Metrics/LineLength:
22
+ Max: 120
23
+ Rails/Date:
24
+ Enabled: false
25
+ Rails/TimeZone:
26
+ Enabled: false
27
+ AllCops:
28
+ TargetRubyVersion: 2.3
29
+ Style/AndOr:
30
+ EnforcedStyle: conditionals
31
+ Style/CaseIndentation:
32
+ IndentOneStep: true
33
+ Style/Documentation:
34
+ Enabled: false
35
+ Style/EachWithObject:
36
+ Enabled: false
37
+ Style/ExtraSpacing:
38
+ Exclude:
39
+ - "config/routes.rb"
40
+ Style/HashSyntax:
41
+ Exclude:
42
+ - "lib/tasks/**/*"
43
+ Style/MultilineOperationIndentation:
44
+ EnforcedStyle: indented
45
+ Style/NumericLiterals:
46
+ Enabled: false
47
+ Style/PercentLiteralDelimiters:
48
+ PreferredDelimiters:
49
+ "%w": "[]"
50
+ "%W": "[]"
51
+ "%i": "[]"
52
+ "%I": "[]"
53
+ "%r": "()"
54
+ Style/SignalException:
55
+ EnforcedStyle: semantic
56
+ Style/SingleLineBlockParams:
57
+ Enabled: false
58
+ Style/StringLiterals:
59
+ EnforcedStyle: double_quotes
60
+ Style/MultilineMethodCallIndentation:
61
+ Exclude:
62
+ - "spec/**/*.rb"
63
+
64
+ # These are better handled by reek
65
+ Metrics/MethodLength:
66
+ Enabled: false
67
+ Metrics/ParameterLists:
68
+ Enabled: false
69
+
70
+ # Rubocop's global exclude seems to fail to exclude the bin/ dir, so set all
71
+ # the cops that are failing manually, since most of these files are
72
+ # auto-generated anyways. Also, if the rules also appear above, we need to copy
73
+ # the same attrs, because YAML won't merge, only overwrite.
74
+
75
+ Style/StringLiterals:
76
+ EnforcedStyle: double_quotes
77
+ Exclude:
78
+ - "bin/**/*"
79
+ Style/FrozenStringLiteralComment:
80
+ Exclude:
81
+ - "bin/**/*"
82
+ Style/LeadingCommentSpace:
83
+ Exclude:
84
+ - "bin/**/*"
85
+ Style/SpaceInsideParens:
86
+ Exclude:
87
+ - "bin/**/*"
88
+ Style/AlignParameters:
89
+ Exclude:
90
+ - "bin/**/*"
91
+ Style/ExtraSpacing:
92
+ Exclude:
93
+ - "bin/**/*"
94
+ Exclude:
95
+ - "config/routes.rb"
96
+ Lint/PercentStringArray:
97
+ Exclude:
98
+ # SecureHeaders needs the single quotes in `%w[https: 'self']`
99
+ - config/initializers/secure_headers.rb
@@ -0,0 +1,31 @@
1
+ # Please don't modify this file, without first checking if it makes sense to
2
+ # adopt for most other Kapost projects. Ideally, we would keep these files as
3
+ # similar as possible to ease maintainability. Instead, open a PR in the
4
+ # codeclimate-common repo at https://github.com/kapost/codeclimate-common
5
+
6
+ # This file is where app-specific Rubocop configs go. I wish every tool supported inherited configs...
7
+
8
+ inherit_from:
9
+ - .rubocop.kapost.yml
10
+
11
+ # Frozen string break everything in napa. This is gonna be a big project to replace
12
+ Style/FrozenStringLiteralComment:
13
+ Exclude:
14
+ - "**/*.rb"
15
+
16
+ Rails/ActionFilter:
17
+ EnforcedStyle: filter
18
+
19
+ Style/TrailingCommaInArguments:
20
+ Enabled: false
21
+
22
+ Style/NumericPredicate:
23
+ Enabled: false
24
+
25
+ Style/BlockDelimiters:
26
+ EnforcedStyle: braces_for_chaining
27
+
28
+ # Disabled since it only applies to Rails 5. Reenable when we migrate our apps to Rails 5.
29
+ # https://github.com/bbatsov/rubocop/issues/3629
30
+ Rails/HttpPositionalArguments:
31
+ Enabled: false
@@ -0,0 +1 @@
1
+ 2.3.1
data/Gemfile ADDED
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ source "https://rubygems.org"
4
+
5
+ # Specify your gem's dependencies in clone_kit.gemspec
6
+ gemspec
@@ -0,0 +1,79 @@
1
+ # CloneKit!
2
+
3
+ An ActiveRecord-ish toolkit library for building database record cloning without the business logic and executing cloning operations, especially for multi-tenant applications using Mongoid.
4
+
5
+ ## Why does cloning require a special toolkit?
6
+
7
+ When operating a multi-tenant system, copying database records is fraught with perils that can wreak havoc on customer integrity. Failing to remap foreign ids does not usually trigger database integrity errors (especially in MongoDB :trollface:) but are even more insidious.
8
+
9
+ There is likely an alternative business logic required when records are copied and merged. CloneKit can help you assemble that logic.
10
+
11
+ ## How do I clone?
12
+
13
+ Let's pretend you have a account that you want to clone to a new account.
14
+
15
+ ```ruby
16
+ class BlogPost
17
+ include Mongoid::Document
18
+
19
+ field :account_id, type: BSON::ObjectId
20
+ field :blog_type_id, type: BSON::ObjectId
21
+ field :body, type: String
22
+ end
23
+ ```
24
+
25
+ You can specify the dependency order of cloning, the scope of the operation, and the specific cloning behavior inside a specification:
26
+
27
+ ```ruby
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
+ )
38
+ end
39
+ ```
40
+
41
+ ## Writing an Emitter
42
+
43
+ You have to write some emitters for your app. By default, CloneKit specifications utilize and empty emitter, making all clones no-operations.
44
+
45
+ TODO
46
+
47
+ ## Writing a Cloner
48
+
49
+ TODO
50
+
51
+ ## Extending the built-in Cloners
52
+
53
+ TODO
54
+
55
+ ## Installation
56
+
57
+ Add this line to your application's Gemfile:
58
+
59
+ ```ruby
60
+ gem 'clone_kit'
61
+ ```
62
+
63
+ And then execute:
64
+
65
+ $ bundle
66
+
67
+ Or install it yourself as:
68
+
69
+ $ gem install clone_kit
70
+
71
+ ## Development
72
+
73
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
74
+
75
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
76
+
77
+ ## Contributing
78
+
79
+ Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/clone_kit.
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "rspec/core/rake_task"
5
+
6
+ RSpec::Core::RakeTask.new(:spec)
7
+
8
+ task default: :spec
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "clone_kit"
5
+
6
+ # You can add fixtures and/or initialization code here to make experimenting
7
+ # with your gem easier. You can also use a different console, if you like.
8
+
9
+ # (If you use this, don't forget to add pry to your Gemfile!)
10
+ # require "pry"
11
+ # Pry.start
12
+
13
+ require "irb"
14
+ IRB.start
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+ # coding: utf-8
3
+ lib = File.expand_path("../lib", __FILE__)
4
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
5
+ require "clone_kit/version"
6
+
7
+ Gem::Specification.new do |spec|
8
+ spec.name = "clone_kit"
9
+ spec.version = CloneKit::VERSION
10
+ spec.authors = ["Brandon Croft"]
11
+ spec.email = ["brandon@kapost.com"]
12
+
13
+ spec.summary = "A toolkit to assist in complex cloning operations"
14
+ spec.description = "Supports rules-based cloning, Mongoid, and distributed operations"
15
+ spec.homepage = "https://github.com/kapost/clone_kit"
16
+
17
+ spec.files = `git ls-files -z`.split("\x0").reject do |f|
18
+ f.match(%r{^(spec)/})
19
+ end
20
+
21
+ spec.bindir = "exe"
22
+ spec.executables = spec.files.grep(%r(^exe/)) { |f| File.basename(f) }
23
+ spec.require_paths = ["lib"]
24
+
25
+ spec.add_runtime_dependency "redis"
26
+ spec.add_runtime_dependency "activesupport", "> 3.0.0" # For core ext Array#wrap and Object#blank?
27
+
28
+ spec.add_development_dependency "bundler", "~> 1.13"
29
+ spec.add_development_dependency "mongoid", "~> 4.0.2"
30
+ spec.add_development_dependency "database_cleaner", "1.5.3"
31
+ spec.add_development_dependency "rake", "~> 11.0"
32
+ spec.add_development_dependency "rspec", "~> 3.4"
33
+ spec.add_development_dependency "rspec-collection_matchers"
34
+ spec.add_development_dependency "pry-byebug"
35
+ spec.add_development_dependency "pry"
36
+ spec.add_development_dependency "fakeredis"
37
+ end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "clone_kit/version"
4
+ require "clone_kit/graph"
5
+ require "clone_kit/specification"
6
+ require "clone_kit/operation"
7
+ require "clone_kit/shared_id_map"
8
+ require "clone_kit/rule"
9
+
10
+ require "active_support/core_ext/array/wrap"
11
+ require "active_support/core_ext/object/blank"
12
+
13
+ module CloneKit
14
+ def self.graph
15
+ @graph ||= CloneKit::Graph.new
16
+ end
17
+
18
+ def self.load_rails_models!
19
+ Rails.application.eager_load! if defined?(Rails) && !defined?(@eager_loaded_once)
20
+ @eager_loaded_once = true
21
+ end
22
+
23
+ def self.spec
24
+ @spec ||= {}
25
+ end
26
+
27
+ def self.add_specification(specification)
28
+ spec[specification.model.name] = specification
29
+ refresh_specification(specification)
30
+ end
31
+
32
+ def self.refresh_specification(specification)
33
+ graph.add_vertex(specification.model.name, *specification.dependencies)
34
+ end
35
+
36
+ def self.cloneable_models(already_cloned)
37
+ result = []
38
+ graph.nodes.each do |model_name, deps|
39
+ next if already_cloned.include?(model_name)
40
+ next unless deps.all? { |dep| already_cloned.include?(dep) }
41
+
42
+ result << model_name
43
+ end
44
+ result
45
+ end
46
+ end
@@ -0,0 +1,106 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "clone_kit/cloners/mongoid_ruleset_cloner"
4
+
5
+ module CloneKit
6
+ module Cloners
7
+ class MongoidMergingRulesetCloner < MongoidRulesetCloner
8
+ def initialize(model_klass, rules: [], merge_fields: ["name"])
9
+ super(model_klass, rules: rules)
10
+ self.merge_fields = merge_fields
11
+ end
12
+
13
+ def clone_ids(ids, operation)
14
+ @saved_id_map = {}
15
+ initialize_cloner(operation)
16
+ apply_rules_and_save(find_and_merge_existing_records(ids))
17
+ end
18
+
19
+ protected
20
+
21
+ # These methods are super simple and should usually be overridden to merge records in a more nuanced fashion
22
+
23
+ def compare(first, second)
24
+ merge_fields.all? { |name| first[name] == second[name] }
25
+ end
26
+
27
+ def merge(records)
28
+ result = nil
29
+ records.each do |rec|
30
+ clone_all_embedded_fields(rec)
31
+ result = if result.nil?
32
+ rec.deep_dup
33
+ else
34
+ result.merge(rec)
35
+ end
36
+ end
37
+
38
+ result
39
+ end
40
+
41
+ private
42
+
43
+ attr_accessor :merge_fields
44
+
45
+ def find_and_merge_existing_records(ids)
46
+ all_records = []
47
+ each_existing_record(ids) do |rec|
48
+ all_records << rec
49
+ end
50
+
51
+ result = []
52
+ skip = Set.new
53
+
54
+ all_records.each_with_index do |record, i|
55
+ next if skip.include?(record["_id"])
56
+ mergeable = [record]
57
+
58
+ all_records[i + 1..all_records.length].each do |other_record|
59
+ next unless compare(record, other_record)
60
+
61
+ mergeable << other_record
62
+ skip << other_record["_id"]
63
+ end
64
+
65
+ new_id = BSON::ObjectId.new
66
+ new_record = if mergeable.length == 1
67
+ copy = clone(mergeable[0])
68
+ @saved_id_map[copy["_id"]] = new_id
69
+ copy
70
+ else
71
+ merged = merge(mergeable)
72
+
73
+ mergeable.each do |m|
74
+ @saved_id_map[m["_id"]] = new_id
75
+ end
76
+ merged
77
+ end
78
+
79
+ new_record["_id"] = new_id
80
+ result << new_record
81
+ end
82
+
83
+ CloneKit::SharedIdMap.new(current_operation.id).insert_many(model_klass, @saved_id_map)
84
+
85
+ result
86
+ end
87
+
88
+ def apply_rules_and_save(records)
89
+ records.each do |attributes|
90
+ id = attributes["_id"]
91
+ rules.each do |rule|
92
+ begin
93
+ rule.fix(@saved_id_map.key(id), attributes)
94
+ rescue StandardError => e
95
+ id = attributes["_id"]
96
+ message = "Unhandled error when applying rule #{rule.class.name} to #{model_klass} #{id}: #{e.class}"
97
+ current_operation.error(message)
98
+ end
99
+ end
100
+
101
+ save_or_fail(attributes)
102
+ end
103
+ end
104
+ end
105
+ end
106
+ end