Ecosyste.ms: Awesome
An open API service indexing awesome lists of open source software.
https://github.com/palkan/active_event_store
Rails Event Store in a more Rails way
https://github.com/palkan/active_event_store
component-architecture event-sourcing hacktoberfest rails
Last synced: 6 days ago
JSON representation
Rails Event Store in a more Rails way
- Host: GitHub
- URL: https://github.com/palkan/active_event_store
- Owner: palkan
- License: mit
- Created: 2020-04-09T09:09:27.000Z (over 4 years ago)
- Default Branch: master
- Last Pushed: 2024-05-21T21:54:31.000Z (8 months ago)
- Last Synced: 2024-12-20T14:09:44.895Z (13 days ago)
- Topics: component-architecture, event-sourcing, hacktoberfest, rails
- Language: Ruby
- Homepage:
- Size: 83 KB
- Stars: 186
- Watchers: 6
- Forks: 13
- Open Issues: 2
-
Metadata Files:
- Readme: README.md
- Changelog: CHANGELOG.md
- License: LICENSE.txt
Awesome Lists containing this project
README
[![Gem Version](https://badge.fury.io/rb/active_event_store.svg)](https://rubygems.org/gems/active_event_store) [![Build](https://github.com/palkan/active_event_store/workflows/Build/badge.svg)](https://github.com/palkan/active_event_store/actions)
# Active Event Store
Active Event Store is a wrapper over [Rails Event Store](https://railseventstore.org/) which adds conventions and transparent Rails integration.
## Motivation
Why creating a wrapper and not using Rails Event Store itself?
RES is an awesome project but, in our opinion, it lacks Rails simplicity and elegance (=conventions and less boilerplate). It's an advanced tool for advanced developers. We've been using it in multiple projects in a similar way, and decided to extract our approach into this gem (originally private).
Secondly, we wanted to have a store implementation independent API that would allow us to adapterize the actual event store in the future (something like `ActiveEventStore.store_engine = :rails_event_store` or `ActiveEventStore.store_engine = :hanami_events`).
## Installation
Add the gem to your project:
```ruby
# Gemfile
gem "active_event_store", "~> 1.0"
```Setup database according to the [Rails Event Store docs](https://railseventstore.org/docs/install/#setup-data-model):
```sh
rails generate rails_event_store_active_record:migration
rails db:migrate
```### Requirements
- Ruby (MRI) >= 2.6
- Rails >= 6.0
- RailsEventStore >= 2.1## Usage
### Describe events
Events are represented by _event classes_, which describe events payloads and identifiers:
```ruby
class ProfileCompleted < ActiveEventStore::Event
# (optional) event identifier is used for transmitting events
# to subscribers.
#
# By default, identifier is equal to `name.underscore.gsub('/', '.')`.
#
# You don't need to specify identifier manually, only for backward compatibility when
# class name is changed.
self.identifier = "profile_completed"# Add attributes accessors
attributes :user_id# Sync attributes only available for sync subscribers
# (so you can add some optional non-JSON serializable data here)
# For example, we can also add `user` record to the event to avoid
# reloading in sync subscribers
sync_attributes :user
end
```**NOTE:** we use JSON to [serialize events](https://railseventstore.org/docs/mapping_serialization/), thus only the simple field types (numbers, strings, booleans) are supported.
Each event has predefined (_reserved_) fields:
- `event_id` – unique event id
- `type` – event type (=identifier)
- `metadata`We suggest to use a naming convention for event classes, for example, using the past tense and describe what happened (e.g. "ProfileCreated", "EventPublished", etc.).
We recommend to keep event definitions in the `app/events` folder.
### Events registration
Since we use _abstract_ identifiers instead of class names, we need a way to tell our _mapper_ how to infer an event class from its type.
In most cases, we register events automatically when they're published or when a subscription is created.
You can also register events manually:
```ruby
# by passing an event class
ActiveEventStore.mapping.register_event MyEventClass# or more precisely (in that case `event.type` must be equal to "my_event")
ActiveEventStore.mapping.register "my_event", MyEventClass
```### Publish events
To publish an event you must first create an instance of the event class and call `ActiveEventStore.publish` method:
```ruby
event = ProfileCompleted.new(user_id: user.id)# or with metadata
event = ProfileCompleted.new(user_id: user.id, metadata: {ip: request.remote_ip})# then publish the event
ActiveEventStore.publish(event)
```That's it! Your event has been stored and propagated to the subscribers.
### Subscribe to events
To subscribe a handler to an event you must use `ActiveEventStore.subscribe` method.
You can do this in your app or engine initializer:
```ruby
# some/engine.rb# To make sure event store has been initialized use the load hook
# `store` == `ActiveEventStore`
ActiveSupport.on_load :active_event_store do |store|
# async subscriber – invoked from background job, enqueued after the current transaction commits
# NOTE: all subscribers are asynchronous by default
store.subscribe MyEventHandler, to: ProfileCreated# sync subscriber – invoked right "within" `publish` method
store.subscribe MyEventHandler, to: ProfileCreated, sync: true# anonymous handler (could only be synchronous)
store.subscribe(to: ProfileCreated, sync: true) do |event|
# do something
end# you can omit event if your subscriber follows the convention
# for example, the following subscriber would subscribe to
# ProfileCreated event
store.subscribe OnProfileCreated::DoThat
end
```Subscribers could be any callable Ruby objects that accept a single argument (event) as its input or classes that inherit from `Class` and have `#call` as an instance method.
We suggest putting subscribers to the `app/subscribers` folder using the following convention: `app/subscribers/on_/`, e.g. `app/subscribers/on_profile_created/create_chat_user.rb`.
**NOTE:** Active Job must be loaded to use async subscribers (i.e., `require "active_job/railtie"` or `require "rails/all"` in your `config/application.rb`).
**NOTE:** Subscribers that inherit from `Class` and implement `call` as a class method will not be instantiated.
### Testing
You can test subscribers as normal Ruby objects.
**NOTE** To test using minitest include the `ActiveEventStore::TestHelpers` module in your tests.
To test that a given subscriber exists, you can use the `have_enqueued_async_subscriber_for` matcher:
```ruby
# for asynchronous subscriptions (rspec)
it "is subscribed to some event" do
event = MyEvent.new(some: "data")
expect { ActiveEventStore.publish event }
.to have_enqueued_async_subscriber_for(MySubscriberService)
.with(event)
end# for asynchronous subscriptions (minitest)
def test_is_subscribed_to_some_event
event = MyEvent.new(some: "data")assert_async_event_subscriber_enqueued(MySubscriberService, event: event) do
ActiveEventStore.publish event
end
end
```**NOTE** Async event subscribers are queued only after the current transaction has committed so when using `assert_enqued_async_subcriber` in rails
make sure to have `self.use_transactional_fixtures = false` at the top of your test class.**NOTE:** You must have `rspec-rails` gem in your bundle to use `have_enqueued_async_subscriber_for` matcher.
For synchronous subscribers using `have_received` is enough:
```ruby
it "is subscribed to some event" do
allow(MySubscriberService).to receive(:call)event = MyEvent.new(some: "data")
ActiveEventStore.publish event
expect(MySubscriberService).to have_received(:call).with(event)
end
```To test event publishing, use `have_published_event` matcher:
```ruby
# rspec
expect { subject }.to have_published_event(ProfileCreated).with(user_id: user.id)# minitest
assert_event_published(ProfileCreated, with: {user_id: user.id}) { subject }
```**NOTE:** `have_published_event` and `assert_event_published` only supports block expectations.
**NOTE 2** `with` modifier works like `have_attributes` matcher (not `contain_exactly`); you can only specify serializable attributes in `with` (i.e. sync attributes are not supported, 'cause they are not persistent).
## Contributing
Bug reports and pull requests are welcome on GitHub at [https://github.com/palkan/active_event_store](https://github.com/palkan/active_event_store).
## License
The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).