acts_as_scrubbable 1.1.0 → 1.4.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- SHA1:
3
- metadata.gz: ead6fbeb0cee99c7046ad13c00423805d1099550
4
- data.tar.gz: e31b55515b4cef3df1d78e482ec37745fdcf518c
2
+ SHA256:
3
+ metadata.gz: 8d5dd99dfeaccd8c11b9eb2125c5dc9e16aba2f8f5d02eb5fc9fba59d7c36f02
4
+ data.tar.gz: 3551737a717372941339f0a81e95f6142a87a1ebb006437465d458c0fee0ea07
5
5
  SHA512:
6
- metadata.gz: 7c56b941e491ed7e7d76e8d1abaeaddebbea42e6d1d6b023f122698f0c5d6483b6959a0dbb45007b5ef0443401d12fb0b37d7c975282675a7acefdf332603783
7
- data.tar.gz: 6a13305d96f630c4f37494d92ce64a83dbba5d19e0a6528460bb34616d80ba13b172803e3d112fbf5d1a6db113fdc145605f47c1808395a07f811f2814f09660
6
+ metadata.gz: eca9d4ca0de33ea806a4e8c28e7eb97d832c9859ee00f984342d6b041aa31393742942ebee345123459f484fd1a7f157164115afbf5e67070df2c990e9bc5d2d
7
+ data.tar.gz: be3873e9f173fbf7058aa0c5328397465264117f627afbfdd92f527a5f8a7f1e637722b13e2be9d4dce9adf3133182c93c988f324cf3e046c8a0382a015dd891
data/.gitignore CHANGED
@@ -1 +1,3 @@
1
1
  Gemfile.lock
2
+
3
+ .idea/*
data/.travis.yml CHANGED
@@ -1,7 +1,7 @@
1
1
  language: ruby
2
2
 
3
3
  rvm:
4
- - 2.4.3
4
+ - 2.7.2
5
5
 
6
6
  script:
7
7
  - bundle exec rspec
data/Gemfile CHANGED
@@ -1,4 +1,4 @@
1
- ruby '2.4.3'
1
+ ruby '2.7.2'
2
2
 
3
3
  source 'https://rubygems.org'
4
4
 
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
+
@@ -13,9 +13,9 @@ Gem::Specification.new do |s|
13
13
  s.license = "MIT"
14
14
  s.required_ruby_version = '~> 2.0'
15
15
 
16
- s.add_runtime_dependency 'activesupport' , '>= 4.1', '< 6'
17
- s.add_runtime_dependency 'activerecord' , '>= 4.1', '< 6'
18
- s.add_runtime_dependency 'railties' , '>= 4.1', '< 6'
16
+ s.add_runtime_dependency 'activesupport' , '>= 4.1', '< 8'
17
+ s.add_runtime_dependency 'activerecord' , '>= 4.1', '< 8'
18
+ s.add_runtime_dependency 'railties' , '>= 4.1', '< 8'
19
19
  s.add_runtime_dependency 'faker' , '>= 1.4'
20
20
  s.add_runtime_dependency 'highline' , '>= 1.7'
21
21
  s.add_runtime_dependency 'term-ansicolor' , '>= 1.3'
@@ -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
@@ -2,40 +2,28 @@ require "parallel"
2
2
 
3
3
  module ActsAsScrubbable
4
4
  class ParallelTableScrubber
5
- def initialize(ar_class)
5
+ attr_reader :ar_class, :num_of_batches
6
+ private :ar_class, :num_of_batches
7
+
8
+ def initialize(ar_class, num_of_batches)
6
9
  @ar_class = ar_class
10
+ @num_of_batches = num_of_batches
7
11
  end
8
12
 
9
- def scrub(num_batches:)
13
+ def each_query
10
14
  # Removing any find or initialize callbacks from model
11
15
  ar_class.reset_callbacks(:initialize)
12
16
  ar_class.reset_callbacks(:find)
13
17
 
14
- queries = parallel_queries(ar_class: ar_class, num_batches: num_batches)
15
- scrubbed_count = Parallel.map(queries) { |query|
16
- scrubbed_count = 0
17
- ActiveRecord::Base.connection_pool.with_connection do
18
- relation = ar_class
19
- relation = relation.send(:scrubbable_scope) if ar_class.respond_to?(:scrubbable_scope)
20
- relation.where(query).find_in_batches(batch_size: 1000) do |batch|
21
- ActiveRecord::Base.transaction do
22
- batch.each do |obj|
23
- obj.scrub!
24
- scrubbed_count += 1
25
- end
26
- end
27
- end
28
- end
29
- scrubbed_count
30
- }.reduce(:+)
18
+ Parallel.map(parallel_queries) { |query|
19
+ yield(query)
20
+ }.reduce(:+) # returns the aggregated scrub count
31
21
  end
32
22
 
33
23
  private
34
24
 
35
- attr_reader :ar_class
36
-
37
25
  # create even ID ranges for the table
38
- def parallel_queries(ar_class:, num_batches:)
26
+ def parallel_queries
39
27
  raise "Model is missing id column" if ar_class.columns.none? { |column| column.name == "id" }
40
28
 
41
29
  if ar_class.respond_to?(:scrubbable_scope)
@@ -45,22 +33,22 @@ module ActsAsScrubbable
45
33
  end
46
34
  return [] if num_records == 0 # no records to import
47
35
 
48
- record_window_size, modulus = num_records.divmod(num_batches)
36
+ record_window_size, modulus = num_records.divmod(num_of_batches)
49
37
  if record_window_size < 1
50
38
  record_window_size = 1
51
39
  modulus = 0
52
40
  end
53
41
 
54
42
  start_id = next_id(ar_class: ar_class, offset: 0)
55
- queries = num_batches.times.each_with_object([]) do |_, queries|
43
+ queries = num_of_batches.times.each_with_object([]) do |_, queries|
56
44
  next unless start_id
57
45
 
58
- end_id = next_id(ar_class: ar_class, id: start_id, offset: record_window_size-1)
46
+ end_id = next_id(ar_class: ar_class, id: start_id, offset: record_window_size - 1)
59
47
  if modulus > 0
60
48
  end_id = next_id(ar_class: ar_class, id: end_id)
61
49
  modulus -= 1
62
50
  end
63
- queries << {id: start_id..end_id} if end_id
51
+ queries << { id: start_id..end_id } if end_id
64
52
  start_id = next_id(ar_class: ar_class, id: end_id || start_id)
65
53
  end
66
54
 
@@ -76,7 +64,7 @@ module ActsAsScrubbable
76
64
  else
77
65
  collection = ar_class.all
78
66
  end
79
- collection.reorder(:id)
67
+ collection = collection.reorder(:id)
80
68
  collection = collection.where("#{ar_class.quoted_table_name}.id >= :id", id: id) if id
81
69
  collection.offset(offset).limit(1).pluck(:id).first
82
70
  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,77 @@
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, skip_before_hooks: false, skip_after_hooks: false)
53
+ before_hooks unless skip_before_hooks
54
+
55
+ Parallel.each(ar_classes) do |ar_class|
56
+ ActsAsScrubbable::ArClassProcessor.new(ar_class).process(num_of_batches)
57
+ end
58
+ ActiveRecord::Base.connection.verify!
59
+
60
+ after_hooks unless skip_after_hooks
61
+ end
62
+
63
+ def before_hooks
64
+ return if ENV["SKIP_BEFOREHOOK"]
65
+
66
+ ActsAsScrubbable.logger.info Term::ANSIColor.red("Running before hook")
67
+ ActsAsScrubbable.execute_before_hook
68
+ end
69
+
70
+ def after_hooks
71
+ return if ENV["SKIP_AFTERHOOK"]
72
+
73
+ ActsAsScrubbable.logger.info Term::ANSIColor.red("Running after hook")
74
+ ActsAsScrubbable.execute_after_hook
75
+ end
76
+ end
77
+ end
@@ -1,130 +1,24 @@
1
-
2
1
  require 'rake'
2
+ require 'acts_as_scrubbable/task_runner'
3
3
 
4
4
  namespace :scrub do
5
5
 
6
6
  desc "scrub all scrubbable tables"
7
7
  task all: :environment do
8
- require 'highline/import'
9
- require 'term/ansicolor'
10
- require 'logger'
11
- require 'parallel'
12
-
13
- include Term::ANSIColor
14
-
15
- logger = Logger.new($stdout)
16
- logger.formatter = proc do |severity, datetime, progname, msg|
17
- "#{datetime}: [#{severity}] - #{msg}\n"
18
- end
19
-
20
- db_host = ActiveRecord::Base.connection_config[:host]
21
- db_name = ActiveRecord::Base.connection_config[:database]
22
-
23
- logger.warn "Please verify the information below to continue".red
24
- logger.warn "Host: ".red + " #{db_host}".white
25
- logger.warn "Database: ".red + "#{db_name}".white
26
-
27
- unless ENV["SKIP_CONFIRM"] == "true"
28
- answer = ask("Type '#{db_host}' to continue. \n".red + '-> '.white)
29
- unless answer == db_host
30
- logger.error "exiting ...".red
31
- exit
32
- end
33
- end
34
-
35
- logger.warn "Scrubbing classes".red
36
-
37
- Rails.application.eager_load! # make sure all the classes are loaded
38
-
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
-
47
- logger.info "Scrubbable Classes: #{ar_classes.join(', ')}".white
48
-
49
- Parallel.each(ar_classes) do |ar_class|
50
- # Removing any find or initialize callbacks from model
51
- ar_class.reset_callbacks(:initialize)
52
- ar_class.reset_callbacks(:find)
53
-
54
- logger.info "Scrubbing #{ar_class} ...".green
55
-
56
- scrubbed_count = 0
57
-
58
- ActiveRecord::Base.connection_pool.with_connection do
59
- if ar_class.respond_to?(:scrubbable_scope)
60
- relation = ar_class.send(:scrubbable_scope)
61
- else
62
- relation = ar_class.all
63
- end
64
-
65
- relation.find_in_batches(batch_size: 1000) do |batch|
66
- ActiveRecord::Base.transaction do
67
- batch.each do |obj|
68
- obj.scrub!
69
- scrubbed_count += 1
70
- end
71
- end
72
- end
73
- end
74
-
75
- logger.info "#{scrubbed_count} #{ar_class} objects scrubbed".blue
76
- end
77
- ActiveRecord::Base.connection.verify!
78
-
79
- if ENV["SKIP_AFTERHOOK"].blank?
80
- logger.info "Running after hook".red
81
- ActsAsScrubbable.execute_after_hook
82
- end
83
-
84
- logger.info "Scrub Complete!".white
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)
85
13
  end
86
14
 
87
15
  desc "Scrub one table"
88
16
  task :model, [:ar_class] => :environment do |_, args|
89
- require 'highline/import'
90
- require 'term/ansicolor'
91
- require 'logger'
92
- require 'acts_as_scrubbable/parallel_table_scrubber'
93
-
94
- include Term::ANSIColor
95
-
96
- logger = Logger.new($stdout)
97
- logger.formatter = proc do |severity, datetime, progname, msg|
98
- "#{datetime}: [#{severity}] - #{msg}\n"
99
- end
100
-
101
- db_host = ActiveRecord::Base.connection_config[:host]
102
- db_name = ActiveRecord::Base.connection_config[:database]
103
-
104
- logger.warn "Please verify the information below to continue".red
105
- logger.warn "Host: ".red + " #{db_host}".white
106
- logger.warn "Database: ".red + "#{db_name}".white
107
-
108
- unless ENV["SKIP_CONFIRM"] == "true"
109
- answer = ask("Type '#{db_host}' to continue. \n".red + '-> '.white)
110
- unless answer == db_host
111
- logger.error "exiting ...".red
112
- exit
113
- end
114
- end
115
-
116
- Rails.application.eager_load! # make sure all the classes are loaded
117
-
118
- ar_class = args[:ar_class].constantize
119
- logger.info "Scrubbing #{ar_class} ...".green
120
-
121
- num_batches = Integer(ENV.fetch("SCRUB_BATCHES", "256"))
122
- scrubbed_count = ActsAsScrubbable::ParallelTableScrubber.new(ar_class).scrub(num_batches: num_batches)
123
-
124
- logger.info "#{scrubbed_count} #{ar_class} objects scrubbed".blue
125
- ActiveRecord::Base.connection.verify!
126
-
127
- logger.info "Scrub Complete!".white
17
+ task_runner = ActsAsScrubbable::TaskRunner.new
18
+ task_runner.prompt_db_configuration
19
+ exit unless task_runner.confirmed_configuration?
20
+ task_runner.set_ar_class(args[:ar_class].constantize)
21
+ task_runner.scrub(skip_after_hooks: true)
128
22
  end
129
23
  end
130
24
 
@@ -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.1.0'
2
+ VERSION = '1.4.0'
3
3
  end
@@ -2,59 +2,84 @@ 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 before_hook(&block)
28
+ @before_hook = block
29
+ end
30
+
31
+ def execute_before_hook
32
+ @before_hook.call if @before_hook
33
+ end
34
+
35
+ def after_hook(&block)
20
36
  @after_hook = block
21
37
  end
22
38
 
23
- def self.execute_after_hook
39
+ def execute_after_hook
24
40
  @after_hook.call if @after_hook
25
41
  end
26
42
 
27
- def self.add(key, value)
43
+ def logger
44
+ @logger ||= begin
45
+ loggger = Logger.new($stdout)
46
+ loggger.formatter = proc do |severity, datetime, progname, msg|
47
+ "#{datetime}: [#{severity}] - #{msg}\n"
48
+ end
49
+ loggger
50
+ end
51
+ end
52
+
53
+ def add(key, value)
28
54
  ActsAsScrubbable.scrub_map[key] = value
29
55
  end
30
56
 
31
- def self.scrub_map
57
+ def scrub_map
32
58
  require 'faker'
33
59
 
34
60
  @_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 => -> { defined? Faker::Job ? Faker::Job.title : Faker::Name.title },
41
- :company_name => -> { Faker::Company.name },
42
- :street_address => -> { Faker::Address.street_address },
61
+ :first_name => -> { Faker::Name.first_name },
62
+ :last_name => -> { Faker::Name.last_name },
63
+ :middle_name => -> { Faker::Name.name },
64
+ :name => -> { Faker::Name.name },
65
+ :email => -> { Faker::Internet.email },
66
+ :name_title => -> { defined? Faker::Job ? Faker::Job.title : Faker::Name.title },
67
+ :company_name => -> { Faker::Company.name },
68
+ :street_address => -> { Faker::Address.street_address },
43
69
  :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 }
70
+ :zip_code => -> { Faker::Address.zip_code },
71
+ :state_abbr => -> { Faker::Address.state_abbr },
72
+ :state => -> { Faker::Address.state },
73
+ :city => -> { Faker::Address.city },
74
+ :latitude => -> { Faker::Address.latitude },
75
+ :longitude => -> { Faker::Address.longitude },
76
+ :username => -> { Faker::Internet.user_name },
77
+ :boolean => -> { [true, false].sample },
78
+ :school => -> { Faker::University.name }
53
79
  }
54
80
  end
55
81
  end
56
82
 
57
-
58
83
  ActiveSupport.on_load(:active_record) do
59
84
  extend ActsAsScrubbable::Scrubbable
60
85
  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
@@ -30,35 +30,35 @@ RSpec.describe ActsAsScrubbable::Scrub do
30
30
  school: "Eastern Lebsack",
31
31
  }
32
32
  expect {
33
- subject.scrub!
33
+ subject.scrubbed_values
34
34
  }.not_to raise_error
35
35
  end
36
36
 
37
37
  it 'changes the first_name attribute when scrub is run' do
38
38
  subject.first_name = "Ted"
39
39
  allow(Faker::Name).to receive(:first_name).and_return("John")
40
- subject.scrub!
41
- expect(subject.first_name).to eq "John"
40
+ _updates = subject.scrubbed_values
41
+ expect(_updates[:first_name]).to eq "John"
42
42
  end
43
43
 
44
44
  it 'calls street address on faker and updates address1' do
45
45
  subject.address1 = "123 abc"
46
46
  subject.save
47
47
  allow(Faker::Address).to receive(:street_address).and_return("1 Embarcadero")
48
- subject.scrub!
49
- expect(subject.address1).to eq "1 Embarcadero"
48
+ _updates = subject.scrubbed_values
49
+ expect(_updates[:address1]).to eq "1 Embarcadero"
50
50
  end
51
51
 
52
52
  it "doesn't update the field if it's blank" do
53
53
  subject.address1 = nil
54
54
  subject.save
55
55
  allow(Faker::Address).to receive(:street_address).and_return("1 Embarcadero")
56
- subject.scrub!
57
- expect(subject.address1).to be_nil
56
+ _updates = subject.scrubbed_values
57
+ expect(_updates[:address1]).to be_nil
58
58
  end
59
59
 
60
60
  it 'runs scrub callbacks' do
61
- subject.scrub!
61
+ subject.scrubbed_values
62
62
  expect(subject.scrubbing_begun).to be(true)
63
63
  expect(subject.scrubbing_finished).to be(true)
64
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
@@ -1,7 +1,11 @@
1
1
  require 'nulldb/rails'
2
2
  require 'nulldb_rspec'
3
3
 
4
- ActiveRecord::Base.configurations.merge!("test" => {adapter: 'nulldb'})
4
+ if ActiveRecord::Base.configurations.respond_to?(:merge!)
5
+ ActiveRecord::Base.configurations.merge!("test" => {adapter: 'nulldb'})
6
+ else
7
+ ActiveRecord::Base.configurations = ActiveRecord::DatabaseConfigurations.new(test: {adapter: 'nulldb'})
8
+ end
5
9
 
6
10
  NullDB.configure do |c|
7
11
  c.project_root = './spec'
@@ -11,6 +15,10 @@ RSpec.configure do |config|
11
15
  config.include include NullDB::RSpec::NullifiedDatabase
12
16
  end
13
17
 
18
+ module ImportSupport
19
+ def import(*, **); end
20
+ end
21
+
14
22
 
15
23
  class NonScrubbableModel < ActiveRecord::Base; end
16
24
 
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.1.0
4
+ version: 1.4.0
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: 2019-06-12 00:00:00.000000000 Z
11
+ date: 2022-07-19 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport
@@ -19,7 +19,7 @@ dependencies:
19
19
  version: '4.1'
20
20
  - - "<"
21
21
  - !ruby/object:Gem::Version
22
- version: '6'
22
+ version: '8'
23
23
  type: :runtime
24
24
  prerelease: false
25
25
  version_requirements: !ruby/object:Gem::Requirement
@@ -29,7 +29,7 @@ dependencies:
29
29
  version: '4.1'
30
30
  - - "<"
31
31
  - !ruby/object:Gem::Version
32
- version: '6'
32
+ version: '8'
33
33
  - !ruby/object:Gem::Dependency
34
34
  name: activerecord
35
35
  requirement: !ruby/object:Gem::Requirement
@@ -39,7 +39,7 @@ dependencies:
39
39
  version: '4.1'
40
40
  - - "<"
41
41
  - !ruby/object:Gem::Version
42
- version: '6'
42
+ version: '8'
43
43
  type: :runtime
44
44
  prerelease: false
45
45
  version_requirements: !ruby/object:Gem::Requirement
@@ -49,7 +49,7 @@ dependencies:
49
49
  version: '4.1'
50
50
  - - "<"
51
51
  - !ruby/object:Gem::Version
52
- version: '6'
52
+ version: '8'
53
53
  - !ruby/object:Gem::Dependency
54
54
  name: railties
55
55
  requirement: !ruby/object:Gem::Requirement
@@ -59,7 +59,7 @@ dependencies:
59
59
  version: '4.1'
60
60
  - - "<"
61
61
  - !ruby/object:Gem::Version
62
- version: '6'
62
+ version: '8'
63
63
  type: :runtime
64
64
  prerelease: false
65
65
  version_requirements: !ruby/object:Gem::Requirement
@@ -69,7 +69,7 @@ dependencies:
69
69
  version: '4.1'
70
70
  - - "<"
71
71
  - !ruby/object:Gem::Version
72
- version: '6'
72
+ version: '8'
73
73
  - !ruby/object:Gem::Dependency
74
74
  name: faker
75
75
  requirement: !ruby/object:Gem::Requirement
@@ -226,22 +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
229
232
  - lib/acts_as_scrubbable/parallel_table_scrubber.rb
230
233
  - lib/acts_as_scrubbable/scrub.rb
231
234
  - lib/acts_as_scrubbable/scrubbable.rb
235
+ - lib/acts_as_scrubbable/task_runner.rb
232
236
  - lib/acts_as_scrubbable/tasks.rb
237
+ - lib/acts_as_scrubbable/update_processor.rb
233
238
  - lib/acts_as_scrubbable/version.rb
234
239
  - spec/db/database.yml
235
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
236
243
  - spec/lib/acts_as_scrubbable/scrub_spec.rb
237
244
  - spec/lib/acts_as_scrubbable/scrubbable_spec.rb
245
+ - spec/lib/acts_as_scrubbable/update_processor_spec.rb
238
246
  - spec/spec_helper.rb
239
247
  - spec/support/database.rb
240
248
  homepage: https://github.com/smasry/acts_as_scrubbable
241
249
  licenses:
242
250
  - MIT
243
251
  metadata: {}
244
- post_install_message:
252
+ post_install_message:
245
253
  rdoc_options: []
246
254
  require_paths:
247
255
  - lib
@@ -256,15 +264,17 @@ required_rubygems_version: !ruby/object:Gem::Requirement
256
264
  - !ruby/object:Gem::Version
257
265
  version: '0'
258
266
  requirements: []
259
- rubyforge_project:
260
- rubygems_version: 2.6.14
261
- signing_key:
267
+ rubygems_version: 3.1.4
268
+ signing_key:
262
269
  specification_version: 4
263
270
  summary: Scrubbing data made easy
264
271
  test_files:
265
272
  - spec/db/database.yml
266
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
267
276
  - spec/lib/acts_as_scrubbable/scrub_spec.rb
268
277
  - spec/lib/acts_as_scrubbable/scrubbable_spec.rb
278
+ - spec/lib/acts_as_scrubbable/update_processor_spec.rb
269
279
  - spec/spec_helper.rb
270
280
  - spec/support/database.rb