wipe_out 1.0.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 +7 -0
- data/.github/workflows/ci.yml +50 -0
- data/.gitignore +7 -0
- data/.markdownlint.json +7 -0
- data/.rspec +2 -0
- data/.yardopts +6 -0
- data/CHANGELOG.md +5 -0
- data/Gemfile +3 -0
- data/LICENSE +19 -0
- data/README.md +58 -0
- data/bin/rake +29 -0
- data/bin/rspec +29 -0
- data/bin/standardrb +29 -0
- data/bin/yard +29 -0
- data/bin/yardoc +29 -0
- data/bin/yri +29 -0
- data/docs/development.md +57 -0
- data/docs/getting_started.md +350 -0
- data/docs/releasing.md +14 -0
- data/docs/yard_plugin.rb +12 -0
- data/lib/wipe_out.rb +65 -0
- data/lib/wipe_out/attribute_strategies/const_value.rb +13 -0
- data/lib/wipe_out/attribute_strategies/nullify.rb +5 -0
- data/lib/wipe_out/attribute_strategies/randomize.rb +13 -0
- data/lib/wipe_out/callback.rb +25 -0
- data/lib/wipe_out/callbacks_observer.rb +23 -0
- data/lib/wipe_out/config.rb +30 -0
- data/lib/wipe_out/execute.rb +31 -0
- data/lib/wipe_out/execution/context.rb +34 -0
- data/lib/wipe_out/execution/execute_plan.rb +53 -0
- data/lib/wipe_out/plans/built_plan.rb +35 -0
- data/lib/wipe_out/plans/dsl.rb +117 -0
- data/lib/wipe_out/plans/plan.rb +63 -0
- data/lib/wipe_out/plans/union.rb +19 -0
- data/lib/wipe_out/plugin.rb +31 -0
- data/lib/wipe_out/plugins/logger.rb +42 -0
- data/lib/wipe_out/validate.rb +48 -0
- data/lib/wipe_out/validators/attributes.rb +49 -0
- data/lib/wipe_out/validators/base.rb +13 -0
- data/lib/wipe_out/validators/defined_relations.rb +26 -0
- data/lib/wipe_out/validators/relations_plans.rb +26 -0
- data/lib/wipe_out/version.rb +3 -0
- data/wipe_out.gemspec +47 -0
- metadata +274 -0
data/docs/releasing.md
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
# Releasing
|
2
|
+
|
3
|
+
1. Update `VERSION` in `lib/wipe_out/version.rb`. Please follow [semver](https://semver.org/)
|
4
|
+
1. Update [CHANGELOG.md](../CHANGELOG.md)
|
5
|
+
* Make sure any breaking change has notes and is clearly visible
|
6
|
+
1. Commit your changes to `master`
|
7
|
+
1. Create a new tag called `vX.Y.Z` where X, Y and Z are major, minor and patch versions.
|
8
|
+
|
9
|
+
```bash
|
10
|
+
git tag -s vVERSION
|
11
|
+
```
|
12
|
+
|
13
|
+
1. Push changes: `git push && git push --tags`
|
14
|
+
1. Create new [Github Release](https://github.com/GlobalAppTesting/wipe_out/releases/new?tag=vVERSION)
|
data/docs/yard_plugin.rb
ADDED
@@ -0,0 +1,12 @@
|
|
1
|
+
require "yard"
|
2
|
+
# https://github.com/troessner/reek/blob/87b0e75091552c59fcf20105016ba6ce97a57b06/docs/yard_plugin.rb
|
3
|
+
module LocalLinkHelper
|
4
|
+
# Rewrites links to (assumed local) markdown files so they're processed as
|
5
|
+
# {file: } directives.
|
6
|
+
def resolve_links(text)
|
7
|
+
text = text.gsub(%r{<a href="([^"]*.md)">([^<]*)</a>}, '{file:\1 \2}')
|
8
|
+
super(text)
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
YARD::Templates::Template.extra_includes << LocalLinkHelper
|
data/lib/wipe_out.rb
ADDED
@@ -0,0 +1,65 @@
|
|
1
|
+
require "attr_extras"
|
2
|
+
require "forwardable"
|
3
|
+
require "zeitwerk"
|
4
|
+
|
5
|
+
loader = Zeitwerk::Loader.for_gem
|
6
|
+
loader.setup
|
7
|
+
|
8
|
+
# When working with gem please see {file:getting_started.md}
|
9
|
+
#
|
10
|
+
# If you'd like to contribute, check out {file:development.md}
|
11
|
+
#
|
12
|
+
module WipeOut
|
13
|
+
IGNORE_ALL = :ignore_all
|
14
|
+
|
15
|
+
class << self
|
16
|
+
extend Forwardable
|
17
|
+
# Builds a plan for wipe out. When ActiveRecord class is passed,
|
18
|
+
#
|
19
|
+
# @example
|
20
|
+
# UserPlan = WipeOut.build_plan do
|
21
|
+
# wipe_out :name
|
22
|
+
# end
|
23
|
+
#
|
24
|
+
#
|
25
|
+
# For DSL documentation {Plans::Dsl}
|
26
|
+
#
|
27
|
+
# @return [Plans::BuiltPlan]
|
28
|
+
#
|
29
|
+
def build_plan(config: nil, &block)
|
30
|
+
config ||= WipeOut.config.dup
|
31
|
+
plan = Plans::Plan.new(config)
|
32
|
+
Plans::Dsl.build(plan, &block)
|
33
|
+
end
|
34
|
+
|
35
|
+
# Configures the gem, you should call it in the initializer
|
36
|
+
#
|
37
|
+
# @example
|
38
|
+
# WipeOut.configure do |config|
|
39
|
+
# config.ignored_attributes = %i[id inserted_at]
|
40
|
+
# end
|
41
|
+
#
|
42
|
+
# For additional details, {Config}.
|
43
|
+
# You will be also able to modify config for specific plan.
|
44
|
+
# Here you only set defaults.
|
45
|
+
#
|
46
|
+
# @return [Config]
|
47
|
+
def configure
|
48
|
+
raise "Pass block to configure the gem" unless block_given?
|
49
|
+
|
50
|
+
yield config
|
51
|
+
|
52
|
+
config
|
53
|
+
end
|
54
|
+
|
55
|
+
# Returns current configuration
|
56
|
+
#
|
57
|
+
# @return [Config]
|
58
|
+
def config
|
59
|
+
@config ||= Config.new
|
60
|
+
end
|
61
|
+
def_delegators :@config, :logger
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
loader.eager_load
|
@@ -0,0 +1,25 @@
|
|
1
|
+
module WipeOut
|
2
|
+
class Callback
|
3
|
+
attr_reader :name
|
4
|
+
|
5
|
+
def initialize(name, block)
|
6
|
+
@name = name.to_sym
|
7
|
+
@block = block
|
8
|
+
end
|
9
|
+
|
10
|
+
def run(execution)
|
11
|
+
raise("Wrong arity for callback name=#{name}") if block.arity != 1
|
12
|
+
|
13
|
+
block.call(execution)
|
14
|
+
end
|
15
|
+
|
16
|
+
def ==(other)
|
17
|
+
name == other.name &&
|
18
|
+
block == other.block
|
19
|
+
end
|
20
|
+
|
21
|
+
private
|
22
|
+
|
23
|
+
attr_reader :block
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
module WipeOut
|
2
|
+
# @api private
|
3
|
+
class CallbacksObserver
|
4
|
+
def initialize(callbacks, execution)
|
5
|
+
@callbacks = callbacks
|
6
|
+
@execution = execution
|
7
|
+
end
|
8
|
+
|
9
|
+
def update(name)
|
10
|
+
callbacks_by_name(name).each do |callback|
|
11
|
+
callback.run(execution)
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
private
|
16
|
+
|
17
|
+
attr_reader :execution, :callbacks
|
18
|
+
|
19
|
+
def callbacks_by_name(name)
|
20
|
+
callbacks.select { |callback| callback.name == name }
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
module WipeOut
|
2
|
+
# Holds configuration for the gem.
|
3
|
+
#
|
4
|
+
# Configuration options:
|
5
|
+
#
|
6
|
+
# - ignored_attributes - default: `%i[id updated_at created_at archived_at]`
|
7
|
+
# these attributes will be ignored in every plan by default.
|
8
|
+
# - logger - default: Rails.logger
|
9
|
+
# - default_on_execute - default: calls `save!` on record
|
10
|
+
#
|
11
|
+
class Config
|
12
|
+
# @!visibility private
|
13
|
+
attr_accessor :ignored_attributes, :logger, :default_on_execute
|
14
|
+
|
15
|
+
def initialize
|
16
|
+
@default_on_execute = ->(execution) { execution.record.save! }
|
17
|
+
@ignored_attributes = %i[id updated_at created_at archived_at]
|
18
|
+
@logger = Rails.logger
|
19
|
+
end
|
20
|
+
|
21
|
+
# Duplicates config
|
22
|
+
def dup
|
23
|
+
config = self.class.new
|
24
|
+
config.ignored_attributes = ignored_attributes
|
25
|
+
config.logger = logger
|
26
|
+
config.default_on_execute = default_on_execute
|
27
|
+
config
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
module WipeOut
|
2
|
+
# Executes plan for a given record.
|
3
|
+
# Plan execution flow:
|
4
|
+
# - emit event: `#before_plan`
|
5
|
+
#
|
6
|
+
# For each record (recursively, depth first)
|
7
|
+
# - emit event: `#before_execution`
|
8
|
+
# - emit event: `#after_execution`
|
9
|
+
#
|
10
|
+
# After plan had been executed (won't run if exception had been raised)
|
11
|
+
# - emit event: `#after_plan`
|
12
|
+
#
|
13
|
+
# To see how plan is defined, see {Plans::Dsl}
|
14
|
+
# To configure, see {Config}
|
15
|
+
#
|
16
|
+
class Execute
|
17
|
+
method_object :plan, :ar_class, :record
|
18
|
+
|
19
|
+
def call
|
20
|
+
ar_class.transaction do
|
21
|
+
execution = Execution::Context.new(plan, record)
|
22
|
+
|
23
|
+
execution.notify(:before_plan)
|
24
|
+
|
25
|
+
Execution::ExecutePlan.call(execution)
|
26
|
+
|
27
|
+
execution.notify(:after_plan)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
require "observer"
|
2
|
+
|
3
|
+
module WipeOut
|
4
|
+
module Execution
|
5
|
+
class Context
|
6
|
+
include Observable
|
7
|
+
attr_reader :record, :plan, :config
|
8
|
+
|
9
|
+
def initialize(plan, record, config = plan.config)
|
10
|
+
@plan = plan
|
11
|
+
@record = record
|
12
|
+
@config = config
|
13
|
+
|
14
|
+
add_observer(CallbacksObserver.new(plan.callbacks, self))
|
15
|
+
end
|
16
|
+
|
17
|
+
def run
|
18
|
+
on_execute = plan.on_execute || config.default_on_execute
|
19
|
+
|
20
|
+
on_execute.call(self)
|
21
|
+
end
|
22
|
+
|
23
|
+
def notify(name)
|
24
|
+
changed
|
25
|
+
notify_observers(name)
|
26
|
+
end
|
27
|
+
|
28
|
+
def subexecution(sub_plan, record)
|
29
|
+
plan.callbacks.each { |callback| sub_plan.add_callback(callback) }
|
30
|
+
self.class.new(sub_plan, record, config)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
require "forwardable"
|
2
|
+
|
3
|
+
module WipeOut
|
4
|
+
module Execution
|
5
|
+
class ExecutePlan
|
6
|
+
extend Forwardable
|
7
|
+
method_object :execution
|
8
|
+
|
9
|
+
def_delegators :@execution, :plan, :record
|
10
|
+
|
11
|
+
def call
|
12
|
+
execution.notify(:before_execution)
|
13
|
+
|
14
|
+
process_relations
|
15
|
+
plan.attributes.each(&method(:execute_on_attribute))
|
16
|
+
|
17
|
+
execution.run
|
18
|
+
|
19
|
+
execution.notify(:after_execution)
|
20
|
+
end
|
21
|
+
|
22
|
+
def process_relations
|
23
|
+
plan.relations.each do |name, plan|
|
24
|
+
relation = record.send(name)
|
25
|
+
|
26
|
+
next unless relation.present?
|
27
|
+
|
28
|
+
if collection?(record, name)
|
29
|
+
relation.find_each { |record| execute_on_record(plan, record) }
|
30
|
+
else
|
31
|
+
execute_on_record(plan, relation)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def execute_on_attribute(attribute)
|
37
|
+
name, strategy = attribute
|
38
|
+
value = strategy.call(record, name)
|
39
|
+
record.send("#{name}=", value)
|
40
|
+
end
|
41
|
+
|
42
|
+
def execute_on_record(plan, record)
|
43
|
+
execution_plan = plan.establish_execution_plan(record)
|
44
|
+
|
45
|
+
ExecutePlan.call(execution.subexecution(execution_plan, record))
|
46
|
+
end
|
47
|
+
|
48
|
+
def collection?(record, relation_name)
|
49
|
+
record.class.reflect_on_association(relation_name).collection?
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
module WipeOut
|
2
|
+
module Plans
|
3
|
+
# Provides final API after the plan had been build
|
4
|
+
#
|
5
|
+
# Under the hood it contains plans but hides our under the hood
|
6
|
+
# implemention where we have helper methods for adding relations, attributes, etc.
|
7
|
+
class BuiltPlan
|
8
|
+
extend Forwardable
|
9
|
+
|
10
|
+
def initialize(plan)
|
11
|
+
@plan = plan
|
12
|
+
end
|
13
|
+
|
14
|
+
def_delegators :plan, :config
|
15
|
+
attr_reader :plan
|
16
|
+
|
17
|
+
# Validates and returns any errors if validation fails.
|
18
|
+
#
|
19
|
+
# It's not done automatically when plan is defined because plans
|
20
|
+
# can be combined and not be valid standalone.
|
21
|
+
#
|
22
|
+
# @return [Array<String>] empty if everything is OK with the plan.
|
23
|
+
# Returns non-empty list if issues are detected.
|
24
|
+
# You should call it in tests to ensure that plans are OK.
|
25
|
+
def validate(ar_class)
|
26
|
+
WipeOut::Validate.call(plan, ar_class, @plan.config)
|
27
|
+
end
|
28
|
+
|
29
|
+
# Executes plan on a record
|
30
|
+
def execute(record)
|
31
|
+
WipeOut::Execute.call(plan, record.class, record)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,117 @@
|
|
1
|
+
require "forwardable"
|
2
|
+
|
3
|
+
module WipeOut
|
4
|
+
module Plans
|
5
|
+
# Provides DSL methods available during {Plan} building.
|
6
|
+
class Dsl
|
7
|
+
extend Forwardable
|
8
|
+
include WipeOut::Plugin::ClassMethods
|
9
|
+
# {#WipeOut.build_plan WipeOut.build_plan} should be used instead
|
10
|
+
#
|
11
|
+
# @!visibility private
|
12
|
+
def self.build(plan, &block)
|
13
|
+
dsl = Dsl.new(plan)
|
14
|
+
dsl.instance_exec(&block)
|
15
|
+
|
16
|
+
BuiltPlan.new(dsl.plan)
|
17
|
+
end
|
18
|
+
|
19
|
+
# @!visibility private
|
20
|
+
attr_reader :plan
|
21
|
+
|
22
|
+
# @!method on_execute
|
23
|
+
# Overwrites default `#save!` which is called on record after wipe out.
|
24
|
+
# You can use this to switch to `#destroy!` or `#delete!` if needed.
|
25
|
+
# You can also configure this in {Config}
|
26
|
+
#
|
27
|
+
# @yield [WipeOut::Execution::Context] execution
|
28
|
+
# @return [nil]
|
29
|
+
#
|
30
|
+
# @!method include_plan!(other_plan)
|
31
|
+
#
|
32
|
+
# Combines plan with another one. You can use it to create plans
|
33
|
+
# out of other plans via composition.
|
34
|
+
#
|
35
|
+
# @param [WipeOut::Plans::Plan] other_plan
|
36
|
+
# @return {nil}
|
37
|
+
def_delegators :@plan, :on_execute
|
38
|
+
|
39
|
+
# @!visibility private
|
40
|
+
def initialize(plan)
|
41
|
+
@plan = plan
|
42
|
+
end
|
43
|
+
|
44
|
+
# @!visibility private
|
45
|
+
def plugin(plugin)
|
46
|
+
plugin.callbacks.each { |callback| add_callback(callback) }
|
47
|
+
end
|
48
|
+
|
49
|
+
# Defines a strategy for removing data inside attribute(s)
|
50
|
+
#
|
51
|
+
# @param names [Array<Symbol>] any number of attributes to wipe out
|
52
|
+
# @param strategy [#call] defined a strategy which should be used for wiping out the attribute(s).
|
53
|
+
# You can also define a strategy inline by passing a block.
|
54
|
+
# By default it uses {AttributeStrategies::Nullify}.
|
55
|
+
# @return [nil]
|
56
|
+
def wipe_out(*names, strategy: AttributeStrategies::Nullify, &block)
|
57
|
+
strategy = block if block
|
58
|
+
names.each do |name|
|
59
|
+
plan.add_attribute(name, strategy: strategy)
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
# Configures plan for wiping out data in relation. You must pass a block
|
64
|
+
# and use the same DSL to configure it.
|
65
|
+
#
|
66
|
+
# @return [nil]
|
67
|
+
def relation(name, plan = nil, plans: nil, &block)
|
68
|
+
if plans
|
69
|
+
@plan.add_relation_union(name, plans.map(&:plan), &block)
|
70
|
+
else
|
71
|
+
plan ||= Plan.new(@plan.config)
|
72
|
+
plan = plan.plan if plan.is_a?(BuiltPlan)
|
73
|
+
dsl = Dsl.new(plan)
|
74
|
+
dsl.instance_exec(&block) if block.present?
|
75
|
+
|
76
|
+
@plan.add_relation(name, dsl.plan)
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
# Sets given attribute(s) as ignored. Attributes must be ignored explicily
|
81
|
+
# otherwise errors will be raised during validation
|
82
|
+
#
|
83
|
+
# @param names [Array<Symbol>] any number of attributes to ignore
|
84
|
+
# @return [nil]
|
85
|
+
def ignore(*names)
|
86
|
+
names.each do |name|
|
87
|
+
plan.ignore(name)
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
# Ignores all attributes and relations during validation.
|
92
|
+
# It should be used when you're using custom `#on_execute` method that
|
93
|
+
# for example destroys records and you don't care what attributes are there exactly
|
94
|
+
def ignore_all
|
95
|
+
plan.ignore(WipeOut::IGNORE_ALL)
|
96
|
+
end
|
97
|
+
|
98
|
+
def include_plan(built_plan)
|
99
|
+
plan.include_plan(built_plan.plan)
|
100
|
+
end
|
101
|
+
|
102
|
+
# See {Config} to check what options are available.
|
103
|
+
# @todo add test for nested configurations inside plans
|
104
|
+
# @return [Config]
|
105
|
+
def configure
|
106
|
+
yield plan.config
|
107
|
+
|
108
|
+
plan.config
|
109
|
+
end
|
110
|
+
|
111
|
+
# @!visibility private
|
112
|
+
def add_callback(callback)
|
113
|
+
plan.add_callback(callback)
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
117
|
+
end
|