Skip to content

Commit

Permalink
Include reference to anonymised record in Anony::Result
Browse files Browse the repository at this point in the history
When using selectors to anonymise records, an array of `Result` objects
is returned and these results now contain a reference to the record that
was anonymised.

This means that it is significantly easier to match the changes from the
`fields` on each result with the record that those changes came from in
case the changes don't include any unique identifiers like the primary
key field.
  • Loading branch information
Tabby committed Oct 11, 2024
1 parent 1ea0b29 commit c3277a1
Show file tree
Hide file tree
Showing 9 changed files with 38 additions and 22 deletions.
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,7 @@ The result object has 3 attributes:
* `status` - If the model was `destroyed`, `overwritten`, `skipped` or the operation `failed`
* `fields` - In the event the model was `overwritten`, the fields that were updated (excludes timestamps)
* `error` - In the event the anonymisation `failed`, then the associated error. Note only rescues the following errors: `ActiveRecord::RecordNotSaved`, `ActiveRecord::RecordNotDestroyed`. Anything else is thrown.
* `record` - The model instance that was anonymised to produce this result.

For convenience, the result object can also be queried with `destroyed?`, `overwritten?`, `skipped?` and `failed?`, so that it can be directly interrogated or used in a `switch case` with the `status` property.

Expand Down Expand Up @@ -278,6 +279,7 @@ ModelName.anonymise_for!(:user_id, "user_1234")
If you attempt to anonymise records with a selector that has not been defined it
will throw an error.

When anonymising models using selectors, an array of `Anony::Result` objects will be returned, one result per anonymised record in the model. These results contain a reference to the record that was anonymised to produce that result, so that changes made or failures can easily be linked back to the specific record.

### Identifying anonymised records

Expand Down Expand Up @@ -492,7 +494,7 @@ Lint/DefineDeletionStrategy:
If your models use multiple superclasses, you can specify a list of superclasses in your `.rubocop.yml`. Note that you will have to specify `ApplicationRecord` explicitly in this list should you want to lint all models which inherit from `ApplicationRecord`.
```yml
Lint/DefineDeletionStrategy:
ModelSuperclass:
ModelSuperclass:
- Acme::Record
- UmbrellaCorp::Record
Expand Down
1 change: 1 addition & 0 deletions anony.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ Gem::Specification.new do |spec|
spec.required_ruby_version = ">= 3.1"

spec.add_development_dependency "bundler", "~> 2"
spec.add_development_dependency "database_cleaner-active_record", "~> 2.2"
spec.add_development_dependency "gc_ruboconfig", "~> 5.0.0"
spec.add_development_dependency "rspec", "~> 3.9"
spec.add_development_dependency "rspec-github", "~> 2.4.0"
Expand Down
2 changes: 1 addition & 1 deletion lib/anony/anonymisable.rb
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ def anonymise!
self.class.anonymise_config.validate!
self.class.anonymise_config.apply(self)
rescue ActiveRecord::RecordNotSaved, ActiveRecord::RecordNotDestroyed => e
Result.failed(e)
Result.failed(e, self)
end

def anonymised?
Expand Down
2 changes: 1 addition & 1 deletion lib/anony/model_config.rb
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ def initialize(model_class, &block)
# @example
# Anony::ModelConfig.new(Manager).apply(Manager.new)
def apply(instance)
return Result.skipped if @skip_filter && instance.instance_exec(&@skip_filter)
return Result.skipped(instance) if @skip_filter && instance.instance_exec(&@skip_filter)

@strategy.apply(instance)
end
Expand Down
21 changes: 11 additions & 10 deletions lib/anony/result.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,32 +9,33 @@ class Result
OVERWRITTEN = "overwritten"
SKIPPED = "skipped"

attr_reader :status, :fields, :error
attr_reader :status, :fields, :error, :record

delegate :failed?, :overwritten?, :skipped?, :destroyed?, to: :status

def self.failed(error)
new(FAILED, error: error)
def self.failed(error, record = nil)
new(FAILED, record: record, error: error)
end

def self.overwritten(fields)
new(OVERWRITTEN, fields: fields)
def self.overwritten(fields, record = nil)
new(OVERWRITTEN, record: record, fields: fields)
end

def self.skipped
new(SKIPPED)
def self.skipped(record = nil)
new(SKIPPED, record: record)
end

def self.destroyed
new(DESTROYED)
def self.destroyed(record = nil)
new(DESTROYED, record: record)
end

private def initialize(status, fields: [], error: nil)
private def initialize(status, record:, fields: [], error: nil)
raise ArgumentError, "No error provided" if status == FAILED && error.nil?

@status = ActiveSupport::StringInquirer.new(status)
@fields = fields
@error = error
@record = record
end
end
end
2 changes: 1 addition & 1 deletion lib/anony/strategies/destroy.rb
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ def validate!
# @param [ActiveRecord::Base] instance An instance of the model
def apply(instance)
instance.destroy!
Result.destroyed
Result.destroyed(instance)
end
end
end
Expand Down
2 changes: 1 addition & 1 deletion lib/anony/strategies/overwrite.rb
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ def apply(instance)

instance.save!

Result.overwritten(result_fields)
Result.overwritten(result_fields, instance)
end

# Configure a custom strategy for one or more fields. If a block is given that is used
Expand Down
14 changes: 7 additions & 7 deletions spec/anony/anonymisable_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -69,18 +69,18 @@ def some_instance_method?
end
end

context "single record" do
it "changes the matching record" do
klass.anonymise_for!(:first_name, model.first_name)
expect(model.reload.anonymised?).to eq(true)
expect(model_b.reload.anonymised?).to eq(false)
end
it "anonymises only the matching models: first_name" do
results = klass.anonymise_for!(:first_name, model.first_name)
expect(model.reload.anonymised?).to eq(true)
expect(model_b.reload.anonymised?).to eq(false)
expect(results.map(&:record)).to contain_exactly(model)
end

it "anonymises only the matching models: company_name" do
klass.anonymise_for!(:company_name, model.company_name)
results = klass.anonymise_for!(:company_name, model.company_name)
expect(model.reload.anonymised?).to be(true)
expect(model_b.reload.anonymised?).to be(true)
expect(results.map(&:record)).to contain_exactly(model, model_b)
end
end

Expand Down
12 changes: 12 additions & 0 deletions spec/spec_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

require "bundler/setup"
require "anony"
require "database_cleaner/active_record"

RSpec.configure do |config|
# Disable RSpec exposing methods globally on `Module` and `main`
Expand All @@ -10,4 +11,15 @@
config.expect_with :rspec do |c|
c.syntax = :expect
end

config.before(:suite) do
DatabaseCleaner.strategy = :transaction
DatabaseCleaner.clean_with(:truncation)
end

config.around do |example|
DatabaseCleaner.cleaning do
example.run
end
end
end

0 comments on commit c3277a1

Please sign in to comment.