{"id":13877942,"url":"https://github.com/gocardless/anony","last_synced_at":"2025-10-18T08:14:12.816Z","repository":{"id":43860862,"uuid":"220496500","full_name":"gocardless/anony","owner":"gocardless","description":"A small library that defines how ActiveRecord models should be anonymised for deletion purposes.","archived":false,"fork":false,"pushed_at":"2025-02-20T18:06:07.000Z","size":255,"stargazers_count":23,"open_issues_count":1,"forks_count":3,"subscribers_count":52,"default_branch":"master","last_synced_at":"2025-03-30T16:13:28.055Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"Ruby","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/gocardless.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE.txt","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2019-11-08T15:37:08.000Z","updated_at":"2025-02-20T18:06:03.000Z","dependencies_parsed_at":"2024-02-20T18:47:38.137Z","dependency_job_id":"88cac5ef-18d6-4a8d-82fc-9b011cb86c38","html_url":"https://github.com/gocardless/anony","commit_stats":{"total_commits":169,"total_committers":15,"mean_commits":"11.266666666666667","dds":0.6390532544378698,"last_synced_commit":"2c35e67079d628c251130aa34338d42ac940221f"},"previous_names":[],"tags_count":21,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/gocardless%2Fanony","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/gocardless%2Fanony/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/gocardless%2Fanony/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/gocardless%2Fanony/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/gocardless","download_url":"https://codeload.github.com/gocardless/anony/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":247526764,"owners_count":20953143,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2024-08-06T08:01:35.519Z","updated_at":"2025-10-18T08:14:12.790Z","avatar_url":"https://github.com/gocardless.png","language":"Ruby","funding_links":[],"categories":["Ruby"],"sub_categories":[],"readme":"# Anony\n\n[![Yard Documentation](http://img.shields.io/badge/yard-docs-blue.svg)](http://rubydoc.info/gems/anony)\n[![Build status](https://github.com/gocardless/anony/actions/workflows/test.yml/badge.svg)](https://github.com/gocardless/anony/actions/workflows/test.yml)\n\nAnony is a small library that defines how ActiveRecord models should be anonymised for\ndeletion purposes.\n\n```ruby\nclass User \u003c ActiveRecord::Base\n  include Anony::Anonymisable\n\n  anonymise do\n    overwrite do\n      hex :first_name\n    end\n  end\nend\n```\n\n```ruby\nirb(main):001:0\u003e user = User.find(1)\n=\u003e #\u003cUser id=\"1\" first_name=\"Alice\"\u003e\n\nirb(main):002:0\u003e user.anonymise!\n =\u003e #\u003cAnony::Result status=\"overwritten\" fields=[:first_name] error=nil\u003e\n```\n\nFor our policy on compatibility with Ruby and Rails versions, see [COMPATIBILITY.md](docs/COMPATIBILITY.md).\n\n## Installation \u0026 configuration\n\nThis library is distributed as a Ruby gem, and we recommend adding it your Gemfile:\n\n```ruby\ngem \"anony\"\n```\n\nThe library injects itself using a mixin. To add this to a model class, you should include\n`Anony::Anonymisable`:\n\n```ruby\nclass User \u003c ActiveRecord::Base\n  include Anony::Anonymisable\n  # ...\nend\n```\n\nAlternatively, if you have a Rails application, you might wish to expose this behaviour\nfor all of your models: in which case, you can instead add it to `ApplicationRecord` once:\n\n```ruby\n# app/models/application_record.rb\nclass ApplicationRecord \u003c ActiveRecord::Base\n  include Anony::Anonymisable\nend\n```\n\n## Usage\n\nThere are two primary ways to use this library: to either overwrite existing fields on a\nrecord, or to destroy the record altogether.\n\nFirst, you should establish an `anonymise` block in your model class:\n\n```ruby\nclass Employee \u003c ActiveRecord::Base\n  include Anony::Anonymisable\n\n  anonymise do\n  end\nend\n```\n\nIf you want to overwrite certain fields on the model, you should use the `overwrite`\nDSL. There are many different ways (known as \"strategies\") to overwrite your fields (see\n[Field strategies](#field-strategies) below). For now, let's use the `hex` \u0026 `nilable` strategies, which\noverwrites fields using `SecureRandom.hex` or sets them to `nil`:\n\n```ruby\nanonymise do\n  overwrite do\n    hex :field_name\n    nilable :nullable_field\n  end\nend\n```\n\nAlternative, you may wish to simply destroy the record altogether when we call\n`#anonymise!` (this is useful if you're anonymising a collection of different models\ntogether, only some of which need to be destroyed). This can be configured liked so:\n\n```ruby\nanonymise do\n  destroy\nend\n```\n\nPlease note that both the `overwrite` and `destroy` strategies cannot be used simultaneously.\n\nNow, given a model instance, we can use the `#anonymise!` method to apply our strategies:\n\n```ruby\nirb(main):001:0\u003e model = Model.find(1)\n=\u003e #\u003cModel id=\"1\" field_name=\"Previous value\" nullable_field=\"Previous\"\u003e\n\nirb(main):002:0\u003e model.anonymise!\n =\u003e #\u003cAnony::Result status=\"overwritten\" fields=[:field_name, :nullable_field] error=nil\u003e\n```\n\n Or, if you were using the `destroy` strategy:\n\n```ruby\nirb(main):002:0\u003e model.anonymise!\n=\u003e #\u003cAnony::Result status=\"destroyed\" fields=nil error=nil\u003e\n```\n\n### Result object\n\nWhen a model is anonymised, an `Anony::Result` is returned. This allows the library to detail the changes is made and the strategy it used. The result object also contains the errors that may have been raised within Anony, allowing you to handle them elegantly without using the exceptions for flow control.\n\nThe result object has 3 attributes:\n\n* `status` - If the model was `destroyed`, `overwritten`, `skipped` or the operation `failed`\n* `fields` - In the event the model was `overwritten`, the fields that were updated (excludes timestamps)\n* `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.\n* `record` - The model instance that was anonymised to produce this result.\n\nFor 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.\n\n### Field strategies\n\nThis library ships with a number of built-in strategies:\n\n* **nilable** overwrites the field with `nil`\n* **hex** overwrites the field with random hexadecimal characters\n* **email** overwrites the field with an email\n* **phone_number** overwrites the field with a dummy phone number\n* **current_datetime** overwrites the field with `Time.zone.now` (using [ActiveSupport's TimeWithZone](https://api.rubyonrails.org/classes/ActiveSupport/TimeZone.html#method-i-now))\n\n### Custom strategies\n\nYou can override the default strategies, or add your own ones to make them available\neverywhere, using the `Anony::FieldLevelStrategies.register(name, \u0026block)` method somewhere after\nyour application boots (e.g. in a Rails initializer):\n\n```ruby\nAnony::FieldLevelStrategies.register(:reverse) do |original|\n  original.reverse\nend\n\nclass Employee \u003c ApplicationRecord\n  include Anony::Anonymisable\n\n  anonymise do\n    overwrite do\n      reverse :first_name\n    end\n  end\nend\n```\n\n\u003e One strategy you might want to override is `:email`, if your application has a more\n\u003e specific replacement. For example, at GoCardless we use an email on the\n\u003e `@gocardless.com` domain so we can ensure any emails accidentally sent to this address\n\u003e would be quickly identified and fixed. `:phone_number` is another strategy that you\n\u003e might wish to replace (depending on your primary location).\n\nYou can also use strategies on a case-by-case basis, by honouring the\n`.call(existing_value)` signature:\n\n```ruby\nmodule OverwriteUUID\n  def self.call(_existing_value)\n    SecureRandom.uuid\n  end\nend\n```\n\n```ruby\nrequire \"overwrite_uuid\"\n\nclass Manager \u003c ApplicationRecord\n  include Anony::Anonymisable\n\n  anonymise do\n    overwrite do\n      with_strategy OverwriteUUID, :id\n    end\n  end\nend\n```\n\nIf your strategy doesn't respond to `.call`, then it will be used as a constant value\nwhenever the field is anonymised.\n\n```ruby\nclass Manager \u003c ApplicationRecord\n  include Anony::Anonymisable\n\n  anonymise do\n    overwrite do\n      with_strategy 123, :id\n    end\n  end\nend\n```\n\n```ruby\nirb(main):001:0\u003e manager = Manager.first\n =\u003e #\u003cManager id=42\u003e\n\nirb(main):002:0\u003e manager.anonymise!\n =\u003e #\u003cAnony::Result status=\"overwritten\" fields=[:id] error=nil\u003e\n\nirb(main):003:0\u003e manager\n =\u003e #\u003cManager id=123\u003e\n```\n\nYou can also use a block, which is executed in the context of the model so it can\naccess local properties \u0026 methods. Blocks take the existing value of the column as the\nonly argument:\n\n```ruby\nclass Manager \u003c ApplicationRecord\n  include Anony::Anonymisable\n\n  anonymise do\n    overwrite do\n      with_strategy(:first_name) { |name| Digest::SHA2.hexdigest(name) }\n      with_strategy(:last_name) { \"previous-name-of-#{id}\" }\n    end\n  end\nend\n```\n\n```ruby\nirb(main):001:0\u003e manager = Manager.first\n =\u003e #\u003cManager id=42\u003e\n\nirb(main):002:0\u003e manager.anonymise!\n=\u003e #\u003cAnony::Result status=\"overwritten\" fields=[:first_name, :last_name] error=nil\u003e\n\nirb(main):003:0\u003e manager\n =\u003e #\u003cManager first_name=\"e9ab2800-d4b9-4227-94a7-7f81118d8a8a\" last_name=\"previous-name-of-42\"\u003e\n```\n\n### Anonymising many records, or anonymising by subject\n\n**Note**: This is an experimental feature and has not been tested widely\nin production environments.\n\nYou can use selectors to anonymise multiple records. You first define a block for\na specific subject that returns a list of anonymisable records.\n\n```ruby\nanonymise do\n  selectors do\n    for_subject(:user_id) { |user_id| find_all_users(user_id) }\n  end\nend\n```\n\nYou can also use `scopes`, `where`, etc when defining your selectors:\n\n```ruby\nanonymise do\n  selectors do\n    for_subject(:user_id) { |user_id| where(user_id: user_id) }\n  end\nend\n```\n\nThis can then be used to anonymise all those subject using this API:\n\n```ruby\nModelName.anonymise_for!(:user_id, \"user_1234\")\n```\n\nIf you attempt to anonymise records with a selector that has not been defined it\nwill throw an error.\n\nWhen 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.\n\n### Identifying anonymised records\n\nIf your model has an `anonymised_at` column, Anony will automatically set that value\nwhen calling `#anonymise!` (similar to how Rails will modify the `updated_at` timestamp).\nThis means you could automatically filter out anonymised records without matching on the\nanonymised values.\n\nHere is an example of adding this column with new tables:\n\n```ruby\nclass AddEmployees \u003c ActiveRecord::Migration[6.0]\n  def change\n    create_table(:employees) do |t|\n      # ... the rest of your columns\n      t.column :anonymised_at, :datetime, null: true\n    end\n  end\nend\n```\n\nHere is an example of adding this column to an existing table:\n\n```ruby\nclass AddAnonymisedAtToEmployees \u003c ActiveRecord::Migration[6.0]\n  def change\n    add_column(:employees, :anonymised_at, :datetime, null: true)\n  end\nend\n```\n\nRecords can then be filtered out like so:\n\n```ruby\nclass Employees \u003c ApplicationRecord\n  scope :without_anonymised, -\u003e { where(anonymised_at: nil) }\nend\n```\n\nThere is also a helper defined when `Anony::Anonymisable\" is included:\n\n```ruby\nEmployees.anonymised?\n```\n\n### Preventing anonymisation\n\nYou might have a need to preserve model data in some (or all) circumstances. Anony exposes\nthe `skip_if` DSL for expressing this preference, which runs the given block before\nattempting any strategy.\n\n* If the block returns _truthy_, anonymisation is skipped.\n* If the block returns _falsey_, anonymisation continues.\n\n```ruby\nclass Manager\n  def should_not_be_anonymised?\n    id == 1 # The first manager must be kept\n  end\n\n  anonymise do\n    skip_if { should_not_be_anonymised? }\n  end\nend\n```\n\nThe result object will indicate the model was skipped:\n\n```ruby\nirb(main):001:0\u003e manager = Manager.find(1)\n =\u003e #\u003cManager id=1\u003e\n\nirb(main):002:0\u003e manager.anonymise!\n=\u003e #\u003cAnony::Result status=\"skipped\" fields=[] error=nil\u003e\n```\n\n## Incomplete field strategies\n\nOne of the goals of this library is to ensure that your field strategies are _complete_,\ni.e. that the anonymisation behaviour of the model is always correct, even when database\ncolumns are added/removed or the contents of those columns changes.\n\nAs such, Anony will validate your model configuration when you try to anonymise the\nmodel (unfortunately this cannot be safely done at boot as the database might not be\navailable). If your configuration is incomplete, calling `#anonymise!` will raise a\n`FieldsException` and will not return an `Anony:Result` object. This is perceived\nto a critical error as anony cannot safely anonymise the model.\n\n```ruby\nirb(main):001:0\u003e manager = Manager.find(1)\n =\u003e #\u003cManager id=1\u003e\n\nirb(main):002:0\u003e manager.anonymise!\nAnony::FieldException (Invalid anonymisation strategy for field(s) [:username])\n```\n\nWe recommend adding a test for each model that you anonymise (see [Testing](#testing)\nbelow).\n\n### Adding new columns\n\nAnony will fail if you try to anonymise a model without specifying a\nstrategy for all of the columns (to ensure that anonymisation rules aren't missed over\ntime). However, it's fine to define a strategy for a column\nthat hasn't yet been added.\n\nThis means that, in order to add a new column, you should:\n\n  1. Define a strategy for the new column (e.g. `nilable :new_column`)\n  2. Add the column in a database migration.\n\n\u003e At GoCardless we do zero-downtime deploys so we would deploy the first change before\n\u003e then deploying the migration.\n\n### Excluding common Rails columns\n\nRails applications typically have an `id`, `created_at` and `updated_at` column on all new\ntables by default. To avoid anonymising these fields (and thus prevent a\n`FieldsException`), they can be globally ignored:\n\n```ruby\n# config/initializers/anony.rb\n\nAnony::Config.ignore_fields(:id, :created_at, :updated_at)\n```\n\nBy default, `Config.ignore_fields` is an empty array and all fields are considered\nanonymisable.\n\n### Preventing model validation before anonymisation\n\nThere are cases where models may contain data that is not valid according to the validations defined on the model, and\nthe default behaviour of Anony is to validate the model before anonymisation.\n\nIf it is necessary to be able to anonymise models even if they contain invalid data, Anony can be configured to skip this\nvalidation, in which case the validations will only be run when using strategies that modify the model data, and will run\nafter anonymisation when the model is saved.\n\nTo configure this behaviour, you can set the following configuration option in an initializer:\n\n```ruby\n# config/initializers/anony.rb\n\nAnony::Config.validate_before_anonymisation = false\n```\n\n\u003e Note: If you set this configuration option to `false`, be aware that a model that is invalid before anonymisation and continues\n  to be invalid after anonymisation may have new validation failures caused by the anonymisation if a strategy is incorrectly\n  defined for the model. It is recommended that you perform the anonymisation in a database transaction and roll it back if there\n  are validation errors after anonymisation.\n\n## Testing\n\nThis library ships with a set of useful RSpec examples for your specs. Just require them\nsomewhere before running your spec:\n\n```ruby\nrequire \"anony/rspec_shared_examples\"\n```\n\n```ruby\n# spec/models/employee_spec.rb\n\nRSpec.describe Employee do\n  # We use FactoryBot at GoCardless, but\n  # however you setup a model instance is fine\n  subject { FactoryBot.build(:employee) }\n\n  # If you just anonymise fields normally\n  it_behaves_like \"overwritten anonymisable model\"\n\n  # Or, if your anonymised model should be skipped\n  it_behaves_like \"skipped anonymisable model\"\n\n  # Or, if you anonymise by destroying the record\n  it_behaves_like \"destroyed anonymisable model\"\nend\n```\n\nYou can also override the subject _inside_ the shared example if it helps (e.g. if you\nneed to persist the record before anonymising it):\n\n```ruby\nRSpec.describe Employee do\n  it_behaves_like \"anonymisable model with destruction\" do\n    subject { FactoryBot.create(:employee) }\n  end\nend\n```\n\nIf you're not using RSpec, or want more control over the tests, Anony also exposes an\ninstance method called `#valid_anonymisation?`. A simple spec would be:\n\n```ruby\nRSpec.describe Employee do\n  subject { described_class.new }\n\n  it { is_expected.to be_valid_anonymisation }\nend\n```\n\n## Integration with Rubocop\n\nAt GoCardless, we use Rubocop heavily to ensure consistency in our applications. This\nlibrary includes some Rubocop cops, which can be used by adding `anony/cops` to the\n`require` list in your `.rubocop.yml`:\n\n```yml\nrequire:\n  - anony/cops\n```\n\n### `Lint/DefineDeletionStrategy`\n\nThis cop ensures that all models in your application have defined an `anonymise` block.\nThe output looks like this:\n\n```text\napp/models/employee.rb:7:1: W: Lint/DefineDeletionStrategy:\n  Define .anonymise for Employee, see https://github.com/gocardless/anony/blob/master/README.md for details:\n  class Employee \u003c ApplicationRecord ...\n  ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n```\n\nIf your models do not inherit from `ApplicationRecord`, you can specify their superclass\nin your `.rubocop.yml`:\n\n```yml\nLint/DefineDeletionStrategy:\n  ModelSuperclass: Acme::Record\n```\n\nIf 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`.\n\n```yml\nLint/DefineDeletionStrategy:\n  ModelSuperclass:\n  - Acme::Record\n  - UmbrellaCorp::Record\n```\n\n## License \u0026 Contributing\n\n* Anony is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).\n* Bug reports and pull requests are welcome on GitHub at \u003chttps://github.com/gocardless/anony\u003e.\n\nGoCardless ♥ open source. If you do too, come [join us](https://gocardless.com/about/jobs).\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fgocardless%2Fanony","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fgocardless%2Fanony","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fgocardless%2Fanony/lists"}