acts_as_scrubbable 1.0.2 → 1.2.1

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
2
  SHA256:
3
- metadata.gz: a11d2fbbda7f3984de9911cba7b449d3f6c6720b17430f6511cf46407cd8a5c9
4
- data.tar.gz: 69a93697794fc1c7abd9b2acd2f9c068639b04f03391b5cc028492ebdebf44a1
3
+ metadata.gz: 98b03e06478fe5c46d1be95047d60b28b91f3e21699743940e18ae0ee6ac8075
4
+ data.tar.gz: 28e6b08a628f43b121b4419b1571466f4810c9ad619849ce8f1b7bdc7700a658
5
5
  SHA512:
6
- metadata.gz: 4b40343ddb3c0b7f764fd48167d3ce3c0dd2cc91f2221652f515849effd1b6da67040fe376e2543240a0efd391aa91eb0347d9e2780b184bee500ac5cad2a319
7
- data.tar.gz: 18c59e0bb668fe98bbf584297d56fb41b14d2bef2e3f4e9e052b330e012a61b5eb9988ab493233db953959776390e529218bff2e0bc8203033712a48054cad7f
6
+ metadata.gz: 74246e4b9ea4209288e2709cf86a0b35e8d5f4abc1f7548c6a5cac4d628fbfc11c1d5a69c61d2de357748e4b6157a107ad7bc34551c3734f9d1630f62b0986e2
7
+ data.tar.gz: 4c5407c1f0689663ff28562880784b89442f1bce5d0cdb26e9ae0c089d4a0435ac516b692399dd8eb4cf15a115c771545bad2367aa75a8b0e9b727e6dcc43fe6
data/.gitignore CHANGED
@@ -1 +1,3 @@
1
1
  Gemfile.lock
2
+
3
+ .idea/*
data/Gemfile CHANGED
@@ -1,5 +1,3 @@
1
- ruby '2.4.3'
2
-
3
1
  source 'https://rubygems.org'
4
2
 
5
3
  gemspec
data/README.md CHANGED
@@ -97,3 +97,22 @@ ActsAsScrubbable.configure do |c|
97
97
  end
98
98
  end
99
99
  ```
100
+
101
+ ### UPDATE VS UPSERT
102
+
103
+ By default, the scrubbing proces will run independent UPDATE statements for each scrubbed model. This can be time
104
+ consuming if you are scrubbing a large number of records. As an alternative, some
105
+ databases support doing bulk database updates using an upsert using the INSERT command. `activerecord-import`
106
+ gives us easy support for this and as such it is a requirement to using
107
+ this feature. Some details about the implementation can be found here. https://github.com/zdennis/activerecord-import#duplicate-key-update
108
+
109
+ Note that we only support the MySQL implementation at this time.
110
+
111
+ To use UPSERT over UPDATE, it can be enabled by specifying the environment variable `USE_UPSERT='true'` or through configuration.
112
+
113
+ ```ruby
114
+ ActsAsScrubbable.configure do |c|
115
+ c.use_upsert = true
116
+ end
117
+ ```
118
+
@@ -0,0 +1,36 @@
1
+ require 'acts_as_scrubbable/import_processor'
2
+ require 'acts_as_scrubbable/update_processor'
3
+ require 'term/ansicolor'
4
+
5
+ module ActsAsScrubbable
6
+ class ArClassProcessor
7
+
8
+ attr_reader :ar_class, :query_processor
9
+
10
+ def initialize(ar_class)
11
+ @ar_class = ar_class
12
+
13
+ if ActsAsScrubbable.use_upsert
14
+ ActsAsScrubbable.logger.info Term::ANSIColor.white("Using Upsert")
15
+ @query_processor = ImportProcessor.new(ar_class)
16
+ else
17
+ ActsAsScrubbable.logger.info Term::ANSIColor.white("Using Update")
18
+ @query_processor = UpdateProcessor.new(ar_class)
19
+ end
20
+ end
21
+
22
+ def process(num_of_batches)
23
+ ActsAsScrubbable.logger.info Term::ANSIColor.green("Scrubbing #{ar_class} ...")
24
+
25
+ num_of_batches = Integer(ENV.fetch("SCRUB_BATCHES", "256")) if num_of_batches.nil?
26
+ scrubbed_count = ActsAsScrubbable::ParallelTableScrubber.new(ar_class, num_of_batches).each_query do |query|
27
+ query_processor.scrub_query(query)
28
+ end
29
+
30
+ ActsAsScrubbable.logger.info Term::ANSIColor.blue("#{scrubbed_count} #{ar_class} objects scrubbed")
31
+ ActiveRecord::Base.connection.verify!
32
+
33
+ ActsAsScrubbable.logger.info Term::ANSIColor.white("Scrub Complete!")
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,28 @@
1
+ module ActsAsScrubbable
2
+ module BaseProcessor
3
+ attr_reader :ar_class
4
+ private :ar_class
5
+
6
+ def initialize(ar_class)
7
+ @ar_class = ar_class
8
+ end
9
+
10
+ def scrub_query(query = nil)
11
+ scrubbed_count = 0
12
+ ActiveRecord::Base.connection_pool.with_connection do
13
+ if ar_class.respond_to?(:scrubbable_scope)
14
+ relation = ar_class.send(:scrubbable_scope)
15
+ else
16
+ relation = ar_class.all
17
+ end
18
+
19
+ relation.where(query).find_in_batches(batch_size: 1000) do |batch|
20
+ ActiveRecord::Base.transaction do
21
+ scrubbed_count += handle_batch(batch)
22
+ end
23
+ end
24
+ end
25
+ scrubbed_count
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,24 @@
1
+ require 'acts_as_scrubbable/base_processor'
2
+
3
+ module ActsAsScrubbable
4
+ class ImportProcessor
5
+ include BaseProcessor
6
+
7
+ private
8
+ def handle_batch(batch)
9
+ scrubbed_count = 0
10
+ batch.each do |obj|
11
+ _updates = obj.scrubbed_values
12
+ obj.assign_attributes(_updates)
13
+ scrubbed_count += 1
14
+ end
15
+ ar_class.import(
16
+ batch,
17
+ on_duplicate_key_update: ar_class.scrubbable_fields.keys.map { |x| "`#{x}` = values(`#{x}`)" }.join(" , "),
18
+ validate: false,
19
+ timestamps: false
20
+ )
21
+ scrubbed_count
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,72 @@
1
+ require "parallel"
2
+
3
+ module ActsAsScrubbable
4
+ class ParallelTableScrubber
5
+ attr_reader :ar_class, :num_of_batches
6
+ private :ar_class, :num_of_batches
7
+
8
+ def initialize(ar_class, num_of_batches)
9
+ @ar_class = ar_class
10
+ @num_of_batches = num_of_batches
11
+ end
12
+
13
+ def each_query
14
+ # Removing any find or initialize callbacks from model
15
+ ar_class.reset_callbacks(:initialize)
16
+ ar_class.reset_callbacks(:find)
17
+
18
+ Parallel.map(parallel_queries) { |query|
19
+ yield(query)
20
+ }.reduce(:+) # returns the aggregated scrub count
21
+ end
22
+
23
+ private
24
+
25
+ # create even ID ranges for the table
26
+ def parallel_queries
27
+ raise "Model is missing id column" if ar_class.columns.none? { |column| column.name == "id" }
28
+
29
+ if ar_class.respond_to?(:scrubbable_scope)
30
+ num_records = ar_class.send(:scrubbable_scope).count
31
+ else
32
+ num_records = ar_class.count
33
+ end
34
+ return [] if num_records == 0 # no records to import
35
+
36
+ record_window_size, modulus = num_records.divmod(num_of_batches)
37
+ if record_window_size < 1
38
+ record_window_size = 1
39
+ modulus = 0
40
+ end
41
+
42
+ start_id = next_id(ar_class: ar_class, offset: 0)
43
+ queries = num_of_batches.times.each_with_object([]) do |_, queries|
44
+ next unless start_id
45
+
46
+ end_id = next_id(ar_class: ar_class, id: start_id, offset: record_window_size - 1)
47
+ if modulus > 0
48
+ end_id = next_id(ar_class: ar_class, id: end_id)
49
+ modulus -= 1
50
+ end
51
+ queries << { id: start_id..end_id } if end_id
52
+ start_id = next_id(ar_class: ar_class, id: end_id || start_id)
53
+ end
54
+
55
+ # just in case new records are added since we started, extend the end ID
56
+ queries[-1] = ["#{ar_class.quoted_table_name}.id >= ?", queries[-1][:id].begin] if queries.any?
57
+
58
+ queries
59
+ end
60
+
61
+ def next_id(ar_class:, id: nil, offset: 1)
62
+ if ar_class.respond_to?(:scrubbable_scope)
63
+ collection = ar_class.send(:scrubbable_scope)
64
+ else
65
+ collection = ar_class.all
66
+ end
67
+ collection = collection.reorder(:id)
68
+ collection = collection.where("#{ar_class.quoted_table_name}.id >= :id", id: id) if id
69
+ collection.offset(offset).limit(1).pluck(:id).first
70
+ end
71
+ end
72
+ end
@@ -1,7 +1,7 @@
1
1
  module ActsAsScrubbable
2
2
  module Scrub
3
3
 
4
- def scrub!
4
+ def scrubbed_values
5
5
  return unless self.class.scrubbable?
6
6
 
7
7
  run_callbacks(:scrub) do
@@ -20,7 +20,7 @@ module ActsAsScrubbable
20
20
  end
21
21
  end
22
22
 
23
- self.update_columns(_updates) unless _updates.empty?
23
+ _updates
24
24
  end
25
25
  end
26
26
  end
@@ -0,0 +1,66 @@
1
+ require 'acts_as_scrubbable/parallel_table_scrubber'
2
+ require 'highline/import'
3
+ require 'acts_as_scrubbable/ar_class_processor'
4
+ require 'term/ansicolor'
5
+
6
+ module ActsAsScrubbable
7
+ class TaskRunner
8
+ attr_reader :ar_classes
9
+ private :ar_classes
10
+
11
+ def initialize
12
+ @ar_classes = []
13
+ end
14
+
15
+ def prompt_db_configuration
16
+ db_host = ActiveRecord::Base.connection_config[:host]
17
+ db_name = ActiveRecord::Base.connection_config[:database]
18
+
19
+ ActsAsScrubbable.logger.warn Term::ANSIColor.red("Please verify the information below to continue")
20
+ ActsAsScrubbable.logger.warn Term::ANSIColor.red("Host: ") + Term::ANSIColor.white(" #{db_host}")
21
+ ActsAsScrubbable.logger.warn Term::ANSIColor.red("Database: ") + Term::ANSIColor.white("#{db_name}")
22
+ end
23
+
24
+ def confirmed_configuration?
25
+ db_host = ActiveRecord::Base.connection_config[:host]
26
+
27
+ unless ENV["SKIP_CONFIRM"] == "true"
28
+ answer = ask("Type '#{db_host}' to continue. \n".red + '-> '.white)
29
+ unless answer == db_host
30
+ ActsAsScrubbable.logger.error Term::ANSIColor.red("exiting ...")
31
+ return false
32
+ end
33
+ end
34
+ true
35
+ end
36
+
37
+ def extract_ar_classes
38
+ Rails.application.eager_load! # make sure all the classes are loaded
39
+ @ar_classes = ActiveRecord::Base.descendants.select { |d| d.scrubbable? }.sort_by { |d| d.to_s }
40
+
41
+ if ENV["SCRUB_CLASSES"].present?
42
+ class_list = ENV["SCRUB_CLASSES"].split(",")
43
+ class_list = class_list.map { |_class_str| _class_str.constantize }
44
+ @ar_classes = ar_classes & class_list
45
+ end
46
+ end
47
+
48
+ def set_ar_class(ar_class)
49
+ ar_classes << ar_class
50
+ end
51
+
52
+ def scrub(num_of_batches: nil)
53
+ Parallel.each(ar_classes) do |ar_class|
54
+ ActsAsScrubbable::ArClassProcessor.new(ar_class).process(num_of_batches)
55
+ end
56
+ ActiveRecord::Base.connection.verify!
57
+ end
58
+
59
+ def after_hooks
60
+ if ENV["SKIP_AFTERHOOK"].blank?
61
+ ActsAsScrubbable.logger.info Term::ANSIColor.red("Running after hook")
62
+ ActsAsScrubbable.execute_after_hook
63
+ end
64
+ end
65
+ end
66
+ end
@@ -1,96 +1,25 @@
1
-
2
1
  require 'rake'
2
+ require 'acts_as_scrubbable/task_runner'
3
3
 
4
4
  namespace :scrub do
5
5
 
6
- desc "scrub all"
6
+ desc "scrub all scrubbable tables"
7
7
  task all: :environment do
8
+ task_runner = ActsAsScrubbable::TaskRunner.new
9
+ task_runner.prompt_db_configuration
10
+ exit unless task_runner.confirmed_configuration?
11
+ task_runner.extract_ar_classes
12
+ task_runner.scrub(num_of_batches: 1)
13
+ task_runner.after_hooks
14
+ end
8
15
 
9
- require 'highline/import'
10
- require 'term/ansicolor'
11
- require 'logger'
12
- require 'parallel'
13
-
14
-
15
- include Term::ANSIColor
16
-
17
- @logger = Logger.new($stdout)
18
- @logger.formatter = proc do |severity, datetime, progname, msg|
19
- "#{datetime}: [#{severity}] - #{msg}\n"
20
- end
21
-
22
- db_host = ActiveRecord::Base.connection_config[:host]
23
- db_name = ActiveRecord::Base.connection_config[:database]
24
-
25
- @logger.warn "Please verify the information below to continue".red
26
- @logger.warn "Host: ".red + " #{db_host}".white
27
- @logger.warn "Database: ".red + "#{db_name}".white
28
-
29
- unless ENV["SKIP_CONFIRM"] == "true"
30
-
31
- answer = ask("Type '#{db_host}' to continue. \n".red + '-> '.white)
32
- unless answer == db_host
33
- @logger.error "exiting ...".red
34
- exit
35
- end
36
- end
37
-
38
- @logger.warn "Scrubbing classes".red
39
-
40
- Rails.application.eager_load! # make sure all the classes are loaded
41
-
42
- @total_scrubbed = 0
43
-
44
- ar_classes = ActiveRecord::Base.descendants.select{|d| d.scrubbable? }.sort_by{|d| d.to_s }
45
-
46
-
47
- # if the ENV variable is set
48
-
49
- unless ENV["SCRUB_CLASSES"].blank?
50
- class_list = ENV["SCRUB_CLASSES"].split(",")
51
- class_list = class_list.map {|_class_str| _class_str.constantize }
52
- ar_classes = ar_classes & class_list
53
- end
54
-
55
- @logger.info "Srubbable Classes: #{ar_classes.join(', ')}".white
56
-
57
- Parallel.each(ar_classes) do |ar_class|
58
-
59
- # Removing any find or initialize callbacks from model
60
- ar_class.reset_callbacks(:initialize)
61
- ar_class.reset_callbacks(:find)
62
-
63
- @logger.info "Scrubbing #{ar_class} ...".green
64
-
65
- scrubbed_count = 0
66
-
67
- ActiveRecord::Base.connection_pool.with_connection do
68
- if ar_class.respond_to?(:scrubbable_scope)
69
- relation = ar_class.send(:scrubbable_scope)
70
- else
71
- relation = ar_class.all
72
- end
73
-
74
- relation.find_in_batches(batch_size: 1000) do |batch|
75
- ActiveRecord::Base.transaction do
76
- batch.each do |obj|
77
- obj.scrub!
78
- scrubbed_count += 1
79
- end
80
- end
81
- end
82
- end
83
-
84
- @logger.info "#{scrubbed_count} #{ar_class} objects scrubbed".blue
85
- end
86
- ActiveRecord::Base.connection.verify!
87
-
88
- if ENV["SKIP_AFTERHOOK"].blank?
89
- @logger.info "Running after hook".red
90
- ActsAsScrubbable.execute_after_hook
91
- end
92
-
93
- @logger.info "Scrub Complete!".white
16
+ desc "Scrub one table"
17
+ task :model, [:ar_class] => :environment do |_, args|
18
+ task_runner = ActsAsScrubbable::TaskRunner.new
19
+ task_runner.prompt_db_configuration
20
+ exit unless task_runner.confirmed_configuration?
21
+ task_runner.set_ar_class(args[:ar_class].constantize)
22
+ task_runner.scrub
94
23
  end
95
24
  end
96
25
 
@@ -0,0 +1,18 @@
1
+ require 'acts_as_scrubbable/base_processor'
2
+
3
+ module ActsAsScrubbable
4
+ class UpdateProcessor
5
+ include BaseProcessor
6
+
7
+ private
8
+ def handle_batch(batch)
9
+ scrubbed_count = 0
10
+ batch.each do |obj|
11
+ _updates = obj.scrubbed_values
12
+ obj.update_columns(_updates) unless _updates.empty?
13
+ scrubbed_count += 1
14
+ end
15
+ scrubbed_count
16
+ end
17
+ end
18
+ end
@@ -1,3 +1,3 @@
1
1
  module ActsAsScrubbable
2
- VERSION = '1.0.2'
2
+ VERSION = '1.2.1'
3
3
  end
@@ -2,59 +2,76 @@ require 'active_record'
2
2
  require 'active_record/version'
3
3
  require 'active_support/core_ext/module'
4
4
  require 'acts_as_scrubbable/tasks'
5
-
5
+ require 'term/ansicolor'
6
+ require 'logger'
6
7
 
7
8
  module ActsAsScrubbable
9
+ extend self
8
10
  extend ActiveSupport::Autoload
11
+ include Term::ANSIColor
9
12
 
10
13
  autoload :Scrubbable
11
14
  autoload :Scrub
12
15
  autoload :VERSION
13
16
 
17
+ attr_accessor :use_upsert
18
+
19
+ class << self
20
+ def configure(&block)
21
+ self.use_upsert = ENV["USE_UPSERT"] == "true"
14
22
 
15
- def self.configure(&block)
16
- yield self
23
+ yield self
24
+ end
17
25
  end
18
26
 
19
- def self.after_hook(&block)
27
+ def after_hook(&block)
20
28
  @after_hook = block
21
29
  end
22
30
 
23
- def self.execute_after_hook
31
+ def execute_after_hook
24
32
  @after_hook.call if @after_hook
25
33
  end
26
34
 
27
- def self.add(key, value)
35
+ def logger
36
+ @logger ||= begin
37
+ loggger = Logger.new($stdout)
38
+ loggger.formatter = proc do |severity, datetime, progname, msg|
39
+ "#{datetime}: [#{severity}] - #{msg}\n"
40
+ end
41
+ loggger
42
+ end
43
+ end
44
+
45
+ def add(key, value)
28
46
  ActsAsScrubbable.scrub_map[key] = value
29
47
  end
30
48
 
31
- def self.scrub_map
49
+ def scrub_map
32
50
  require 'faker'
33
51
 
34
52
  @_scrub_map ||= {
35
- :first_name => -> { Faker::Name.first_name },
36
- :last_name => -> { Faker::Name.last_name },
37
- :middle_name => -> { Faker::Name.name },
38
- :name => -> { Faker::Name.name },
39
- :email => -> { Faker::Internet.email },
40
- :name_title => -> { Faker::Name.title },
41
- :company_name => -> { Faker::Company.name },
42
- :street_address => -> { Faker::Address.street_address },
53
+ :first_name => -> { Faker::Name.first_name },
54
+ :last_name => -> { Faker::Name.last_name },
55
+ :middle_name => -> { Faker::Name.name },
56
+ :name => -> { Faker::Name.name },
57
+ :email => -> { Faker::Internet.email },
58
+ :name_title => -> { defined? Faker::Job ? Faker::Job.title : Faker::Name.title },
59
+ :company_name => -> { Faker::Company.name },
60
+ :street_address => -> { Faker::Address.street_address },
43
61
  :secondary_address => -> { Faker::Address.secondary_address },
44
- :zip_code => -> { Faker::Address.zip_code },
45
- :state_abbr => -> { Faker::Address.state_abbr },
46
- :state => -> { Faker::Address.state },
47
- :city => -> { Faker::Address.city },
48
- :latitude => -> { Faker::Address.latitude },
49
- :longitude => -> { Faker::Address.longitude },
50
- :username => -> { Faker::Internet.user_name },
51
- :boolean => -> { [true, false ].sample },
52
- :school => -> { Faker::University.name }
62
+ :zip_code => -> { Faker::Address.zip_code },
63
+ :state_abbr => -> { Faker::Address.state_abbr },
64
+ :state => -> { Faker::Address.state },
65
+ :city => -> { Faker::Address.city },
66
+ :latitude => -> { Faker::Address.latitude },
67
+ :longitude => -> { Faker::Address.longitude },
68
+ :username => -> { Faker::Internet.user_name },
69
+ :boolean => -> { [true, false].sample },
70
+ :school => -> { Faker::University.name }
53
71
  }
54
72
  end
55
73
  end
56
74
 
57
-
58
75
  ActiveSupport.on_load(:active_record) do
59
76
  extend ActsAsScrubbable::Scrubbable
60
77
  end
data/spec/db/schema.rb CHANGED
@@ -2,8 +2,23 @@ ActiveRecord::Schema.define(version: 20150421224501) do
2
2
 
3
3
  create_table "scrubbable_models", force: true do |t|
4
4
  t.string "first_name"
5
+ t.string "last_name"
6
+ t.string "middle_name"
7
+ t.string "name"
8
+ t.string "email"
9
+ t.string "title"
10
+ t.string "company_name"
5
11
  t.string "address1"
12
+ t.string "address2"
13
+ t.string "zip_code"
14
+ t.string "state"
15
+ t.string "state_short"
16
+ t.string "city"
6
17
  t.string "lat"
18
+ t.string "lon"
19
+ t.string "username"
20
+ t.boolean "active"
21
+ t.string "school"
7
22
  end
8
23
 
9
24
  end
@@ -0,0 +1,56 @@
1
+ require 'spec_helper'
2
+
3
+ RSpec.describe ActsAsScrubbable::ArClassProcessor do
4
+ let(:ar_class) { ScrubbableModel }
5
+
6
+ describe "#initialize" do
7
+ subject { described_class.new(ar_class) }
8
+
9
+ context "with upsert enabled" do
10
+ before do
11
+ allow(ActsAsScrubbable).to receive(:use_upsert).and_return(true)
12
+ end
13
+
14
+ it "includes the ImportProcessor module" do
15
+ expect(subject.query_processor).to be_kind_of(ActsAsScrubbable::ImportProcessor)
16
+ end
17
+ end
18
+
19
+ context "without upsert enabled" do
20
+ it "includes the UpdateProcessor module" do
21
+ expect(subject.query_processor).to be_kind_of(ActsAsScrubbable::UpdateProcessor)
22
+ end
23
+ end
24
+ end
25
+
26
+ describe "#process" do
27
+ let(:num_of_batches) { nil }
28
+ let(:query) { nil }
29
+ let(:parallel_table_scrubber_mock) { instance_double("ParallelTableScrubber") }
30
+ let(:update_processor_mock) { instance_double("UpdateProcessor", scrub_query: nil) }
31
+ subject { described_class.new(ar_class) }
32
+
33
+ before do
34
+ allow(ActsAsScrubbable::ParallelTableScrubber).to receive(:new).and_return(parallel_table_scrubber_mock)
35
+ allow(ActsAsScrubbable::UpdateProcessor).to receive(:new).and_return(update_processor_mock)
36
+ allow(parallel_table_scrubber_mock).to receive(:each_query).and_yield(query)
37
+ end
38
+
39
+ it "calls the expected helper classes with the expected batch size" do
40
+ expect(ActiveRecord::Base.connection).to receive(:verify!)
41
+ expect(update_processor_mock).to receive(:scrub_query).with(query)
42
+ subject.process(num_of_batches)
43
+ expect(ActsAsScrubbable::ParallelTableScrubber).to have_received(:new).with(ar_class, 256)
44
+ end
45
+
46
+ context "with an inputted batch size" do
47
+ let(:num_of_batches) { 10 }
48
+
49
+ it "calls ParallelTableScrubber with the passed batch size" do
50
+ subject.process(num_of_batches)
51
+ expect(ActsAsScrubbable::ParallelTableScrubber).to have_received(:new).with(ar_class, num_of_batches)
52
+ end
53
+ end
54
+
55
+ end
56
+ end
@@ -0,0 +1,25 @@
1
+ require 'spec_helper'
2
+
3
+ RSpec.describe ActsAsScrubbable::ImportProcessor do
4
+ let(:ar_class) { ScrubbableModel }
5
+ let(:model) { ar_class.new }
6
+ subject { described_class.new(ar_class) }
7
+
8
+ before do
9
+ ar_class.extend(ImportSupport)
10
+ end
11
+
12
+ describe "#handle_batch" do
13
+ it "calls import with the correct parameters" do
14
+ expect(model).to receive(:scrubbed_values).and_call_original
15
+ expect(ar_class).to receive(:import).with(
16
+ [model],
17
+ on_duplicate_key_update: "`first_name` = values(`first_name`) , `last_name` = values(`last_name`) , `middle_name` = values(`middle_name`) , `name` = values(`name`) , `email` = values(`email`) , `company_name` = values(`company_name`) , `zip_code` = values(`zip_code`) , `state` = values(`state`) , `city` = values(`city`) , `username` = values(`username`) , `school` = values(`school`) , `title` = values(`title`) , `address1` = values(`address1`) , `address2` = values(`address2`) , `state_short` = values(`state_short`) , `lat` = values(`lat`) , `lon` = values(`lon`) , `active` = values(`active`)",
18
+ validate: false,
19
+ timestamps: false
20
+ )
21
+
22
+ expect(subject.send(:handle_batch, [model])).to eq 1
23
+ end
24
+ end
25
+ end
@@ -5,34 +5,60 @@ RSpec.describe ActsAsScrubbable::Scrub do
5
5
  describe '.scrub' do
6
6
 
7
7
  # update_columns cannot be run on a new record
8
- subject{ ScrubbableModel.new }
8
+ subject { ScrubbableModel.new }
9
9
  before(:each) { subject.save }
10
10
 
11
+ it 'scrubs all columns' do
12
+ subject.attributes = {
13
+ first_name: "Ted",
14
+ last_name: "Lowe",
15
+ middle_name: "Cassidy",
16
+ name: "Miss Vincenzo Smitham",
17
+ email: "trentdibbert@wiza.com",
18
+ title: "Internal Consultant",
19
+ company_name: "Greenfelder, Collier and Lesch",
20
+ address1: "86780 Watsica Flats",
21
+ address2: "Apt. 913",
22
+ zip_code: "49227",
23
+ state: "Ohio",
24
+ state_short: "OH",
25
+ city: "Port Hildegard",
26
+ lat: -79.5855309778974,
27
+ lon: 13.517352691513906,
28
+ username: "oscar.hermann",
29
+ active: false,
30
+ school: "Eastern Lebsack",
31
+ }
32
+ expect {
33
+ subject.scrubbed_values
34
+ }.not_to raise_error
35
+ end
36
+
11
37
  it 'changes the first_name attribute when scrub is run' do
12
38
  subject.first_name = "Ted"
13
39
  allow(Faker::Name).to receive(:first_name).and_return("John")
14
- subject.scrub!
15
- expect(subject.first_name).to eq "John"
40
+ _updates = subject.scrubbed_values
41
+ expect(_updates[:first_name]).to eq "John"
16
42
  end
17
43
 
18
44
  it 'calls street address on faker and updates address1' do
19
45
  subject.address1 = "123 abc"
20
46
  subject.save
21
47
  allow(Faker::Address).to receive(:street_address).and_return("1 Embarcadero")
22
- subject.scrub!
23
- expect(subject.address1).to eq "1 Embarcadero"
48
+ _updates = subject.scrubbed_values
49
+ expect(_updates[:address1]).to eq "1 Embarcadero"
24
50
  end
25
51
 
26
52
  it "doesn't update the field if it's blank" do
27
53
  subject.address1 = nil
28
54
  subject.save
29
55
  allow(Faker::Address).to receive(:street_address).and_return("1 Embarcadero")
30
- subject.scrub!
31
- expect(subject.address1).to be_nil
56
+ _updates = subject.scrubbed_values
57
+ expect(_updates[:address1]).to be_nil
32
58
  end
33
59
 
34
60
  it 'runs scrub callbacks' do
35
- subject.scrub!
61
+ subject.scrubbed_values
36
62
  expect(subject.scrubbing_begun).to be(true)
37
63
  expect(subject.scrubbing_finished).to be(true)
38
64
  end
@@ -0,0 +1,19 @@
1
+ require 'spec_helper'
2
+
3
+ RSpec.describe ActsAsScrubbable::UpdateProcessor do
4
+ let(:ar_class) { ScrubbableModel }
5
+ let(:model) { ar_class.create(
6
+ first_name: "Ted",
7
+ last_name: "Lowe",
8
+ ) }
9
+ subject { described_class.new(ar_class) }
10
+
11
+ describe "#handle_batch" do
12
+ it "calls update with the updated attributes" do
13
+ expect(model).to receive(:scrubbed_values).and_call_original
14
+ expect(model).to receive(:update_columns).and_call_original
15
+
16
+ expect(subject.send(:handle_batch, [model])).to eq 1
17
+ end
18
+ end
19
+ end
@@ -11,11 +11,32 @@ RSpec.configure do |config|
11
11
  config.include include NullDB::RSpec::NullifiedDatabase
12
12
  end
13
13
 
14
+ module ImportSupport
15
+ def import(*, **); end
16
+ end
17
+
14
18
 
15
19
  class NonScrubbableModel < ActiveRecord::Base; end
16
20
 
17
21
  class ScrubbableModel < ActiveRecord::Base
18
- acts_as_scrubbable :first_name, :address1 => :street_address, :lat => :latitude
22
+ acts_as_scrubbable :first_name,
23
+ :last_name,
24
+ :middle_name,
25
+ :name,
26
+ :email,
27
+ :company_name,
28
+ :zip_code,
29
+ :state,
30
+ :city,
31
+ :username,
32
+ :school,
33
+ :title => :name_title,
34
+ :address1 => :street_address,
35
+ :address2 => :secondary_address,
36
+ :state_short => :state_abbr,
37
+ :lat => :latitude,
38
+ :lon => :longitude,
39
+ :active => :boolean
19
40
  attr_accessor :scrubbing_begun, :scrubbing_finished
20
41
  set_callback :scrub, :before do
21
42
  self.scrubbing_begun = true
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: acts_as_scrubbable
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.2
4
+ version: 1.2.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Samer Masry
8
- autorequire:
8
+ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2018-08-22 00:00:00.000000000 Z
11
+ date: 2021-08-24 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport
@@ -226,21 +226,30 @@ files:
226
226
  - README.md
227
227
  - acts_as_scrubbable.gemspec
228
228
  - lib/acts_as_scrubbable.rb
229
+ - lib/acts_as_scrubbable/ar_class_processor.rb
230
+ - lib/acts_as_scrubbable/base_processor.rb
231
+ - lib/acts_as_scrubbable/import_processor.rb
232
+ - lib/acts_as_scrubbable/parallel_table_scrubber.rb
229
233
  - lib/acts_as_scrubbable/scrub.rb
230
234
  - lib/acts_as_scrubbable/scrubbable.rb
235
+ - lib/acts_as_scrubbable/task_runner.rb
231
236
  - lib/acts_as_scrubbable/tasks.rb
237
+ - lib/acts_as_scrubbable/update_processor.rb
232
238
  - lib/acts_as_scrubbable/version.rb
233
239
  - spec/db/database.yml
234
240
  - spec/db/schema.rb
241
+ - spec/lib/acts_as_scrubbable/ar_class_processor_spec.rb
242
+ - spec/lib/acts_as_scrubbable/import_processor_spec.rb
235
243
  - spec/lib/acts_as_scrubbable/scrub_spec.rb
236
244
  - spec/lib/acts_as_scrubbable/scrubbable_spec.rb
245
+ - spec/lib/acts_as_scrubbable/update_processor_spec.rb
237
246
  - spec/spec_helper.rb
238
247
  - spec/support/database.rb
239
248
  homepage: https://github.com/smasry/acts_as_scrubbable
240
249
  licenses:
241
250
  - MIT
242
251
  metadata: {}
243
- post_install_message:
252
+ post_install_message:
244
253
  rdoc_options: []
245
254
  require_paths:
246
255
  - lib
@@ -255,15 +264,17 @@ required_rubygems_version: !ruby/object:Gem::Requirement
255
264
  - !ruby/object:Gem::Version
256
265
  version: '0'
257
266
  requirements: []
258
- rubyforge_project:
259
- rubygems_version: 2.7.6
260
- signing_key:
267
+ rubygems_version: 3.1.4
268
+ signing_key:
261
269
  specification_version: 4
262
270
  summary: Scrubbing data made easy
263
271
  test_files:
264
272
  - spec/db/database.yml
265
273
  - spec/db/schema.rb
274
+ - spec/lib/acts_as_scrubbable/ar_class_processor_spec.rb
275
+ - spec/lib/acts_as_scrubbable/import_processor_spec.rb
266
276
  - spec/lib/acts_as_scrubbable/scrub_spec.rb
267
277
  - spec/lib/acts_as_scrubbable/scrubbable_spec.rb
278
+ - spec/lib/acts_as_scrubbable/update_processor_spec.rb
268
279
  - spec/spec_helper.rb
269
280
  - spec/support/database.rb