Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow to dynamically enrich events metadata by using with_metadata #327

Merged
merged 16 commits into from
May 3, 2018
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 28 additions & 6 deletions rails_event_store/lib/rails_event_store/middleware.rb
Original file line number Diff line number Diff line change
@@ -1,15 +1,37 @@
module RailsEventStore
class Middleware
def initialize(app, request_metadata_proc)
def initialize(app)
@app = app
@request_metadata_proc = request_metadata_proc
end

def call(env)
Thread.current[:rails_event_store] = @request_metadata_proc.(env)
@app.call(env)
ensure
Thread.current[:rails_event_store] = nil
if @app.config.respond_to?(:event_store)
@app.config.event_store.with_metadata(request_metadata(env)) do
@app.call(env)
end
else
@app.call(env)
end
end

def request_metadata(env)
(metadata_proc || default_request_metadata).call(env)
end

private

def metadata_proc
@app.config.x.rails_event_store.request_metadata if @app.config.x.rails_event_store.request_metadata.respond_to?(:call)
end

def default_request_metadata
->(env) do
request = ActionDispatch::Request.new(env)
{
remote_ip: request.remote_ip,
request_id: request.uuid
}
end
end
end
end
23 changes: 1 addition & 22 deletions rails_event_store/lib/rails_event_store/railtie.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,28 +4,7 @@
module RailsEventStore
class Railtie < ::Rails::Railtie
initializer 'rails_event_store.middleware' do |rails|
rails.middleware.use(::RailsEventStore::Middleware, RailsConfig.new(rails.config).request_metadata)
end

class RailsConfig
def initialize(config)
@config = config
end

def request_metadata
return default_request_metadata unless @config.respond_to?(:rails_event_store)
@config.rails_event_store.fetch(:request_metadata, default_request_metadata)
end

private
def default_request_metadata
->(env) do
request = ActionDispatch::Request.new(env)
{ remote_ip: request.remote_ip,
request_id: request.uuid,
}
end
end
rails.middleware.use(::RailsEventStore::Middleware)
end
end
end
Expand Down
39 changes: 33 additions & 6 deletions rails_event_store/spec/middleware_integration_spec.rb
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
require 'spec_helper'
require 'action_controller/railtie'
require 'rails_event_store/railtie'
require 'securerandom'
require 'rails_event_store/middleware'
require 'rack/test'
require 'rack/lint'
Expand All @@ -7,16 +10,40 @@ module RailsEventStore
RSpec.describe Middleware do
DummyEvent = Class.new(RailsEventStore::Event)

specify do
event_store = Client.new
specify 'works without event store instance' do
request = ::Rack::MockRequest.new(Middleware.new(app))
expect {request.get('/')}.not_to raise_error
end

specify 'sets domain events metadata for events published with global event store instance' do
app.config.event_store = event_store
app.config.x.rails_event_store = {
request_metadata: -> env { {server_name: env['SERVER_NAME']} }
}

request = ::Rack::MockRequest.new(Middleware.new(
->(env) { event_store.publish_event(DummyEvent.new); [200, {}, ["Hello World from #{env["SERVER_NAME"]}"]] },
->(env) { { server_name: env['SERVER_NAME'] }}))
request = ::Rack::MockRequest.new(Middleware.new(app))
request.get('/')

event_store.read_all_streams_forward.map(&:metadata).each do |metadata|
expect(metadata[:server_name]).to eq('example.org')
expect(metadata[:server_name]).to eq('example.org')
end
end

def event_store
@event_store ||= Client.new
end

def app
Class.new(::Rails::Application) do
def self.name
"TestRails::Application"
end
end.tap do |app|
app.config.eager_load = false
app.config.secret_key_base = SecureRandom.hex
app.initialize!
app.routes.draw { root(to: ->(env) {event_store.publish_event(DummyEvent.new); [200, {}, ['']]}) }
app.default_url_options = { host: 'example.com' }
end
end
end
Expand Down
81 changes: 58 additions & 23 deletions rails_event_store/spec/middleware_spec.rb
Original file line number Diff line number Diff line change
@@ -1,42 +1,77 @@
require 'spec_helper'
require 'action_controller/railtie'
require 'rails_event_store/railtie'
require 'securerandom'
require 'rails_event_store/middleware'
require 'rack/lint'

module RailsEventStore
RSpec.describe Middleware do
specify 'lint' do
request = ::Rack::MockRequest.new(::Rack::Lint.new(Middleware.new(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why have you decided to removeRack::Lint that verifies Middleware in accordance to Rack specification?

->(env) { [200, {}, ['Hello World']] },
->(env) { { kaka: 'dudu' } } )))
specify 'calls app within with_metadata block when app has configured the event store instance' do
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Isn't that a bit too much of testing implementation details, something which one would call klasotesty?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nvmd, changed later

app.config.event_store = event_store = Client.new
expect(app).to receive(:call).with(dummy_env)
middleware = Middleware.new(app)
expect(event_store).to receive(:with_metadata).with(request_id: 'dummy_id', remote_ip: 'dummy_ip').and_call_original
middleware.call(dummy_env)
end

specify 'just calls the app when app has not configured the event store instance' do
expect(app).to receive(:call).with(dummy_env)
middleware = Middleware.new(app)
middleware.call(dummy_env)
end

specify 'use config.rails_event_store.request_metadata' do
app.config.x.rails_event_store.request_metadata = kaka_dudu
middleware = Middleware.new(app)

expect { request.get('/') }.to_not raise_error
expect(middleware.request_metadata(dummy_env)).to eq({
kaka: 'dudu'
})
end

specify do
request = ::Rack::MockRequest.new(Middleware.new(
->(env) { [200, {}, ['Hello World']] },
->(env) { { kaka: 'dudu' } } ))
request.get('/')
specify 'use config.rails_event_store.request_metadata is not callable' do
app.config.x.rails_event_store.request_metadata = {}
middleware = Middleware.new(app)

expect(Thread.current[:rails_event_store]).to be_nil
expect(middleware.request_metadata(dummy_env)).to eq({
request_id: 'dummy_id',
remote_ip: 'dummy_ip'
})
end

specify do
request = ::Rack::MockRequest.new(Middleware.new(
->(env) { raise },
->(env) { { kaka: 'dudu' } } ))
specify 'use config.rails_event_store.request_metadata is not set' do
middleware = Middleware.new(app)

expect { request.get('/') }.to raise_error(RuntimeError)
expect(Thread.current[:rails_event_store]).to be_nil
expect(middleware.request_metadata(dummy_env)).to eq({
request_id: 'dummy_id',
remote_ip: 'dummy_ip'
})
end

specify do
request = ::Rack::MockRequest.new(Middleware.new(
->(env) { [200, {}, ['Hello World']] },
->(env) { raise } ))
def kaka_dudu
->(env) { { kaka: 'dudu' } }
end

def dummy_env
{
'action_dispatch.request_id' => 'dummy_id',
'action_dispatch.remote_ip' => 'dummy_ip'
}
end

expect { request.get('/') }.to raise_error(RuntimeError)
expect(Thread.current[:rails_event_store]).to be_nil
def app
@app ||= Class.new(::Rails::Application) do
def self.name
"TestRails::Application"
end
end.tap do |app|
app.config.eager_load = false
app.config.secret_key_base = SecureRandom.hex
app.initialize!
app.routes.draw { root(to: ->(env) {[200, {}, ['']]}) }
app.default_url_options = { host: 'example.com' }
end
end
end
end
Expand Down
50 changes: 0 additions & 50 deletions rails_event_store/spec/railtie_spec.rb

This file was deleted.

21 changes: 0 additions & 21 deletions rails_event_store/spec/request_metadata_spec.rb

This file was deleted.

33 changes: 0 additions & 33 deletions rails_event_store/spec/support/test_rails.rb

This file was deleted.

57 changes: 57 additions & 0 deletions railseventstore.org/source/docs/request_metadata.html.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,3 +66,60 @@ end
```

You can read more about your possible options by reading [ActionDispatch::Request](http://api.rubyonrails.org/classes/ActionDispatch/Request.html) documentation.

## Passing your own metadata using `with_metadata` method

Apart from using the middleware, you can also set your metadata with `RubyEventStore::Client#with_metadata` method. You can specify custom metadata that will be added to all events published inside a block:

```ruby
event_store.with_metadata(remote_ip: '1.2.3.4', request_id: SecureRandom.uuid) do
event_store.publish(MyEvent.new(data: {foo: 'bar'}))
end

my_event = event_store.read_all_events(RailsEventStore::GLOBAL_STREAM).last

my_event.metadata[:remote_ip] #=> '1.2.3.4'
my_event.metadata[:request_id] #=> unique ID
```

When using `with_metadata`, the `timestamp` is still added to the metadata unless you explicitly specify it on your own. Additionally, if you are also using the middleware & `request_metadata`, your metadata passed as `with_metadata` argument will be merged with the result of `rails_event_store.request_metadata` proc:

```ruby
event_store.with_metadata(causation_id: 1234567890) do
event_store.publish(MyEvent.new(data: {foo: 'bar'}))
end

my_event = event_store.read_all_events(RailsEventStore::GLOBAL_STREAM).last
my_event.metadata[:remote_ip] #=> your IP from request metadata proc
my_event.metadata[:request_id #=> unique ID from request metadata proc
my_event.metadata[:causation_id] #=> 1234567890 from with_metadata argument
my_event.metadata[:timestamp] #=> a timestamp
```

You can nest multiple `with_metadata` calls, in such case the inmost argument is used:

```ruby
event_store.with_metadata(foo: 'bar') do
event_store.with_metadata(foo: 'baz') do
event_store.publish(MyEvent.new)
end
end

my_event = event_store.read_all_events(RailsEventStore::GLOBAL_STREAM).last
my_event.metadata[:foo] #=> 'baz'
```

When you want to clear the metadata for some published events while having them set with `with_metadata`, you can just pass `nil` as an argument (please note that timestamp will still be included in the metadata hash):

```ruby
event_store.with_metadata(foo: 'bar') do
event_store.with_metadata(nil) do
event_store.publish(MyEvent.new)
end
end

my_event = event_store.read_all_events(RailsEventStore::GLOBAL_STREAM).last
my_event.metadata[:foo] #=> nil
my_event.metadata[:timestamp] #=> a timestamp
```

Loading