azeroth 2.1.0 → 2.2.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 +4 -4
- data/.circleci/config.yml +6 -9
- data/.github/active_ext-usage.md +137 -0
- data/.github/azeroth-usage.md +277 -0
- data/.github/copilot-instructions.md +70 -0
- data/.github/core_ext-usage.md +324 -0
- data/.github/jace-usage.md +241 -0
- data/.rubocop.yml +14 -1
- data/.rubocop_todo.yml +1 -15
- data/Dockerfile +2 -2
- data/Gemfile +20 -16
- data/README.md +15 -7
- data/Rakefile +3 -0
- data/azeroth.gemspec +3 -3
- data/lib/azeroth/decorator/key_value_extractor.rb +1 -1
- data/lib/azeroth/resourceable/class_methods.rb +2 -2
- data/lib/azeroth/version.rb +1 -1
- data/spec/controllers/documents_controller_spec.rb +5 -5
- data/spec/controllers/documents_with_error_controller_spec.rb +3 -3
- data/spec/controllers/index_documents_controller_spec.rb +1 -1
- data/spec/controllers/paginated_documents_controller_spec.rb +1 -1
- data/spec/controllers/rendering_controller_spec.rb +1 -1
- data/spec/dummy/app/controllers/games_controller.rb +1 -0
- data/spec/dummy/app/controllers/publishers_controller.rb +1 -0
- data/spec/dummy/app/models/document.rb +1 -1
- data/spec/dummy/app/models/dummy_model.rb +1 -1
- data/spec/dummy/app/models/factory.rb +3 -3
- data/spec/dummy/app/models/game/decorator.rb +1 -1
- data/spec/dummy/app/models/game.rb +1 -1
- data/spec/dummy/app/models/movie.rb +1 -1
- data/spec/dummy/app/models/pokemon/decorator.rb +1 -3
- data/spec/dummy/app/models/pokemon.rb +8 -2
- data/spec/dummy/app/models/pokemon_master.rb +5 -3
- data/spec/dummy/app/models/product.rb +1 -1
- data/spec/dummy/app/models/publisher.rb +2 -2
- data/spec/dummy/app/models/user.rb +1 -1
- data/spec/dummy/app/models/website/decorator.rb +1 -1
- data/spec/dummy/app/models/website/with_location.rb +1 -1
- data/spec/dummy/app/models/website.rb +1 -1
- data/spec/dummy/bin/setup +0 -4
- data/spec/dummy/bin/update +0 -4
- data/spec/dummy/config/environments/development.rb +1 -1
- data/spec/lib/azeroth/model_spec.rb +1 -0
- data/spec/lib/azeroth/request_handler/index_spec.rb +5 -5
- data/spec/lib/azeroth/routes_builder_spec.rb +1 -1
- data/spec/spec_helper.rb +7 -0
- data/spec/support/app/controllers/controller.rb +1 -1
- data/spec/support/app/controllers/request_handler_controller.rb +1 -1
- metadata +13 -8
|
@@ -0,0 +1,324 @@
|
|
|
1
|
+
# Using `darthjee-core_ext` in Your Project
|
|
2
|
+
|
|
3
|
+
This file is intended to be copied into the `.github/` folder of other projects.
|
|
4
|
+
It provides GitHub Copilot (and developers) with a clear, actionable guide on how
|
|
5
|
+
to use the [`darthjee-core_ext`](https://github.com/darthjee/core_ext) Ruby gem.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## What Is `core_ext`?
|
|
10
|
+
|
|
11
|
+
`darthjee-core_ext` is a Ruby gem that extends Ruby's built-in (core) classes with
|
|
12
|
+
additional utility methods. It follows the convention of "core extensions" — monkey-patching
|
|
13
|
+
standard objects such as `Array`, `Hash`, `Symbol`, `Enumerable`, `Date`, `Object`,
|
|
14
|
+
`Numeric`, and `Math` with useful helpers, while maintaining predictable behavior.
|
|
15
|
+
|
|
16
|
+
- **Gem name**: `darthjee-core_ext`
|
|
17
|
+
- **Source**: <https://github.com/darthjee/core_ext>
|
|
18
|
+
- **YARD docs**: <https://www.rubydoc.info/gems/darthjee-core_ext>
|
|
19
|
+
- **Current stable release**: 3.0.0
|
|
20
|
+
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
## Adding the Gem to Your Project
|
|
24
|
+
|
|
25
|
+
### Via RubyGems (recommended)
|
|
26
|
+
|
|
27
|
+
Add to your `Gemfile`:
|
|
28
|
+
|
|
29
|
+
```ruby
|
|
30
|
+
gem 'darthjee-core_ext'
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Or pin a specific version for reproducible builds:
|
|
34
|
+
|
|
35
|
+
```ruby
|
|
36
|
+
gem 'darthjee-core_ext', '~> 3.0'
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
### Via GitHub (source)
|
|
40
|
+
|
|
41
|
+
```ruby
|
|
42
|
+
gem 'darthjee-core_ext',
|
|
43
|
+
github: 'darthjee/core_ext',
|
|
44
|
+
branch: 'main'
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
---
|
|
48
|
+
|
|
49
|
+
## Installing
|
|
50
|
+
|
|
51
|
+
After adding the gem to your `Gemfile`, run:
|
|
52
|
+
|
|
53
|
+
```shell
|
|
54
|
+
bundle install
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Or install it system-wide:
|
|
58
|
+
|
|
59
|
+
```shell
|
|
60
|
+
gem install darthjee-core_ext
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
---
|
|
64
|
+
|
|
65
|
+
## Loading the Gem
|
|
66
|
+
|
|
67
|
+
When using Bundler (Rails, typical Ruby apps), the gem is loaded automatically via
|
|
68
|
+
`Bundler.require`. No explicit `require` is needed.
|
|
69
|
+
|
|
70
|
+
In plain Ruby scripts or when Bundler auto-require is disabled, add:
|
|
71
|
+
|
|
72
|
+
```ruby
|
|
73
|
+
require 'darthjee/core_ext'
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
---
|
|
77
|
+
|
|
78
|
+
## Extensions Provided
|
|
79
|
+
|
|
80
|
+
### `Hash`
|
|
81
|
+
|
|
82
|
+
| Method | Description |
|
|
83
|
+
|---|---|
|
|
84
|
+
| `#append_to_keys` | Appends a string to every key |
|
|
85
|
+
| `#prepend_to_keys` | Prepends a string to every key |
|
|
86
|
+
| `#camelize_keys` / `#camelize_keys!` | Converts keys to CamelCase |
|
|
87
|
+
| `#lower_camelize_keys` / `#lower_camelize_keys!` | Converts keys to lowerCamelCase |
|
|
88
|
+
| `#underscore_keys` / `#underscore_keys!` | Converts keys to snake_case |
|
|
89
|
+
| `#change_keys` / `#change_keys!` | Transforms keys with a block |
|
|
90
|
+
| `#chain_change_keys` / `#chain_change_keys!` | Transforms keys by chaining method calls |
|
|
91
|
+
| `#change_values` / `#change_values!` | Transforms values with a block (optionally recursive) |
|
|
92
|
+
| `#chain_fetch` | Fetches nested keys in a chain |
|
|
93
|
+
| `#exclusive_merge` / `#exclusive_merge!` | Merges only existing keys |
|
|
94
|
+
| `#remap_keys` / `#remap_keys!` | Renames keys based on a mapping hash |
|
|
95
|
+
| `#sort_keys` / `#sort_keys!` | Sorts the hash by its keys |
|
|
96
|
+
| `#squash` / `#squash!` | Flattens a deep hash into a single-level hash |
|
|
97
|
+
| `#to_deep_hash` / `#to_deep_hash!` | Expands a squashed hash back into a deep hash |
|
|
98
|
+
| `#map_to_hash` | Maps values keeping original keys |
|
|
99
|
+
| `#transpose` / `#transpose!` | Swaps keys with values |
|
|
100
|
+
|
|
101
|
+
### `Array`
|
|
102
|
+
|
|
103
|
+
| Method | Description |
|
|
104
|
+
|---|---|
|
|
105
|
+
| `#as_hash` | Zips the array with a keys array into a Hash |
|
|
106
|
+
| `#average` | Returns the arithmetic average of the elements |
|
|
107
|
+
| `#chain_map` | Applies `.map` in a chain of method calls |
|
|
108
|
+
| `#mapk` | Maps by fetching nested keys from hashes inside the array |
|
|
109
|
+
| `#procedural_join` | Joins elements with a dynamically computed separator |
|
|
110
|
+
| `#random` | Returns a random element |
|
|
111
|
+
| `#random!` | Removes and returns a random element |
|
|
112
|
+
|
|
113
|
+
### `Symbol`
|
|
114
|
+
|
|
115
|
+
| Method | Description |
|
|
116
|
+
|---|---|
|
|
117
|
+
| `#camelize` | Camelizes the symbol (`:my_sym` → `:MySym`) |
|
|
118
|
+
| `#underscore` | Underscores a camelized symbol (`:MySym` → `:my_sym`) |
|
|
119
|
+
|
|
120
|
+
### `Enumerable` (available on `Array`, `Hash`, and any `Enumerable`)
|
|
121
|
+
|
|
122
|
+
| Method | Description |
|
|
123
|
+
|---|---|
|
|
124
|
+
| `#clean` | Returns a copy with empty values removed |
|
|
125
|
+
| `#clean!` | Removes empty values in place (recursive) |
|
|
126
|
+
| `#map_and_find` | Maps and stops at the first truthy result |
|
|
127
|
+
| `#map_and_select` | Maps and returns only truthy results |
|
|
128
|
+
| `#map_to_hash` | Maps values, using original elements as keys |
|
|
129
|
+
|
|
130
|
+
### `Object` (available on every Ruby object)
|
|
131
|
+
|
|
132
|
+
| Method | Description |
|
|
133
|
+
|---|---|
|
|
134
|
+
| `#is_any?` | Returns `true` if the object is an instance of any of the given classes |
|
|
135
|
+
| `#trueful?` | Returns `true` only when the object is not `nil` (unlike `#present?`, `[]` and `{}` are trueful) |
|
|
136
|
+
|
|
137
|
+
### `Date`
|
|
138
|
+
|
|
139
|
+
| Method | Description |
|
|
140
|
+
|---|---|
|
|
141
|
+
| `#days_between` | Returns the absolute number of days between two dates |
|
|
142
|
+
|
|
143
|
+
### `Math`
|
|
144
|
+
|
|
145
|
+
| Method | Description |
|
|
146
|
+
|---|---|
|
|
147
|
+
| `.average` | Calculates the (optionally weighted) average of values |
|
|
148
|
+
|
|
149
|
+
### `Class`
|
|
150
|
+
|
|
151
|
+
| Method | Description |
|
|
152
|
+
|---|---|
|
|
153
|
+
| `.default_value` | Adds a reader that returns the same default instance every time |
|
|
154
|
+
| `.default_values` | Adds multiple readers sharing the same default instance |
|
|
155
|
+
| `.default_reader` | Adds a reader that returns a default value only when the instance variable was never set |
|
|
156
|
+
| `.default_readers` | Adds multiple default readers sharing the same default value |
|
|
157
|
+
|
|
158
|
+
---
|
|
159
|
+
|
|
160
|
+
## Code Examples
|
|
161
|
+
|
|
162
|
+
### Hash key transformations
|
|
163
|
+
|
|
164
|
+
```ruby
|
|
165
|
+
# Converting API responses from camelCase to snake_case
|
|
166
|
+
response = { 'userId' => 1, 'firstName' => 'Alice' }
|
|
167
|
+
response.underscore_keys # => { 'user_id' => 1, 'first_name' => 'Alice' }
|
|
168
|
+
|
|
169
|
+
# Preparing a payload for a camelCase API
|
|
170
|
+
params = { user_id: 1, first_name: 'Alice' }
|
|
171
|
+
params.lower_camelize_keys
|
|
172
|
+
# => { userId: 1, firstName: 'Alice' }
|
|
173
|
+
|
|
174
|
+
# Equivalent long form with explicit option
|
|
175
|
+
params.camelize_keys(uppercase_first_letter: false)
|
|
176
|
+
# => { userId: 1, firstName: 'Alice' }
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
### Fetching nested values safely
|
|
180
|
+
|
|
181
|
+
```ruby
|
|
182
|
+
config = { database: { primary: { host: 'localhost' } } }
|
|
183
|
+
|
|
184
|
+
config.chain_fetch(:database, :primary, :host) # => 'localhost'
|
|
185
|
+
config.chain_fetch(:database, :replica, :host) { |key, _rest| "default-#{key}" }
|
|
186
|
+
# => 'default-replica'
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
### Flattening and restoring deep hashes
|
|
190
|
+
|
|
191
|
+
```ruby
|
|
192
|
+
deep = { a: { b: [1, 2] } }
|
|
193
|
+
flat = deep.squash # => { 'a.b[0]' => 1, 'a.b[1]' => 2 }
|
|
194
|
+
flat.to_deep_hash # => { 'a' => { 'b' => [1, 2] } }
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
### Exclusive merge (update only existing keys)
|
|
198
|
+
|
|
199
|
+
```ruby
|
|
200
|
+
defaults = { timeout: 30, retries: 3 }
|
|
201
|
+
overrides = { retries: 5, unknown_key: 'ignored' }
|
|
202
|
+
|
|
203
|
+
defaults.exclusive_merge(overrides) # => { timeout: 30, retries: 5 }
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
### Array utilities
|
|
207
|
+
|
|
208
|
+
```ruby
|
|
209
|
+
# Zip an array with keys into a Hash
|
|
210
|
+
values = [10, 20, 30]
|
|
211
|
+
values.as_hash(%i[x y z]) # => { x: 10, y: 20, z: 30 }
|
|
212
|
+
|
|
213
|
+
# Chain map calls
|
|
214
|
+
[:hello, :world].chain_map(:to_s, :upcase) # => ['HELLO', 'WORLD']
|
|
215
|
+
|
|
216
|
+
# Fetch nested keys from an array of hashes
|
|
217
|
+
records = [{ user: { id: 1 } }, { user: { id: 2 } }]
|
|
218
|
+
records.mapk(:user, :id) # => [1, 2]
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
### Symbol utilities
|
|
222
|
+
|
|
223
|
+
```ruby
|
|
224
|
+
:my_method_name.camelize # => :MyMethodName
|
|
225
|
+
:my_method_name.camelize(:lower) # => :myMethodName
|
|
226
|
+
:MyMethodName.underscore # => :my_method_name
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
### Enumerable cleaning
|
|
230
|
+
|
|
231
|
+
```ruby
|
|
232
|
+
data = { name: 'Alice', nickname: nil, tags: [], meta: {} }
|
|
233
|
+
data.clean # => { name: 'Alice' }
|
|
234
|
+
|
|
235
|
+
mixed = [1, nil, '', [], {}, 'hello']
|
|
236
|
+
mixed.clean # => [1, 'hello']
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
### Object helpers
|
|
240
|
+
|
|
241
|
+
```ruby
|
|
242
|
+
value = 42
|
|
243
|
+
value.is_any?(String, Symbol) # => false
|
|
244
|
+
value.is_any?(String, Symbol, Integer) # => true
|
|
245
|
+
|
|
246
|
+
nil.trueful? # => false
|
|
247
|
+
[].trueful? # => true (unlike blank?/present?)
|
|
248
|
+
''.trueful? # => true
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
### Class default readers
|
|
252
|
+
|
|
253
|
+
```ruby
|
|
254
|
+
class Report
|
|
255
|
+
attr_writer :title
|
|
256
|
+
default_reader :title, 'Untitled Report'
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
r = Report.new
|
|
260
|
+
r.title # => 'Untitled Report'
|
|
261
|
+
r.title = 'Q1'
|
|
262
|
+
r.title # => 'Q1'
|
|
263
|
+
r.title = nil
|
|
264
|
+
r.title # => nil (nil is respected; differs from default_value)
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
### Math weighted average
|
|
268
|
+
|
|
269
|
+
```ruby
|
|
270
|
+
# Simple average
|
|
271
|
+
Math.average([10, 20, 30]) # => 20.0
|
|
272
|
+
|
|
273
|
+
# Weighted average (value => weight)
|
|
274
|
+
Math.average(10 => 1, 20 => 2, 30 => 1) # => 20.0
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
---
|
|
278
|
+
|
|
279
|
+
## Best Practices and Caveats
|
|
280
|
+
|
|
281
|
+
### Monkey-patching awareness
|
|
282
|
+
|
|
283
|
+
`core_ext` extends Ruby's built-in classes globally. This means:
|
|
284
|
+
|
|
285
|
+
- All objects of the extended classes gain the new methods **throughout your entire application**, including third-party gems.
|
|
286
|
+
- **Name collision risk**: if another gem or your application already defines a method with the same name on the same class, the last `require`/`load` wins. Review the method list above before adoption and check for conflicts.
|
|
287
|
+
- In libraries (gems) intended for wide reuse, prefer not requiring `core_ext` at the top level unless you own all consumers.
|
|
288
|
+
|
|
289
|
+
### Recursive options
|
|
290
|
+
|
|
291
|
+
Several `Hash` methods (`change_keys`, `camelize_keys`, `underscore_keys`, `change_values`, `clean!`) operate recursively by default, descending into nested arrays and hashes. Pass `recursive: false` when you only need shallow transformation to avoid unintended side-effects on nested structures.
|
|
292
|
+
|
|
293
|
+
### Bang (`!`) methods vs. non-bang methods
|
|
294
|
+
|
|
295
|
+
Methods ending with `!` mutate the receiver **in place**. Use them when you are sure you do not need the original structure. Use the non-bang variants to return a transformed copy.
|
|
296
|
+
|
|
297
|
+
### Versioning
|
|
298
|
+
|
|
299
|
+
Pin to a minor version to avoid unexpected breaking changes:
|
|
300
|
+
|
|
301
|
+
```ruby
|
|
302
|
+
gem 'darthjee-core_ext', '~> 3.0'
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
Check the [CHANGELOG / releases page](https://github.com/darthjee/core_ext/releases) before upgrading.
|
|
306
|
+
|
|
307
|
+
---
|
|
308
|
+
|
|
309
|
+
## Running the Test Suite / Contributing
|
|
310
|
+
|
|
311
|
+
If you are contributing to `core_ext` itself or want to verify its behavior locally:
|
|
312
|
+
|
|
313
|
+
```shell
|
|
314
|
+
# Install dependencies
|
|
315
|
+
bundle install
|
|
316
|
+
|
|
317
|
+
# Run the full test suite
|
|
318
|
+
bundle exec rspec
|
|
319
|
+
|
|
320
|
+
# Run a single spec file
|
|
321
|
+
bundle exec rspec spec/lib/darthjee/core_ext/hash_spec.rb
|
|
322
|
+
```
|
|
323
|
+
|
|
324
|
+
Tests live under `spec/` and mirror the structure of `lib/`. Every public method must have a corresponding spec. See the [README](https://github.com/darthjee/core_ext/blob/main/README.md) and the repository's Copilot instructions (`.github/copilot-instructions.md`) for more details.
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
# Using the Jace Gem
|
|
2
|
+
|
|
3
|
+
## What is Jace?
|
|
4
|
+
|
|
5
|
+
**Jace** is a Ruby gem for event-driven development **within a single application**.
|
|
6
|
+
It is not about distributed architecture or message queues — it is about building
|
|
7
|
+
internal event-oriented logic inside a Ruby gem or application.
|
|
8
|
+
|
|
9
|
+
With Jace, you register handlers for named events and trigger those events from
|
|
10
|
+
anywhere in your codebase. When an event is triggered, Jace executes the registered
|
|
11
|
+
`before` handlers (inside the context via `instance_eval`), then the main block,
|
|
12
|
+
then the `after` handlers (also inside the context via `instance_eval`).
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
## Installation
|
|
17
|
+
|
|
18
|
+
Add `jace` to your `Gemfile`:
|
|
19
|
+
|
|
20
|
+
```ruby
|
|
21
|
+
gem 'jace'
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Then run:
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
bundle install
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Or install it directly:
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
gem install jace
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
---
|
|
37
|
+
|
|
38
|
+
## Core Concepts
|
|
39
|
+
|
|
40
|
+
### `Jace::Registry`
|
|
41
|
+
|
|
42
|
+
The central object. It stores event-to-handler mappings and exposes two public
|
|
43
|
+
methods: `register` and `trigger`.
|
|
44
|
+
|
|
45
|
+
```ruby
|
|
46
|
+
registry = Jace::Registry.new
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
### `register(event, instant = :after, &block)`
|
|
50
|
+
|
|
51
|
+
Adds a handler block for a named event. The `instant` parameter controls whether
|
|
52
|
+
the block runs before or after the main event block.
|
|
53
|
+
|
|
54
|
+
| `instant` | When the handler runs |
|
|
55
|
+
|-----------|----------------------|
|
|
56
|
+
| `:after` (default) | After the main block |
|
|
57
|
+
| `:before` | Before the main block |
|
|
58
|
+
|
|
59
|
+
```ruby
|
|
60
|
+
registry.register(:payment_processed) { send_receipt }
|
|
61
|
+
registry.register(:payment_processed, :before) { validate_payment }
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
### `trigger(event, context, &block)`
|
|
65
|
+
|
|
66
|
+
Fires the named event. Jace runs the `before` handlers, then the given block,
|
|
67
|
+
then the `after` handlers. The `before` and `after` handlers are `instance_eval`'d
|
|
68
|
+
inside `context`, so bare method calls in handlers resolve against the context.
|
|
69
|
+
The main block is called normally (not `instance_eval`'d), so it uses the
|
|
70
|
+
surrounding scope's receiver.
|
|
71
|
+
|
|
72
|
+
```ruby
|
|
73
|
+
registry.trigger(:payment_processed, payment_object) do
|
|
74
|
+
charge_credit_card
|
|
75
|
+
end
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
---
|
|
79
|
+
|
|
80
|
+
## Basic Usage
|
|
81
|
+
|
|
82
|
+
```ruby
|
|
83
|
+
class Order
|
|
84
|
+
attr_reader :log
|
|
85
|
+
|
|
86
|
+
def initialize
|
|
87
|
+
@log = []
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def validate
|
|
91
|
+
log << 'validated'
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def persist
|
|
95
|
+
log << 'persisted'
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def notify
|
|
99
|
+
log << 'notified'
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
registry = Jace::Registry.new
|
|
104
|
+
order = Order.new
|
|
105
|
+
|
|
106
|
+
registry.register(:save, :before) { validate }
|
|
107
|
+
registry.register(:save) { notify }
|
|
108
|
+
|
|
109
|
+
registry.trigger(:save, order) do
|
|
110
|
+
order.persist # main block uses the surrounding scope, so explicit receiver is needed
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
order.log
|
|
114
|
+
# => ['validated', 'persisted', 'notified']
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
---
|
|
118
|
+
|
|
119
|
+
## Handler Types
|
|
120
|
+
|
|
121
|
+
Handlers are registered as **blocks** (procs) and are `instance_eval`'d inside
|
|
122
|
+
the context object when the event fires, so bare method calls resolve against
|
|
123
|
+
the context.
|
|
124
|
+
|
|
125
|
+
```ruby
|
|
126
|
+
registry.register(:shipment_sent) { send_confirmation_email }
|
|
127
|
+
registry.register(:shipment_sent, :before) { freeze_order }
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
---
|
|
131
|
+
|
|
132
|
+
## Multiple Handlers per Event
|
|
133
|
+
|
|
134
|
+
You can register as many `before` and `after` handlers as you like for the same
|
|
135
|
+
event. They execute in registration order.
|
|
136
|
+
|
|
137
|
+
```ruby
|
|
138
|
+
registry.register(:user_created, :before) { sanitize_input }
|
|
139
|
+
registry.register(:user_created, :before) { check_duplicates }
|
|
140
|
+
registry.register(:user_created) { send_welcome_email }
|
|
141
|
+
registry.register(:user_created) { notify_admin }
|
|
142
|
+
|
|
143
|
+
registry.trigger(:user_created, user_context) do
|
|
144
|
+
persist_user
|
|
145
|
+
end
|
|
146
|
+
# Order: sanitize_input → check_duplicates → persist_user
|
|
147
|
+
# → send_welcome_email → notify_admin
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
---
|
|
151
|
+
|
|
152
|
+
## Triggering Events Without a Main Block
|
|
153
|
+
|
|
154
|
+
The main block is optional. If omitted, only the registered handlers run:
|
|
155
|
+
|
|
156
|
+
```ruby
|
|
157
|
+
registry.trigger(:cache_invalidated, cache_context)
|
|
158
|
+
# Runs all :before handlers, then all :after handlers; no main block
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
---
|
|
162
|
+
|
|
163
|
+
## Triggering an Unregistered Event
|
|
164
|
+
|
|
165
|
+
Triggering an event that has no registered handlers is safe. The main block is
|
|
166
|
+
still executed, and no error is raised:
|
|
167
|
+
|
|
168
|
+
```ruby
|
|
169
|
+
registry.trigger(:unknown_event, some_context) do
|
|
170
|
+
do_work
|
|
171
|
+
end
|
|
172
|
+
# do_work runs normally; no handlers fire
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
---
|
|
176
|
+
|
|
177
|
+
## Typical Integration Pattern
|
|
178
|
+
|
|
179
|
+
The most common pattern is to hold a `Jace::Registry` instance inside a service
|
|
180
|
+
or module and expose `register` to callers so they can hook into lifecycle events:
|
|
181
|
+
|
|
182
|
+
```ruby
|
|
183
|
+
module PaymentService
|
|
184
|
+
REGISTRY = Jace::Registry.new
|
|
185
|
+
|
|
186
|
+
def self.on(event, instant = :after, &block)
|
|
187
|
+
REGISTRY.register(event, instant, &block)
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
def self.process(payment)
|
|
191
|
+
REGISTRY.trigger(:payment_processed, payment) do
|
|
192
|
+
payment.charge!
|
|
193
|
+
end
|
|
194
|
+
end
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
# In an initializer or plugin:
|
|
198
|
+
PaymentService.on(:payment_processed) { send_receipt }
|
|
199
|
+
PaymentService.on(:payment_processed, :before) { log_attempt }
|
|
200
|
+
|
|
201
|
+
# Elsewhere in the application:
|
|
202
|
+
PaymentService.process(payment)
|
|
203
|
+
# Runs: log_attempt → payment.charge! → send_receipt
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
---
|
|
207
|
+
|
|
208
|
+
## Execution Model (summary)
|
|
209
|
+
|
|
210
|
+
```
|
|
211
|
+
registry.trigger(:event, context) { main_block }
|
|
212
|
+
|
|
213
|
+
Execution order:
|
|
214
|
+
1. All :before handlers (in registration order, instance_eval'd in context)
|
|
215
|
+
2. main_block (called as-is, context is NOT the receiver)
|
|
216
|
+
3. All :after handlers (in registration order, instance_eval'd in context)
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
> **Note:** The `before` and `after` handlers are `instance_eval`'d inside the
|
|
220
|
+
> context object, so bare method calls inside them (`send_receipt`, `validate`,
|
|
221
|
+
> etc.) resolve against the context. The main block, however, is a regular
|
|
222
|
+
> `call` (not `instance_eval`), so its receiver is the surrounding scope.
|
|
223
|
+
|
|
224
|
+
---
|
|
225
|
+
|
|
226
|
+
## API Reference
|
|
227
|
+
|
|
228
|
+
| Method | Signature | Description |
|
|
229
|
+
|--------|-----------|-------------|
|
|
230
|
+
| `Registry#register` | `(event, instant = :after, &block)` | Adds a handler to the named event |
|
|
231
|
+
| `Registry#trigger` | `(event, context, &block)` | Fires the event, running handlers around the block |
|
|
232
|
+
| `Registry#events` | `()` | Returns all registered event names as `Array<Symbol>` |
|
|
233
|
+
| `Registry#registry` | `()` | Returns the raw `Hash` of event → `Dispatcher` mappings |
|
|
234
|
+
|
|
235
|
+
---
|
|
236
|
+
|
|
237
|
+
## YARD Documentation
|
|
238
|
+
|
|
239
|
+
Full API docs: [https://www.rubydoc.info/gems/jace](https://www.rubydoc.info/gems/jace)
|
|
240
|
+
|
|
241
|
+
Source: [https://github.com/darthjee/jace](https://github.com/darthjee/jace)
|
data/.rubocop.yml
CHANGED
|
@@ -1,4 +1,9 @@
|
|
|
1
|
-
|
|
1
|
+
plugins:
|
|
2
|
+
- rubocop-rspec
|
|
3
|
+
- rubocop-rake
|
|
4
|
+
- rubocop-factory_bot
|
|
5
|
+
- rubocop-rails
|
|
6
|
+
- rubocop-rspec_rails
|
|
2
7
|
inherit_from: .rubocop_todo.yml
|
|
3
8
|
|
|
4
9
|
AllCops:
|
|
@@ -49,3 +54,11 @@ Lint/EmptyBlock:
|
|
|
49
54
|
|
|
50
55
|
Lint/EmptyClass:
|
|
51
56
|
Enabled: false
|
|
57
|
+
|
|
58
|
+
Style/OneClassPerFile:
|
|
59
|
+
Exclude:
|
|
60
|
+
- 'spec/support/matchers/add_method.rb'
|
|
61
|
+
|
|
62
|
+
FactoryBot/ExcessiveCreateList:
|
|
63
|
+
Exclude:
|
|
64
|
+
- 'spec/integration/yard/controllers/paginated_documents_controller_spec.rb'
|
data/.rubocop_todo.yml
CHANGED
|
@@ -1,21 +1,7 @@
|
|
|
1
1
|
# This configuration was generated by
|
|
2
2
|
# `rubocop --auto-gen-config`
|
|
3
|
-
# on
|
|
3
|
+
# on 2026-03-12 10:44:24 UTC using RuboCop version 1.85.1.
|
|
4
4
|
# The point is for the user to remove these configuration records
|
|
5
5
|
# one by one as the offenses are removed from the code base.
|
|
6
6
|
# Note that changes in the inspected code, or installation of new
|
|
7
7
|
# versions of RuboCop, may require this file to be generated again.
|
|
8
|
-
|
|
9
|
-
# Offense count: 8
|
|
10
|
-
Style/Documentation:
|
|
11
|
-
Exclude:
|
|
12
|
-
- 'spec/**/*'
|
|
13
|
-
- 'test/**/*'
|
|
14
|
-
- 'lib/azeroth.rb'
|
|
15
|
-
- 'lib/azeroth/model.rb'
|
|
16
|
-
- 'lib/azeroth/options.rb'
|
|
17
|
-
- 'lib/azeroth/resource_builder.rb'
|
|
18
|
-
- 'lib/azeroth/resource_route_builder.rb'
|
|
19
|
-
- 'lib/azeroth/resourceable.rb'
|
|
20
|
-
- 'lib/azeroth/resourceable/builder.rb'
|
|
21
|
-
- 'lib/azeroth/routes_builder.rb'
|
data/Dockerfile
CHANGED
data/Gemfile
CHANGED
|
@@ -6,30 +6,34 @@ gemspec
|
|
|
6
6
|
|
|
7
7
|
gem 'actionpack', '7.2.2.1'
|
|
8
8
|
gem 'activerecord', '7.2.2.1'
|
|
9
|
-
gem 'bundler', '
|
|
10
|
-
gem 'factory_bot', '6.
|
|
11
|
-
gem '
|
|
12
|
-
gem 'nokogiri', '1.18.8'
|
|
9
|
+
gem 'bundler', '>= 2.5.13'
|
|
10
|
+
gem 'factory_bot', '6.5.6'
|
|
11
|
+
gem 'nokogiri', '1.19.1'
|
|
13
12
|
gem 'pry', '0.14.2'
|
|
14
13
|
gem 'pry-nav', '1.0.0'
|
|
15
14
|
gem 'rails', '7.2.2.1'
|
|
16
15
|
gem 'rails-controller-testing', '1.0.5'
|
|
17
16
|
gem 'rake', '13.2.1'
|
|
18
|
-
gem 'reek', '6.
|
|
19
|
-
gem 'rspec', '3.13.
|
|
17
|
+
gem 'reek', '6.5.0'
|
|
18
|
+
gem 'rspec', '3.13.2'
|
|
20
19
|
gem 'rspec-collection_matchers', '1.2.1'
|
|
21
|
-
gem 'rspec-core', '3.13.
|
|
22
|
-
gem 'rspec-expectations', '3.13.
|
|
23
|
-
gem 'rspec-mocks', '3.13.
|
|
24
|
-
gem 'rspec-rails', '8.0.
|
|
25
|
-
gem 'rspec-support', '3.13.
|
|
26
|
-
gem 'rubocop',
|
|
27
|
-
gem 'rubocop-
|
|
28
|
-
gem '
|
|
29
|
-
gem '
|
|
20
|
+
gem 'rspec-core', '3.13.6'
|
|
21
|
+
gem 'rspec-expectations', '3.13.5'
|
|
22
|
+
gem 'rspec-mocks', '3.13.8'
|
|
23
|
+
gem 'rspec-rails', '8.0.3'
|
|
24
|
+
gem 'rspec-support', '3.13.7'
|
|
25
|
+
gem 'rubocop-factory_bot', '2.28.0'
|
|
26
|
+
gem 'rubocop-rails', '2.34.3'
|
|
27
|
+
gem 'rubocop-rake', '0.7.1'
|
|
28
|
+
gem 'rubocop-rspec', '3.9.0'
|
|
29
|
+
gem 'rubocop-rspec_rails', '2.32.0'
|
|
30
|
+
gem 'rubycritic', '5.0.0'
|
|
31
|
+
gem 'shoulda-matchers', '7.0.1'
|
|
30
32
|
gem 'simplecov', '0.22.0'
|
|
33
|
+
gem 'simplecov-html', '0.13.2'
|
|
34
|
+
gem 'simplecov-lcov', '0.9.0'
|
|
31
35
|
gem 'sprockets-rails', '3.5.2'
|
|
32
36
|
gem 'sqlite3', '1.4.2'
|
|
33
37
|
gem 'tzinfo-data', '~> 1.2025.2'
|
|
34
|
-
gem 'yard', '0.9.
|
|
38
|
+
gem 'yard', '0.9.38'
|
|
35
39
|
gem 'yardstick', '0.9.9'
|