easyop 0.1.2 → 0.1.3
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 +136 -3
- data/README.md +273 -0
- data/lib/easyop/configuration.rb +8 -0
- data/lib/easyop/events/bus/active_support_notifications.rb +82 -0
- data/lib/easyop/events/bus/adapter.rb +119 -0
- data/lib/easyop/events/bus/custom.rb +51 -0
- data/lib/easyop/events/bus/memory.rb +64 -0
- data/lib/easyop/events/bus.rb +66 -0
- data/lib/easyop/events/event.rb +44 -0
- data/lib/easyop/events/registry.rb +128 -0
- data/lib/easyop/plugins/event_handlers.rb +76 -0
- data/lib/easyop/plugins/events.rb +162 -0
- data/lib/easyop/version.rb +1 -1
- data/lib/easyop.rb +11 -0
- metadata +10 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 4c77d9aa5925ea30b3de85fc74618f355ee3498223f79d6184cda21e95574b8d
|
|
4
|
+
data.tar.gz: 8bd775d8fa095a50b46aafeedec78f2046d54005b6316a1f22ae02b5657c361c
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: a01be26e27050c1569f85ee610043ca42268c75054355c4faf230b65599354d9a824b07955f4f13cda25672681d1c934bbeaec15d89be2cfd8ad55681dd29511
|
|
7
|
+
data.tar.gz: 41d8d5bf9ca01e02146764678c343eac787c2d5445670caa9e8c1a0f0b60ff26b57906f2fb4abd16b7e59d05b05fe65a1400cff4458934df0110bf1af10f5d3a
|
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,141 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.1.3] — 2026-04-14
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- **Minitest test suite** — a full parallel test suite covering all 21 modules in the gem. Tests live in `test/` and run via `bundle exec rake test` (or `bundle exec ruby -Ilib:test ...`). The suite complements the existing RSpec specs and is tracked as a separate SimpleCov report (command name `'Minitest'`).
|
|
15
|
+
|
|
16
|
+
Coverage across 258 tests, 360 assertions:
|
|
17
|
+
|
|
18
|
+
| Area | Files |
|
|
19
|
+
|------|-------|
|
|
20
|
+
| Core | `ctx_test`, `operation_test`, `hooks_test`, `rescuable_test`, `schema_test`, `skip_test`, `flow_test`, `flow_builder_test` |
|
|
21
|
+
| Events infrastructure | `events/event_test`, `events/registry_test`, `events/bus/memory_test`, `events/bus/adapter_test`, `events/bus/custom_test`, `events/bus/active_support_notifications_test` |
|
|
22
|
+
| Plugins | `plugins/base_test`, `plugins/recording_test`, `plugins/instrumentation_test`, `plugins/async_test`, `plugins/transactional_test`, `plugins/events_test`, `plugins/event_handlers_test` |
|
|
23
|
+
|
|
24
|
+
Key test patterns: anonymous `Class.new` operations, `set_const` helper for named-constant scenarios (Recording, Async), shared stubs for `ActiveSupport::Notifications`, `ActiveRecord::Base`, `ActiveJob::Base`, and `String#constantize` — all in `test/test_helper.rb` so individual files stay focused.
|
|
25
|
+
|
|
26
|
+
- **`Rakefile`** — adds a `test` task (Minitest) as the default Rake task:
|
|
27
|
+
|
|
28
|
+
```ruby
|
|
29
|
+
bundle exec rake test
|
|
30
|
+
# or simply:
|
|
31
|
+
bundle exec rake
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
- **`rake` gem** added to the `development/test` group in `Gemfile`.
|
|
35
|
+
|
|
36
|
+
- **`Easyop::Events::Bus::Adapter`** — a new inheritable base class for custom bus implementations. Subclass this instead of `Bus::Base` when building a transport adapter (RabbitMQ, Kafka, Redis, etc.). Provides two protected utilities on top of `Bus::Base`:
|
|
37
|
+
|
|
38
|
+
- `_safe_invoke(handler, event)` — calls `handler.call(event)` and rescues `StandardError`, so one broken subscriber never prevents others from running
|
|
39
|
+
- `_compile_pattern(pattern)` — converts a glob string or exact string to a `Regexp`, memoized per unique pattern per bus instance (glob→Regexp conversion happens only once regardless of publish volume)
|
|
40
|
+
|
|
41
|
+
```ruby
|
|
42
|
+
require "easyop/events/bus/adapter"
|
|
43
|
+
|
|
44
|
+
# Decorator: wrap any inner bus and add structured logging
|
|
45
|
+
class LoggingBus < Easyop::Events::Bus::Adapter
|
|
46
|
+
def initialize(inner = Easyop::Events::Bus::Memory.new)
|
|
47
|
+
super(); @inner = inner
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def publish(event)
|
|
51
|
+
Rails.logger.info "[bus] #{event.name} payload=#{event.payload}"
|
|
52
|
+
@inner.publish(event)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def subscribe(pattern, &block) = @inner.subscribe(pattern, &block)
|
|
56
|
+
def unsubscribe(handle) = @inner.unsubscribe(handle)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
Easyop::Events::Registry.bus = LoggingBus.new
|
|
60
|
+
|
|
61
|
+
# Full external broker example — RabbitMQ via Bunny gem:
|
|
62
|
+
class RabbitBus < Easyop::Events::Bus::Adapter
|
|
63
|
+
EXCHANGE_NAME = "easyop.events"
|
|
64
|
+
def initialize(url = ENV.fetch("AMQP_URL")) = (super(); @url = url)
|
|
65
|
+
def publish(event)
|
|
66
|
+
exchange.publish(event.to_h.to_json, routing_key: event.name)
|
|
67
|
+
end
|
|
68
|
+
def subscribe(pattern, &block)
|
|
69
|
+
q = channel.queue("", exclusive: true, auto_delete: true)
|
|
70
|
+
q.bind(exchange, routing_key: pattern.gsub("**", "#"))
|
|
71
|
+
q.subscribe { |_, _, body| _safe_invoke(block, decode(body)) }
|
|
72
|
+
end
|
|
73
|
+
private
|
|
74
|
+
def decode(body) = Easyop::Events::Event.new(**JSON.parse(body, symbolize_names: true))
|
|
75
|
+
def connection = @conn ||= Bunny.new(@url).tap(&:start)
|
|
76
|
+
def channel = @ch ||= connection.create_channel
|
|
77
|
+
def exchange = @exch ||= channel.topic(EXCHANGE_NAME, durable: true)
|
|
78
|
+
end
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
- **`Plugins::Events`** — a new producer plugin that emits domain events after an operation completes. Install on any operation class and declare events with the `emits` DSL:
|
|
82
|
+
|
|
83
|
+
```ruby
|
|
84
|
+
class PlaceOrder < ApplicationOperation
|
|
85
|
+
plugin Easyop::Plugins::Events
|
|
86
|
+
|
|
87
|
+
emits "order.placed", on: :success, payload: [:order_id, :total]
|
|
88
|
+
emits "order.failed", on: :failure, payload: ->(ctx) { { error: ctx.error } }
|
|
89
|
+
emits "order.attempted", on: :always
|
|
90
|
+
end
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
Options for `emits`: `on:` (`:success` / `:failure` / `:always`), `payload:` (Proc, Array of ctx keys, or nil for full ctx), `guard:` (optional Proc condition). Events fire in an `ensure` block so they are published even when `call!` raises `Ctx::Failure`. Individual publish failures are swallowed and never crash the operation. Subclasses inherit parent declarations.
|
|
94
|
+
|
|
95
|
+
- **`Plugins::EventHandlers`** — a new subscriber plugin that wires an operation as a domain event handler:
|
|
96
|
+
|
|
97
|
+
```ruby
|
|
98
|
+
class SendConfirmation < ApplicationOperation
|
|
99
|
+
plugin Easyop::Plugins::EventHandlers
|
|
100
|
+
|
|
101
|
+
on "order.placed"
|
|
102
|
+
|
|
103
|
+
def call
|
|
104
|
+
OrderMailer.confirm(ctx.order_id).deliver_later
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# Async dispatch (requires Plugins::Async also installed):
|
|
109
|
+
class IndexOrder < ApplicationOperation
|
|
110
|
+
plugin Easyop::Plugins::Async, queue: "indexing"
|
|
111
|
+
plugin Easyop::Plugins::EventHandlers
|
|
112
|
+
|
|
113
|
+
on "order.*", async: true
|
|
114
|
+
on "inventory.**", async: true, queue: "low"
|
|
115
|
+
|
|
116
|
+
def call
|
|
117
|
+
SearchIndex.reindex(ctx.order_id)
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
Supports glob patterns: `"order.*"` matches within one segment; `"order.**"` matches across segments. Registration happens at class-load time. Handler operations receive `ctx.event` (the `Easyop::Events::Event` object) and payload keys merged into ctx.
|
|
123
|
+
|
|
124
|
+
- **`Easyop::Events::Event`** — immutable, frozen domain event value object. Carries `name`, `payload`, `source` (emitting class name), `metadata`, and `timestamp`. Serializable to a plain Hash via `#to_h`.
|
|
125
|
+
|
|
126
|
+
- **`Easyop::Events::Bus`** — pluggable bus adapter system with three built-in adapters:
|
|
127
|
+
- `Bus::Memory` — in-process synchronous bus (default). Thread-safe via Mutex. Supports glob patterns and Regexp subscriptions. Test-friendly: `clear!`, `subscriber_count`.
|
|
128
|
+
- `Bus::ActiveSupportNotifications` — wraps `ActiveSupport::Notifications`. Lazy-checks for the library.
|
|
129
|
+
- `Bus::Custom` — wraps any user object responding to `#publish` and `#subscribe`. Validates the interface at construction time.
|
|
130
|
+
|
|
131
|
+
- **`Easyop::Events::Registry`** — thread-safe global coordination point. Configure once at boot; handler subscriptions are registered against it at class-load time:
|
|
132
|
+
|
|
133
|
+
```ruby
|
|
134
|
+
# Globally:
|
|
135
|
+
Easyop::Events::Registry.bus = :memory # default
|
|
136
|
+
Easyop::Events::Registry.bus = :active_support
|
|
137
|
+
Easyop::Events::Registry.bus = MyRabbitBus.new # custom adapter
|
|
138
|
+
|
|
139
|
+
# Or via config:
|
|
140
|
+
Easyop.configure { |c| c.event_bus = :active_support }
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
- **`Easyop::Configuration#event_bus`** — new configuration key. Accepts `:memory`, `:active_support`, or a bus adapter instance.
|
|
144
|
+
|
|
10
145
|
## [0.1.2] — 2026-04-13
|
|
11
146
|
|
|
12
147
|
### Added
|
|
@@ -66,6 +201,4 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
66
201
|
- `examples/usage.rb` — 13 runnable plain-Ruby examples
|
|
67
202
|
|
|
68
203
|
[Unreleased]: https://github.com/pniemczyk/easyop/compare/v0.1.2...HEAD
|
|
69
|
-
[0.1.
|
|
70
|
-
[0.1.1]: https://github.com/pniemczyk/easyop/compare/v0.1.0...v0.1.1
|
|
71
|
-
[0.1.0]: https://github.com/pniemczyk/easyop/releases/tag/v0.1.0
|
|
204
|
+
[0.1.3]: https://github.com/pniemczyk/easyop/compare/v0.1.2...v0.1.3
|
data/README.md
CHANGED
|
@@ -699,6 +699,198 @@ The plugin defines `Easyop::Plugins::Async::Job` lazily (on first call to `.call
|
|
|
699
699
|
|
|
700
700
|
---
|
|
701
701
|
|
|
702
|
+
### Plugin: Events
|
|
703
|
+
|
|
704
|
+
Emits domain events after an operation completes. Unlike the Instrumentation plugin (which is for operation-level tracing), Events carries business domain events through a configurable bus.
|
|
705
|
+
|
|
706
|
+
```ruby
|
|
707
|
+
require "easyop/events/event"
|
|
708
|
+
require "easyop/events/bus"
|
|
709
|
+
require "easyop/events/bus/memory"
|
|
710
|
+
require "easyop/events/registry"
|
|
711
|
+
require "easyop/plugins/events"
|
|
712
|
+
require "easyop/plugins/event_handlers"
|
|
713
|
+
|
|
714
|
+
class PlaceOrder < ApplicationOperation
|
|
715
|
+
plugin Easyop::Plugins::Events
|
|
716
|
+
|
|
717
|
+
emits "order.placed", on: :success, payload: [:order_id, :total]
|
|
718
|
+
emits "order.failed", on: :failure, payload: ->(ctx) { { error: ctx.error } }
|
|
719
|
+
emits "order.attempted", on: :always
|
|
720
|
+
|
|
721
|
+
def call
|
|
722
|
+
ctx.order_id = Order.create!(ctx.to_h).id
|
|
723
|
+
end
|
|
724
|
+
end
|
|
725
|
+
```
|
|
726
|
+
|
|
727
|
+
**`emits` DSL options:**
|
|
728
|
+
|
|
729
|
+
| Option | Values | Default | Description |
|
|
730
|
+
|---|---|---|---|
|
|
731
|
+
| `on:` | `:success`, `:failure`, `:always` | `:success` | When to fire |
|
|
732
|
+
| `payload:` | `Proc`, `Array`, `nil` | `nil` (full ctx) | Proc receives ctx; Array slices ctx keys |
|
|
733
|
+
| `guard:` | `Proc`, `nil` | `nil` | Extra condition — fires only when truthy |
|
|
734
|
+
|
|
735
|
+
Events fire in an `ensure` block and are emitted even when `call!` raises. Individual publish failures are swallowed and never crash the operation. `emits` declarations are inherited by subclasses.
|
|
736
|
+
|
|
737
|
+
---
|
|
738
|
+
|
|
739
|
+
### Plugin: EventHandlers
|
|
740
|
+
|
|
741
|
+
Wires an operation class as a domain event handler. Handler operations receive `ctx.event` (the `Easyop::Events::Event` object) and all payload keys merged into ctx.
|
|
742
|
+
|
|
743
|
+
```ruby
|
|
744
|
+
class SendConfirmation < ApplicationOperation
|
|
745
|
+
plugin Easyop::Plugins::EventHandlers
|
|
746
|
+
|
|
747
|
+
on "order.placed"
|
|
748
|
+
|
|
749
|
+
def call
|
|
750
|
+
event = ctx.event # Easyop::Events::Event
|
|
751
|
+
order_id = ctx.order_id # payload keys merged into ctx
|
|
752
|
+
OrderMailer.confirm(order_id).deliver_later
|
|
753
|
+
end
|
|
754
|
+
end
|
|
755
|
+
```
|
|
756
|
+
|
|
757
|
+
**Async dispatch** (requires `Plugins::Async` also installed):
|
|
758
|
+
|
|
759
|
+
```ruby
|
|
760
|
+
class IndexOrder < ApplicationOperation
|
|
761
|
+
plugin Easyop::Plugins::Async, queue: "indexing"
|
|
762
|
+
plugin Easyop::Plugins::EventHandlers
|
|
763
|
+
|
|
764
|
+
on "order.*", async: true
|
|
765
|
+
on "inventory.**", async: true, queue: "low"
|
|
766
|
+
|
|
767
|
+
def call
|
|
768
|
+
# ctx.event_data is a Hash when dispatched async (serializable for ActiveJob)
|
|
769
|
+
SearchIndex.reindex(ctx.order_id)
|
|
770
|
+
end
|
|
771
|
+
end
|
|
772
|
+
```
|
|
773
|
+
|
|
774
|
+
**Glob patterns:**
|
|
775
|
+
- `"order.*"` — matches within one dot-segment (`order.placed`, `order.shipped`)
|
|
776
|
+
- `"warehouse.**"` — matches across segments (`warehouse.stock.updated`, `warehouse.zone.moved`)
|
|
777
|
+
- Plain strings match exactly; `Regexp` is also accepted
|
|
778
|
+
|
|
779
|
+
Registration happens at class-load time. Configure the bus **before** loading handler classes.
|
|
780
|
+
|
|
781
|
+
---
|
|
782
|
+
|
|
783
|
+
### Events Bus — Configurable Transport
|
|
784
|
+
|
|
785
|
+
The bus adapter controls how events are delivered. Configure it once at boot:
|
|
786
|
+
|
|
787
|
+
```ruby
|
|
788
|
+
# Default — in-process synchronous (great for tests and simple setups)
|
|
789
|
+
Easyop::Events::Registry.bus = :memory
|
|
790
|
+
|
|
791
|
+
# ActiveSupport::Notifications (integrates with Rails instrumentation)
|
|
792
|
+
Easyop::Events::Registry.bus = :active_support
|
|
793
|
+
|
|
794
|
+
# Any custom adapter (RabbitMQ, Kafka, Redis Pub/Sub…)
|
|
795
|
+
Easyop::Events::Registry.bus = MyRabbitBus.new
|
|
796
|
+
|
|
797
|
+
# Or via config block:
|
|
798
|
+
Easyop.configure { |c| c.event_bus = :active_support }
|
|
799
|
+
```
|
|
800
|
+
|
|
801
|
+
**Built-in adapters:**
|
|
802
|
+
|
|
803
|
+
| Adapter | Class | Notes |
|
|
804
|
+
|---|---|---|
|
|
805
|
+
| Memory | `Easyop::Events::Bus::Memory` | Default. Thread-safe, in-process, synchronous. |
|
|
806
|
+
| ActiveSupport | `Easyop::Events::Bus::ActiveSupportNotifications` | Wraps `AS::Notifications`. Requires `activesupport`. |
|
|
807
|
+
| Custom | `Easyop::Events::Bus::Custom` | Wraps any object with `#publish` and `#subscribe`. |
|
|
808
|
+
|
|
809
|
+
**Building a custom bus — two approaches:**
|
|
810
|
+
|
|
811
|
+
**Option A — subclass `Bus::Adapter`** (recommended for real transports). Inherits glob helpers, `_safe_invoke`, and `_compile_pattern`:
|
|
812
|
+
|
|
813
|
+
```ruby
|
|
814
|
+
require "easyop/events/bus/adapter"
|
|
815
|
+
|
|
816
|
+
# Decorator: wraps any inner bus and adds structured logging
|
|
817
|
+
class LoggingBus < Easyop::Events::Bus::Adapter
|
|
818
|
+
def initialize(inner = Easyop::Events::Bus::Memory.new)
|
|
819
|
+
super()
|
|
820
|
+
@inner = inner
|
|
821
|
+
end
|
|
822
|
+
|
|
823
|
+
def publish(event)
|
|
824
|
+
Rails.logger.info "[bus:publish] #{event.name} payload=#{event.payload}"
|
|
825
|
+
@inner.publish(event)
|
|
826
|
+
end
|
|
827
|
+
|
|
828
|
+
def subscribe(pattern, &block) = @inner.subscribe(pattern, &block)
|
|
829
|
+
def unsubscribe(handle) = @inner.unsubscribe(handle)
|
|
830
|
+
end
|
|
831
|
+
|
|
832
|
+
Easyop::Events::Registry.bus = LoggingBus.new
|
|
833
|
+
|
|
834
|
+
# Full RabbitMQ example (Bunny gem) — uses _safe_invoke for handler safety:
|
|
835
|
+
class RabbitBus < Easyop::Events::Bus::Adapter
|
|
836
|
+
EXCHANGE_NAME = "easyop.events"
|
|
837
|
+
|
|
838
|
+
def initialize(url = ENV.fetch("AMQP_URL", "amqp://localhost"))
|
|
839
|
+
super()
|
|
840
|
+
@url = url; @mutex = Mutex.new; @handles = {}
|
|
841
|
+
end
|
|
842
|
+
|
|
843
|
+
def publish(event)
|
|
844
|
+
exchange.publish(event.to_h.to_json,
|
|
845
|
+
routing_key: event.name, content_type: "application/json")
|
|
846
|
+
end
|
|
847
|
+
|
|
848
|
+
def subscribe(pattern, &block)
|
|
849
|
+
q = channel.queue("", exclusive: true, auto_delete: true)
|
|
850
|
+
q.bind(exchange, routing_key: _to_amqp(pattern))
|
|
851
|
+
consumer = q.subscribe { |_, _, body| _safe_invoke(block, decode(body)) }
|
|
852
|
+
handle = Object.new
|
|
853
|
+
@mutex.synchronize { @handles[handle.object_id] = { queue: q, consumer: consumer } }
|
|
854
|
+
handle
|
|
855
|
+
end
|
|
856
|
+
|
|
857
|
+
def unsubscribe(handle)
|
|
858
|
+
@mutex.synchronize do
|
|
859
|
+
e = @handles.delete(handle.object_id); return unless e
|
|
860
|
+
e[:consumer].cancel; e[:queue].delete
|
|
861
|
+
end
|
|
862
|
+
end
|
|
863
|
+
|
|
864
|
+
def disconnect
|
|
865
|
+
@mutex.synchronize { @connection&.close; @connection = @channel = @exchange = nil }
|
|
866
|
+
end
|
|
867
|
+
|
|
868
|
+
private
|
|
869
|
+
|
|
870
|
+
def _to_amqp(p) = p.is_a?(Regexp) ? p.source : p.gsub("**", "#")
|
|
871
|
+
def decode(body) = Easyop::Events::Event.new(**JSON.parse(body, symbolize_names: true))
|
|
872
|
+
def connection = @connection ||= Bunny.new(@url, recover_from_connection_close: true).tap(&:start)
|
|
873
|
+
def channel = @channel ||= connection.create_channel
|
|
874
|
+
def exchange = @exchange ||= channel.topic(EXCHANGE_NAME, durable: true)
|
|
875
|
+
end
|
|
876
|
+
|
|
877
|
+
Easyop::Events::Registry.bus = RabbitBus.new
|
|
878
|
+
at_exit { Easyop::Events::Registry.bus.disconnect }
|
|
879
|
+
```
|
|
880
|
+
|
|
881
|
+
**Option B — duck-typed object** (no subclassing). Pass any object with `#publish` and `#subscribe`; Registry auto-wraps it in `Bus::Custom`:
|
|
882
|
+
|
|
883
|
+
```ruby
|
|
884
|
+
class MyKafkaBus
|
|
885
|
+
def publish(event) = Kafka.produce(event.name, event.to_h.to_json)
|
|
886
|
+
def subscribe(pattern, &block) = Kafka.subscribe(pattern) { |msg| block.call(decode(msg)) }
|
|
887
|
+
end
|
|
888
|
+
|
|
889
|
+
Easyop::Events::Registry.bus = MyKafkaBus.new
|
|
890
|
+
```
|
|
891
|
+
|
|
892
|
+
---
|
|
893
|
+
|
|
702
894
|
### Plugin: Transactional
|
|
703
895
|
|
|
704
896
|
Wraps every operation call in a database transaction. On `ctx.fail!` or any unhandled exception the transaction is rolled back. Supports **ActiveRecord** and **Sequel**.
|
|
@@ -1122,3 +1314,84 @@ See the **[AI Tools docs page](https://pniemczyk.github.io/easyop/ai-tools.html)
|
|
|
1122
1314
|
| `Easyop::Plugins::Async` | `easyop/plugins/async` | Adds `.call_async` via ActiveJob with AR object serialization |
|
|
1123
1315
|
| `Easyop::Plugins::Async::Job` | (created lazily) | The ActiveJob class that deserializes and runs the operation |
|
|
1124
1316
|
| `Easyop::Plugins::Transactional` | `easyop/plugins/transactional` | Wraps operation in an AR/Sequel transaction; `transactional false` to opt out |
|
|
1317
|
+
| `Easyop::Plugins::Events` | `easyop/plugins/events` | Emits domain events after execution; `emits` DSL with `on:`, `payload:`, `guard:` |
|
|
1318
|
+
| `Easyop::Plugins::EventHandlers` | `easyop/plugins/event_handlers` | Subscribes an operation to handle domain events; `on` DSL with glob patterns |
|
|
1319
|
+
|
|
1320
|
+
### Domain Events Infrastructure
|
|
1321
|
+
|
|
1322
|
+
| Class/Module | Require | Description |
|
|
1323
|
+
|---|---|---|
|
|
1324
|
+
| `Easyop::Events::Event` | `easyop/events/event` | Immutable frozen value object: `name`, `payload`, `metadata`, `timestamp`, `source` |
|
|
1325
|
+
| `Easyop::Events::Bus::Base` | `easyop/events/bus` | Abstract adapter interface (`publish`, `subscribe`, `unsubscribe`) and glob helpers |
|
|
1326
|
+
| `Easyop::Events::Bus::Adapter` | `easyop/events/bus/adapter` | **Inheritable base for custom buses.** Adds `_safe_invoke` + `_compile_pattern` (memoized). Subclass this. |
|
|
1327
|
+
| `Easyop::Events::Bus::Memory` | `easyop/events/bus/memory` | In-process synchronous bus (default). Thread-safe. |
|
|
1328
|
+
| `Easyop::Events::Bus::ActiveSupportNotifications` | `easyop/events/bus/active_support_notifications` | `ActiveSupport::Notifications` adapter |
|
|
1329
|
+
| `Easyop::Events::Bus::Custom` | `easyop/events/bus/custom` | Wraps any user-provided bus object (duck-typed, no subclassing needed) |
|
|
1330
|
+
| `Easyop::Events::Registry` | `easyop/events/registry` | Global bus holder + handler subscription registry |
|
|
1331
|
+
|
|
1332
|
+
---
|
|
1333
|
+
|
|
1334
|
+
## Releasing a New Version
|
|
1335
|
+
|
|
1336
|
+
Follow these steps to bump the version, update the changelog, and publish a tagged release.
|
|
1337
|
+
|
|
1338
|
+
### 1. Bump the version number
|
|
1339
|
+
|
|
1340
|
+
Edit `lib/easyop/version.rb` and increment the version string following [Semantic Versioning](https://semver.org/):
|
|
1341
|
+
|
|
1342
|
+
```ruby
|
|
1343
|
+
# lib/easyop/version.rb
|
|
1344
|
+
module Easyop
|
|
1345
|
+
VERSION = "0.1.4" # was 0.1.3
|
|
1346
|
+
end
|
|
1347
|
+
```
|
|
1348
|
+
|
|
1349
|
+
### 2. Update the changelog
|
|
1350
|
+
|
|
1351
|
+
In `CHANGELOG.md`, move everything under `[Unreleased]` into a new versioned section:
|
|
1352
|
+
|
|
1353
|
+
```markdown
|
|
1354
|
+
## [Unreleased]
|
|
1355
|
+
|
|
1356
|
+
## [0.1.4] — YYYY-MM-DD # ← new section
|
|
1357
|
+
|
|
1358
|
+
### Added
|
|
1359
|
+
- …
|
|
1360
|
+
|
|
1361
|
+
## [0.1.3] — 2026-04-14
|
|
1362
|
+
```
|
|
1363
|
+
|
|
1364
|
+
Add a comparison link at the bottom of the file:
|
|
1365
|
+
|
|
1366
|
+
```markdown
|
|
1367
|
+
[Unreleased]: https://github.com/pniemczyk/easyop/compare/v0.1.4...HEAD
|
|
1368
|
+
[0.1.4]: https://github.com/pniemczyk/easyop/compare/v0.1.3...v0.1.4
|
|
1369
|
+
[0.1.3]: https://github.com/pniemczyk/easyop/compare/v0.1.2...v0.1.3
|
|
1370
|
+
```
|
|
1371
|
+
|
|
1372
|
+
### 3. Commit the release changes
|
|
1373
|
+
|
|
1374
|
+
```bash
|
|
1375
|
+
git add lib/easyop/version.rb CHANGELOG.md
|
|
1376
|
+
git commit -m "Release v0.1.4"
|
|
1377
|
+
```
|
|
1378
|
+
|
|
1379
|
+
### 4. Tag the commit
|
|
1380
|
+
|
|
1381
|
+
```bash
|
|
1382
|
+
git tag -a v0.1.4 -m "Release v0.1.4"
|
|
1383
|
+
```
|
|
1384
|
+
|
|
1385
|
+
### 5. Push the commit and tag
|
|
1386
|
+
|
|
1387
|
+
```bash
|
|
1388
|
+
git push origin master
|
|
1389
|
+
git push origin v0.1.4
|
|
1390
|
+
```
|
|
1391
|
+
|
|
1392
|
+
### 6. Build and push the gem (optional)
|
|
1393
|
+
|
|
1394
|
+
```bash
|
|
1395
|
+
gem build easyop.gemspec
|
|
1396
|
+
gem push easyop-0.1.4.gem
|
|
1397
|
+
```
|
data/lib/easyop/configuration.rb
CHANGED
|
@@ -8,9 +8,17 @@ module Easyop
|
|
|
8
8
|
# When false (default), mismatches emit a warning and execution continues.
|
|
9
9
|
attr_accessor :strict_types
|
|
10
10
|
|
|
11
|
+
# Bus adapter for domain events (Easyop::Plugins::Events / EventHandlers).
|
|
12
|
+
# Options: :memory (default), :active_support, or a bus adapter instance.
|
|
13
|
+
#
|
|
14
|
+
# @example
|
|
15
|
+
# Easyop.configure { |c| c.event_bus = :active_support }
|
|
16
|
+
attr_accessor :event_bus
|
|
17
|
+
|
|
11
18
|
def initialize
|
|
12
19
|
@type_adapter = :native
|
|
13
20
|
@strict_types = false
|
|
21
|
+
@event_bus = nil # nil = Memory bus (see Easyop::Events::Registry)
|
|
14
22
|
end
|
|
15
23
|
end
|
|
16
24
|
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Easyop
|
|
4
|
+
module Events
|
|
5
|
+
module Bus
|
|
6
|
+
# Bus adapter backed by ActiveSupport::Notifications.
|
|
7
|
+
#
|
|
8
|
+
# Requires activesupport (not auto-required). Raises LoadError if unavailable.
|
|
9
|
+
#
|
|
10
|
+
# @example
|
|
11
|
+
# Easyop::Events::Registry.bus = :active_support
|
|
12
|
+
#
|
|
13
|
+
# # or manually:
|
|
14
|
+
# bus = Easyop::Events::Bus::ActiveSupportNotifications.new
|
|
15
|
+
# Easyop::Events::Registry.bus = bus
|
|
16
|
+
class ActiveSupportNotifications < Base
|
|
17
|
+
# Publish +event+ via ActiveSupport::Notifications.instrument.
|
|
18
|
+
# The full event hash (name, payload, metadata, timestamp, source) is passed
|
|
19
|
+
# as the AS notification payload.
|
|
20
|
+
#
|
|
21
|
+
# @param event [Easyop::Events::Event]
|
|
22
|
+
def publish(event)
|
|
23
|
+
_ensure_as!
|
|
24
|
+
::ActiveSupport::Notifications.instrument(event.name, event.to_h)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# Subscribe to events matching +pattern+ via ActiveSupport::Notifications.subscribe.
|
|
28
|
+
# Glob patterns are converted to Regexp before passing to AS.
|
|
29
|
+
#
|
|
30
|
+
# @param pattern [String, Regexp]
|
|
31
|
+
# @return [Object] AS subscription handle
|
|
32
|
+
def subscribe(pattern, &block)
|
|
33
|
+
_ensure_as!
|
|
34
|
+
|
|
35
|
+
as_pattern = _as_pattern(pattern)
|
|
36
|
+
|
|
37
|
+
::ActiveSupport::Notifications.subscribe(as_pattern) do |*args|
|
|
38
|
+
as_event = ::ActiveSupport::Notifications::Event.new(*args)
|
|
39
|
+
p = as_event.payload
|
|
40
|
+
|
|
41
|
+
# Reconstruct an Easyop::Events::Event from the AS notification payload.
|
|
42
|
+
easyop_event = Event.new(
|
|
43
|
+
name: (p[:name] || as_event.name).to_s,
|
|
44
|
+
payload: p[:payload] || {},
|
|
45
|
+
metadata: p[:metadata] || {},
|
|
46
|
+
timestamp: p[:timestamp],
|
|
47
|
+
source: p[:source]
|
|
48
|
+
)
|
|
49
|
+
block.call(easyop_event)
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Unsubscribe using the handle returned by #subscribe.
|
|
54
|
+
# @param handle [Object] AS subscription object
|
|
55
|
+
def unsubscribe(handle)
|
|
56
|
+
_ensure_as!
|
|
57
|
+
::ActiveSupport::Notifications.unsubscribe(handle)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
private
|
|
61
|
+
|
|
62
|
+
# Convert a pattern for use with AS::Notifications.subscribe.
|
|
63
|
+
# AS accepts exact strings or Regexp (not globs), so convert globs first.
|
|
64
|
+
def _as_pattern(pattern)
|
|
65
|
+
case pattern
|
|
66
|
+
when Regexp then pattern
|
|
67
|
+
when String
|
|
68
|
+
pattern.include?("*") ? _glob_to_regex(pattern) : pattern
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def _ensure_as!
|
|
73
|
+
return if defined?(::ActiveSupport::Notifications)
|
|
74
|
+
|
|
75
|
+
raise LoadError,
|
|
76
|
+
"ActiveSupport::Notifications is required for this bus adapter. " \
|
|
77
|
+
"Add 'activesupport' to your Gemfile."
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Easyop
|
|
4
|
+
module Events
|
|
5
|
+
module Bus
|
|
6
|
+
# Inheritable base class for custom bus implementations.
|
|
7
|
+
#
|
|
8
|
+
# Subclass this (not +Bus::Base+) when building a transport adapter for
|
|
9
|
+
# an external message broker, pub/sub system, or any custom delivery
|
|
10
|
+
# mechanism. +Bus::Base+ defines the required interface and glob helpers.
|
|
11
|
+
# +Bus::Adapter+ adds two production-grade utilities on top:
|
|
12
|
+
#
|
|
13
|
+
# _safe_invoke(handler, event) — call a handler, swallow StandardError
|
|
14
|
+
# _compile_pattern(pattern) — glob/string → cached Regexp
|
|
15
|
+
#
|
|
16
|
+
# Both are protected so they are accessible in subclasses but not part
|
|
17
|
+
# of the external bus interface.
|
|
18
|
+
#
|
|
19
|
+
# == Minimum contract
|
|
20
|
+
#
|
|
21
|
+
# Override +#publish+ and +#subscribe+. Override +#unsubscribe+ if your
|
|
22
|
+
# transport supports subscription cancellation.
|
|
23
|
+
#
|
|
24
|
+
# == Example — minimal adapter
|
|
25
|
+
#
|
|
26
|
+
# class PrintBus < Easyop::Events::Bus::Adapter
|
|
27
|
+
# def initialize
|
|
28
|
+
# super
|
|
29
|
+
# @subs = []
|
|
30
|
+
# @mutex = Mutex.new
|
|
31
|
+
# end
|
|
32
|
+
#
|
|
33
|
+
# def publish(event)
|
|
34
|
+
# snap = @mutex.synchronize { @subs.dup }
|
|
35
|
+
# snap.each do |sub|
|
|
36
|
+
# _safe_invoke(sub[:handler], event) if _pattern_matches?(sub[:pattern], event.name)
|
|
37
|
+
# end
|
|
38
|
+
# end
|
|
39
|
+
#
|
|
40
|
+
# def subscribe(pattern, &block)
|
|
41
|
+
# handle = { pattern: _compile_pattern(pattern), handler: block }
|
|
42
|
+
# @mutex.synchronize { @subs << handle }
|
|
43
|
+
# handle
|
|
44
|
+
# end
|
|
45
|
+
#
|
|
46
|
+
# def unsubscribe(handle)
|
|
47
|
+
# @mutex.synchronize { @subs.delete(handle) }
|
|
48
|
+
# end
|
|
49
|
+
# end
|
|
50
|
+
#
|
|
51
|
+
# == Example — decorator (wraps another bus)
|
|
52
|
+
#
|
|
53
|
+
# class LoggingBus < Easyop::Events::Bus::Adapter
|
|
54
|
+
# def initialize(inner)
|
|
55
|
+
# super()
|
|
56
|
+
# @inner = inner
|
|
57
|
+
# end
|
|
58
|
+
#
|
|
59
|
+
# def publish(event)
|
|
60
|
+
# Rails.logger.info "[bus:publish] #{event.name} payload=#{event.payload}"
|
|
61
|
+
# @inner.publish(event)
|
|
62
|
+
# end
|
|
63
|
+
#
|
|
64
|
+
# def subscribe(pattern, &block)
|
|
65
|
+
# @inner.subscribe(pattern, &block)
|
|
66
|
+
# end
|
|
67
|
+
#
|
|
68
|
+
# def unsubscribe(handle)
|
|
69
|
+
# @inner.unsubscribe(handle)
|
|
70
|
+
# end
|
|
71
|
+
# end
|
|
72
|
+
#
|
|
73
|
+
# Easyop::Events::Registry.bus = LoggingBus.new(Easyop::Events::Bus::Memory.new)
|
|
74
|
+
class Adapter < Base
|
|
75
|
+
protected
|
|
76
|
+
|
|
77
|
+
# Call +handler+ with +event+, rescuing any StandardError.
|
|
78
|
+
#
|
|
79
|
+
# Use this inside your +#publish+ implementation so one broken handler
|
|
80
|
+
# never prevents other handlers from running and never surfaces an
|
|
81
|
+
# exception to the event producer.
|
|
82
|
+
#
|
|
83
|
+
# @param handler [#call]
|
|
84
|
+
# @param event [Easyop::Events::Event]
|
|
85
|
+
# @return [void]
|
|
86
|
+
def _safe_invoke(handler, event)
|
|
87
|
+
handler.call(event)
|
|
88
|
+
rescue StandardError
|
|
89
|
+
# Intentionally swallowed — individual handler failures must not
|
|
90
|
+
# propagate to the caller or block remaining handlers.
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# Compile +pattern+ into a +Regexp+, memoized per unique pattern value.
|
|
94
|
+
#
|
|
95
|
+
# Results are cached in a per-instance hash so glob→Regexp conversion
|
|
96
|
+
# happens only once regardless of how many events are published.
|
|
97
|
+
#
|
|
98
|
+
# String patterns without wildcards are anchored literally.
|
|
99
|
+
# Glob patterns follow the same rules as +Bus::Base#_glob_to_regex+:
|
|
100
|
+
# "*" — any single dot-separated segment
|
|
101
|
+
# "**" — any sequence of characters including dots
|
|
102
|
+
#
|
|
103
|
+
# @param pattern [String, Regexp]
|
|
104
|
+
# @return [Regexp]
|
|
105
|
+
def _compile_pattern(pattern)
|
|
106
|
+
return pattern if pattern.is_a?(Regexp)
|
|
107
|
+
|
|
108
|
+
@_pattern_cache ||= {}
|
|
109
|
+
@_pattern_cache[pattern] ||=
|
|
110
|
+
if pattern.include?("*")
|
|
111
|
+
_glob_to_regex(pattern)
|
|
112
|
+
else
|
|
113
|
+
Regexp.new("\\A#{Regexp.escape(pattern)}\\z")
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
end
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Easyop
|
|
4
|
+
module Events
|
|
5
|
+
module Bus
|
|
6
|
+
# Wraps any user-supplied bus adapter.
|
|
7
|
+
#
|
|
8
|
+
# The adapter must respond to:
|
|
9
|
+
# #publish(event)
|
|
10
|
+
# #subscribe(pattern, &block)
|
|
11
|
+
#
|
|
12
|
+
# Optionally:
|
|
13
|
+
# #unsubscribe(handle)
|
|
14
|
+
#
|
|
15
|
+
# @example Wrapping a RabbitMQ adapter
|
|
16
|
+
# class MyRabbitBus
|
|
17
|
+
# def publish(event) = rabbit.publish(event.to_h, routing_key: event.name)
|
|
18
|
+
# def subscribe(pattern, &block) = rabbit.subscribe(pattern) { |msg| block.call(reconstruct(msg)) }
|
|
19
|
+
# end
|
|
20
|
+
#
|
|
21
|
+
# Easyop::Events::Registry.bus = MyRabbitBus.new
|
|
22
|
+
#
|
|
23
|
+
# @example Passing via Custom wrapper explicitly
|
|
24
|
+
# Easyop::Events::Registry.bus = Easyop::Events::Bus::Custom.new(MyRabbitBus.new)
|
|
25
|
+
class Custom < Base
|
|
26
|
+
# @param adapter [Object] must respond to #publish and #subscribe
|
|
27
|
+
# @raise [ArgumentError] if adapter does not meet the interface
|
|
28
|
+
def initialize(adapter)
|
|
29
|
+
unless adapter.respond_to?(:publish) && adapter.respond_to?(:subscribe)
|
|
30
|
+
raise ArgumentError,
|
|
31
|
+
"Custom bus adapter must respond to #publish(event) and " \
|
|
32
|
+
"#subscribe(pattern, &block). Got: #{adapter.inspect}"
|
|
33
|
+
end
|
|
34
|
+
@adapter = adapter
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def publish(event)
|
|
38
|
+
@adapter.publish(event)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def subscribe(pattern, &block)
|
|
42
|
+
@adapter.subscribe(pattern, &block)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def unsubscribe(handle)
|
|
46
|
+
@adapter.unsubscribe(handle) if @adapter.respond_to?(:unsubscribe)
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Easyop
|
|
4
|
+
module Events
|
|
5
|
+
module Bus
|
|
6
|
+
# In-process, synchronous bus. Default adapter — requires no external gems.
|
|
7
|
+
#
|
|
8
|
+
# Thread-safe: subscriptions are protected by a Mutex, and publish snapshots
|
|
9
|
+
# the subscriber list before calling handlers so the lock is not held
|
|
10
|
+
# during handler execution (prevents deadlocks).
|
|
11
|
+
#
|
|
12
|
+
# @example
|
|
13
|
+
# bus = Easyop::Events::Bus::Memory.new
|
|
14
|
+
# bus.subscribe("order.placed") { |e| puts e.name }
|
|
15
|
+
# bus.publish(Easyop::Events::Event.new(name: "order.placed"))
|
|
16
|
+
class Memory < Base
|
|
17
|
+
def initialize
|
|
18
|
+
@subscribers = []
|
|
19
|
+
@mutex = Mutex.new
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# Deliver +event+ to all matching subscribers.
|
|
23
|
+
# Handlers are called outside the lock; failures in individual handlers
|
|
24
|
+
# are swallowed so other handlers still run.
|
|
25
|
+
#
|
|
26
|
+
# @param event [Easyop::Events::Event]
|
|
27
|
+
def publish(event)
|
|
28
|
+
subs = @mutex.synchronize { @subscribers.dup }
|
|
29
|
+
subs.each do |sub|
|
|
30
|
+
sub[:handler].call(event) if _pattern_matches?(sub[:pattern], event.name)
|
|
31
|
+
rescue StandardError
|
|
32
|
+
# Individual handler failures must not prevent other handlers from running.
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Register a handler block for events matching +pattern+.
|
|
37
|
+
#
|
|
38
|
+
# @param pattern [String, Regexp] exact name, glob, or Regexp
|
|
39
|
+
# @return [Hash] subscription handle (pass to #unsubscribe to remove)
|
|
40
|
+
def subscribe(pattern, &block)
|
|
41
|
+
entry = { pattern: pattern, handler: block }
|
|
42
|
+
@mutex.synchronize { @subscribers << entry }
|
|
43
|
+
entry
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Remove a previously registered subscription.
|
|
47
|
+
# @param handle [Hash] the value returned by #subscribe
|
|
48
|
+
def unsubscribe(handle)
|
|
49
|
+
@mutex.synchronize { @subscribers.delete(handle) }
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Remove all subscriptions. Useful in tests.
|
|
53
|
+
def clear!
|
|
54
|
+
@mutex.synchronize { @subscribers.clear }
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# @return [Integer] number of active subscriptions
|
|
58
|
+
def subscriber_count
|
|
59
|
+
@mutex.synchronize { @subscribers.size }
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Easyop
|
|
4
|
+
module Events
|
|
5
|
+
# Namespace for bus adapters.
|
|
6
|
+
# All adapters inherit from Easyop::Events::Bus::Base.
|
|
7
|
+
module Bus
|
|
8
|
+
# Abstract base for bus adapters.
|
|
9
|
+
#
|
|
10
|
+
# Subclasses must implement:
|
|
11
|
+
# #publish(event) — deliver event to matching subscribers
|
|
12
|
+
# #subscribe(pattern, &block) — register a handler block
|
|
13
|
+
#
|
|
14
|
+
# Optionally override:
|
|
15
|
+
# #unsubscribe(handle) — remove a subscription (handle is return value of #subscribe)
|
|
16
|
+
class Base
|
|
17
|
+
# @param event [Easyop::Events::Event]
|
|
18
|
+
def publish(event)
|
|
19
|
+
raise NotImplementedError, "#{self.class}#publish must be implemented"
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# @param pattern [String, Regexp] event name, glob ("order.*"), or Regexp
|
|
23
|
+
# @yield [event] called when a matching event is published
|
|
24
|
+
# @return [Object] subscription handle (adapter-specific)
|
|
25
|
+
def subscribe(pattern, &block)
|
|
26
|
+
raise NotImplementedError, "#{self.class}#subscribe must be implemented"
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Remove a subscription created by #subscribe.
|
|
30
|
+
# @param handle [Object] the value returned by #subscribe
|
|
31
|
+
def unsubscribe(handle)
|
|
32
|
+
# default no-op — adapters may override
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
private
|
|
36
|
+
|
|
37
|
+
# Returns true when +pattern+ matches +event_name+.
|
|
38
|
+
# Supports exact strings, glob patterns ("order.*", "order.**"), and Regexp.
|
|
39
|
+
#
|
|
40
|
+
# Glob rules:
|
|
41
|
+
# "*" — matches any segment that doesn't cross a dot
|
|
42
|
+
# "**" — matches any string including dots (greedy)
|
|
43
|
+
#
|
|
44
|
+
# @param pattern [String, Regexp]
|
|
45
|
+
# @param event_name [String]
|
|
46
|
+
def _pattern_matches?(pattern, event_name)
|
|
47
|
+
case pattern
|
|
48
|
+
when Regexp
|
|
49
|
+
pattern.match?(event_name)
|
|
50
|
+
when String
|
|
51
|
+
pattern.include?("*") ? _glob_to_regex(pattern).match?(event_name) : pattern == event_name
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# @param glob [String] e.g. "order.*" or "warehouse.**"
|
|
56
|
+
# @return [Regexp]
|
|
57
|
+
def _glob_to_regex(glob)
|
|
58
|
+
escaped = Regexp.escape(glob)
|
|
59
|
+
.gsub("\\*\\*", ".+")
|
|
60
|
+
.gsub("\\*", "[^.]+")
|
|
61
|
+
Regexp.new("\\A#{escaped}\\z")
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Easyop
|
|
4
|
+
module Events
|
|
5
|
+
# An immutable domain event value object.
|
|
6
|
+
#
|
|
7
|
+
# @example
|
|
8
|
+
# event = Easyop::Events::Event.new(
|
|
9
|
+
# name: "order.placed",
|
|
10
|
+
# payload: { order_id: 42, total: 9900 },
|
|
11
|
+
# source: "PlaceOrder"
|
|
12
|
+
# )
|
|
13
|
+
# event.name # => "order.placed"
|
|
14
|
+
# event.payload # => { order_id: 42, total: 9900 }
|
|
15
|
+
# event.frozen? # => true
|
|
16
|
+
class Event
|
|
17
|
+
attr_reader :name, :payload, :metadata, :timestamp, :source
|
|
18
|
+
|
|
19
|
+
# @param name [String] event name, e.g. "order.placed"
|
|
20
|
+
# @param payload [Hash] domain data extracted from ctx
|
|
21
|
+
# @param metadata [Hash] extra envelope data (correlation_id, etc.)
|
|
22
|
+
# @param timestamp [Time] defaults to Time.now
|
|
23
|
+
# @param source [String] class name of the emitting operation
|
|
24
|
+
def initialize(name:, payload: {}, metadata: {}, timestamp: nil, source: nil)
|
|
25
|
+
@name = name.to_s.freeze
|
|
26
|
+
@payload = payload.freeze
|
|
27
|
+
@metadata = metadata.freeze
|
|
28
|
+
@timestamp = (timestamp || Time.now).freeze
|
|
29
|
+
@source = source&.freeze
|
|
30
|
+
freeze
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# @return [Hash] serializable hash representation
|
|
34
|
+
def to_h
|
|
35
|
+
{ name: @name, payload: @payload, metadata: @metadata,
|
|
36
|
+
timestamp: @timestamp, source: @source }
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def inspect
|
|
40
|
+
"#<Easyop::Events::Event name=#{@name.inspect} source=#{@source.inspect}>"
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Easyop
|
|
4
|
+
module Events
|
|
5
|
+
# Thread-safe global registry for the event bus and handler subscriptions.
|
|
6
|
+
#
|
|
7
|
+
# The Registry is the coordination point between the Events plugin (producer)
|
|
8
|
+
# and the EventHandlers plugin (subscriber). Neither plugin references the other
|
|
9
|
+
# directly — they communicate only through the bus managed here.
|
|
10
|
+
#
|
|
11
|
+
# Configure the bus once at boot (e.g. in a Rails initializer):
|
|
12
|
+
#
|
|
13
|
+
# Easyop::Events::Registry.bus = :memory # default, in-process
|
|
14
|
+
# Easyop::Events::Registry.bus = :active_support # ActiveSupport::Notifications
|
|
15
|
+
# Easyop::Events::Registry.bus = MyRabbitBus.new # custom adapter
|
|
16
|
+
#
|
|
17
|
+
# Or via the global config:
|
|
18
|
+
#
|
|
19
|
+
# Easyop.configure { |c| c.event_bus = :active_support }
|
|
20
|
+
#
|
|
21
|
+
# IMPORTANT: Configure the bus BEFORE handler classes are loaded (before
|
|
22
|
+
# autoloading runs in Rails). Subscriptions are registered at class load time
|
|
23
|
+
# and are bound to whatever bus is active at that moment.
|
|
24
|
+
class Registry
|
|
25
|
+
@mutex = Mutex.new
|
|
26
|
+
|
|
27
|
+
class << self
|
|
28
|
+
# Set the global bus adapter.
|
|
29
|
+
#
|
|
30
|
+
# @param bus_or_symbol [:memory, :active_support, Bus::Base, Object]
|
|
31
|
+
def bus=(bus_or_symbol)
|
|
32
|
+
@mutex.synchronize { @bus = _resolve_bus(bus_or_symbol) }
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Returns the active bus adapter. Defaults to a Memory bus.
|
|
36
|
+
#
|
|
37
|
+
# Falls back to Easyop.config.event_bus if set, then :memory.
|
|
38
|
+
#
|
|
39
|
+
# @return [Bus::Base]
|
|
40
|
+
def bus
|
|
41
|
+
@mutex.synchronize do
|
|
42
|
+
@bus ||= _resolve_bus(
|
|
43
|
+
defined?(Easyop.config) && Easyop.config.respond_to?(:event_bus) && Easyop.config.event_bus ||
|
|
44
|
+
:memory
|
|
45
|
+
)
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Register a handler operation as a subscriber for +pattern+.
|
|
50
|
+
#
|
|
51
|
+
# Called automatically by the EventHandlers plugin when `on` is declared.
|
|
52
|
+
#
|
|
53
|
+
# @param pattern [String, Regexp] event name or glob
|
|
54
|
+
# @param handler_class [Class] operation class to invoke
|
|
55
|
+
# @param async [Boolean] use call_async (requires Async plugin)
|
|
56
|
+
# @param options [Hash] e.g. queue: "low"
|
|
57
|
+
def register_handler(pattern:, handler_class:, async: false, **options)
|
|
58
|
+
entry = { pattern: pattern, handler_class: handler_class,
|
|
59
|
+
async: async, options: options }
|
|
60
|
+
|
|
61
|
+
@mutex.synchronize { _subscriptions << entry }
|
|
62
|
+
|
|
63
|
+
bus.subscribe(pattern) { |event| _dispatch(event, entry) }
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Returns a copy of all registered handler entries (for introspection).
|
|
67
|
+
#
|
|
68
|
+
# @return [Array<Hash>]
|
|
69
|
+
def subscriptions
|
|
70
|
+
@mutex.synchronize { _subscriptions.dup }
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# Reset the registry: drop all subscriptions and replace the bus with a
|
|
74
|
+
# fresh Memory instance. Intended for use in tests (called in before/after hooks).
|
|
75
|
+
def reset!
|
|
76
|
+
@mutex.synchronize do
|
|
77
|
+
@bus = nil
|
|
78
|
+
@subscriptions = nil
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
private
|
|
83
|
+
|
|
84
|
+
def _subscriptions
|
|
85
|
+
@subscriptions ||= []
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# @api private — exposed for specs
|
|
89
|
+
def _dispatch(event, entry)
|
|
90
|
+
handler_class = entry[:handler_class]
|
|
91
|
+
|
|
92
|
+
if entry[:async] && handler_class.respond_to?(:call_async)
|
|
93
|
+
# Serialize Event to a plain hash for ActiveJob compatibility.
|
|
94
|
+
attrs = event.payload.merge(event_data: event.to_h)
|
|
95
|
+
queue = entry[:options][:queue]
|
|
96
|
+
handler_class.call_async(attrs, **(queue ? { queue: queue } : {}))
|
|
97
|
+
else
|
|
98
|
+
# Sync: pass Event object directly — ctx.event is the live Event instance.
|
|
99
|
+
handler_class.call(event: event, **event.payload)
|
|
100
|
+
end
|
|
101
|
+
rescue StandardError
|
|
102
|
+
# Handler failures must not propagate back to the publisher.
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def _resolve_bus(bus_or_symbol)
|
|
106
|
+
case bus_or_symbol
|
|
107
|
+
when :memory
|
|
108
|
+
Bus::Memory.new
|
|
109
|
+
when :active_support
|
|
110
|
+
Bus::ActiveSupportNotifications.new
|
|
111
|
+
when Bus::Base
|
|
112
|
+
bus_or_symbol
|
|
113
|
+
when nil
|
|
114
|
+
Bus::Memory.new
|
|
115
|
+
else
|
|
116
|
+
if bus_or_symbol.respond_to?(:publish) && bus_or_symbol.respond_to?(:subscribe)
|
|
117
|
+
Bus::Custom.new(bus_or_symbol)
|
|
118
|
+
else
|
|
119
|
+
raise ArgumentError,
|
|
120
|
+
"Unknown bus: #{bus_or_symbol.inspect}. " \
|
|
121
|
+
"Use :memory, :active_support, or a bus adapter instance."
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
end
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Easyop
|
|
4
|
+
module Plugins
|
|
5
|
+
# Subscribes an operation class to handle domain events.
|
|
6
|
+
#
|
|
7
|
+
# The handler operation receives the event payload merged into ctx, plus
|
|
8
|
+
# ctx.event (the Easyop::Events::Event object itself for sync dispatch).
|
|
9
|
+
#
|
|
10
|
+
# Basic usage:
|
|
11
|
+
#
|
|
12
|
+
# class SendOrderConfirmation < ApplicationOperation
|
|
13
|
+
# plugin Easyop::Plugins::EventHandlers
|
|
14
|
+
#
|
|
15
|
+
# on "order.placed"
|
|
16
|
+
#
|
|
17
|
+
# def call
|
|
18
|
+
# event = ctx.event # Easyop::Events::Event
|
|
19
|
+
# order_id = ctx.order_id # payload keys merged into ctx
|
|
20
|
+
# OrderMailer.confirm(order_id).deliver_later
|
|
21
|
+
# end
|
|
22
|
+
# end
|
|
23
|
+
#
|
|
24
|
+
# Async dispatch (requires Easyop::Plugins::Async also installed):
|
|
25
|
+
#
|
|
26
|
+
# class IndexOrder < ApplicationOperation
|
|
27
|
+
# plugin Easyop::Plugins::Async, queue: "indexing"
|
|
28
|
+
# plugin Easyop::Plugins::EventHandlers
|
|
29
|
+
#
|
|
30
|
+
# on "order.*", async: true
|
|
31
|
+
# on "inventory.**", async: true, queue: "low"
|
|
32
|
+
#
|
|
33
|
+
# def call
|
|
34
|
+
# # For async dispatch ctx.event_data is a Hash (serialized for ActiveJob).
|
|
35
|
+
# # Reconstruct if needed: Easyop::Events::Event.new(**ctx.event_data)
|
|
36
|
+
# SearchIndex.reindex(ctx.order_id)
|
|
37
|
+
# end
|
|
38
|
+
# end
|
|
39
|
+
#
|
|
40
|
+
# Wildcard patterns:
|
|
41
|
+
# "order.*" — matches order.placed, order.shipped (not order.payment.failed)
|
|
42
|
+
# "warehouse.**" — matches warehouse.stock.updated, warehouse.zone.moved, etc.
|
|
43
|
+
module EventHandlers
|
|
44
|
+
def self.install(base, **_options)
|
|
45
|
+
base.extend(ClassMethods)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
module ClassMethods
|
|
49
|
+
# Subscribe this operation to events matching +pattern+.
|
|
50
|
+
#
|
|
51
|
+
# Registration happens at class-load time and is bound to the bus that is
|
|
52
|
+
# active when the class is evaluated. Configure the bus before loading
|
|
53
|
+
# handler classes (e.g. in a Rails initializer that runs before autoloading).
|
|
54
|
+
#
|
|
55
|
+
# @param pattern [String, Regexp] event name or glob
|
|
56
|
+
# @param async [Boolean] enqueue via call_async (requires Async plugin)
|
|
57
|
+
# @param options [Hash] e.g. queue: "low" (overrides Async default)
|
|
58
|
+
def on(pattern, async: false, **options)
|
|
59
|
+
_event_handler_registrations << { pattern: pattern, async: async, options: options }
|
|
60
|
+
|
|
61
|
+
Easyop::Events::Registry.register_handler(
|
|
62
|
+
pattern: pattern,
|
|
63
|
+
handler_class: self,
|
|
64
|
+
async: async,
|
|
65
|
+
**options
|
|
66
|
+
)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# @api private — list of registrations declared on this class
|
|
70
|
+
def _event_handler_registrations
|
|
71
|
+
@_event_handler_registrations ||= []
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Easyop
|
|
4
|
+
module Plugins
|
|
5
|
+
# Emits domain events after an operation completes.
|
|
6
|
+
#
|
|
7
|
+
# Install on a base operation class to inherit into all subclasses:
|
|
8
|
+
#
|
|
9
|
+
# class ApplicationOperation
|
|
10
|
+
# include Easyop::Operation
|
|
11
|
+
# plugin Easyop::Plugins::Events
|
|
12
|
+
# end
|
|
13
|
+
#
|
|
14
|
+
# Then declare events on individual operations:
|
|
15
|
+
#
|
|
16
|
+
# class PlaceOrder < ApplicationOperation
|
|
17
|
+
# emits "order.placed", on: :success, payload: [:order_id, :total]
|
|
18
|
+
# emits "order.failed", on: :failure, payload: ->(ctx) { { error: ctx.error } }
|
|
19
|
+
# emits "order.attempted", on: :always
|
|
20
|
+
#
|
|
21
|
+
# def call
|
|
22
|
+
# ctx.order_id = Order.create!(ctx.to_h).id
|
|
23
|
+
# end
|
|
24
|
+
# end
|
|
25
|
+
#
|
|
26
|
+
# Plugin options:
|
|
27
|
+
# bus: [Bus::Base, nil] per-class bus override (default: global registry bus)
|
|
28
|
+
# metadata: [Hash, Proc, nil] extra metadata merged into every event from this class
|
|
29
|
+
#
|
|
30
|
+
# `emits` DSL options:
|
|
31
|
+
# on: [:success (default), :failure, :always]
|
|
32
|
+
# payload: [Proc, Array, nil] Proc receives ctx; Array slices ctx keys; nil = full ctx.to_h
|
|
33
|
+
# guard: [Proc, nil] extra condition — event only fires if truthy
|
|
34
|
+
module Events
|
|
35
|
+
def self.install(base, bus: nil, metadata: nil, **_options)
|
|
36
|
+
base.extend(ClassMethods)
|
|
37
|
+
base.prepend(RunWrapper)
|
|
38
|
+
base.instance_variable_set(:@_events_bus, bus)
|
|
39
|
+
base.instance_variable_set(:@_events_metadata, metadata)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
module ClassMethods
|
|
43
|
+
# Declare an event this operation emits after execution.
|
|
44
|
+
#
|
|
45
|
+
# @example
|
|
46
|
+
# emits "order.placed", on: :success, payload: [:order_id]
|
|
47
|
+
# emits "order.failed", on: :failure, payload: ->(ctx) { { error: ctx.error } }
|
|
48
|
+
#
|
|
49
|
+
# @param name [String]
|
|
50
|
+
# @param on [Symbol] :success (default), :failure, or :always
|
|
51
|
+
# @param payload [Proc, Array, nil]
|
|
52
|
+
# @param guard [Proc, nil] optional condition — fires only when truthy
|
|
53
|
+
def emits(name, on: :success, payload: nil, guard: nil)
|
|
54
|
+
_emitted_events << { name: name.to_s, on: on, payload: payload, guard: guard }
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# @api private — inheritable list of emits declarations
|
|
58
|
+
def _emitted_events
|
|
59
|
+
@_emitted_events ||= _inherited_emitted_events
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# @api private — bus for this class (falls back to superclass, then global registry)
|
|
63
|
+
def _events_bus
|
|
64
|
+
if instance_variable_defined?(:@_events_bus)
|
|
65
|
+
@_events_bus
|
|
66
|
+
elsif superclass.respond_to?(:_events_bus)
|
|
67
|
+
superclass._events_bus
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# @api private — metadata for this class (falls back to superclass)
|
|
72
|
+
def _events_metadata
|
|
73
|
+
if instance_variable_defined?(:@_events_metadata)
|
|
74
|
+
@_events_metadata
|
|
75
|
+
elsif superclass.respond_to?(:_events_metadata)
|
|
76
|
+
superclass._events_metadata
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
private
|
|
81
|
+
|
|
82
|
+
def _inherited_emitted_events
|
|
83
|
+
superclass.respond_to?(:_emitted_events) ? superclass._emitted_events.dup : []
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
module RunWrapper
|
|
88
|
+
# Wraps _easyop_run to publish declared events in an ensure block.
|
|
89
|
+
# Events fire AFTER the operation completes — success, failure, or exception.
|
|
90
|
+
def _easyop_run(ctx, raise_on_failure:)
|
|
91
|
+
super
|
|
92
|
+
ensure
|
|
93
|
+
_events_publish_all(ctx)
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
private
|
|
97
|
+
|
|
98
|
+
def _events_publish_all(ctx)
|
|
99
|
+
declarations = self.class._emitted_events
|
|
100
|
+
return if declarations.empty?
|
|
101
|
+
|
|
102
|
+
bus = self.class._events_bus || Easyop::Events::Registry.bus
|
|
103
|
+
|
|
104
|
+
declarations.each do |decl|
|
|
105
|
+
next unless _events_should_fire?(decl, ctx)
|
|
106
|
+
|
|
107
|
+
event = _events_build_event(decl, ctx)
|
|
108
|
+
bus.publish(event)
|
|
109
|
+
rescue StandardError
|
|
110
|
+
# Individual publish failures must never crash the operation.
|
|
111
|
+
# Log if Rails is available.
|
|
112
|
+
_events_log_warning($ERROR_INFO)
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def _events_should_fire?(decl, ctx)
|
|
117
|
+
case decl[:on]
|
|
118
|
+
when :success then return false unless ctx.success?
|
|
119
|
+
when :failure then return false unless ctx.failure?
|
|
120
|
+
when :always then nil # always fire
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
guard = decl[:guard]
|
|
124
|
+
guard ? guard.call(ctx) : true
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def _events_build_event(decl, ctx)
|
|
128
|
+
Easyop::Events::Event.new(
|
|
129
|
+
name: decl[:name],
|
|
130
|
+
payload: _events_extract_payload(decl[:payload], ctx),
|
|
131
|
+
metadata: _events_build_metadata(ctx),
|
|
132
|
+
source: self.class.name
|
|
133
|
+
)
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def _events_extract_payload(payload_spec, ctx)
|
|
137
|
+
case payload_spec
|
|
138
|
+
when Proc then payload_spec.call(ctx)
|
|
139
|
+
when Array then ctx.slice(*payload_spec)
|
|
140
|
+
when nil then ctx.to_h
|
|
141
|
+
else payload_spec
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def _events_build_metadata(ctx)
|
|
146
|
+
meta = self.class._events_metadata
|
|
147
|
+
case meta
|
|
148
|
+
when Proc then meta.call(ctx)
|
|
149
|
+
when Hash then meta.dup
|
|
150
|
+
else {}
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def _events_log_warning(err)
|
|
155
|
+
return unless defined?(Rails) && Rails.respond_to?(:logger) && Rails.logger
|
|
156
|
+
|
|
157
|
+
Rails.logger.warn "[EasyOp::Events] Failed to publish event: #{err.message}"
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
end
|
data/lib/easyop/version.rb
CHANGED
data/lib/easyop.rb
CHANGED
|
@@ -18,6 +18,17 @@ require_relative "easyop/flow"
|
|
|
18
18
|
# require "easyop/plugins/recording"
|
|
19
19
|
# require "easyop/plugins/async"
|
|
20
20
|
|
|
21
|
+
# Domain event plugins — require together or individually:
|
|
22
|
+
# require "easyop/events/event"
|
|
23
|
+
# require "easyop/events/bus"
|
|
24
|
+
# require "easyop/events/bus/memory"
|
|
25
|
+
# require "easyop/events/bus/active_support_notifications"
|
|
26
|
+
# require "easyop/events/bus/custom"
|
|
27
|
+
# require "easyop/events/bus/adapter" # inherit this to build a custom bus
|
|
28
|
+
# require "easyop/events/registry"
|
|
29
|
+
# require "easyop/plugins/events"
|
|
30
|
+
# require "easyop/plugins/event_handlers"
|
|
31
|
+
|
|
21
32
|
module Easyop
|
|
22
33
|
# Convenience: inherit from this instead of including Easyop::Operation
|
|
23
34
|
# when you want a common base class for all your operations.
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: easyop
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.1.
|
|
4
|
+
version: 0.1.3
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Pawel Niemczyk
|
|
@@ -53,12 +53,21 @@ files:
|
|
|
53
53
|
- lib/easyop.rb
|
|
54
54
|
- lib/easyop/configuration.rb
|
|
55
55
|
- lib/easyop/ctx.rb
|
|
56
|
+
- lib/easyop/events/bus.rb
|
|
57
|
+
- lib/easyop/events/bus/active_support_notifications.rb
|
|
58
|
+
- lib/easyop/events/bus/adapter.rb
|
|
59
|
+
- lib/easyop/events/bus/custom.rb
|
|
60
|
+
- lib/easyop/events/bus/memory.rb
|
|
61
|
+
- lib/easyop/events/event.rb
|
|
62
|
+
- lib/easyop/events/registry.rb
|
|
56
63
|
- lib/easyop/flow.rb
|
|
57
64
|
- lib/easyop/flow_builder.rb
|
|
58
65
|
- lib/easyop/hooks.rb
|
|
59
66
|
- lib/easyop/operation.rb
|
|
60
67
|
- lib/easyop/plugins/async.rb
|
|
61
68
|
- lib/easyop/plugins/base.rb
|
|
69
|
+
- lib/easyop/plugins/event_handlers.rb
|
|
70
|
+
- lib/easyop/plugins/events.rb
|
|
62
71
|
- lib/easyop/plugins/instrumentation.rb
|
|
63
72
|
- lib/easyop/plugins/recording.rb
|
|
64
73
|
- lib/easyop/plugins/transactional.rb
|