event_sourcing 0.0.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +24 -0
- data/.rspec +2 -0
- data/Gemfile +7 -0
- data/Guardfile +12 -0
- data/LICENSE.txt +22 -0
- data/README.md +29 -0
- data/Rakefile +13 -0
- data/event_sourcing.gemspec +28 -0
- data/features/steps/whole_stack.rb +29 -0
- data/features/support/env.rb +5 -0
- data/features/support/logger.log +1 -0
- data/features/support/sample_app.rb +40 -0
- data/features/support/spinach.log +0 -0
- data/features/whole_stack.feature +6 -0
- data/lib/event_sourcing.rb +3 -0
- data/lib/event_sourcing/aggregate.rb +30 -0
- data/lib/event_sourcing/aggregate/actor.rb +29 -0
- data/lib/event_sourcing/aggregate/manager.rb +33 -0
- data/lib/event_sourcing/aggregate/manager/cache.rb +17 -0
- data/lib/event_sourcing/aggregate/manager/instance_of.rb +11 -0
- data/lib/event_sourcing/aggregate/manager/reference.rb +14 -0
- data/lib/event_sourcing/aggregate/message.rb +7 -0
- data/lib/event_sourcing/aggregate/wrapper.rb +18 -0
- data/lib/event_sourcing/application.rb +37 -0
- data/lib/event_sourcing/application/actor.rb +40 -0
- data/lib/event_sourcing/application/actor/reference.rb +23 -0
- data/lib/event_sourcing/command.rb +30 -0
- data/lib/event_sourcing/command/bus.rb +16 -0
- data/lib/event_sourcing/event.rb +29 -0
- data/lib/event_sourcing/event/bus.rb +29 -0
- data/lib/event_sourcing/event/bus/reference.rb +28 -0
- data/lib/event_sourcing/event/publisher.rb +47 -0
- data/lib/event_sourcing/event/publisher/reference.rb +11 -0
- data/lib/event_sourcing/event/store.rb +8 -0
- data/lib/event_sourcing/event/store/memory.rb +43 -0
- data/lib/event_sourcing/event/stream.rb +23 -0
- data/lib/event_sourcing/event/subscriber.rb +16 -0
- data/lib/event_sourcing/version.rb +3 -0
- data/spec/concurrent_logging.rb +6 -0
- data/spec/spec_helper.rb +8 -0
- data/spec/support/actor_helpers.rb +11 -0
- data/spec/support/shared_examples/a_store_implementation.rb +70 -0
- data/spec/unit/aggregate/actor_spec.rb +59 -0
- data/spec/unit/aggregate/manager/cache_spec.rb +26 -0
- data/spec/unit/aggregate/manager/reference_spec.rb +14 -0
- data/spec/unit/aggregate/manager_spec.rb +32 -0
- data/spec/unit/aggregate/wrapper_spec.rb +22 -0
- data/spec/unit/aggregate_spec.rb +75 -0
- data/spec/unit/application/actor/reference_spec.rb +25 -0
- data/spec/unit/application/actor_spec.rb +36 -0
- data/spec/unit/application_spec.rb +41 -0
- data/spec/unit/command/bus_spec.rb +15 -0
- data/spec/unit/command_spec.rb +38 -0
- data/spec/unit/event/bus/reference_spec.rb +48 -0
- data/spec/unit/event/bus_spec.rb +41 -0
- data/spec/unit/event/publisher_spec.rb +28 -0
- data/spec/unit/event/store/memory_spec.rb +6 -0
- data/spec/unit/event/stream_spec.rb +30 -0
- data/spec/unit/event/subscriber_spec.rb +27 -0
- data/spec/unit/event_spec.rb +27 -0
- data/spec/unit_helper.rb +14 -0
- metadata +233 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 41f92fcb759e0e8e3aad1d3a38b5a497cb5434c4
|
4
|
+
data.tar.gz: 30ae75bea380d66f7a92b1e314559822f4132ff8
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 4c84d2d1defebb5d6b31766548bd1f68166855c3d26019bf86f90fd7aafe3bc5b143baa82b8a7329c8d1ce9a4b92c8cca3289f1b73ca7c76c004bcdee9e9a0d1
|
7
|
+
data.tar.gz: 20482096bc9d26104f9d30d190196e759d63e6c10dbb7ca019a74737a73b63445e2d4eae9f2165d073e6bfc42bd914029c474cf835c88c94856786bf49513683
|
data/.gitignore
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
*.gem
|
2
|
+
*.rbc
|
3
|
+
.bundle
|
4
|
+
.config
|
5
|
+
.yardoc
|
6
|
+
Gemfile.lock
|
7
|
+
InstalledFiles
|
8
|
+
_yardoc
|
9
|
+
bin/
|
10
|
+
coverage
|
11
|
+
doc/
|
12
|
+
lib/bundler/man
|
13
|
+
log/*
|
14
|
+
pkg
|
15
|
+
rdoc
|
16
|
+
spec/reports
|
17
|
+
test/tmp
|
18
|
+
test/version_tmp
|
19
|
+
tmp
|
20
|
+
*.bundle
|
21
|
+
*.so
|
22
|
+
*.o
|
23
|
+
*.a
|
24
|
+
mkmf.log
|
data/.rspec
ADDED
data/Gemfile
ADDED
data/Guardfile
ADDED
@@ -0,0 +1,12 @@
|
|
1
|
+
guard 'spinach', all_on_start: false, backtrace: true do
|
2
|
+
watch(%r|^features/(.*)\.feature|)
|
3
|
+
watch(%r|^features/steps/(.*)([^/]+)\.rb|) do |m|
|
4
|
+
"features/#{m[1]}#{m[2]}.feature"
|
5
|
+
end
|
6
|
+
end
|
7
|
+
|
8
|
+
guard :rspec, cmd: "bin/rspec --tty" do
|
9
|
+
watch(%r{^spec/.+_spec\.rb$})
|
10
|
+
watch(%r{^lib\/event_sourcing\/(.+)\.rb$}) { |m| "spec/unit/#{m[1]}_spec.rb" }
|
11
|
+
watch('spec/unit_helper.rb') { "spec" }
|
12
|
+
end
|
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2014 Rodrigo Alvarez
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
# EventSourcing
|
2
|
+
|
3
|
+
EventSourcing, as its name suggests, is an Actor based Event Sourcing library for ruby.
|
4
|
+
|
5
|
+
## Installation
|
6
|
+
|
7
|
+
Add this line to your application's Gemfile:
|
8
|
+
|
9
|
+
gem 'event_sourcing'
|
10
|
+
|
11
|
+
And then execute:
|
12
|
+
|
13
|
+
$ bundle
|
14
|
+
|
15
|
+
Or install it yourself as:
|
16
|
+
|
17
|
+
$ gem install event_sourcing
|
18
|
+
|
19
|
+
## Usage
|
20
|
+
|
21
|
+
TODO: Write usage instructions here
|
22
|
+
|
23
|
+
## Contributing
|
24
|
+
|
25
|
+
1. Fork it ( https://github.com/[my-github-username]/event_sourcing/fork )
|
26
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
27
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
28
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
29
|
+
5. Create a new Pull Request
|
data/Rakefile
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
require "bundler/gem_tasks"
|
2
|
+
require 'rspec/core/rake_task'
|
3
|
+
|
4
|
+
RSpec::Core::RakeTask.new(:spec) do |t|
|
5
|
+
t.rspec_opts = "--tty"
|
6
|
+
end
|
7
|
+
|
8
|
+
desc 'Run spinach features'
|
9
|
+
task :spinach do
|
10
|
+
exec "bin/spinach"
|
11
|
+
end
|
12
|
+
|
13
|
+
task :default => [:spec, :spinach]
|
@@ -0,0 +1,28 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'event_sourcing/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "event_sourcing"
|
8
|
+
spec.version = EventSourcing::VERSION
|
9
|
+
spec.authors = ["Rodrigo Alvarez"]
|
10
|
+
spec.email = ["papipo@gmail.com"]
|
11
|
+
spec.summary = %q{Actor based EventSourcing framework for ruby.}
|
12
|
+
spec.homepage = "https://github.com/Papipo/event_sourcing"
|
13
|
+
spec.license = "MIT"
|
14
|
+
|
15
|
+
spec.files = `git ls-files -z`.split("\x0")
|
16
|
+
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
17
|
+
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
18
|
+
spec.require_paths = ["lib"]
|
19
|
+
|
20
|
+
spec.add_dependency "values"
|
21
|
+
|
22
|
+
spec.add_development_dependency "bundler", "~> 1.6"
|
23
|
+
spec.add_development_dependency "rake"
|
24
|
+
spec.add_development_dependency "rspec"
|
25
|
+
spec.add_development_dependency "spinach"
|
26
|
+
spec.add_development_dependency "guard-spinach"
|
27
|
+
spec.add_development_dependency "guard-rspec"
|
28
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
require_relative "../support/sample_app"
|
2
|
+
require "event_sourcing/event/store/memory"
|
3
|
+
|
4
|
+
class Spinach::Features::WholeStack < Spinach::FeatureSteps
|
5
|
+
before do
|
6
|
+
File.open(SampleApp::Logger.file_path, "w")
|
7
|
+
end
|
8
|
+
|
9
|
+
step 'my app is running' do
|
10
|
+
@app = SampleApp.run!(event_store: event_store)
|
11
|
+
end
|
12
|
+
|
13
|
+
step 'I send a command to an aggregate' do
|
14
|
+
@app.execute_command SampleApp::PublishPost.new(post_id: "some-id", title: "My post")
|
15
|
+
end
|
16
|
+
|
17
|
+
step 'an event is raised' do
|
18
|
+
sleep 0.01 #FIXME wait with timeout instead
|
19
|
+
expect(File.read(SampleApp::Logger.file_path)).to eq("Post with title \"My post\" has been published!\n")
|
20
|
+
end
|
21
|
+
|
22
|
+
after do
|
23
|
+
@app.shutdown if @app
|
24
|
+
end
|
25
|
+
|
26
|
+
def event_store
|
27
|
+
@event_store ||= EventSourcing::Event::Store::Memory.new
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1 @@
|
|
1
|
+
Post with title "My post" has been published!
|
@@ -0,0 +1,40 @@
|
|
1
|
+
require "event_sourcing/application"
|
2
|
+
require "event_sourcing/event"
|
3
|
+
require "event_sourcing/event/subscriber"
|
4
|
+
require "event_sourcing/command"
|
5
|
+
require "event_sourcing/aggregate"
|
6
|
+
|
7
|
+
SampleApp = EventSourcing::Application.new(:sample_app)
|
8
|
+
|
9
|
+
SampleApp::PostPublished = EventSourcing::Event.define(:title)
|
10
|
+
|
11
|
+
SampleApp::PublishPost = EventSourcing::Command.define(:post_id, :title) do |aggregate_manager|
|
12
|
+
aggregate_manager.instance_of(SampleApp::Post, post_id).publish(title)
|
13
|
+
end
|
14
|
+
|
15
|
+
class SampleApp::Post
|
16
|
+
include EventSourcing::Aggregate
|
17
|
+
|
18
|
+
def publish(title)
|
19
|
+
::SampleApp::PostPublished.new(title: title) unless @title
|
20
|
+
end
|
21
|
+
|
22
|
+
handle ::SampleApp::PostPublished do |e|
|
23
|
+
@title = e.title
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
class SampleApp::Logger < EventSourcing::Event::Subscriber
|
28
|
+
|
29
|
+
@file_path = File.dirname(__FILE__) + '/logger.log'
|
30
|
+
|
31
|
+
class << self
|
32
|
+
attr_reader :file_path
|
33
|
+
end
|
34
|
+
|
35
|
+
subscribe_to SampleApp::PostPublished do |e|
|
36
|
+
File.open(self.class.file_path, 'a') do |f|
|
37
|
+
f.puts "Post with title \"#{e.title}\" has been published!"
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
File without changes
|
@@ -0,0 +1,30 @@
|
|
1
|
+
require "event_sourcing/aggregate/actor"
|
2
|
+
|
3
|
+
module EventSourcing
|
4
|
+
module Aggregate
|
5
|
+
def self.included(base)
|
6
|
+
base.const_set("Actor", EventSourcing::Aggregate::Actor.for(base))
|
7
|
+
base.extend ClassMethods
|
8
|
+
end
|
9
|
+
|
10
|
+
def initialize(events = [])
|
11
|
+
events.each do |e|
|
12
|
+
_apply(e)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
def _apply(e)
|
17
|
+
if respond_to?("apply_#{e}")
|
18
|
+
send("apply_#{e}", e)
|
19
|
+
else
|
20
|
+
raise "unsupported event #{e}"
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
module ClassMethods
|
25
|
+
def handle(event_name, &block)
|
26
|
+
define_method "apply_#{event_name}", &block
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
require "concurrent/actor"
|
2
|
+
|
3
|
+
module EventSourcing
|
4
|
+
module Aggregate
|
5
|
+
class Actor
|
6
|
+
def self.for(aggregate)
|
7
|
+
Class.new(Concurrent::Actor::RestartingContext) do
|
8
|
+
define_method :initialize do |event_bus, event_stream|
|
9
|
+
@aggregate = aggregate.new(event_stream)
|
10
|
+
@event_bus = event_bus
|
11
|
+
@event_stream = event_stream
|
12
|
+
end
|
13
|
+
|
14
|
+
#FIXME: this is doing too many things
|
15
|
+
|
16
|
+
def on_message(message) # Format is [:method, arg1, arg2]
|
17
|
+
if @aggregate.respond_to?(message.first)
|
18
|
+
events = @aggregate.send(*message) # FIXME: what happens if events is empty or falsy?
|
19
|
+
@event_stream.append(events) # FIXME: Event Stream is now stale
|
20
|
+
@event_bus.publish(events)
|
21
|
+
# FIXME: spec what happens if event hasn't been stored for some reason
|
22
|
+
@aggregate._apply(events)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
require "concurrent/actor"
|
2
|
+
|
3
|
+
module EventSourcing
|
4
|
+
module Aggregate
|
5
|
+
class Manager < Concurrent::Actor::RestartingContext
|
6
|
+
|
7
|
+
require "event_sourcing/aggregate/manager/reference"
|
8
|
+
require "event_sourcing/aggregate/manager/cache"
|
9
|
+
require "event_sourcing/aggregate/message"
|
10
|
+
|
11
|
+
def initialize(event_bus)
|
12
|
+
@event_bus = event_bus
|
13
|
+
end
|
14
|
+
|
15
|
+
def on_message(message)
|
16
|
+
case message
|
17
|
+
when Aggregate::Message
|
18
|
+
cache.instance_of(message.aggregate, message.id).tell(message.message)
|
19
|
+
#TODO Handle aggregate timeout and failure (remove from cache)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def default_reference_class
|
24
|
+
Reference
|
25
|
+
end
|
26
|
+
|
27
|
+
private
|
28
|
+
def cache
|
29
|
+
@cache ||= Cache.new(@event_bus)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
require "event_sourcing/aggregate/manager"
|
2
|
+
|
3
|
+
module EventSourcing
|
4
|
+
module Aggregate
|
5
|
+
class Manager
|
6
|
+
class Cache < Hash
|
7
|
+
def initialize(event_bus)
|
8
|
+
@event_bus = event_bus
|
9
|
+
end
|
10
|
+
|
11
|
+
def instance_of(aggregate, id)
|
12
|
+
self[id] ||= aggregate::Actor.spawn!(name: id, supervise: true, args: [@event_bus, @event_bus.get_stream(id)])
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
require "concurrent/actor"
|
2
|
+
require "event_sourcing/aggregate/wrapper"
|
3
|
+
|
4
|
+
module EventSourcing
|
5
|
+
module Aggregate
|
6
|
+
class Manager
|
7
|
+
class Reference < Concurrent::Actor::Reference
|
8
|
+
def instance_of(aggregate, id)
|
9
|
+
EventSourcing::Aggregate::Wrapper.new(self, aggregate, id)
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
require "event_sourcing/aggregate/message"
|
2
|
+
require "values"
|
3
|
+
|
4
|
+
module EventSourcing
|
5
|
+
module Aggregate
|
6
|
+
class Wrapper < Value.new(:manager, :aggregate, :id)
|
7
|
+
|
8
|
+
def method_missing(name, *args)
|
9
|
+
manager.tell(wrap(name, args))
|
10
|
+
end
|
11
|
+
|
12
|
+
protected
|
13
|
+
def wrap(method, args)
|
14
|
+
Message.new(aggregate, id, [method] + args)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
module EventSourcing
|
2
|
+
module Application
|
3
|
+
require "event_sourcing/application/actor"
|
4
|
+
|
5
|
+
def self.new(name)
|
6
|
+
Class.new do
|
7
|
+
|
8
|
+
@name = name
|
9
|
+
|
10
|
+
class << self
|
11
|
+
|
12
|
+
attr_reader :name
|
13
|
+
|
14
|
+
def run!(options = {})
|
15
|
+
new(Array(options[:event_store]))
|
16
|
+
end
|
17
|
+
|
18
|
+
def inspect
|
19
|
+
"EventSourcing::Application(#{name})"
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def initialize(options)
|
24
|
+
@actor = Actor.spawn!(name: self.class.name, args: options)
|
25
|
+
end
|
26
|
+
|
27
|
+
def shutdown
|
28
|
+
@actor.terminate!
|
29
|
+
end
|
30
|
+
|
31
|
+
def execute_command(command)
|
32
|
+
@actor.execute_command(command)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|