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 +7 -0
- data/.rspec +3 -0
- data/.rubocop.yml +36 -0
- data/.semaphore/semaphore.yml +52 -0
- data/.semaphore/version-and-tag.yml +46 -0
- data/CHANGELOG.md +26 -0
- data/Gemfile +14 -0
- data/Gemfile.lock +93 -0
- data/README.md +340 -0
- data/Rakefile +8 -0
- data/lib/generators/purrrge/add_scrubbed_at_generator.rb +30 -0
- data/lib/generators/purrrge/templates/add_scrubbed_at_migration.rb.erb +6 -0
- data/lib/purrrge/registry.rb +62 -0
- data/lib/purrrge/scrubbable.rb +119 -0
- data/lib/purrrge/version.rb +5 -0
- data/lib/purrrge.rb +13 -0
- data/sig/purrrge.rbs +4 -0
- metadata +77 -0
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
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,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
|
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
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: []
|