beaconable 0.3.4 → 1.0.0.alpha
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 +4 -4
- data/.github/workflows/test.yml +42 -0
- data/.rubocop.yml +32 -0
- data/CHANGELOG.md +22 -0
- data/Gemfile +3 -1
- data/Gemfile.lock +95 -40
- data/README.md +101 -4
- data/Rakefile +3 -1
- data/beaconable-0.3.4.gem +0 -0
- data/beaconable.gemspec +10 -8
- data/lib/beaconable/attribute_snapshot.rb +36 -0
- data/lib/beaconable/object_was.rb +34 -5
- data/lib/beaconable/version.rb +3 -1
- data/lib/beaconable.rb +6 -4
- data/lib/generators/beacon/beacon_generator.rb +3 -3
- metadata +32 -29
- data/.travis.yml +0 -8
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 9802334bbbb09aa5c2e3645edccd67de7a0a275b90db63378bea6c1109db6cd9
|
|
4
|
+
data.tar.gz: '08c9da8e967b8db445922ff804d7ea9f1e61e47ff2a680bad5747a1f46e583ca'
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 3e9f16be4ca9624b41e3763e40241709401a0bbcf530d5b3ee1b40006793bfba7187904b4eb732d51202d110b49187484954fc27d24a81a11997a5f46755b5f7
|
|
7
|
+
data.tar.gz: 331fe6033b16a1739f05ff33b509073b088640defe59144d4b010be30696d9f841593b208b77a11239c76fa4bfbc008d23c5f5f70e0d50e31c7152312e6a7c0d
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
branches: [main]
|
|
8
|
+
|
|
9
|
+
jobs:
|
|
10
|
+
test:
|
|
11
|
+
runs-on: ubuntu-latest
|
|
12
|
+
|
|
13
|
+
strategy:
|
|
14
|
+
matrix:
|
|
15
|
+
ruby-version: ['3.2', '3.3']
|
|
16
|
+
|
|
17
|
+
steps:
|
|
18
|
+
- uses: actions/checkout@v4
|
|
19
|
+
|
|
20
|
+
- name: Set up Ruby ${{ matrix.ruby-version }}
|
|
21
|
+
uses: ruby/setup-ruby@v1
|
|
22
|
+
with:
|
|
23
|
+
ruby-version: ${{ matrix.ruby-version }}
|
|
24
|
+
bundler-cache: true
|
|
25
|
+
|
|
26
|
+
- name: Run tests
|
|
27
|
+
run: bundle exec rake test
|
|
28
|
+
|
|
29
|
+
lint:
|
|
30
|
+
runs-on: ubuntu-latest
|
|
31
|
+
|
|
32
|
+
steps:
|
|
33
|
+
- uses: actions/checkout@v4
|
|
34
|
+
|
|
35
|
+
- name: Set up Ruby
|
|
36
|
+
uses: ruby/setup-ruby@v1
|
|
37
|
+
with:
|
|
38
|
+
ruby-version: '3.3'
|
|
39
|
+
bundler-cache: true
|
|
40
|
+
|
|
41
|
+
- name: Run RuboCop
|
|
42
|
+
run: bundle exec rubocop
|
data/.rubocop.yml
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
AllCops:
|
|
2
|
+
NewCops: enable
|
|
3
|
+
TargetRubyVersion: 3.2
|
|
4
|
+
SuggestExtensions: false
|
|
5
|
+
Exclude:
|
|
6
|
+
- "vendor/**/*"
|
|
7
|
+
- "bin/*"
|
|
8
|
+
- "*.gemspec"
|
|
9
|
+
|
|
10
|
+
Style/StringLiterals:
|
|
11
|
+
EnforcedStyle: double_quotes
|
|
12
|
+
|
|
13
|
+
Style/StringLiteralsInInterpolation:
|
|
14
|
+
EnforcedStyle: double_quotes
|
|
15
|
+
|
|
16
|
+
Style/Documentation:
|
|
17
|
+
Enabled: false
|
|
18
|
+
|
|
19
|
+
Metrics/BlockLength:
|
|
20
|
+
Exclude:
|
|
21
|
+
- 'test/**/*'
|
|
22
|
+
|
|
23
|
+
Metrics/ClassLength:
|
|
24
|
+
Exclude:
|
|
25
|
+
- 'test/**/*'
|
|
26
|
+
|
|
27
|
+
Metrics/MethodLength:
|
|
28
|
+
Max: 15
|
|
29
|
+
|
|
30
|
+
# We intentionally memoize @object_was in save_for_beacon
|
|
31
|
+
Naming/MemoizedInstanceVariableName:
|
|
32
|
+
Enabled: false
|
data/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,25 @@
|
|
|
1
|
+
## 1.0.0.alpha (2025-12-17)
|
|
2
|
+
|
|
3
|
+
### Breaking Changes
|
|
4
|
+
|
|
5
|
+
- **Minimum Ruby version:** 3.2.0
|
|
6
|
+
- **Minimum Rails version:** 7.0
|
|
7
|
+
|
|
8
|
+
### Changed
|
|
9
|
+
|
|
10
|
+
- Replaced `OpenStruct` with new `AttributeSnapshot` class for better performance and explicit immutability
|
|
11
|
+
- `ObjectWas` now uses modern Rails 7+ dirty tracking API (`changes_to_save`) instead of iterating columns with `_was` methods
|
|
12
|
+
- Updated development dependencies to modern versions
|
|
13
|
+
- Fixed typo in gemspec: "patern" → "pattern"
|
|
14
|
+
|
|
15
|
+
### Why this change?
|
|
16
|
+
|
|
17
|
+
- `OpenStruct` is no longer auto-loaded in Ruby 3.2+ and shows deprecation warnings
|
|
18
|
+
- Modern Rails provides cleaner dirty tracking APIs
|
|
19
|
+
- This release marks the gem as stable and production-ready
|
|
20
|
+
|
|
21
|
+
---
|
|
22
|
+
|
|
1
23
|
## 0.3.4 (2022-03-14)
|
|
2
24
|
|
|
3
25
|
### Improvements
|
data/Gemfile
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
source "https://rubygems.org"
|
|
2
4
|
|
|
3
|
-
git_source(:github) {|repo_name| "https://github.com/#{repo_name}" }
|
|
5
|
+
git_source(:github) { |repo_name| "https://github.com/#{repo_name}" }
|
|
4
6
|
|
|
5
7
|
# Specify your gem's dependencies in beaconable.gemspec
|
|
6
8
|
gemspec
|
data/Gemfile.lock
CHANGED
|
@@ -1,68 +1,123 @@
|
|
|
1
1
|
PATH
|
|
2
2
|
remote: .
|
|
3
3
|
specs:
|
|
4
|
-
beaconable (0.
|
|
5
|
-
activerecord (>=
|
|
4
|
+
beaconable (1.0.0.alpha)
|
|
5
|
+
activerecord (>= 7.0)
|
|
6
6
|
|
|
7
7
|
GEM
|
|
8
8
|
remote: https://rubygems.org/
|
|
9
9
|
specs:
|
|
10
|
-
activemodel (
|
|
11
|
-
activesupport (=
|
|
12
|
-
activerecord (
|
|
13
|
-
activemodel (=
|
|
14
|
-
activesupport (=
|
|
15
|
-
|
|
16
|
-
|
|
10
|
+
activemodel (8.1.1)
|
|
11
|
+
activesupport (= 8.1.1)
|
|
12
|
+
activerecord (8.1.1)
|
|
13
|
+
activemodel (= 8.1.1)
|
|
14
|
+
activesupport (= 8.1.1)
|
|
15
|
+
timeout (>= 0.4.0)
|
|
16
|
+
activesupport (8.1.1)
|
|
17
|
+
base64
|
|
18
|
+
bigdecimal
|
|
19
|
+
concurrent-ruby (~> 1.0, >= 1.3.1)
|
|
20
|
+
connection_pool (>= 2.2.5)
|
|
21
|
+
drb
|
|
17
22
|
i18n (>= 1.6, < 2)
|
|
23
|
+
json
|
|
24
|
+
logger (>= 1.4.2)
|
|
18
25
|
minitest (>= 5.1)
|
|
19
|
-
|
|
20
|
-
|
|
26
|
+
securerandom (>= 0.3)
|
|
27
|
+
tzinfo (~> 2.0, >= 2.0.5)
|
|
28
|
+
uri (>= 0.13.1)
|
|
21
29
|
ansi (1.5.0)
|
|
22
|
-
ast (2.4.
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
30
|
+
ast (2.4.3)
|
|
31
|
+
base64 (0.3.0)
|
|
32
|
+
bigdecimal (4.0.0)
|
|
33
|
+
builder (3.3.0)
|
|
34
|
+
concurrent-ruby (1.3.6)
|
|
35
|
+
connection_pool (3.0.2)
|
|
36
|
+
date (3.5.1)
|
|
37
|
+
debug (1.11.0)
|
|
38
|
+
irb (~> 1.10)
|
|
39
|
+
reline (>= 0.3.8)
|
|
40
|
+
drb (2.2.3)
|
|
41
|
+
erb (6.0.1)
|
|
42
|
+
i18n (1.14.7)
|
|
27
43
|
concurrent-ruby (~> 1.0)
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
44
|
+
io-console (0.8.2)
|
|
45
|
+
irb (1.16.0)
|
|
46
|
+
pp (>= 0.6.0)
|
|
47
|
+
rdoc (>= 4.0.0)
|
|
48
|
+
reline (>= 0.4.2)
|
|
49
|
+
json (2.18.0)
|
|
50
|
+
language_server-protocol (3.17.0.5)
|
|
51
|
+
lint_roller (1.1.0)
|
|
52
|
+
logger (1.7.0)
|
|
53
|
+
minitest (5.27.0)
|
|
54
|
+
minitest-reporters (1.7.1)
|
|
31
55
|
ansi
|
|
32
56
|
builder
|
|
33
57
|
minitest (>= 5.0)
|
|
34
58
|
ruby-progressbar
|
|
35
|
-
parallel (1.
|
|
36
|
-
parser (
|
|
37
|
-
ast (~> 2.4.
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
59
|
+
parallel (1.27.0)
|
|
60
|
+
parser (3.3.10.0)
|
|
61
|
+
ast (~> 2.4.1)
|
|
62
|
+
racc
|
|
63
|
+
pp (0.6.3)
|
|
64
|
+
prettyprint
|
|
65
|
+
prettyprint (0.2.0)
|
|
66
|
+
prism (1.6.0)
|
|
67
|
+
psych (5.3.1)
|
|
68
|
+
date
|
|
69
|
+
stringio
|
|
70
|
+
racc (1.8.1)
|
|
71
|
+
rainbow (3.1.1)
|
|
72
|
+
rake (13.3.1)
|
|
73
|
+
rdoc (6.17.0)
|
|
74
|
+
erb
|
|
75
|
+
psych (>= 4.0.0)
|
|
76
|
+
tsort
|
|
77
|
+
regexp_parser (2.11.3)
|
|
78
|
+
reline (0.6.3)
|
|
79
|
+
io-console (~> 0.5)
|
|
80
|
+
rubocop (1.82.0)
|
|
81
|
+
json (~> 2.3)
|
|
82
|
+
language_server-protocol (~> 3.17.0.2)
|
|
83
|
+
lint_roller (~> 1.1.0)
|
|
42
84
|
parallel (~> 1.10)
|
|
43
|
-
parser (>= 2
|
|
85
|
+
parser (>= 3.3.0.2)
|
|
44
86
|
rainbow (>= 2.2.2, < 4.0)
|
|
87
|
+
regexp_parser (>= 2.9.3, < 3.0)
|
|
88
|
+
rubocop-ast (>= 1.48.0, < 2.0)
|
|
45
89
|
ruby-progressbar (~> 1.7)
|
|
46
|
-
unicode-display_width (>=
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
90
|
+
unicode-display_width (>= 2.4.0, < 4.0)
|
|
91
|
+
rubocop-ast (1.48.0)
|
|
92
|
+
parser (>= 3.3.7.2)
|
|
93
|
+
prism (~> 1.4)
|
|
94
|
+
ruby-progressbar (1.13.0)
|
|
95
|
+
securerandom (0.4.1)
|
|
96
|
+
sqlite3 (2.8.1-arm64-darwin)
|
|
97
|
+
sqlite3 (2.8.1-x86_64-linux-gnu)
|
|
98
|
+
stringio (3.2.0)
|
|
99
|
+
timeout (0.6.0)
|
|
100
|
+
tsort (0.2.0)
|
|
101
|
+
tzinfo (2.0.6)
|
|
50
102
|
concurrent-ruby (~> 1.0)
|
|
51
|
-
unicode-display_width (
|
|
52
|
-
|
|
103
|
+
unicode-display_width (3.2.0)
|
|
104
|
+
unicode-emoji (~> 4.1)
|
|
105
|
+
unicode-emoji (4.1.0)
|
|
106
|
+
uri (1.1.1)
|
|
53
107
|
|
|
54
108
|
PLATFORMS
|
|
55
|
-
|
|
109
|
+
arm64-darwin-24
|
|
110
|
+
x86_64-linux
|
|
56
111
|
|
|
57
112
|
DEPENDENCIES
|
|
58
113
|
beaconable!
|
|
59
|
-
bundler (
|
|
60
|
-
|
|
114
|
+
bundler (>= 2.0)
|
|
115
|
+
debug (~> 1.8)
|
|
61
116
|
minitest (~> 5.0)
|
|
62
|
-
minitest-reporters (~> 1.
|
|
117
|
+
minitest-reporters (~> 1.6)
|
|
63
118
|
rake (~> 13.0)
|
|
64
|
-
rubocop (~>
|
|
65
|
-
sqlite3 (~>
|
|
119
|
+
rubocop (~> 1.50)
|
|
120
|
+
sqlite3 (~> 2.0)
|
|
66
121
|
|
|
67
122
|
BUNDLED WITH
|
|
68
|
-
|
|
123
|
+
2.4.19
|
data/README.md
CHANGED
|
@@ -1,8 +1,26 @@
|
|
|
1
1
|
# Beaconable
|
|
2
2
|
|
|
3
|
-
[](https://badge.fury.io/rb/beaconable) [](https://github.com/Lastimoso/beaconable/actions/workflows/test.yml) [](https://github.com/Lastimoso/beaconable/issues)
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
A lightweight Ruby gem that provides an elegant, object-oriented pattern for isolating side-effects and callbacks from your ActiveRecord models.
|
|
6
|
+
|
|
7
|
+
## Why Beaconable?
|
|
8
|
+
|
|
9
|
+
ActiveRecord callbacks (`after_save`, `after_commit`, etc.) are convenient, but they can quickly turn your models into tangled messes of business logic, external API calls, and notification triggers. Before you know it, your `User` model is sending emails, syncing with Salesforce, updating Elasticsearch, and making your tests take forever.
|
|
10
|
+
|
|
11
|
+
**Beaconable solves this by:**
|
|
12
|
+
|
|
13
|
+
- **Separating concerns** — Side-effects live in dedicated Beacon classes, not your models
|
|
14
|
+
- **Improving testability** — Test your model logic and side-effects independently
|
|
15
|
+
- **Providing context** — Your Beacon knows exactly what changed (`object_was` vs `object`)
|
|
16
|
+
- **Keeping models lean** — Your ActiveRecord models stay focused on data and validations
|
|
17
|
+
- **Offering flexibility** — Skip beacons when needed, pass metadata for conditional logic
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
## Requirements
|
|
21
|
+
|
|
22
|
+
- Ruby >= 3.2.0
|
|
23
|
+
- Rails >= 7.0
|
|
6
24
|
|
|
7
25
|
## Installation
|
|
8
26
|
|
|
@@ -118,6 +136,85 @@ end
|
|
|
118
136
|
|
|
119
137
|
**Important**: once the beacon has been _fired_ the `beacon_metadata` will be cleared.
|
|
120
138
|
|
|
139
|
+
### Multiple saves in a transaction (a.k.a. "Please don't do this")
|
|
140
|
+
|
|
141
|
+
> **Fair warning:** If you're doing multiple saves on the same record within a single transaction, you might want to reconsider your life choices. But hey, we don't judge — we just handle it gracefully.
|
|
142
|
+
|
|
143
|
+
Beaconable captures the state of your object at the **first** `before_save` and holds onto it until `after_commit`. This means that if you do something like this:
|
|
144
|
+
|
|
145
|
+
```ruby
|
|
146
|
+
ActiveRecord::Base.transaction do
|
|
147
|
+
user.update!(status: 'pending')
|
|
148
|
+
user.update!(status: 'verified')
|
|
149
|
+
user.update!(status: 'active')
|
|
150
|
+
end
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
Your beacon will see:
|
|
154
|
+
- `object.status` → `'active'` (the final value)
|
|
155
|
+
- `object_was.status` → whatever it was **before** the transaction started
|
|
156
|
+
|
|
157
|
+
This is intentional! We track the change from the **original state** to the **final committed state**, not the intermediate chaos in between.
|
|
158
|
+
|
|
159
|
+
```ruby
|
|
160
|
+
# In your beacon:
|
|
161
|
+
def call
|
|
162
|
+
if field_changed?(:status)
|
|
163
|
+
# This will be true if status changed from its original value
|
|
164
|
+
# to the final committed value — regardless of how many
|
|
165
|
+
# intermediate updates happened
|
|
166
|
+
NotifyStatusChange.perform_later(user.id)
|
|
167
|
+
end
|
|
168
|
+
end
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
**Why?** Because Rails' native `saved_changes` only shows the last save, which would miss the fact that `status` changed at all if only the first update modified it. We've got your back.
|
|
172
|
+
|
|
173
|
+
## Known Limitations
|
|
174
|
+
|
|
175
|
+
### Encrypted columns with non-deterministic encryption
|
|
176
|
+
|
|
177
|
+
If you're using Rails encrypted attributes with a non-deterministic algorithm, `field_changed?` will return `true` even when the plaintext value hasn't changed. This happens because the ciphertext changes on every save:
|
|
178
|
+
|
|
179
|
+
```ruby
|
|
180
|
+
bank_account.routing_number = bank_account.routing_number # Same value!
|
|
181
|
+
bank_account.changes_to_save
|
|
182
|
+
# => {"routing_number_ciphertext" => ["QnPUYDD...", "IDZIchs..."]}
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
The underlying ciphertext is different, so Rails (and Beaconable) sees it as a change. If this is a problem for your use case, consider using deterministic encryption for those columns, or handle the comparison manually in your beacon.
|
|
186
|
+
|
|
187
|
+
### Store accessor attributes
|
|
188
|
+
|
|
189
|
+
Attributes defined via `store_accessor` are not directly supported by `field_changed?`. Rails tracks changes on the underlying store column, not on individual accessor keys:
|
|
190
|
+
|
|
191
|
+
```ruby
|
|
192
|
+
# Given: store_accessor :data_store, :a_store_attribute
|
|
193
|
+
|
|
194
|
+
user.a_store_attribute = "hello"
|
|
195
|
+
user.changes_to_save
|
|
196
|
+
# => {"data_store" => [{}, {"a_store_attribute" => "hello"}]}
|
|
197
|
+
|
|
198
|
+
# In your beacon:
|
|
199
|
+
field_changed?(:a_store_attribute) # => false (won't work)
|
|
200
|
+
field_changed?(:data_store) # => true (works, but less precise)
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
**Workaround:** Check the underlying store column and inspect its contents:
|
|
204
|
+
|
|
205
|
+
```ruby
|
|
206
|
+
def call
|
|
207
|
+
if field_changed?(:data_store)
|
|
208
|
+
old_store = object_was[:data_store] || {}
|
|
209
|
+
new_store = object.data_store || {}
|
|
210
|
+
|
|
211
|
+
if old_store["a_store_attribute"] != new_store["a_store_attribute"]
|
|
212
|
+
# Handle the change
|
|
213
|
+
end
|
|
214
|
+
end
|
|
215
|
+
end
|
|
216
|
+
```
|
|
217
|
+
|
|
121
218
|
## Development
|
|
122
219
|
|
|
123
220
|
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
|
@@ -126,7 +223,7 @@ To install this gem onto your local machine, run `bundle exec rake install`. To
|
|
|
126
223
|
|
|
127
224
|
## Contributing
|
|
128
225
|
|
|
129
|
-
Bug reports and pull requests are welcome on GitHub at https://github.com/
|
|
226
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/Lastimoso/beaconable. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Contributor Covenant](http://contributor-covenant.org) code of conduct.
|
|
130
227
|
|
|
131
228
|
## License
|
|
132
229
|
|
|
@@ -134,4 +231,4 @@ The gem is available as open source under the terms of the [MIT License](https:/
|
|
|
134
231
|
|
|
135
232
|
## Code of Conduct
|
|
136
233
|
|
|
137
|
-
Everyone interacting in the Beaconable project
|
|
234
|
+
Everyone interacting in the Beaconable project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/Lastimoso/beaconable/blob/main/CODE_OF_CONDUCT.md).
|
data/Rakefile
CHANGED
|
Binary file
|
data/beaconable.gemspec
CHANGED
|
@@ -9,8 +9,8 @@ Gem::Specification.new do |spec|
|
|
|
9
9
|
spec.authors = ["Gerardo Raiden"]
|
|
10
10
|
spec.email = ["gerardoraiden@gmail.com"]
|
|
11
11
|
|
|
12
|
-
spec.summary = %q{Small OO
|
|
13
|
-
spec.description = %q{Small OO
|
|
12
|
+
spec.summary = %q{Small OO pattern to isolate side-effects and callbacks for your ActiveRecord Models}
|
|
13
|
+
spec.description = %q{Small OO pattern to isolate side-effects and callbacks for your ActiveRecord Models}
|
|
14
14
|
spec.homepage = "https://github.com/Lastimoso/beaconable"
|
|
15
15
|
spec.license = "MIT"
|
|
16
16
|
|
|
@@ -30,13 +30,15 @@ Gem::Specification.new do |spec|
|
|
|
30
30
|
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
|
31
31
|
spec.require_paths = ["lib"]
|
|
32
32
|
|
|
33
|
-
spec.
|
|
33
|
+
spec.required_ruby_version = '>= 3.2.0'
|
|
34
34
|
|
|
35
|
-
spec.
|
|
35
|
+
spec.add_dependency 'activerecord', '>= 7.0'
|
|
36
|
+
|
|
37
|
+
spec.add_development_dependency 'bundler', '>= 2.0'
|
|
36
38
|
spec.add_development_dependency 'minitest', '~> 5.0'
|
|
37
|
-
spec.add_development_dependency 'minitest-reporters', '~> 1.
|
|
39
|
+
spec.add_development_dependency 'minitest-reporters', '~> 1.6'
|
|
38
40
|
spec.add_development_dependency 'rake', '~> 13.0'
|
|
39
|
-
spec.add_development_dependency '
|
|
40
|
-
spec.add_development_dependency '
|
|
41
|
-
spec.add_development_dependency '
|
|
41
|
+
spec.add_development_dependency 'sqlite3', '~> 2.0'
|
|
42
|
+
spec.add_development_dependency 'debug', '~> 1.8'
|
|
43
|
+
spec.add_development_dependency 'rubocop', '~> 1.50'
|
|
42
44
|
end
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Beaconable
|
|
4
|
+
# Immutable snapshot of an ActiveRecord object's attributes at a point in time.
|
|
5
|
+
# Replaces OpenStruct for better performance and explicit immutability.
|
|
6
|
+
class AttributeSnapshot
|
|
7
|
+
def initialize(attributes)
|
|
8
|
+
@attributes = attributes.freeze
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def method_missing(method_name, *args)
|
|
12
|
+
key = method_name.to_sym
|
|
13
|
+
return @attributes[key] if @attributes.key?(key)
|
|
14
|
+
|
|
15
|
+
super
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def respond_to_missing?(method_name, include_private = false)
|
|
19
|
+
@attributes.key?(method_name.to_sym) || super
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# Direct hash-style access for attribute values
|
|
23
|
+
def [](key)
|
|
24
|
+
@attributes[key.to_sym]
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# Returns a copy of the attributes hash (for debugging/inspection)
|
|
28
|
+
def to_h
|
|
29
|
+
@attributes.dup
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def inspect
|
|
33
|
+
"#<#{self.class.name} #{@attributes.inspect}>"
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Beaconable
|
|
4
|
+
# Captures the database state of an ActiveRecord object before changes.
|
|
5
|
+
# Used to track what changed during a save/transaction.
|
|
4
6
|
class ObjectWas
|
|
5
7
|
attr_reader :object
|
|
6
8
|
|
|
@@ -8,13 +10,40 @@ module Beaconable
|
|
|
8
10
|
@object = object
|
|
9
11
|
end
|
|
10
12
|
|
|
13
|
+
# Captures the current database state and returns an immutable snapshot.
|
|
14
|
+
# For new records, returns a snapshot with nil values for all columns.
|
|
11
15
|
def call
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
+
AttributeSnapshot.new(build_attributes)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
private
|
|
20
|
+
|
|
21
|
+
def build_attributes
|
|
22
|
+
if object.new_record?
|
|
23
|
+
build_new_record_attributes
|
|
24
|
+
else
|
|
25
|
+
build_persisted_record_attributes
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# For new records, all "was" values are nil
|
|
30
|
+
# This preserves backward compatibility with new_entry? detection
|
|
31
|
+
def build_new_record_attributes
|
|
32
|
+
object.class.column_names.each_with_object({}) do |column, hash|
|
|
33
|
+
hash[column.to_sym] = nil
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# For persisted records, capture database values before current changes
|
|
38
|
+
# Uses modern Rails 7+ dirty tracking API
|
|
39
|
+
def build_persisted_record_attributes
|
|
40
|
+
# Start with current attributes (which equal DB values for unchanged attrs)
|
|
41
|
+
result = object.attributes.symbolize_keys
|
|
42
|
+
# Replace changed attributes with their original database values
|
|
43
|
+
object.changes_to_save.each do |attr, (old_value, _new_value)|
|
|
44
|
+
result[attr.to_sym] = old_value
|
|
16
45
|
end
|
|
17
|
-
|
|
46
|
+
result
|
|
18
47
|
end
|
|
19
48
|
end
|
|
20
49
|
end
|
data/lib/beaconable/version.rb
CHANGED
data/lib/beaconable.rb
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require
|
|
4
|
-
require
|
|
5
|
-
require
|
|
6
|
-
require
|
|
3
|
+
require "beaconable/version"
|
|
4
|
+
require "beaconable/attribute_snapshot"
|
|
5
|
+
require "beaconable/object_was"
|
|
6
|
+
require "beaconable/base_beacon"
|
|
7
|
+
require "active_record"
|
|
7
8
|
|
|
8
9
|
module Beaconable
|
|
9
10
|
extend ActiveSupport::Concern
|
|
11
|
+
|
|
10
12
|
included do
|
|
11
13
|
attr_accessor :beacon_metadata
|
|
12
14
|
attr_accessor :skip_beacon
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require
|
|
3
|
+
require "rails/generators/base"
|
|
4
4
|
|
|
5
5
|
class BeaconGenerator < Rails::Generators::NamedBase
|
|
6
|
-
source_root File.expand_path(
|
|
6
|
+
source_root File.expand_path("templates", __dir__)
|
|
7
7
|
|
|
8
8
|
def create_beacon_file
|
|
9
|
-
template
|
|
9
|
+
template "beacon.rb.tt", File.join("app", "beacons", class_path, "#{file_name}_beacon.rb")
|
|
10
10
|
end
|
|
11
11
|
|
|
12
12
|
def insert_inclusion_into_model_file
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: beaconable
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 1.0.0.alpha
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Gerardo Raiden
|
|
8
|
-
autorequire:
|
|
8
|
+
autorequire:
|
|
9
9
|
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date:
|
|
11
|
+
date: 2025-12-17 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: activerecord
|
|
@@ -16,28 +16,28 @@ dependencies:
|
|
|
16
16
|
requirements:
|
|
17
17
|
- - ">="
|
|
18
18
|
- !ruby/object:Gem::Version
|
|
19
|
-
version: '
|
|
19
|
+
version: '7.0'
|
|
20
20
|
type: :runtime
|
|
21
21
|
prerelease: false
|
|
22
22
|
version_requirements: !ruby/object:Gem::Requirement
|
|
23
23
|
requirements:
|
|
24
24
|
- - ">="
|
|
25
25
|
- !ruby/object:Gem::Version
|
|
26
|
-
version: '
|
|
26
|
+
version: '7.0'
|
|
27
27
|
- !ruby/object:Gem::Dependency
|
|
28
28
|
name: bundler
|
|
29
29
|
requirement: !ruby/object:Gem::Requirement
|
|
30
30
|
requirements:
|
|
31
|
-
- - "
|
|
31
|
+
- - ">="
|
|
32
32
|
- !ruby/object:Gem::Version
|
|
33
|
-
version: '
|
|
33
|
+
version: '2.0'
|
|
34
34
|
type: :development
|
|
35
35
|
prerelease: false
|
|
36
36
|
version_requirements: !ruby/object:Gem::Requirement
|
|
37
37
|
requirements:
|
|
38
|
-
- - "
|
|
38
|
+
- - ">="
|
|
39
39
|
- !ruby/object:Gem::Version
|
|
40
|
-
version: '
|
|
40
|
+
version: '2.0'
|
|
41
41
|
- !ruby/object:Gem::Dependency
|
|
42
42
|
name: minitest
|
|
43
43
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -58,14 +58,14 @@ dependencies:
|
|
|
58
58
|
requirements:
|
|
59
59
|
- - "~>"
|
|
60
60
|
- !ruby/object:Gem::Version
|
|
61
|
-
version: '1.
|
|
61
|
+
version: '1.6'
|
|
62
62
|
type: :development
|
|
63
63
|
prerelease: false
|
|
64
64
|
version_requirements: !ruby/object:Gem::Requirement
|
|
65
65
|
requirements:
|
|
66
66
|
- - "~>"
|
|
67
67
|
- !ruby/object:Gem::Version
|
|
68
|
-
version: '1.
|
|
68
|
+
version: '1.6'
|
|
69
69
|
- !ruby/object:Gem::Dependency
|
|
70
70
|
name: rake
|
|
71
71
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -81,48 +81,48 @@ dependencies:
|
|
|
81
81
|
- !ruby/object:Gem::Version
|
|
82
82
|
version: '13.0'
|
|
83
83
|
- !ruby/object:Gem::Dependency
|
|
84
|
-
name:
|
|
84
|
+
name: sqlite3
|
|
85
85
|
requirement: !ruby/object:Gem::Requirement
|
|
86
86
|
requirements:
|
|
87
87
|
- - "~>"
|
|
88
88
|
- !ruby/object:Gem::Version
|
|
89
|
-
version:
|
|
89
|
+
version: '2.0'
|
|
90
90
|
type: :development
|
|
91
91
|
prerelease: false
|
|
92
92
|
version_requirements: !ruby/object:Gem::Requirement
|
|
93
93
|
requirements:
|
|
94
94
|
- - "~>"
|
|
95
95
|
- !ruby/object:Gem::Version
|
|
96
|
-
version:
|
|
96
|
+
version: '2.0'
|
|
97
97
|
- !ruby/object:Gem::Dependency
|
|
98
|
-
name:
|
|
98
|
+
name: debug
|
|
99
99
|
requirement: !ruby/object:Gem::Requirement
|
|
100
100
|
requirements:
|
|
101
101
|
- - "~>"
|
|
102
102
|
- !ruby/object:Gem::Version
|
|
103
|
-
version: '1.
|
|
103
|
+
version: '1.8'
|
|
104
104
|
type: :development
|
|
105
105
|
prerelease: false
|
|
106
106
|
version_requirements: !ruby/object:Gem::Requirement
|
|
107
107
|
requirements:
|
|
108
108
|
- - "~>"
|
|
109
109
|
- !ruby/object:Gem::Version
|
|
110
|
-
version: '1.
|
|
110
|
+
version: '1.8'
|
|
111
111
|
- !ruby/object:Gem::Dependency
|
|
112
|
-
name:
|
|
112
|
+
name: rubocop
|
|
113
113
|
requirement: !ruby/object:Gem::Requirement
|
|
114
114
|
requirements:
|
|
115
115
|
- - "~>"
|
|
116
116
|
- !ruby/object:Gem::Version
|
|
117
|
-
version: '
|
|
117
|
+
version: '1.50'
|
|
118
118
|
type: :development
|
|
119
119
|
prerelease: false
|
|
120
120
|
version_requirements: !ruby/object:Gem::Requirement
|
|
121
121
|
requirements:
|
|
122
122
|
- - "~>"
|
|
123
123
|
- !ruby/object:Gem::Version
|
|
124
|
-
version: '
|
|
125
|
-
description: Small OO
|
|
124
|
+
version: '1.50'
|
|
125
|
+
description: Small OO pattern to isolate side-effects and callbacks for your ActiveRecord
|
|
126
126
|
Models
|
|
127
127
|
email:
|
|
128
128
|
- gerardoraiden@gmail.com
|
|
@@ -130,8 +130,9 @@ executables: []
|
|
|
130
130
|
extensions: []
|
|
131
131
|
extra_rdoc_files: []
|
|
132
132
|
files:
|
|
133
|
+
- ".github/workflows/test.yml"
|
|
133
134
|
- ".gitignore"
|
|
134
|
-
- ".
|
|
135
|
+
- ".rubocop.yml"
|
|
135
136
|
- CHANGELOG.md
|
|
136
137
|
- CODE_OF_CONDUCT.md
|
|
137
138
|
- Gemfile
|
|
@@ -139,10 +140,12 @@ files:
|
|
|
139
140
|
- LICENSE.txt
|
|
140
141
|
- README.md
|
|
141
142
|
- Rakefile
|
|
143
|
+
- beaconable-0.3.4.gem
|
|
142
144
|
- beaconable.gemspec
|
|
143
145
|
- bin/console
|
|
144
146
|
- bin/setup
|
|
145
147
|
- lib/beaconable.rb
|
|
148
|
+
- lib/beaconable/attribute_snapshot.rb
|
|
146
149
|
- lib/beaconable/base_beacon.rb
|
|
147
150
|
- lib/beaconable/object_was.rb
|
|
148
151
|
- lib/beaconable/version.rb
|
|
@@ -154,7 +157,7 @@ licenses:
|
|
|
154
157
|
- MIT
|
|
155
158
|
metadata:
|
|
156
159
|
allowed_push_host: https://rubygems.org
|
|
157
|
-
post_install_message:
|
|
160
|
+
post_install_message:
|
|
158
161
|
rdoc_options: []
|
|
159
162
|
require_paths:
|
|
160
163
|
- lib
|
|
@@ -162,16 +165,16 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
|
162
165
|
requirements:
|
|
163
166
|
- - ">="
|
|
164
167
|
- !ruby/object:Gem::Version
|
|
165
|
-
version:
|
|
168
|
+
version: 3.2.0
|
|
166
169
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
167
170
|
requirements:
|
|
168
|
-
- - "
|
|
171
|
+
- - ">"
|
|
169
172
|
- !ruby/object:Gem::Version
|
|
170
|
-
version:
|
|
173
|
+
version: 1.3.1
|
|
171
174
|
requirements: []
|
|
172
|
-
rubygems_version: 3.
|
|
173
|
-
signing_key:
|
|
175
|
+
rubygems_version: 3.4.19
|
|
176
|
+
signing_key:
|
|
174
177
|
specification_version: 4
|
|
175
|
-
summary: Small OO
|
|
178
|
+
summary: Small OO pattern to isolate side-effects and callbacks for your ActiveRecord
|
|
176
179
|
Models
|
|
177
180
|
test_files: []
|