dexkit 0.4.1 → 0.6.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/CHANGELOG.md +61 -0
- data/README.md +66 -59
- data/guides/llm/EVENT.md +64 -0
- data/guides/llm/OPERATION.md +13 -4
- data/guides/llm/QUERY.md +2 -2
- data/lib/dex/event/handler.rb +20 -1
- data/lib/dex/event.rb +0 -2
- data/lib/dex/executable.rb +44 -0
- data/lib/dex/operation/async_proxy.rb +1 -1
- data/lib/dex/operation/transaction_adapter.rb +17 -10
- data/lib/dex/operation/transaction_wrapper.rb +61 -9
- data/lib/dex/operation.rb +1 -35
- data/lib/dex/pipeline.rb +58 -0
- data/lib/dex/version.rb +1 -1
- data/lib/dexkit.rb +3 -0
- metadata +36 -6
- data/lib/dex/operation/pipeline.rb +0 -60
- /data/lib/dex/{operation/settings.rb → settings.rb} +0 -0
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: f8d7274727e727937b55704faa81e4da8a54bb3af7758b11e9144e59fb2dba14
|
|
4
|
+
data.tar.gz: cb899ee88b5e83ec57a913f0173036ac228f672c7850d88e95209f4804218f96
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 7758fe2c0d5b9cdd2bcf62aa510c4146ef29898335ddffb118abb35409367d5f7cbd1aeba6365f865bb36d4011b1030f136cfb9b6eee27fb3aa3d06d11eeb21e
|
|
7
|
+
data.tar.gz: e437a4af9b4aa0c121245de966775cd3be0bad5ceb5918f139cfaf059545159631797b630a9b21f3add8855308c832f5846efb78f97f36141042f40d28b32341
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,66 @@
|
|
|
1
1
|
## [Unreleased]
|
|
2
2
|
|
|
3
|
+
## [0.6.0] - 2026-03-07
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- **Handler callbacks** — `Dex::Event::Handler` now supports `before`, `after`, and `around` callbacks, same DSL as operations
|
|
8
|
+
- **Handler transactions** — `Dex::Event::Handler` supports `transaction` and `after_commit` DSL. Transactions are disabled by default on handlers (opt in with `transaction`)
|
|
9
|
+
- **Handler pipeline** — `Dex::Event::Handler` supports `use` for adding custom wrapper modules, same as operations
|
|
10
|
+
- **`Dex::Executable`** — shared execution skeleton (Settings, Pipeline, `use` DSL) extracted from Operation and used by both Operation and Handler
|
|
11
|
+
|
|
12
|
+
### Breaking
|
|
13
|
+
|
|
14
|
+
- **`transaction false` fully opts out** — operations with `transaction false` no longer route `after_commit` through the database adapter. Previously, `after_commit` on a non-transactional operation would still detect and defer to ambient database transactions (e.g., `ActiveRecord::Base.transaction { op.call }`); now it fires callbacks directly after the pipeline completes. To restore ambient transaction awareness, remove `transaction false` or use `transaction` (enabled)
|
|
15
|
+
|
|
16
|
+
## 0.5.0 - 2026-03-05
|
|
17
|
+
|
|
18
|
+
### Added
|
|
19
|
+
|
|
20
|
+
- **Verified Mongoid support** — operations (transactions, recording, async) and queries now have dedicated Mongoid test coverage running against a MongoDB replica set
|
|
21
|
+
- **CI workflow for Mongoid** — GitHub Actions matrix includes a MongoDB replica-set job that runs Mongoid-specific tests
|
|
22
|
+
|
|
23
|
+
### Breaking
|
|
24
|
+
|
|
25
|
+
- **`after_commit` now always defers** — non-transactional operations queue callbacks in memory and flush them after the operation pipeline succeeds, matching the behavior of transactional operations. Previously, `after_commit` fired immediately inline when no transaction was active — code that relied on immediate execution (e.g., reading side effects of the callback later in `perform`) must account for the new deferred timing. Callbacks are discarded on `error!` or exception. Nested operations flush once at the outermost successful boundary. Ambient database transactions (e.g., `ActiveRecord::Base.transaction { op.call }`) are still respected via the adapter.
|
|
26
|
+
|
|
27
|
+
### Fixed
|
|
28
|
+
|
|
29
|
+
- **Mongoid async recording** — `record_id` is now serialized with `.to_s` so BSON::ObjectId values pass through ActiveJob correctly
|
|
30
|
+
- **Mongoid transaction adapter** — simplified nesting logic; the outermost `Mongoid.transaction` block now reliably owns callback flushing, fixing edge cases where nested rollbacks could leak callbacks
|
|
31
|
+
|
|
32
|
+
## [0.4.1] - 2026-03-04
|
|
33
|
+
|
|
34
|
+
### Added
|
|
35
|
+
|
|
36
|
+
- **`after_commit` in operations** — `after_commit { ... }` defers a block until the surrounding database transaction commits
|
|
37
|
+
- ActiveRecord adapter uses `ActiveRecord.after_all_transactions_commit` (requires Rails 7.2+)
|
|
38
|
+
- Mongoid adapter manually tracks callbacks and fires them after the outermost `Mongoid.transaction` commits
|
|
39
|
+
- If called outside a transaction, the block executes immediately
|
|
40
|
+
|
|
41
|
+
## [0.4.0] - 2026-03-04
|
|
42
|
+
|
|
43
|
+
### Added
|
|
44
|
+
|
|
45
|
+
- **Query objects** — `Dex::Query` base class for encapsulating database queries with filtering, sorting, and parameter binding
|
|
46
|
+
- Typed properties via `prop`/`prop?` DSL (same as Operation and Event)
|
|
47
|
+
- `scope { Model.all }` DSL for declaring the base scope
|
|
48
|
+
- **Filter DSL** — `filter :name, :strategy` with built-in strategies: `eq`, `not_eq`, `contains`, `starts_with`, `ends_with`, `gt`, `gte`, `lt`, `lte`, `in`, `not_in`
|
|
49
|
+
- Custom filter blocks: `filter(:name) { |scope, value| ... }`
|
|
50
|
+
- Optional filters (nilable props) are automatically skipped when `nil`
|
|
51
|
+
- **Sort DSL** — `sort :col1, :col2` for column sorts with automatic `asc`/`desc` via `"-col"` prefix convention
|
|
52
|
+
- Custom sort blocks: `sort(:name) { |scope| ... }`
|
|
53
|
+
- Default sort: `sort :name, default: "-created_at"`
|
|
54
|
+
- **Backend adapters** for both ActiveRecord and Mongoid (auto-detected from scope)
|
|
55
|
+
- `scope:` injection for pre-scoping (e.g., `current_user.posts`)
|
|
56
|
+
- `from_params` for binding from controller params with automatic type coercion and sort validation
|
|
57
|
+
- `to_params` for round-tripping query state back to URL params
|
|
58
|
+
- `param_key` DSL for customizing the params namespace
|
|
59
|
+
- ActiveModel::Naming / ActiveModel::Conversion for Rails form compatibility
|
|
60
|
+
- Convenience class methods: `.call`, `.count`, `.exists?`, `.any?`
|
|
61
|
+
- Inheritance support — filters, sorts, and scope are inherited by subclasses
|
|
62
|
+
- `Dex::Match` is now included in `Dex::Form` — `Ok`/`Err` are available without prefix inside forms
|
|
63
|
+
|
|
3
64
|
## [0.3.0] - 2026-03-03
|
|
4
65
|
|
|
5
66
|
### Added
|
data/README.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
#
|
|
1
|
+
# dexkit
|
|
2
2
|
|
|
3
3
|
Rails patterns toolbelt. Equip to gain +4 DEX.
|
|
4
4
|
|
|
@@ -9,26 +9,28 @@ Rails patterns toolbelt. Equip to gain +4 DEX.
|
|
|
9
9
|
Service objects with typed properties, transactions, error handling, and more.
|
|
10
10
|
|
|
11
11
|
```ruby
|
|
12
|
-
class
|
|
13
|
-
prop :
|
|
14
|
-
prop :
|
|
12
|
+
class Order::Place < Dex::Operation
|
|
13
|
+
prop :customer, _Ref(Customer)
|
|
14
|
+
prop :product, _Ref(Product)
|
|
15
|
+
prop :quantity, _Integer(1..)
|
|
16
|
+
prop? :note, String
|
|
15
17
|
|
|
16
|
-
success _Ref(
|
|
17
|
-
error
|
|
18
|
+
success _Ref(Order)
|
|
19
|
+
error :out_of_stock
|
|
18
20
|
|
|
19
21
|
def perform
|
|
20
|
-
error!(:
|
|
22
|
+
error!(:out_of_stock) unless product.in_stock?
|
|
21
23
|
|
|
22
|
-
|
|
24
|
+
order = Order.create!(customer: customer, product: product, quantity: quantity, note: note)
|
|
23
25
|
|
|
24
|
-
after_commit {
|
|
26
|
+
after_commit { Order::Placed.publish(order_id: order.id, total: order.total) }
|
|
25
27
|
|
|
26
|
-
|
|
28
|
+
order
|
|
27
29
|
end
|
|
28
30
|
end
|
|
29
31
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
+
order = Order::Place.call(customer: 42, product: 7, quantity: 2)
|
|
33
|
+
order.id # => 1
|
|
32
34
|
```
|
|
33
35
|
|
|
34
36
|
### What you get out of the box
|
|
@@ -36,35 +38,35 @@ user.name # => "Alice"
|
|
|
36
38
|
**Typed properties** – powered by [literal](https://github.com/joeldrapper/literal). Plain classes, ranges, unions, arrays, nilable, and model references with auto-find:
|
|
37
39
|
|
|
38
40
|
```ruby
|
|
39
|
-
prop :
|
|
41
|
+
prop :quantity, _Integer(1..)
|
|
40
42
|
prop :currency, _Union("USD", "EUR", "GBP")
|
|
41
|
-
prop :
|
|
42
|
-
prop? :note,
|
|
43
|
+
prop :customer, _Ref(Customer) # accepts Customer instance or ID
|
|
44
|
+
prop? :note, String # optional (nil by default)
|
|
43
45
|
```
|
|
44
46
|
|
|
45
47
|
**Structured errors** with `error!`, `assert!`, and `rescue_from`:
|
|
46
48
|
|
|
47
49
|
```ruby
|
|
48
|
-
|
|
50
|
+
product = assert!(:not_found) { Product.find_by(id: product_id) }
|
|
49
51
|
|
|
50
|
-
rescue_from Stripe::CardError, as: :
|
|
52
|
+
rescue_from Stripe::CardError, as: :payment_declined
|
|
51
53
|
```
|
|
52
54
|
|
|
53
55
|
**Ok / Err** – pattern match on operation outcomes with `.safe.call`:
|
|
54
56
|
|
|
55
57
|
```ruby
|
|
56
|
-
case
|
|
57
|
-
in Ok
|
|
58
|
-
|
|
59
|
-
in Err(code: :
|
|
60
|
-
|
|
58
|
+
case Order::Place.new(customer: 42, product: 7, quantity: 2).safe.call
|
|
59
|
+
in Ok => result
|
|
60
|
+
redirect_to order_path(result.id)
|
|
61
|
+
in Err(code: :out_of_stock)
|
|
62
|
+
flash[:error] = "Product is out of stock"
|
|
61
63
|
end
|
|
62
64
|
```
|
|
63
65
|
|
|
64
66
|
**Async execution** via ActiveJob:
|
|
65
67
|
|
|
66
68
|
```ruby
|
|
67
|
-
|
|
69
|
+
Order::Fulfill.new(order_id: 123).async(queue: "fulfillment").call
|
|
68
70
|
```
|
|
69
71
|
|
|
70
72
|
**Transactions** on by default, **advisory locking**, **recording** to database, **callbacks**, and a customizable **pipeline** – all composable, all optional.
|
|
@@ -74,15 +76,16 @@ SendWelcomeEmail.new(user_id: 123).async(queue: "mailers").call
|
|
|
74
76
|
First-class test helpers for Minitest:
|
|
75
77
|
|
|
76
78
|
```ruby
|
|
77
|
-
class
|
|
78
|
-
testing
|
|
79
|
+
class PlaceOrderTest < Minitest::Test
|
|
80
|
+
testing Order::Place
|
|
79
81
|
|
|
80
|
-
def
|
|
81
|
-
assert_operation(
|
|
82
|
+
def test_places_order
|
|
83
|
+
assert_operation(customer: customer.id, product: product.id, quantity: 2)
|
|
82
84
|
end
|
|
83
85
|
|
|
84
|
-
def
|
|
85
|
-
assert_operation_error(:
|
|
86
|
+
def test_rejects_out_of_stock
|
|
87
|
+
assert_operation_error(:out_of_stock, customer: customer.id,
|
|
88
|
+
product: out_of_stock_product.id, quantity: 1)
|
|
86
89
|
end
|
|
87
90
|
end
|
|
88
91
|
```
|
|
@@ -92,14 +95,14 @@ end
|
|
|
92
95
|
Typed, immutable event objects with publish/subscribe, async dispatch, and causality tracing.
|
|
93
96
|
|
|
94
97
|
```ruby
|
|
95
|
-
class
|
|
98
|
+
class Order::Placed < Dex::Event
|
|
96
99
|
prop :order_id, Integer
|
|
97
100
|
prop :total, BigDecimal
|
|
98
101
|
prop? :coupon_code, String
|
|
99
102
|
end
|
|
100
103
|
|
|
101
104
|
class NotifyWarehouse < Dex::Event::Handler
|
|
102
|
-
on
|
|
105
|
+
on Order::Placed
|
|
103
106
|
retries 3
|
|
104
107
|
|
|
105
108
|
def perform
|
|
@@ -107,7 +110,7 @@ class NotifyWarehouse < Dex::Event::Handler
|
|
|
107
110
|
end
|
|
108
111
|
end
|
|
109
112
|
|
|
110
|
-
|
|
113
|
+
Order::Placed.publish(order_id: 1, total: 99.99)
|
|
111
114
|
```
|
|
112
115
|
|
|
113
116
|
### What you get out of the box
|
|
@@ -120,22 +123,26 @@ OrderPlaced.publish(order_id: 1, total: 99.99)
|
|
|
120
123
|
|
|
121
124
|
```ruby
|
|
122
125
|
order_placed.trace do
|
|
123
|
-
|
|
126
|
+
Shipment::Reserved.publish(order_id: 1)
|
|
124
127
|
end
|
|
125
128
|
```
|
|
126
129
|
|
|
130
|
+
**Callbacks** — `before`, `after`, `around` hooks on handlers, same DSL as operations.
|
|
131
|
+
|
|
132
|
+
**Transactions** — opt-in `transaction` and `after_commit` for handlers that write to the database.
|
|
133
|
+
|
|
127
134
|
**Suppression**, optional **persistence**, **context capture**, and **retries** with exponential backoff.
|
|
128
135
|
|
|
129
136
|
### Testing
|
|
130
137
|
|
|
131
138
|
```ruby
|
|
132
|
-
class
|
|
139
|
+
class PlaceOrderTest < Minitest::Test
|
|
133
140
|
include Dex::Event::TestHelpers
|
|
134
141
|
|
|
135
142
|
def test_publishes_order_placed
|
|
136
143
|
capture_events do
|
|
137
|
-
|
|
138
|
-
assert_event_published(
|
|
144
|
+
Order::Place.call(customer: customer.id, product: product.id, quantity: 2)
|
|
145
|
+
assert_event_published(Order::Placed)
|
|
139
146
|
end
|
|
140
147
|
end
|
|
141
148
|
end
|
|
@@ -146,8 +153,8 @@ end
|
|
|
146
153
|
Form objects with typed attributes, normalization, nested forms, and Rails form builder compatibility.
|
|
147
154
|
|
|
148
155
|
```ruby
|
|
149
|
-
class
|
|
150
|
-
model
|
|
156
|
+
class Employee::Form < Dex::Form
|
|
157
|
+
model Employee
|
|
151
158
|
|
|
152
159
|
attribute :first_name, :string
|
|
153
160
|
attribute :last_name, :string
|
|
@@ -165,7 +172,7 @@ class OnboardingForm < Dex::Form
|
|
|
165
172
|
end
|
|
166
173
|
end
|
|
167
174
|
|
|
168
|
-
form =
|
|
175
|
+
form = Employee::Form.new(email: " ALICE@EXAMPLE.COM ", first_name: "Alice", last_name: "Smith")
|
|
169
176
|
form.email # => "alice@example.com"
|
|
170
177
|
form.valid?
|
|
171
178
|
```
|
|
@@ -177,10 +184,10 @@ form.valid?
|
|
|
177
184
|
**Nested forms** — `nested_one` and `nested_many` with automatic Hash coercion, `_destroy` support, and error propagation:
|
|
178
185
|
|
|
179
186
|
```ruby
|
|
180
|
-
nested_many :
|
|
181
|
-
attribute :
|
|
182
|
-
attribute :
|
|
183
|
-
validates :
|
|
187
|
+
nested_many :emergency_contacts do
|
|
188
|
+
attribute :name, :string
|
|
189
|
+
attribute :phone, :string
|
|
190
|
+
validates :name, :phone, presence: true
|
|
184
191
|
end
|
|
185
192
|
```
|
|
186
193
|
|
|
@@ -188,7 +195,7 @@ end
|
|
|
188
195
|
|
|
189
196
|
**Uniqueness validation** against the database, with scope, case-sensitivity, and current-record exclusion.
|
|
190
197
|
|
|
191
|
-
**Multi-model forms** — when a form spans
|
|
198
|
+
**Multi-model forms** — when a form spans Employee, Department, and Address, define a `.for` convention method to map records and a `#save` method that delegates to a `Dex::Operation`:
|
|
192
199
|
|
|
193
200
|
```ruby
|
|
194
201
|
def save
|
|
@@ -203,24 +210,24 @@ end
|
|
|
203
210
|
|
|
204
211
|
## Queries
|
|
205
212
|
|
|
206
|
-
Declarative query objects for filtering and sorting ActiveRecord relations.
|
|
213
|
+
Declarative query objects for filtering and sorting ActiveRecord relations and Mongoid criteria.
|
|
207
214
|
|
|
208
215
|
```ruby
|
|
209
|
-
class
|
|
210
|
-
scope {
|
|
216
|
+
class Order::Query < Dex::Query
|
|
217
|
+
scope { Order.all }
|
|
211
218
|
|
|
212
|
-
prop? :
|
|
213
|
-
prop? :
|
|
214
|
-
prop? :
|
|
219
|
+
prop? :status, String
|
|
220
|
+
prop? :customer, _Ref(Customer)
|
|
221
|
+
prop? :total_min, Integer
|
|
215
222
|
|
|
216
|
-
filter :
|
|
217
|
-
filter :
|
|
218
|
-
filter :
|
|
223
|
+
filter :status
|
|
224
|
+
filter :customer
|
|
225
|
+
filter :total_min, :gte, column: :total
|
|
219
226
|
|
|
220
|
-
sort :
|
|
227
|
+
sort :created_at, :total, default: "-created_at"
|
|
221
228
|
end
|
|
222
229
|
|
|
223
|
-
|
|
230
|
+
orders = Order::Query.call(status: "pending", sort: "-total")
|
|
224
231
|
```
|
|
225
232
|
|
|
226
233
|
### What you get out of the box
|
|
@@ -232,10 +239,10 @@ users = UserSearch.call(name: "ali", role: %w[admin], sort: "name")
|
|
|
232
239
|
**`from_params`** — HTTP boundary handling with automatic coercion, blank stripping, and invalid value fallback:
|
|
233
240
|
|
|
234
241
|
```ruby
|
|
235
|
-
class
|
|
242
|
+
class OrdersController < ApplicationController
|
|
236
243
|
def index
|
|
237
|
-
query =
|
|
238
|
-
@
|
|
244
|
+
query = Order::Query.from_params(params, scope: policy_scope(Order))
|
|
245
|
+
@orders = pagy(query.resolve)
|
|
239
246
|
end
|
|
240
247
|
end
|
|
241
248
|
```
|
|
@@ -256,7 +263,7 @@ Full documentation at **[dex.razorjack.net](https://dex.razorjack.net)**.
|
|
|
256
263
|
|
|
257
264
|
## AI Coding Assistant Setup
|
|
258
265
|
|
|
259
|
-
|
|
266
|
+
dexkit ships LLM-optimized guides. Copy them into your project so AI agents automatically know the API:
|
|
260
267
|
|
|
261
268
|
```bash
|
|
262
269
|
cp $(bundle show dexkit)/guides/llm/OPERATION.md app/operations/CLAUDE.md
|
data/guides/llm/EVENT.md
CHANGED
|
@@ -111,6 +111,70 @@ end
|
|
|
111
111
|
|
|
112
112
|
When retries exhausted, exception propagates normally.
|
|
113
113
|
|
|
114
|
+
### Callbacks
|
|
115
|
+
|
|
116
|
+
Same `before`/`after`/`around` DSL as operations:
|
|
117
|
+
|
|
118
|
+
```ruby
|
|
119
|
+
class ProcessPayment < Dex::Event::Handler
|
|
120
|
+
on PaymentReceived
|
|
121
|
+
|
|
122
|
+
before :log_start
|
|
123
|
+
after :log_end
|
|
124
|
+
|
|
125
|
+
around ->(cont) {
|
|
126
|
+
Instrumentation.measure("payment") { cont.call }
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
def perform
|
|
130
|
+
PaymentGateway.charge(event.amount)
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
private
|
|
134
|
+
|
|
135
|
+
def log_start = Rails.logger.info("Processing payment...")
|
|
136
|
+
def log_end = Rails.logger.info("Payment processed")
|
|
137
|
+
end
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
Callbacks are inherited. Child handlers run parent callbacks first.
|
|
141
|
+
|
|
142
|
+
### Transactions
|
|
143
|
+
|
|
144
|
+
Handlers can opt into database transactions and deferred `after_commit`:
|
|
145
|
+
|
|
146
|
+
```ruby
|
|
147
|
+
class FulfillOrder < Dex::Event::Handler
|
|
148
|
+
on OrderPlaced
|
|
149
|
+
transaction
|
|
150
|
+
|
|
151
|
+
def perform
|
|
152
|
+
order = Order.find(event.order_id)
|
|
153
|
+
order.update!(status: "fulfilled")
|
|
154
|
+
|
|
155
|
+
after_commit { Shipment::Ship.new(order_id: order.id).async.call }
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
Transactions are **disabled by default** on handlers (unlike operations). Opt in with `transaction`. The `after_commit` block defers until the transaction commits; on exception, deferred blocks are discarded.
|
|
161
|
+
|
|
162
|
+
### Custom Pipeline
|
|
163
|
+
|
|
164
|
+
Handlers support the same `use` DSL as operations for adding custom wrappers:
|
|
165
|
+
|
|
166
|
+
```ruby
|
|
167
|
+
class Monitored < Dex::Event::Handler
|
|
168
|
+
use MetricsWrapper, as: :metrics
|
|
169
|
+
|
|
170
|
+
def perform
|
|
171
|
+
# ...
|
|
172
|
+
end
|
|
173
|
+
end
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
Default handler pipeline: `[:transaction, :callback]`.
|
|
177
|
+
|
|
114
178
|
---
|
|
115
179
|
|
|
116
180
|
## Tracing (Causality)
|
data/guides/llm/OPERATION.md
CHANGED
|
@@ -273,11 +273,18 @@ def perform
|
|
|
273
273
|
end
|
|
274
274
|
```
|
|
275
275
|
|
|
276
|
-
|
|
276
|
+
Callbacks are always deferred — they run after the outermost operation boundary succeeds:
|
|
277
277
|
|
|
278
|
-
|
|
278
|
+
- **Transactional operations:** deferred until the DB transaction commits.
|
|
279
|
+
- **Non-transactional operations:** queued in memory, flushed after the operation pipeline completes successfully.
|
|
280
|
+
- **Nested operations:** callbacks queue up and flush once at the outermost successful boundary.
|
|
281
|
+
- **On error (`error!` or exception):** queued callbacks are discarded.
|
|
279
282
|
|
|
280
|
-
|
|
283
|
+
Multiple blocks run in registration order.
|
|
284
|
+
|
|
285
|
+
**ActiveRecord:** requires Rails 7.2+ (`after_all_transactions_commit`).
|
|
286
|
+
|
|
287
|
+
**Mongoid:** deferred across nested Dex operations. Ambient `Mongoid.transaction` blocks opened outside Dex are not detected — callbacks will fire immediately in that case.
|
|
281
288
|
|
|
282
289
|
---
|
|
283
290
|
|
|
@@ -340,7 +347,7 @@ record params: false # response only
|
|
|
340
347
|
|
|
341
348
|
Recording happens inside the transaction — rolled back on `error!`/`assert!`. Missing columns silently skipped.
|
|
342
349
|
|
|
343
|
-
When both async and recording are enabled,
|
|
350
|
+
When both async and recording are enabled, dexkit automatically stores only the record ID in the job payload instead of full params. The record tracks `status` (pending → running → done/failed) and `error` (code or exception class name).
|
|
344
351
|
|
|
345
352
|
---
|
|
346
353
|
|
|
@@ -371,6 +378,8 @@ end
|
|
|
371
378
|
|
|
372
379
|
Not autoloaded — stays out of production. TestLog and stubs are auto-cleared in `setup`.
|
|
373
380
|
|
|
381
|
+
For Mongoid-backed operation tests, run against a MongoDB replica set (MongoDB transactions require it).
|
|
382
|
+
|
|
374
383
|
### Subject & Execution
|
|
375
384
|
|
|
376
385
|
```ruby
|
data/guides/llm/QUERY.md
CHANGED
|
@@ -159,7 +159,7 @@ Only one default per class. Applied when no sort is provided.
|
|
|
159
159
|
|
|
160
160
|
### `.call`
|
|
161
161
|
|
|
162
|
-
Returns
|
|
162
|
+
Returns a queryable scope (`ActiveRecord::Relation` or `Mongoid::Criteria`):
|
|
163
163
|
|
|
164
164
|
```ruby
|
|
165
165
|
users = UserSearch.call(name: "ali", role: %w[admin], sort: "-name")
|
|
@@ -295,7 +295,7 @@ end
|
|
|
295
295
|
|
|
296
296
|
## Testing
|
|
297
297
|
|
|
298
|
-
Queries return standard ActiveRecord
|
|
298
|
+
Queries return standard scopes (`ActiveRecord::Relation` or `Mongoid::Criteria`). Test them with plain Minitest:
|
|
299
299
|
|
|
300
300
|
```ruby
|
|
301
301
|
class UserSearchTest < Minitest::Test
|
data/lib/dex/event/handler.rb
CHANGED
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
module Dex
|
|
4
4
|
class Event
|
|
5
5
|
class Handler
|
|
6
|
+
include Dex::Executable
|
|
7
|
+
|
|
6
8
|
attr_reader :event
|
|
7
9
|
|
|
8
10
|
def self.on(*event_classes)
|
|
@@ -36,7 +38,7 @@ module Dex
|
|
|
36
38
|
def self._event_handle(event)
|
|
37
39
|
instance = new
|
|
38
40
|
instance.instance_variable_set(:@event, event)
|
|
39
|
-
instance.
|
|
41
|
+
instance.send(:call)
|
|
40
42
|
end
|
|
41
43
|
|
|
42
44
|
def self._event_handle_from_payload(event_class_name, payload, metadata_hash)
|
|
@@ -69,6 +71,23 @@ module Dex
|
|
|
69
71
|
end
|
|
70
72
|
end
|
|
71
73
|
|
|
74
|
+
use TransactionWrapper
|
|
75
|
+
use CallbackWrapper
|
|
76
|
+
|
|
77
|
+
transaction false
|
|
78
|
+
private :call
|
|
79
|
+
|
|
80
|
+
# Guard must be defined after `include Executable` (which defines #call).
|
|
81
|
+
def self.method_added(method_name)
|
|
82
|
+
super
|
|
83
|
+
|
|
84
|
+
if method_name == :call
|
|
85
|
+
raise ArgumentError, "#{name || "Handler"} must not define #call — define #perform instead"
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
private :perform if method_name == :perform
|
|
89
|
+
end
|
|
90
|
+
|
|
72
91
|
def perform
|
|
73
92
|
raise NotImplementedError, "#{self.class.name} must implement #perform"
|
|
74
93
|
end
|
data/lib/dex/event.rb
CHANGED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Dex
|
|
4
|
+
module Executable
|
|
5
|
+
def self.included(base)
|
|
6
|
+
base.include(Dex::Settings)
|
|
7
|
+
base.extend(ClassMethods)
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
module ClassMethods
|
|
11
|
+
def inherited(subclass)
|
|
12
|
+
subclass.instance_variable_set(:@_pipeline, pipeline.dup)
|
|
13
|
+
super
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def pipeline
|
|
17
|
+
@_pipeline ||= Pipeline.new
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def use(mod, as: nil, wrap: nil, before: nil, after: nil, at: nil)
|
|
21
|
+
step_name = as || _derive_step_name(mod)
|
|
22
|
+
wrap_method = wrap || :"_#{step_name}_wrap"
|
|
23
|
+
pipeline.add(step_name, method: wrap_method, before: before, after: after, at: at)
|
|
24
|
+
include mod
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
private
|
|
28
|
+
|
|
29
|
+
def _derive_step_name(mod)
|
|
30
|
+
base = mod.name&.split("::")&.last
|
|
31
|
+
raise ArgumentError, "anonymous modules require explicit as: parameter" unless base
|
|
32
|
+
|
|
33
|
+
base.sub(/Wrapper\z/, "")
|
|
34
|
+
.gsub(/([a-z])([A-Z])/, '\1_\2')
|
|
35
|
+
.downcase
|
|
36
|
+
.to_sym
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def call
|
|
41
|
+
self.class.pipeline.execute(self) { perform }
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
@@ -32,7 +32,7 @@ module Dex
|
|
|
32
32
|
)
|
|
33
33
|
begin
|
|
34
34
|
job = apply_options(Operation::RecordJob)
|
|
35
|
-
job.perform_later(class_name: operation_class_name, record_id: record.id)
|
|
35
|
+
job.perform_later(class_name: operation_class_name, record_id: record.id.to_s)
|
|
36
36
|
rescue => e
|
|
37
37
|
begin
|
|
38
38
|
record.destroy
|
|
@@ -53,10 +53,23 @@ module Dex
|
|
|
53
53
|
raise LoadError, "Mongoid is required for transactions"
|
|
54
54
|
end
|
|
55
55
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
56
|
+
callbacks = Thread.current[AFTER_COMMIT_KEY]
|
|
57
|
+
outermost = callbacks.nil?
|
|
58
|
+
|
|
59
|
+
unless outermost
|
|
60
|
+
snapshot = callbacks.length
|
|
61
|
+
begin
|
|
62
|
+
return block.call
|
|
63
|
+
rescue rollback_exception_class
|
|
64
|
+
callbacks.slice!(snapshot..)
|
|
65
|
+
return nil
|
|
66
|
+
rescue StandardError # rubocop:disable Style/RescueStandardError
|
|
67
|
+
callbacks.slice!(snapshot..)
|
|
68
|
+
raise
|
|
69
|
+
end
|
|
70
|
+
end
|
|
59
71
|
|
|
72
|
+
Thread.current[AFTER_COMMIT_KEY] = []
|
|
60
73
|
block_completed = false
|
|
61
74
|
result = Mongoid.transaction do
|
|
62
75
|
value = block.call
|
|
@@ -64,17 +77,11 @@ module Dex
|
|
|
64
77
|
value
|
|
65
78
|
end
|
|
66
79
|
|
|
67
|
-
if
|
|
80
|
+
if block_completed
|
|
68
81
|
Thread.current[AFTER_COMMIT_KEY].each(&:call)
|
|
69
|
-
elsif !block_completed
|
|
70
|
-
# Mongoid swallowed a Rollback exception — discard callbacks from this level
|
|
71
|
-
Thread.current[AFTER_COMMIT_KEY].slice!(snapshot..)
|
|
72
82
|
end
|
|
73
83
|
|
|
74
84
|
result
|
|
75
|
-
rescue StandardError # rubocop:disable Style/RescueStandardError
|
|
76
|
-
Thread.current[AFTER_COMMIT_KEY]&.slice!(snapshot..) unless outermost
|
|
77
|
-
raise
|
|
78
85
|
ensure
|
|
79
86
|
Thread.current[AFTER_COMMIT_KEY] = nil if outermost
|
|
80
87
|
end
|
|
@@ -4,26 +4,37 @@ module Dex
|
|
|
4
4
|
module TransactionWrapper
|
|
5
5
|
extend Dex::Concern
|
|
6
6
|
|
|
7
|
+
DEFERRED_CALLBACKS_KEY = :_dex_after_commit_queue
|
|
8
|
+
|
|
7
9
|
def _transaction_wrap
|
|
8
|
-
|
|
10
|
+
deferred = Thread.current[DEFERRED_CALLBACKS_KEY]
|
|
11
|
+
outermost = deferred.nil?
|
|
12
|
+
Thread.current[DEFERRED_CALLBACKS_KEY] = [] if outermost
|
|
13
|
+
snapshot = Thread.current[DEFERRED_CALLBACKS_KEY].length
|
|
9
14
|
|
|
10
|
-
interceptor =
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
interceptor.result
|
|
15
|
+
result, interceptor = if _transaction_enabled?
|
|
16
|
+
_transaction_run_adapter(snapshot) { yield }
|
|
17
|
+
else
|
|
18
|
+
_transaction_run_deferred(snapshot) { yield }
|
|
15
19
|
end
|
|
16
20
|
|
|
21
|
+
_transaction_flush_deferred if outermost
|
|
22
|
+
|
|
17
23
|
interceptor&.rethrow!
|
|
18
24
|
result
|
|
25
|
+
rescue # rubocop:disable Style/RescueStandardError -- explicit for clarity
|
|
26
|
+
Thread.current[DEFERRED_CALLBACKS_KEY]&.slice!(snapshot..)
|
|
27
|
+
raise
|
|
28
|
+
ensure
|
|
29
|
+
Thread.current[DEFERRED_CALLBACKS_KEY] = nil if outermost
|
|
19
30
|
end
|
|
20
31
|
|
|
21
32
|
def after_commit(&block)
|
|
22
33
|
raise ArgumentError, "after_commit requires a block" unless block
|
|
23
34
|
|
|
24
|
-
|
|
25
|
-
if
|
|
26
|
-
|
|
35
|
+
deferred = Thread.current[DEFERRED_CALLBACKS_KEY]
|
|
36
|
+
if deferred
|
|
37
|
+
deferred << block
|
|
27
38
|
else
|
|
28
39
|
block.call
|
|
29
40
|
end
|
|
@@ -65,6 +76,33 @@ module Dex
|
|
|
65
76
|
|
|
66
77
|
private
|
|
67
78
|
|
|
79
|
+
def _transaction_run_adapter(snapshot)
|
|
80
|
+
interceptor = nil
|
|
81
|
+
result = _transaction_execute do
|
|
82
|
+
interceptor = Operation::HaltInterceptor.new { yield }
|
|
83
|
+
raise _transaction_adapter.rollback_exception_class if interceptor.error?
|
|
84
|
+
interceptor.result
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
if interceptor&.error?
|
|
88
|
+
Thread.current[DEFERRED_CALLBACKS_KEY]&.slice!(snapshot..)
|
|
89
|
+
interceptor.rethrow!
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
[result, interceptor]
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def _transaction_run_deferred(snapshot)
|
|
96
|
+
interceptor = Operation::HaltInterceptor.new { yield }
|
|
97
|
+
|
|
98
|
+
if interceptor.error?
|
|
99
|
+
Thread.current[DEFERRED_CALLBACKS_KEY]&.slice!(snapshot..)
|
|
100
|
+
interceptor.rethrow!
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
[interceptor.result, interceptor]
|
|
104
|
+
end
|
|
105
|
+
|
|
68
106
|
def _transaction_enabled?
|
|
69
107
|
settings = self.class.settings_for(:transaction)
|
|
70
108
|
return false unless settings.fetch(:enabled, true)
|
|
@@ -78,6 +116,20 @@ module Dex
|
|
|
78
116
|
Operation::TransactionAdapter.for(adapter_name)
|
|
79
117
|
end
|
|
80
118
|
|
|
119
|
+
def _transaction_flush_deferred
|
|
120
|
+
callbacks = Thread.current[DEFERRED_CALLBACKS_KEY]
|
|
121
|
+
return if callbacks.empty?
|
|
122
|
+
|
|
123
|
+
flush = -> { callbacks.each(&:call) }
|
|
124
|
+
enabled = self.class.settings_for(:transaction).fetch(:enabled, true)
|
|
125
|
+
adapter = enabled && _transaction_adapter
|
|
126
|
+
if adapter
|
|
127
|
+
adapter.after_commit(&flush)
|
|
128
|
+
else
|
|
129
|
+
flush.call
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
|
|
81
133
|
def _transaction_execute(&block)
|
|
82
134
|
_transaction_adapter.wrap(&block)
|
|
83
135
|
end
|
data/lib/dex/operation.rb
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
# Wrapper modules (loaded before class body so `include`/`use` can find them)
|
|
4
|
-
require_relative "operation/settings"
|
|
5
4
|
require_relative "operation/result_wrapper"
|
|
6
5
|
require_relative "operation/record_wrapper"
|
|
7
6
|
require_relative "operation/transaction_wrapper"
|
|
@@ -11,9 +10,6 @@ require_relative "operation/safe_wrapper"
|
|
|
11
10
|
require_relative "operation/rescue_wrapper"
|
|
12
11
|
require_relative "operation/callback_wrapper"
|
|
13
12
|
|
|
14
|
-
# Pipeline (referenced inside class body)
|
|
15
|
-
require_relative "operation/pipeline"
|
|
16
|
-
|
|
17
13
|
module Dex
|
|
18
14
|
class Operation
|
|
19
15
|
Halt = Struct.new(:type, :value, :error_code, :error_message, :error_details, keyword_init: true) do
|
|
@@ -46,6 +42,7 @@ module Dex
|
|
|
46
42
|
|
|
47
43
|
RESERVED_PROP_NAMES = %i[call perform async safe initialize].to_set.freeze
|
|
48
44
|
|
|
45
|
+
include Executable
|
|
49
46
|
include PropsSetup
|
|
50
47
|
include TypeCoercion
|
|
51
48
|
|
|
@@ -60,22 +57,6 @@ module Dex
|
|
|
60
57
|
)
|
|
61
58
|
end
|
|
62
59
|
|
|
63
|
-
def inherited(subclass)
|
|
64
|
-
subclass.instance_variable_set(:@_pipeline, pipeline.dup)
|
|
65
|
-
super
|
|
66
|
-
end
|
|
67
|
-
|
|
68
|
-
def pipeline
|
|
69
|
-
@_pipeline ||= Pipeline.new
|
|
70
|
-
end
|
|
71
|
-
|
|
72
|
-
def use(mod, as: nil, wrap: nil, before: nil, after: nil, at: nil)
|
|
73
|
-
step_name = as || _derive_step_name(mod)
|
|
74
|
-
wrap_method = wrap || :"_#{step_name}_wrap"
|
|
75
|
-
pipeline.add(step_name, method: wrap_method, before: before, after: after, at: at)
|
|
76
|
-
include mod
|
|
77
|
-
end
|
|
78
|
-
|
|
79
60
|
private
|
|
80
61
|
|
|
81
62
|
def _contract_params
|
|
@@ -85,25 +66,11 @@ module Dex
|
|
|
85
66
|
hash[prop.name] = prop.type
|
|
86
67
|
end
|
|
87
68
|
end
|
|
88
|
-
|
|
89
|
-
def _derive_step_name(mod)
|
|
90
|
-
base = mod.name&.split("::")&.last
|
|
91
|
-
raise ArgumentError, "anonymous modules require explicit as: parameter" unless base
|
|
92
|
-
|
|
93
|
-
base.sub(/Wrapper\z/, "")
|
|
94
|
-
.gsub(/([a-z])([A-Z])/, '\1_\2')
|
|
95
|
-
.downcase
|
|
96
|
-
.to_sym
|
|
97
|
-
end
|
|
98
69
|
end
|
|
99
70
|
|
|
100
71
|
def perform(*, **)
|
|
101
72
|
end
|
|
102
73
|
|
|
103
|
-
def call
|
|
104
|
-
self.class.pipeline.execute(self) { perform }
|
|
105
|
-
end
|
|
106
|
-
|
|
107
74
|
def self.method_added(method_name)
|
|
108
75
|
super
|
|
109
76
|
return unless method_name == :perform
|
|
@@ -117,7 +84,6 @@ module Dex
|
|
|
117
84
|
new(**kwargs).call
|
|
118
85
|
end
|
|
119
86
|
|
|
120
|
-
include Settings
|
|
121
87
|
include AsyncWrapper
|
|
122
88
|
include SafeWrapper
|
|
123
89
|
|
data/lib/dex/pipeline.rb
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Dex
|
|
4
|
+
class Pipeline
|
|
5
|
+
Step = Data.define(:name, :method)
|
|
6
|
+
|
|
7
|
+
def initialize(steps = [])
|
|
8
|
+
@steps = steps.dup
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def dup
|
|
12
|
+
self.class.new(@steps)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def steps
|
|
16
|
+
@steps.dup.freeze
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def add(name, method: :"_#{name}_wrap", before: nil, after: nil, at: nil)
|
|
20
|
+
validate_positioning!(before, after, at)
|
|
21
|
+
step = Step.new(name: name, method: method)
|
|
22
|
+
|
|
23
|
+
if at == :outer then @steps.unshift(step)
|
|
24
|
+
elsif at == :inner then @steps.push(step)
|
|
25
|
+
elsif before then @steps.insert(find_index!(before), step)
|
|
26
|
+
elsif after then @steps.insert(find_index!(after) + 1, step)
|
|
27
|
+
else @steps.push(step)
|
|
28
|
+
end
|
|
29
|
+
self
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def remove(name)
|
|
33
|
+
@steps.reject! { |s| s.name == name }
|
|
34
|
+
self
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def execute(target)
|
|
38
|
+
chain = @steps.reverse_each.reduce(-> { yield }) do |next_step, step|
|
|
39
|
+
-> { target.send(step.method, &next_step) }
|
|
40
|
+
end
|
|
41
|
+
chain.call
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
private
|
|
45
|
+
|
|
46
|
+
def validate_positioning!(before, after, at)
|
|
47
|
+
count = [before, after, at].count { |v| !v.nil? }
|
|
48
|
+
raise ArgumentError, "specify only one of before:, after:, at:" if count > 1
|
|
49
|
+
raise ArgumentError, "at: must be :outer or :inner" if at && !%i[outer inner].include?(at)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def find_index!(name)
|
|
53
|
+
idx = @steps.index { |s| s.name == name }
|
|
54
|
+
raise ArgumentError, "pipeline step :#{name} not found" unless idx
|
|
55
|
+
idx
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
data/lib/dex/version.rb
CHANGED
data/lib/dexkit.rb
CHANGED
|
@@ -15,6 +15,9 @@ require_relative "dex/ref_type"
|
|
|
15
15
|
require_relative "dex/type_coercion"
|
|
16
16
|
require_relative "dex/props_setup"
|
|
17
17
|
require_relative "dex/error"
|
|
18
|
+
require_relative "dex/settings"
|
|
19
|
+
require_relative "dex/pipeline"
|
|
20
|
+
require_relative "dex/executable"
|
|
18
21
|
require_relative "dex/operation"
|
|
19
22
|
require_relative "dex/event"
|
|
20
23
|
require_relative "dex/form"
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: dexkit
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.6.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Jacek Galanciak
|
|
@@ -79,6 +79,20 @@ dependencies:
|
|
|
79
79
|
- - ">="
|
|
80
80
|
- !ruby/object:Gem::Version
|
|
81
81
|
version: '6.1'
|
|
82
|
+
- !ruby/object:Gem::Dependency
|
|
83
|
+
name: ostruct
|
|
84
|
+
requirement: !ruby/object:Gem::Requirement
|
|
85
|
+
requirements:
|
|
86
|
+
- - ">="
|
|
87
|
+
- !ruby/object:Gem::Version
|
|
88
|
+
version: '0'
|
|
89
|
+
type: :development
|
|
90
|
+
prerelease: false
|
|
91
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
92
|
+
requirements:
|
|
93
|
+
- - ">="
|
|
94
|
+
- !ruby/object:Gem::Version
|
|
95
|
+
version: '0'
|
|
82
96
|
- !ruby/object:Gem::Dependency
|
|
83
97
|
name: actionpack
|
|
84
98
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -107,6 +121,20 @@ dependencies:
|
|
|
107
121
|
- - ">="
|
|
108
122
|
- !ruby/object:Gem::Version
|
|
109
123
|
version: '6.1'
|
|
124
|
+
- !ruby/object:Gem::Dependency
|
|
125
|
+
name: mongoid
|
|
126
|
+
requirement: !ruby/object:Gem::Requirement
|
|
127
|
+
requirements:
|
|
128
|
+
- - ">="
|
|
129
|
+
- !ruby/object:Gem::Version
|
|
130
|
+
version: '8.0'
|
|
131
|
+
type: :development
|
|
132
|
+
prerelease: false
|
|
133
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
134
|
+
requirements:
|
|
135
|
+
- - ">="
|
|
136
|
+
- !ruby/object:Gem::Version
|
|
137
|
+
version: '8.0'
|
|
110
138
|
- !ruby/object:Gem::Dependency
|
|
111
139
|
name: sqlite3
|
|
112
140
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -149,6 +177,7 @@ files:
|
|
|
149
177
|
- lib/dex/event/trace.rb
|
|
150
178
|
- lib/dex/event_test_helpers.rb
|
|
151
179
|
- lib/dex/event_test_helpers/assertions.rb
|
|
180
|
+
- lib/dex/executable.rb
|
|
152
181
|
- lib/dex/form.rb
|
|
153
182
|
- lib/dex/form/nesting.rb
|
|
154
183
|
- lib/dex/form/uniqueness_validator.rb
|
|
@@ -160,21 +189,21 @@ files:
|
|
|
160
189
|
- lib/dex/operation/jobs.rb
|
|
161
190
|
- lib/dex/operation/lock_wrapper.rb
|
|
162
191
|
- lib/dex/operation/outcome.rb
|
|
163
|
-
- lib/dex/operation/pipeline.rb
|
|
164
192
|
- lib/dex/operation/record_backend.rb
|
|
165
193
|
- lib/dex/operation/record_wrapper.rb
|
|
166
194
|
- lib/dex/operation/rescue_wrapper.rb
|
|
167
195
|
- lib/dex/operation/result_wrapper.rb
|
|
168
196
|
- lib/dex/operation/safe_wrapper.rb
|
|
169
|
-
- lib/dex/operation/settings.rb
|
|
170
197
|
- lib/dex/operation/transaction_adapter.rb
|
|
171
198
|
- lib/dex/operation/transaction_wrapper.rb
|
|
199
|
+
- lib/dex/pipeline.rb
|
|
172
200
|
- lib/dex/props_setup.rb
|
|
173
201
|
- lib/dex/query.rb
|
|
174
202
|
- lib/dex/query/backend.rb
|
|
175
203
|
- lib/dex/query/filtering.rb
|
|
176
204
|
- lib/dex/query/sorting.rb
|
|
177
205
|
- lib/dex/ref_type.rb
|
|
206
|
+
- lib/dex/settings.rb
|
|
178
207
|
- lib/dex/test_helpers.rb
|
|
179
208
|
- lib/dex/test_helpers/assertions.rb
|
|
180
209
|
- lib/dex/test_helpers/execution.rb
|
|
@@ -183,14 +212,15 @@ files:
|
|
|
183
212
|
- lib/dex/type_coercion.rb
|
|
184
213
|
- lib/dex/version.rb
|
|
185
214
|
- lib/dexkit.rb
|
|
186
|
-
homepage: https://
|
|
215
|
+
homepage: https://dex.razorjack.net/
|
|
187
216
|
licenses:
|
|
188
217
|
- MIT
|
|
189
218
|
metadata:
|
|
190
219
|
allowed_push_host: https://rubygems.org
|
|
191
|
-
homepage_uri: https://
|
|
220
|
+
homepage_uri: https://dex.razorjack.net/
|
|
192
221
|
source_code_uri: https://github.com/razorjack/dexkit
|
|
193
222
|
changelog_uri: https://github.com/razorjack/dexkit/blob/master/CHANGELOG.md
|
|
223
|
+
documentation_uri: https://dex.razorjack.net/
|
|
194
224
|
rdoc_options: []
|
|
195
225
|
require_paths:
|
|
196
226
|
- lib
|
|
@@ -207,5 +237,5 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
207
237
|
requirements: []
|
|
208
238
|
rubygems_version: 4.0.3
|
|
209
239
|
specification_version: 4
|
|
210
|
-
summary: '
|
|
240
|
+
summary: 'dexkit: Rails Patterns Toolbelt. Equip to gain +4 DEX'
|
|
211
241
|
test_files: []
|
|
@@ -1,60 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Dex
|
|
4
|
-
class Operation
|
|
5
|
-
class Pipeline
|
|
6
|
-
Step = Data.define(:name, :method)
|
|
7
|
-
|
|
8
|
-
def initialize(steps = [])
|
|
9
|
-
@steps = steps.dup
|
|
10
|
-
end
|
|
11
|
-
|
|
12
|
-
def dup
|
|
13
|
-
self.class.new(@steps)
|
|
14
|
-
end
|
|
15
|
-
|
|
16
|
-
def steps
|
|
17
|
-
@steps.dup.freeze
|
|
18
|
-
end
|
|
19
|
-
|
|
20
|
-
def add(name, method: :"_#{name}_wrap", before: nil, after: nil, at: nil)
|
|
21
|
-
validate_positioning!(before, after, at)
|
|
22
|
-
step = Step.new(name: name, method: method)
|
|
23
|
-
|
|
24
|
-
if at == :outer then @steps.unshift(step)
|
|
25
|
-
elsif at == :inner then @steps.push(step)
|
|
26
|
-
elsif before then @steps.insert(find_index!(before), step)
|
|
27
|
-
elsif after then @steps.insert(find_index!(after) + 1, step)
|
|
28
|
-
else @steps.push(step)
|
|
29
|
-
end
|
|
30
|
-
self
|
|
31
|
-
end
|
|
32
|
-
|
|
33
|
-
def remove(name)
|
|
34
|
-
@steps.reject! { |s| s.name == name }
|
|
35
|
-
self
|
|
36
|
-
end
|
|
37
|
-
|
|
38
|
-
def execute(operation)
|
|
39
|
-
chain = @steps.reverse_each.reduce(-> { yield }) do |next_step, step|
|
|
40
|
-
-> { operation.send(step.method, &next_step) }
|
|
41
|
-
end
|
|
42
|
-
chain.call
|
|
43
|
-
end
|
|
44
|
-
|
|
45
|
-
private
|
|
46
|
-
|
|
47
|
-
def validate_positioning!(before, after, at)
|
|
48
|
-
count = [before, after, at].count { |v| !v.nil? }
|
|
49
|
-
raise ArgumentError, "specify only one of before:, after:, at:" if count > 1
|
|
50
|
-
raise ArgumentError, "at: must be :outer or :inner" if at && !%i[outer inner].include?(at)
|
|
51
|
-
end
|
|
52
|
-
|
|
53
|
-
def find_index!(name)
|
|
54
|
-
idx = @steps.index { |s| s.name == name }
|
|
55
|
-
raise ArgumentError, "pipeline step :#{name} not found" unless idx
|
|
56
|
-
idx
|
|
57
|
-
end
|
|
58
|
-
end
|
|
59
|
-
end
|
|
60
|
-
end
|
|
File without changes
|