wipe_out 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (44) hide show
  1. checksums.yaml +7 -0
  2. data/.github/workflows/ci.yml +50 -0
  3. data/.gitignore +7 -0
  4. data/.markdownlint.json +7 -0
  5. data/.rspec +2 -0
  6. data/.yardopts +6 -0
  7. data/CHANGELOG.md +5 -0
  8. data/Gemfile +3 -0
  9. data/LICENSE +19 -0
  10. data/README.md +58 -0
  11. data/bin/rake +29 -0
  12. data/bin/rspec +29 -0
  13. data/bin/standardrb +29 -0
  14. data/bin/yard +29 -0
  15. data/bin/yardoc +29 -0
  16. data/bin/yri +29 -0
  17. data/docs/development.md +57 -0
  18. data/docs/getting_started.md +350 -0
  19. data/docs/releasing.md +14 -0
  20. data/docs/yard_plugin.rb +12 -0
  21. data/lib/wipe_out.rb +65 -0
  22. data/lib/wipe_out/attribute_strategies/const_value.rb +13 -0
  23. data/lib/wipe_out/attribute_strategies/nullify.rb +5 -0
  24. data/lib/wipe_out/attribute_strategies/randomize.rb +13 -0
  25. data/lib/wipe_out/callback.rb +25 -0
  26. data/lib/wipe_out/callbacks_observer.rb +23 -0
  27. data/lib/wipe_out/config.rb +30 -0
  28. data/lib/wipe_out/execute.rb +31 -0
  29. data/lib/wipe_out/execution/context.rb +34 -0
  30. data/lib/wipe_out/execution/execute_plan.rb +53 -0
  31. data/lib/wipe_out/plans/built_plan.rb +35 -0
  32. data/lib/wipe_out/plans/dsl.rb +117 -0
  33. data/lib/wipe_out/plans/plan.rb +63 -0
  34. data/lib/wipe_out/plans/union.rb +19 -0
  35. data/lib/wipe_out/plugin.rb +31 -0
  36. data/lib/wipe_out/plugins/logger.rb +42 -0
  37. data/lib/wipe_out/validate.rb +48 -0
  38. data/lib/wipe_out/validators/attributes.rb +49 -0
  39. data/lib/wipe_out/validators/base.rb +13 -0
  40. data/lib/wipe_out/validators/defined_relations.rb +26 -0
  41. data/lib/wipe_out/validators/relations_plans.rb +26 -0
  42. data/lib/wipe_out/version.rb +3 -0
  43. data/wipe_out.gemspec +47 -0
  44. 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)
@@ -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,13 @@
1
+ module WipeOut
2
+ module AttributeStrategies
3
+ class ConstValue
4
+ def initialize(value)
5
+ @value = value
6
+ end
7
+
8
+ def call(*)
9
+ @value
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,5 @@
1
+ module WipeOut
2
+ module AttributeStrategies
3
+ Nullify = ConstValue.new(nil)
4
+ end
5
+ end
@@ -0,0 +1,13 @@
1
+ module WipeOut
2
+ module AttributeStrategies
3
+ class Randomize
4
+ def initialize(format: "destroyed_%s")
5
+ @format = format
6
+ end
7
+
8
+ def call(*)
9
+ @format % SecureRandom.hex(10)
10
+ end
11
+ end
12
+ end
13
+ end
@@ -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