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.
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