purrrge 1.5.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.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: c7b2333776af99cfacf97761bf7dbfd6207abd7b06f2a20320ac81ec4e2c29e3
4
+ data.tar.gz: 6094f74be0f2e172c525e9470cb8aba63e22d1f2e758e6f372d606e299379986
5
+ SHA512:
6
+ metadata.gz: 1f457fbe59ee707dbf2fc7c65bdb3934fd8643fd8c00d89e79ef44440b6de2b9091e9e7dfdbe576a14ccb830511e4f66440ef8666043c27f56e959dafb67410b
7
+ data.tar.gz: 42ee9fa831fad3da6b91f897d73d3fc5f7198af2f8df70e5bfc952c1f9651565acbd077bd2c1cd4fcaadd4e4eba0c37aedd0325d8f968dcdf6cd587b27b0d60b
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --format documentation
2
+ --color
3
+ --require spec_helper
data/.rubocop.yml ADDED
@@ -0,0 +1,36 @@
1
+ AllCops:
2
+ TargetRubyVersion: 2.6
3
+ NewCops: enable
4
+ Exclude:
5
+ - 'vendor/**/*'
6
+ - 'bin/**/*'
7
+ - 'spec/spec_helper.rb'
8
+
9
+ Style/StringLiterals:
10
+ Enabled: true
11
+ EnforcedStyle: double_quotes
12
+
13
+ Style/StringLiteralsInInterpolation:
14
+ Enabled: true
15
+ EnforcedStyle: double_quotes
16
+
17
+ Layout/LineLength:
18
+ Max: 120
19
+
20
+ Metrics/BlockLength:
21
+ Exclude:
22
+ - 'spec/**/*'
23
+ - '*.gemspec'
24
+
25
+ Metrics/MethodLength:
26
+ Max: 20
27
+
28
+ Style/Documentation:
29
+ Enabled: true
30
+
31
+ Style/FrozenStringLiteralComment:
32
+ Enabled: true
33
+ EnforcedStyle: always
34
+
35
+ Style/ClassAndModuleChildren:
36
+ Enabled: false
@@ -0,0 +1,52 @@
1
+ version: v1.0
2
+ name: Purrrge
3
+ agent:
4
+ machine:
5
+ type: e1-standard-2
6
+ os_image: ubuntu2004
7
+
8
+ auto_cancel:
9
+ running:
10
+ when: "branch != 'main'"
11
+
12
+ global_job_config:
13
+ prologue:
14
+ commands:
15
+ - checkout
16
+ - sem-version ruby 3.2
17
+ - bundle config set --local path 'vendor/bundle'
18
+ - bundle install
19
+
20
+ blocks:
21
+ - name: "Test"
22
+ dependencies: []
23
+ task:
24
+ jobs:
25
+ - name: "RSpec"
26
+ commands:
27
+ - bundle exec rspec
28
+
29
+ - name: "Build"
30
+ dependencies: ["Test"]
31
+ task:
32
+ jobs:
33
+ - name: "Build Gem"
34
+ commands:
35
+ - gem build purrrge.gemspec
36
+ - "test -f purrrge-*.gem"
37
+
38
+ - name: "Lint"
39
+ dependencies: []
40
+ task:
41
+ jobs:
42
+ - name: "Ruby Lint"
43
+ commands:
44
+ - gem install rubocop
45
+ - rubocop --version
46
+ - rubocop
47
+
48
+ promotions:
49
+ - name: Version & Tag
50
+ pipeline_file: version-and-tag.yml
51
+ auto_promote:
52
+ when: "result = 'passed' and branch = 'main'"
@@ -0,0 +1,46 @@
1
+ version: v1.0
2
+ name: Version and Tag
3
+ agent:
4
+ machine:
5
+ type: e1-standard-2
6
+ os_image: ubuntu2004
7
+
8
+ global_job_config:
9
+ prologue:
10
+ commands:
11
+ - checkout
12
+ - sem-version ruby 3.2
13
+ - bundle config set --local path 'vendor/bundle'
14
+ - bundle install
15
+
16
+ blocks:
17
+ - name: "Version & Tag"
18
+ task:
19
+ secrets:
20
+ - name: purrrge-github-token
21
+ jobs:
22
+ - name: "Bump Version & Push Tag"
23
+ commands:
24
+ - git config --global user.email "ci@semaphore.com"
25
+ - git config --global user.name "Semaphore CI"
26
+ - 'ruby -e ''require "./lib/purrrge/version"; puts "CURRENT_VERSION=#{Purrrge::VERSION}"'' > version.env'
27
+ - source version.env
28
+ - 'echo "Current version: $CURRENT_VERSION"'
29
+ - 'IFS="." read -r MAJOR MINOR PATCH <<< "$CURRENT_VERSION"'
30
+ - 'echo "Parsed version: $MAJOR.$MINOR.$PATCH"'
31
+ - 'LAST_TAG=$(git describe --tags --abbrev=0 2>/dev/null || echo "none")'
32
+ - 'if [ "$LAST_TAG" = "none" ]; then COMMITS=$(git log --pretty=format:"%s" --no-merges); else COMMITS=$(git log $LAST_TAG..HEAD --pretty=format:"%s" --no-merges); fi'
33
+ - 'BUMP_TYPE="patch"'
34
+ - 'if echo "$COMMITS" | grep -qE "BREAKING CHANGE|MAJOR"; then BUMP_TYPE="major"; elif echo "$COMMITS" | grep -qE "feat:|feature:|MINOR"; then BUMP_TYPE="minor"; fi'
35
+ - 'echo "Bump type: $BUMP_TYPE"'
36
+ - 'if [ "$BUMP_TYPE" = "major" ]; then NEW_MAJOR=$((MAJOR + 1)); NEW_MINOR=0; NEW_PATCH=0; elif [ "$BUMP_TYPE" = "minor" ]; then NEW_MAJOR=$MAJOR; NEW_MINOR=$((MINOR + 1)); NEW_PATCH=0; else NEW_MAJOR=$MAJOR; NEW_MINOR=$MINOR; NEW_PATCH=$((PATCH + 1)); fi'
37
+ - 'NEW_VERSION="$NEW_MAJOR.$NEW_MINOR.$NEW_PATCH"'
38
+ - 'echo "New version: $NEW_VERSION"'
39
+ - 'sed -i "s/VERSION = \"$CURRENT_VERSION\"/VERSION = \"$NEW_VERSION\"/" lib/purrrge/version.rb'
40
+ - git add lib/purrrge/version.rb
41
+ - 'git commit -m "Bump version to $NEW_VERSION [skip ci]"'
42
+ - 'git tag -a "v$NEW_VERSION" -m "Version $NEW_VERSION"'
43
+ # Use GITHUB_TOKEN for Git operations
44
+ - 'git remote set-url origin https://x-access-token:${PURRRGE_GITHUB_TOKEN}@github.com/Grayscale-Labs/purrrge.git'
45
+ - git push origin main
46
+ - 'git push origin "v$NEW_VERSION"'
data/CHANGELOG.md ADDED
@@ -0,0 +1,26 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [1.1.0] - 2025-03-11
9
+
10
+ ### Added
11
+ - Added callback support with `before_scrub` and `after_scrub` methods
12
+ - Added support for running callbacks around the scrubbing process
13
+
14
+ ## [1.0.0] - 2025-03-10
15
+
16
+ ### Added
17
+ - Initial stable release
18
+ - Core functionality for marking fields as scrubbable
19
+ - Support for custom scrubbing methods
20
+ - Support for setting specific scrub values
21
+
22
+ ## [0.1.1] - 2025-03-07
23
+
24
+ ### Added
25
+ - Initial gem functionality
26
+ - Basic scrubbing support
data/Gemfile ADDED
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ source "https://rubygems.org"
4
+
5
+ # Specify your gem's dependencies in purrrge.gemspec
6
+ gemspec
7
+
8
+ # Development dependencies
9
+ group :development, :test do
10
+ gem "rspec", "~> 3.0"
11
+ gem "rubocop", "~> 1.74"
12
+ end
13
+
14
+ gem "rake", "~> 13.0"
data/Gemfile.lock ADDED
@@ -0,0 +1,93 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ purrrge (1.5.0)
5
+ activesupport (>= 5.0)
6
+
7
+ GEM
8
+ remote: https://rubygems.org/
9
+ specs:
10
+ activesupport (8.0.1)
11
+ base64
12
+ benchmark (>= 0.3)
13
+ bigdecimal
14
+ concurrent-ruby (~> 1.0, >= 1.3.1)
15
+ connection_pool (>= 2.2.5)
16
+ drb
17
+ i18n (>= 1.6, < 2)
18
+ logger (>= 1.4.2)
19
+ minitest (>= 5.1)
20
+ securerandom (>= 0.3)
21
+ tzinfo (~> 2.0, >= 2.0.5)
22
+ uri (>= 0.13.1)
23
+ ast (2.4.3)
24
+ base64 (0.2.0)
25
+ benchmark (0.4.0)
26
+ bigdecimal (3.1.9)
27
+ concurrent-ruby (1.3.5)
28
+ connection_pool (2.5.0)
29
+ diff-lcs (1.6.0)
30
+ drb (2.2.1)
31
+ i18n (1.14.7)
32
+ concurrent-ruby (~> 1.0)
33
+ json (2.18.0)
34
+ language_server-protocol (3.17.0.5)
35
+ lint_roller (1.1.0)
36
+ logger (1.6.6)
37
+ minitest (5.25.4)
38
+ parallel (1.27.0)
39
+ parser (3.3.10.1)
40
+ ast (~> 2.4.1)
41
+ racc
42
+ prism (1.8.0)
43
+ racc (1.8.1)
44
+ rainbow (3.1.1)
45
+ rake (13.2.1)
46
+ regexp_parser (2.11.3)
47
+ rspec (3.13.0)
48
+ rspec-core (~> 3.13.0)
49
+ rspec-expectations (~> 3.13.0)
50
+ rspec-mocks (~> 3.13.0)
51
+ rspec-core (3.13.3)
52
+ rspec-support (~> 3.13.0)
53
+ rspec-expectations (3.13.3)
54
+ diff-lcs (>= 1.2.0, < 2.0)
55
+ rspec-support (~> 3.13.0)
56
+ rspec-mocks (3.13.2)
57
+ diff-lcs (>= 1.2.0, < 2.0)
58
+ rspec-support (~> 3.13.0)
59
+ rspec-support (3.13.2)
60
+ rubocop (1.84.0)
61
+ json (~> 2.3)
62
+ language_server-protocol (~> 3.17.0.2)
63
+ lint_roller (~> 1.1.0)
64
+ parallel (~> 1.10)
65
+ parser (>= 3.3.0.2)
66
+ rainbow (>= 2.2.2, < 4.0)
67
+ regexp_parser (>= 2.9.3, < 3.0)
68
+ rubocop-ast (>= 1.49.0, < 2.0)
69
+ ruby-progressbar (~> 1.7)
70
+ unicode-display_width (>= 2.4.0, < 4.0)
71
+ rubocop-ast (1.49.0)
72
+ parser (>= 3.3.7.2)
73
+ prism (~> 1.7)
74
+ ruby-progressbar (1.13.0)
75
+ securerandom (0.4.1)
76
+ tzinfo (2.0.6)
77
+ concurrent-ruby (~> 1.0)
78
+ unicode-display_width (3.2.0)
79
+ unicode-emoji (~> 4.1)
80
+ unicode-emoji (4.2.0)
81
+ uri (1.0.3)
82
+
83
+ PLATFORMS
84
+ arm64-darwin-24
85
+
86
+ DEPENDENCIES
87
+ purrrge!
88
+ rake (~> 13.0)
89
+ rspec (~> 3.0)
90
+ rubocop (~> 1.74)
91
+
92
+ BUNDLED WITH
93
+ 2.4.10
data/README.md ADDED
@@ -0,0 +1,340 @@
1
+ # Purrrge
2
+
3
+ Purrrge is a data retention and scrubbing library for Ruby applications. It provides tools for implementing data retention policies and scrubbing sensitive data in compliance with privacy regulations.
4
+
5
+ ## Installation
6
+
7
+ Add this line to your application's Gemfile:
8
+
9
+ ```ruby
10
+ gem 'purrrge'
11
+ ```
12
+
13
+ And then execute:
14
+
15
+ ```
16
+ $ bundle install
17
+ ```
18
+
19
+ Or install it yourself as:
20
+
21
+ ```
22
+ $ gem install purrrge
23
+ ```
24
+
25
+ ## Usage
26
+
27
+ ### Scrubbable Concern
28
+
29
+ The `Scrubbable` concern allows you to define fields in your models that should be scrubbed (anonymized or removed) when data retention periods expire. This makes it easy to implement data retention policies across your application.
30
+
31
+ #### Basic Usage
32
+
33
+ ```ruby
34
+ class User
35
+ include Purrrge::Scrubbable
36
+
37
+ # Define fields to be scrubbed and how they should be scrubbed
38
+ scrub_field :email, scrub_value: nil # Set to nil
39
+ scrub_field :name, scrub_value: "REDACTED" # Replace with static text
40
+ scrub_field :phone, scrub_method: :anonymize_phone # Use custom method
41
+
42
+ # Define custom scrubbing method
43
+ def anonymize_phone(field)
44
+ self[field] = "xxx-xxx-xxxx"
45
+ end
46
+ end
47
+ ```
48
+
49
+ #### Implementing Scrubbing Logic
50
+
51
+ You can scrub individual records by calling `scrub!` on them:
52
+
53
+ ```ruby
54
+ user = User.find(123)
55
+ user.scrub! # Apply scrubbing to this user
56
+ ```
57
+
58
+ For scrubbing based on retention policies, you might implement a background job:
59
+
60
+ ```ruby
61
+ class DataRetentionWorker
62
+ include Sidekiq::Worker
63
+
64
+ def perform
65
+ Organization.where.not(data_retention_period: nil).find_each do |org|
66
+ cutoff_date = org.data_retention_period.days.ago
67
+
68
+ # Find users to be scrubbed for this organization
69
+ User.where(organization_id: org.id)
70
+ .where('created_at < ?', cutoff_date)
71
+ .find_each do |user|
72
+ user.scrub!
73
+ end
74
+ end
75
+ end
76
+ end
77
+ ```
78
+
79
+ ### Tracking Scrubbed Records
80
+
81
+ By default, Purrrge will scrub records based on their creation date and any other criteria you define. However, this can lead to inefficiency as the same records may be processed in each scrubbing run.
82
+
83
+ To optimize this process, Purrrge supports tracking scrubbed records with a `scrubbed_at` timestamp.
84
+
85
+ #### Adding the `scrubbed_at` Column
86
+
87
+ Add a `scrubbed_at` datetime column to your model:
88
+
89
+ ```ruby
90
+ # Generate the migration
91
+ rails generate purrrge:add_scrubbed_at MODEL_NAME
92
+
93
+ # Run the migration
94
+ rails db:migrate
95
+ ```
96
+
97
+ This creates a migration that adds both the column and an index for performance:
98
+
99
+ ```ruby
100
+ add_column :your_table_name, :scrubbed_at, :datetime
101
+ add_index :your_table_name, :scrubbed_at
102
+ ```
103
+
104
+ #### How It Works
105
+
106
+ When the `scrubbed_at` column exists:
107
+
108
+ 1. Purrrge automatically sets `scrubbed_at = Time.current` when `scrub!` is called on a record
109
+ 2. Your query scopes can filter out already-scrubbed records with `WHERE scrubbed_at IS NULL`
110
+
111
+ #### Implementation Example
112
+
113
+ ```ruby
114
+ # Model definition
115
+ class User < ApplicationRecord
116
+ include Purrrge::Scrubbable
117
+
118
+ # Define fields to scrub
119
+ scrub_field :email, scrub_value: nil
120
+ scrub_field :name, scrub_value: "REDACTED"
121
+ scrub_field :phone, scrub_value: "xxx-xxx-xxxx"
122
+
123
+ # Optional: Add your own custom callbacks to run after scrubbing
124
+ after_scrub :log_scrubbing_event
125
+
126
+ private
127
+
128
+ def log_scrubbing_event
129
+ Rails.logger.info "User #{id} was scrubbed at #{scrubbed_at}"
130
+ end
131
+ end
132
+
133
+ # In your scrubbing service
134
+ def scrub_user_data(organization, retention_date)
135
+ # Only select records that haven't been scrubbed yet
136
+ users = organization.users.where("created_at < ? AND scrubbed_at IS NULL", retention_date)
137
+
138
+ users.find_in_batches do |batch|
139
+ batch.each(&:scrub!)
140
+ end
141
+ end
142
+ ```
143
+
144
+ #### Fallback Behavior
145
+
146
+ If the `scrubbed_at` column doesn't exist on a model, Purrrge will still work normally - it just won't track which records have been scrubbed.
147
+
148
+ ### Registry for Automatic Model Discovery
149
+
150
+ Purrrge includes a Registry system that automatically keeps track of all models that include the `Scrubbable` concern. This makes it easy to implement application-wide data retention policies.
151
+
152
+ #### How it Works
153
+
154
+ When a model includes the `Purrrge::Scrubbable` concern, it's automatically registered with `Purrrge::Registry`. You can then discover and process all scrubbable models in your application.
155
+
156
+ ```ruby
157
+ # Get all registered scrubbable models
158
+ scrubbable_models = Purrrge::Registry.scrubbable_models
159
+
160
+ # Discover all scrubbable models in a Rails application
161
+ # (This will eager load models and find those that include Purrrge::Scrubbable)
162
+ all_models = Purrrge::Registry.discover_models
163
+ ```
164
+
165
+ #### Using the Registry
166
+
167
+ The Registry is designed to be application-agnostic and doesn't impose any specific data model. Here are some general ways to use it:
168
+
169
+ ```ruby
170
+ # Get all registered scrubbable models
171
+ models = Purrrge::Registry.scrubbable_models
172
+
173
+ # Process each model according to your application's requirements
174
+ models.each do |model|
175
+ # Implement your application-specific logic here
176
+ # This could involve filtering records based on dates, categories,
177
+ # or any other business logic relevant to your application
178
+
179
+ # Example: Finding records older than a certain date
180
+ if model.respond_to?(:created_at)
181
+ records = model.where('created_at < ?', 1.year.ago)
182
+ records.find_in_batches(batch_size: 1000) do |batch|
183
+ batch.each(&:scrub!)
184
+ end
185
+ end
186
+ end
187
+ ```
188
+
189
+ #### Extending with Custom Behavior
190
+
191
+ You can define custom class methods on your models to standardize how records are selected for scrubbing:
192
+
193
+ ```ruby
194
+ # Example custom methods for your models
195
+ class User < ApplicationRecord
196
+ include Purrrge::Scrubbable
197
+
198
+ scrub_field :email, scrub_value: nil
199
+
200
+ # Custom method for finding records to scrub
201
+ def self.scrubbable_records(cutoff_date)
202
+ where('created_at < ?', cutoff_date)
203
+ end
204
+ end
205
+
206
+ # Using the custom method in a worker
207
+ Purrrge::Registry.scrubbable_models.each do |model|
208
+ if model.respond_to?(:scrubbable_records)
209
+ records = model.scrubbable_records(1.year.ago)
210
+ records.find_in_batches(batch_size: 1000) do |batch|
211
+ batch.each(&:scrub!)
212
+ end
213
+ end
214
+ end
215
+ ```
216
+
217
+ #### Testing the Registry
218
+
219
+ For testing purposes, you can clear the registry:
220
+
221
+ ```ruby
222
+ # Clear registry between tests
223
+ Purrrge::Registry.clear
224
+ ```
225
+
226
+ #### Configuration Options
227
+
228
+ When defining fields to be scrubbed, you have several options:
229
+
230
+ 1. **Set to a specific value**:
231
+ ```ruby
232
+ scrub_field :email, scrub_value: nil
233
+ scrub_field :name, scrub_value: "REDACTED"
234
+ ```
235
+
236
+ 2. **Use a custom method**:
237
+ ```ruby
238
+ scrub_field :address, scrub_method: :anonymize_address
239
+
240
+ def anonymize_address(field)
241
+ self[field] = "#{self.country}, #{self.state}" # Keep only country and state
242
+ end
243
+ ```
244
+
245
+ #### Callback Hooks
246
+
247
+ Purrrge provides callback hooks that allow you to execute code before and after scrubbing occurs:
248
+
249
+ ```ruby
250
+ class User
251
+ include Purrrge::Scrubbable
252
+
253
+ scrub_field :email, scrub_value: nil
254
+ scrub_field :name, scrub_value: "REDACTED"
255
+
256
+ # Register callbacks
257
+ before_scrub :log_scrubbing_event
258
+ before_scrub :notify_admin, if: :admin_user?
259
+ after_scrub :update_scrubbed_timestamp
260
+
261
+ private
262
+
263
+ def log_scrubbing_event
264
+ Rails.logger.info("Scrubbing user data for User ##{id}")
265
+ # Return false to halt the callback chain and prevent scrubbing
266
+ true
267
+ end
268
+
269
+ def notify_admin
270
+ AdminMailer.user_data_scrubbed(self).deliver_later
271
+ end
272
+
273
+ def admin_user?
274
+ role == 'admin'
275
+ end
276
+
277
+ def update_scrubbed_timestamp
278
+ update_column(:scrubbed_at, Time.current)
279
+ end
280
+ end
281
+ ```
282
+
283
+ Callbacks support conditions via `:if` and `:unless` options, which can be:
284
+ - Symbol naming an instance method
285
+ - Proc or lambda
286
+ - Array of symbols or procs
287
+
288
+ If a `before_scrub` callback returns `false`, the scrubbing operation will be halted.
289
+
290
+ #### Working with Different ORMs
291
+
292
+ Purrrge is designed to be ORM-agnostic. While it works great with ActiveRecord, you can use it with any Ruby class that:
293
+
294
+ 1. Has attribute accessors for the fields you want to scrub
295
+ 2. Implements a `save` method
296
+
297
+ For non-ActiveRecord classes, just ensure you implement these requirements:
298
+
299
+ ```ruby
300
+ class MyCustomModel
301
+ include Purrrge::Scrubbable
302
+
303
+ attr_accessor :name, :email
304
+
305
+ scrub_field :email, scrub_value: nil
306
+
307
+ def save(options = {})
308
+ # Your custom save implementation
309
+ true
310
+ end
311
+ end
312
+ ```
313
+
314
+ ## Development
315
+
316
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
317
+
318
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
319
+
320
+ ### Semantic Versioning
321
+
322
+ The gem follows [Semantic Versioning](https://semver.org/) principles. When creating pull requests, please indicate the type of change:
323
+
324
+ - **PATCH** version for backwards-compatible bug fixes
325
+ - **MINOR** version for backwards-compatible new features
326
+ - **MAJOR** version for breaking changes
327
+
328
+ Our CI system will automatically determine the appropriate version bump based on commit messages and PR descriptions.
329
+
330
+ ## Contributing
331
+
332
+ Bug reports and pull requests are welcome on GitHub at https://github.com/grayscaleapp/purrrge.
333
+
334
+ 1. Fork the repository
335
+ 2. Create your feature branch (`git checkout -b feature/amazing-feature`)
336
+ 3. Commit your changes (`git commit -m 'Add some amazing feature'`)
337
+ - Use conventional commit messages: `feat:`, `fix:`, `docs:`, etc.
338
+ - Prefix breaking changes with `BREAKING CHANGE:` or include `MAJOR` in the description
339
+ 4. Push to the branch (`git push origin feature/amazing-feature`)
340
+ 5. Open a Pull Request using the provided template
data/Rakefile ADDED
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "rspec/core/rake_task"
5
+
6
+ RSpec::Core::RakeTask.new(:spec)
7
+
8
+ task default: :spec
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails/generators"
4
+ require "rails/generators/active_record"
5
+
6
+ module Purrrge
7
+ module Generators
8
+ class AddScrubbedAtGenerator < Rails::Generators::NamedBase
9
+ include Rails::Generators::Migration
10
+
11
+ source_root File.expand_path("templates", __dir__)
12
+
13
+ # Implementation of the required interface for Rails::Generators::Migration
14
+ def self.next_migration_number(dirname)
15
+ ActiveRecord::Generators::Base.next_migration_number(dirname)
16
+ end
17
+
18
+ def create_migration_file
19
+ migration_template "add_scrubbed_at_migration.rb.erb",
20
+ "db/migrate/add_scrubbed_at_to_#{table_name}.rb"
21
+ end
22
+
23
+ private
24
+
25
+ def table_name
26
+ name.underscore.pluralize
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,6 @@
1
+ class AddScrubbedAtTo<%= class_name.pluralize %> < ActiveRecord::Migration<%= Rails::VERSION::MAJOR >= 5 ? "[#{Rails::VERSION::MAJOR}.#{Rails::VERSION::MINOR}]" : "" %>
2
+ def change
3
+ add_column :<%= table_name %>, :scrubbed_at, :datetime
4
+ add_index :<%= table_name %>, :scrubbed_at
5
+ end
6
+ end
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Purrrge
4
+ # Registry tracks models that include the Scrubbable concern.
5
+ # It provides methods for registration and discovery of scrubbable models.
6
+ #
7
+ # This is used by automatic discovery mechanisms to find models that can be scrubbed
8
+ # without requiring explicit configuration.
9
+ class Registry
10
+ class << self
11
+ # Get all registered scrubbable models
12
+ #
13
+ # @return [Array<Class>] Array of model classes that include Purrrge::Scrubbable
14
+ def scrubbable_models
15
+ @scrubbable_models ||= []
16
+ end
17
+
18
+ # Register a model class with the registry
19
+ #
20
+ # @param model_class [Class] The model class to register
21
+ # @return [Array<Class>] The updated array of scrubbable models
22
+ def register(model_class)
23
+ scrubbable_models << model_class unless scrubbable_models.include?(model_class)
24
+ scrubbable_models
25
+ end
26
+
27
+ # Clear the registry (mainly for testing)
28
+ #
29
+ # @return [Array] An empty array
30
+ def clear
31
+ @scrubbable_models = []
32
+ end
33
+
34
+ # Discover all models that include Scrubbable in a Rails application
35
+ # This method eager loads all models in the Rails app and finds those
36
+ # that include the Scrubbable concern.
37
+ #
38
+ # @return [Array<Class>] Array of discovered model classes
39
+ def discover_models
40
+ return [] unless defined?(Rails)
41
+
42
+ # Eager load all models if not in production (where eager loading is the default)
43
+ Rails.application.eager_load! unless Rails.application.config.eager_load
44
+
45
+ # Find all ActiveRecord models that include Purrrge::Scrubbable
46
+ if defined?(ActiveRecord::Base)
47
+ # Get all models that include Scrubbable
48
+ scrubbable = ActiveRecord::Base.descendants.select do |model|
49
+ model.included_modules.include?(Purrrge::Scrubbable)
50
+ end
51
+
52
+ # Register each one
53
+ scrubbable.each do |model|
54
+ register(model)
55
+ end
56
+ end
57
+
58
+ scrubbable_models
59
+ end
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,119 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support/concern"
4
+ require "active_support/core_ext/class/attribute"
5
+ require "active_support/callbacks"
6
+
7
+ module Purrrge
8
+ # Scrubbable is a concern that provides functionality for data scrubbing/anonymization.
9
+ # It allows the implementing class to define which fields should be scrubbed
10
+ # and how they should be scrubbed (either by setting to a specific value or using a custom method).
11
+ #
12
+ # Usage:
13
+ #
14
+ # class User
15
+ # include Purrrge::Scrubbable
16
+ #
17
+ # scrub_field :email, scrub_value: nil
18
+ # scrub_field :name, scrub_value: "REDACTED"
19
+ # scrub_field :phone, scrub_method: :anonymize_phone
20
+ #
21
+ # def anonymize_phone(field)
22
+ # self[field] = "xxx-xxx-xxxx"
23
+ # end
24
+ # end
25
+ #
26
+ module Scrubbable
27
+ extend ActiveSupport::Concern
28
+ include ActiveSupport::Callbacks
29
+
30
+ included do
31
+ # Auto-register with Registry when included
32
+ Purrrge::Registry.register(self) if defined?(Purrrge::Registry)
33
+
34
+ class_attribute :scrubbable_fields, default: []
35
+
36
+ # Define callbacks
37
+ define_callbacks :scrub
38
+ end
39
+
40
+ class_methods do
41
+ # Registers a field to be scrubbed
42
+ #
43
+ # @param field_name [Symbol] The name of the field to scrub
44
+ # @param scrub_value [Object] The value to set the field to when scrubbing (optional)
45
+ # @param scrub_method [Symbol] The name of a method to call for custom scrubbing (optional)
46
+ #
47
+ # @example
48
+ # scrub_field :email, scrub_value: nil
49
+ # scrub_field :phone, scrub_method: :anonymize_phone
50
+ def scrub_field(field_name, scrub_value: nil, scrub_method: nil)
51
+ scrubbable_fields << {
52
+ field: field_name,
53
+ value: scrub_value,
54
+ method: scrub_method
55
+ }
56
+ end
57
+
58
+ # Registers a callback to be called before scrubbing
59
+ #
60
+ # @param method_name [Symbol] The name of the method to call
61
+ # @param options [Hash] Options to pass to the callback
62
+ #
63
+ # @example
64
+ # before_scrub :do_something
65
+ def before_scrub(method_name, options = {})
66
+ set_callback :scrub, :before, method_name, options
67
+ end
68
+
69
+ # Registers a callback to be called after scrubbing
70
+ #
71
+ # @param method_name [Symbol] The name of the method to call
72
+ # @param options [Hash] Options to pass to the callback
73
+ #
74
+ # @example
75
+ # after_scrub :clean_external_services
76
+ def after_scrub(method_name, options = {})
77
+ set_callback :scrub, :after, method_name, options
78
+ end
79
+ end
80
+
81
+ # Performs the actual scrubbing operation on all registered fields
82
+ #
83
+ # @return [Boolean] Returns the result of the save operation
84
+ def scrub!
85
+ run_callbacks :scrub do
86
+ self.class.scrubbable_fields.each do |field_config|
87
+ if field_config[:method]
88
+ send(field_config[:method], field_config[:field])
89
+ else
90
+ self[field_config[:field]] = field_config[:value]
91
+ end
92
+ end
93
+
94
+ # Automatically set scrubbed_at timestamp if the column exists
95
+ if respond_to?(:scrubbed_at=) && self.class.respond_to?(:column_names) && self.class.column_names.include?("scrubbed_at")
96
+ self.scrubbed_at = Time.now
97
+ end
98
+
99
+ save(validate: false)
100
+ end
101
+ end
102
+
103
+ # Override the [] method to provide access to attributes
104
+ # This allows the gem to be database-agnostic
105
+ def [](field)
106
+ return unless respond_to?(field)
107
+
108
+ send(field)
109
+ end
110
+
111
+ # Override the []= method to set attributes
112
+ # This allows the gem to be database-agnostic
113
+ def []=(field, value)
114
+ return unless respond_to?("#{field}=")
115
+
116
+ send("#{field}=", value)
117
+ end
118
+ end
119
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Purrrge
4
+ VERSION = "1.5.0"
5
+ end
data/lib/purrrge.rb ADDED
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "purrrge/version"
4
+ require_relative "purrrge/registry"
5
+ require_relative "purrrge/scrubbable"
6
+
7
+ # Only require the generator if Rails is defined
8
+ require_relative "generators/purrrge/add_scrubbed_at_generator" if defined?(Rails::Generators)
9
+
10
+ module Purrrge
11
+ class Error < StandardError; end
12
+ # Your code goes here...
13
+ end
data/sig/purrrge.rbs ADDED
@@ -0,0 +1,4 @@
1
+ module Purrrge
2
+ VERSION: String
3
+ # See the writing guide of rbs: https://github.com/ruby/rbs#guides
4
+ end
metadata ADDED
@@ -0,0 +1,77 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: purrrge
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.5.0
5
+ platform: ruby
6
+ authors:
7
+ - Cody Stamps
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2026-01-28 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: activesupport
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '5.0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '5.0'
27
+ description: Purrrge provides tools for implementing data retention policies and scrubbing
28
+ sensitive data in compliance with privacy regulations.
29
+ email:
30
+ - cody.stamps@grayscaleapp.com
31
+ executables: []
32
+ extensions: []
33
+ extra_rdoc_files: []
34
+ files:
35
+ - ".rspec"
36
+ - ".rubocop.yml"
37
+ - ".semaphore/semaphore.yml"
38
+ - ".semaphore/version-and-tag.yml"
39
+ - CHANGELOG.md
40
+ - Gemfile
41
+ - Gemfile.lock
42
+ - README.md
43
+ - Rakefile
44
+ - lib/generators/purrrge/add_scrubbed_at_generator.rb
45
+ - lib/generators/purrrge/templates/add_scrubbed_at_migration.rb.erb
46
+ - lib/purrrge.rb
47
+ - lib/purrrge/registry.rb
48
+ - lib/purrrge/scrubbable.rb
49
+ - lib/purrrge/version.rb
50
+ - sig/purrrge.rbs
51
+ homepage: https://github.com/grayscaleapp/purrrge
52
+ licenses: []
53
+ metadata:
54
+ rubygems_mfa_required: 'true'
55
+ homepage_uri: https://github.com/grayscaleapp/purrrge
56
+ source_code_uri: https://github.com/grayscaleapp/purrrge
57
+ changelog_uri: https://github.com/grayscaleapp/purrrge/blob/master/CHANGELOG.md
58
+ post_install_message:
59
+ rdoc_options: []
60
+ require_paths:
61
+ - lib
62
+ required_ruby_version: !ruby/object:Gem::Requirement
63
+ requirements:
64
+ - - ">="
65
+ - !ruby/object:Gem::Version
66
+ version: 2.6.0
67
+ required_rubygems_version: !ruby/object:Gem::Requirement
68
+ requirements:
69
+ - - ">="
70
+ - !ruby/object:Gem::Version
71
+ version: '0'
72
+ requirements: []
73
+ rubygems_version: 3.4.10
74
+ signing_key:
75
+ specification_version: 4
76
+ summary: A data retention and scrubbing library for Ruby applications
77
+ test_files: []