wipe_out 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|