observable_model 0.1.0 โ 0.1.1
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/README.md +209 -10
- data/lib/observable_model/observers/base.rb +6 -0
- data/lib/observable_model/sources/active_record_observable.rb +9 -0
- data/lib/observable_model/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: d3bd27344026bbc33d4329242d221f0163ee2655cabd79872a7fce079cd10db3
|
|
4
|
+
data.tar.gz: c4fdd4dfb9817e46e7eabcf0832e819c9b4c15520439b8b60412a7f8495e522f
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 58ad1ab5c99f79de7ddade0e4406acd7d2ae9fe105f78bee4bfa6cc61d34fb8b4f24f7dce8ca65da77a68be0aa00a4b74085bae97f91dd4e6dd5987549b1497b
|
|
7
|
+
data.tar.gz: a79492c661e875f5cb3060424d8d0ff6c210db7790030f875d48ffa425824b1760908ff87c763e190e927e9b58b57f1360125099970fc0bd328107abf5ef6f62
|
data/README.md
CHANGED
|
@@ -1,24 +1,201 @@
|
|
|
1
1
|
# ObservableModel
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
A Ruby gem that implements the Observer pattern for ActiveRecord models in Rails applications. ObservableModel provides a clean, organized way to respond to model lifecycle events (create, update, destroy) without cluttering your models with callback logic.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
## Why ObservableModel?
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
As Rails applications grow, model callbacks can become cluttered with business logic, side effects, and external service integrations. ObservableModel helps you:
|
|
8
|
+
|
|
9
|
+
- **Separate concerns** - Keep models focused on data and validation
|
|
10
|
+
- **Organize side effects** - Isolate external service calls, notifications, and async jobs
|
|
11
|
+
- **Improve testability** - Easily skip observers in tests with `skip_observers` flag
|
|
12
|
+
- **Follow patterns** - Implement the classic Observer pattern in a Rails-friendly way
|
|
13
|
+
- **Maintain clarity** - Know exactly where lifecycle-triggered logic lives
|
|
14
|
+
|
|
15
|
+
## Features
|
|
16
|
+
|
|
17
|
+
- ๐ฏ **Simple integration** - Just include a module in your ActiveRecord models
|
|
18
|
+
- ๐ **Lifecycle hooks** - Respond to before and after commit events for create, update, and destroy
|
|
19
|
+
- ๐ซ **Skippable observers** - Disable observers per-instance when needed
|
|
20
|
+
- ๐งฉ **Convention-based** - Auto-discovers observer classes (e.g., `UserObserver` for `User`)
|
|
21
|
+
- โก **Before-commit callbacks** - Enables observer actions to run before changes are committed
|
|
22
|
+
- โก **After-commit callbacks** - Ensures database transactions complete before running observer actions
|
|
23
|
+
- ๐งช **Test-friendly** - Easy to bypass observers in test scenarios
|
|
8
24
|
|
|
9
|
-
|
|
25
|
+
## Requirements
|
|
26
|
+
|
|
27
|
+
- Ruby >= 3.2.0
|
|
28
|
+
- Rails >= 6.1
|
|
29
|
+
|
|
30
|
+
## Installation
|
|
10
31
|
|
|
11
|
-
|
|
32
|
+
Add this line to your application's Gemfile:
|
|
12
33
|
|
|
13
|
-
|
|
34
|
+
```ruby
|
|
35
|
+
gem 'observable_model'
|
|
36
|
+
```
|
|
14
37
|
|
|
15
|
-
|
|
38
|
+
And then execute:
|
|
16
39
|
|
|
17
|
-
|
|
40
|
+
```bash
|
|
41
|
+
$ bundle install
|
|
42
|
+
```
|
|
18
43
|
|
|
19
44
|
## Usage
|
|
20
45
|
|
|
21
|
-
|
|
46
|
+
### Basic Setup
|
|
47
|
+
|
|
48
|
+
1. **Include the module in your ActiveRecord model:**
|
|
49
|
+
|
|
50
|
+
```ruby
|
|
51
|
+
class User < ApplicationRecord
|
|
52
|
+
include ObservableModel::Sources::ActiveRecordObservable
|
|
53
|
+
|
|
54
|
+
# Your model code...
|
|
55
|
+
end
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
2. **Create an observer class:**
|
|
59
|
+
|
|
60
|
+
ObservableModel uses a naming convention: for a model named `User`, create a `UserObserver` class.
|
|
61
|
+
|
|
62
|
+
```ruby
|
|
63
|
+
# app/observers/user_observer.rb
|
|
64
|
+
class UserObserver < ObservableModel::Observers::Base
|
|
65
|
+
def pre_create
|
|
66
|
+
# Called before the record is created
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def pre_update
|
|
70
|
+
# Called before the record is updated
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def pre_destroy
|
|
74
|
+
# Called before the record is destroyed
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def on_create_commit
|
|
78
|
+
# Called after a user is created and committed to the database
|
|
79
|
+
WelcomeMailer.welcome_email(@observable).deliver_later
|
|
80
|
+
AnalyticsService.track_signup(@observable)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def on_update_commit
|
|
84
|
+
# Called after a user is updated and committed
|
|
85
|
+
if @observable.saved_change_to_email?
|
|
86
|
+
EmailChangeNotifier.notify(@observable)
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def on_destroy_commit
|
|
91
|
+
# Called after a user is destroyed and committed
|
|
92
|
+
CleanupService.remove_user_data(@observable.id)
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
The `@observable` instance variable contains the model instance that triggered the event.
|
|
98
|
+
|
|
99
|
+
### Skipping Observers
|
|
100
|
+
|
|
101
|
+
To bypass observers for a specific operation:
|
|
102
|
+
|
|
103
|
+
```ruby
|
|
104
|
+
user = User.new(name: "John Doe")
|
|
105
|
+
user.skip_observers = true
|
|
106
|
+
user.save # No observer callbacks will be triggered
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
### Custom Observer Class Names
|
|
110
|
+
|
|
111
|
+
Override `observer_class_name` to use a non-conventional class name:
|
|
112
|
+
|
|
113
|
+
```ruby
|
|
114
|
+
class User < ApplicationRecord
|
|
115
|
+
include ObservableModel::Sources::ActiveRecordObservable
|
|
116
|
+
|
|
117
|
+
def observer_class_name
|
|
118
|
+
"CustomUserObserver"
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
### Organizing Observers
|
|
124
|
+
|
|
125
|
+
We recommend creating an `app/observers` directory in your Rails application:
|
|
126
|
+
|
|
127
|
+
```
|
|
128
|
+
app/
|
|
129
|
+
observers/
|
|
130
|
+
user_observer.rb
|
|
131
|
+
order_observer.rb
|
|
132
|
+
payment_observer.rb
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
Make sure to add this to your `config/application.rb`:
|
|
136
|
+
|
|
137
|
+
```ruby
|
|
138
|
+
config.autoload_paths += %W(#{config.root}/app/observers)
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
## How It Works
|
|
142
|
+
|
|
143
|
+
When you include `ObservableModel::Sources::ActiveRecordObservable` in your model, six callbacks are registered:
|
|
144
|
+
|
|
145
|
+
- `before_create` โ `pre_create`
|
|
146
|
+
- `before_update` โ `pre_update`
|
|
147
|
+
- `before_destroy` โ `pre_destroy`
|
|
148
|
+
- `after_create_commit` โ `on_create_commit`
|
|
149
|
+
- `after_update_commit` โ `on_update_commit`
|
|
150
|
+
- `after_destroy_commit` โ `on_destroy_commit`
|
|
151
|
+
|
|
152
|
+
On each event, the model looks up the observer class by convention, instantiates it with itself, and delegates the callback to it. If `skip_observers` is `true`, no observer is instantiated and all callbacks are silently skipped.
|
|
153
|
+
|
|
154
|
+
## Examples
|
|
155
|
+
|
|
156
|
+
### Example: Send Welcome Email on User Registration
|
|
157
|
+
|
|
158
|
+
```ruby
|
|
159
|
+
class UserObserver < ObservableModel::Observers::Base
|
|
160
|
+
def on_create_commit
|
|
161
|
+
UserMailer.welcome_email(@observable).deliver_later
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
### Example: Validate State Before Destruction
|
|
167
|
+
|
|
168
|
+
```ruby
|
|
169
|
+
class OrderObserver < ObservableModel::Observers::Base
|
|
170
|
+
def pre_destroy
|
|
171
|
+
raise "Cannot delete a completed order" if @observable.completed?
|
|
172
|
+
end
|
|
173
|
+
end
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
### Example: Track Order Status Changes
|
|
177
|
+
|
|
178
|
+
```ruby
|
|
179
|
+
class OrderObserver < ObservableModel::Observers::Base
|
|
180
|
+
def on_update_commit
|
|
181
|
+
if @observable.saved_change_to_status?
|
|
182
|
+
OrderStatusNotifier.notify_customer(@observable)
|
|
183
|
+
AnalyticsService.track_status_change(@observable)
|
|
184
|
+
end
|
|
185
|
+
end
|
|
186
|
+
end
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
### Example: Cleanup Related Data on Deletion
|
|
190
|
+
|
|
191
|
+
```ruby
|
|
192
|
+
class AccountObserver < ObservableModel::Observers::Base
|
|
193
|
+
def on_destroy_commit
|
|
194
|
+
DeleteUserDataJob.perform_later(@observable.id)
|
|
195
|
+
AuditLog.create(action: 'account_deleted', account_id: @observable.id)
|
|
196
|
+
end
|
|
197
|
+
end
|
|
198
|
+
```
|
|
22
199
|
|
|
23
200
|
## Development
|
|
24
201
|
|
|
@@ -26,6 +203,28 @@ After checking out the repo, run `bin/setup` to install dependencies. Then, run
|
|
|
26
203
|
|
|
27
204
|
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).
|
|
28
205
|
|
|
206
|
+
## Testing
|
|
207
|
+
|
|
208
|
+
Run the test suite with:
|
|
209
|
+
|
|
210
|
+
```bash
|
|
211
|
+
$ bundle exec rake spec
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
## Roadmap
|
|
215
|
+
|
|
216
|
+
- [ ] Observer registration/configuration DSL
|
|
217
|
+
- [ ] Built-in async observer execution
|
|
218
|
+
- [ ] Observer metrics and monitoring hooks
|
|
219
|
+
|
|
29
220
|
## Contributing
|
|
30
221
|
|
|
31
|
-
Bug reports and pull requests are welcome on GitHub at https://github.com/
|
|
222
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/CodeTectonics/observable-model. This project is intended to be a safe, welcoming space for collaboration.
|
|
223
|
+
|
|
224
|
+
## License
|
|
225
|
+
|
|
226
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
|
227
|
+
|
|
228
|
+
## Code of Conduct
|
|
229
|
+
|
|
230
|
+
Everyone interacting in the ObservableModel project's codebases and issue trackers is expected to follow the project's code of conduct.
|
|
@@ -6,6 +6,9 @@ module ObservableModel
|
|
|
6
6
|
included do
|
|
7
7
|
attr_accessor :skip_observers
|
|
8
8
|
|
|
9
|
+
before_create :pre_create
|
|
10
|
+
before_update :pre_update
|
|
11
|
+
before_destroy :pre_destroy
|
|
9
12
|
after_create_commit :on_create_commit
|
|
10
13
|
after_update_commit :on_update_commit
|
|
11
14
|
after_destroy_commit :on_destroy_commit
|
|
@@ -18,6 +21,12 @@ module ObservableModel
|
|
|
18
21
|
observer_class_name.constantize.new(self) unless skip_observers
|
|
19
22
|
end
|
|
20
23
|
|
|
24
|
+
delegate :pre_create, to: :observer, allow_nil: true
|
|
25
|
+
|
|
26
|
+
delegate :pre_update, to: :observer, allow_nil: true
|
|
27
|
+
|
|
28
|
+
delegate :pre_destroy, to: :observer, allow_nil: true
|
|
29
|
+
|
|
21
30
|
delegate :on_create_commit, to: :observer, allow_nil: true
|
|
22
31
|
|
|
23
32
|
delegate :on_update_commit, to: :observer, allow_nil: true
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: observable_model
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.1.
|
|
4
|
+
version: 0.1.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Mark Harbison
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-09-
|
|
11
|
+
date: 2026-09-02 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: rails
|