event_sourcing 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (63) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +24 -0
  3. data/.rspec +2 -0
  4. data/Gemfile +7 -0
  5. data/Guardfile +12 -0
  6. data/LICENSE.txt +22 -0
  7. data/README.md +29 -0
  8. data/Rakefile +13 -0
  9. data/event_sourcing.gemspec +28 -0
  10. data/features/steps/whole_stack.rb +29 -0
  11. data/features/support/env.rb +5 -0
  12. data/features/support/logger.log +1 -0
  13. data/features/support/sample_app.rb +40 -0
  14. data/features/support/spinach.log +0 -0
  15. data/features/whole_stack.feature +6 -0
  16. data/lib/event_sourcing.rb +3 -0
  17. data/lib/event_sourcing/aggregate.rb +30 -0
  18. data/lib/event_sourcing/aggregate/actor.rb +29 -0
  19. data/lib/event_sourcing/aggregate/manager.rb +33 -0
  20. data/lib/event_sourcing/aggregate/manager/cache.rb +17 -0
  21. data/lib/event_sourcing/aggregate/manager/instance_of.rb +11 -0
  22. data/lib/event_sourcing/aggregate/manager/reference.rb +14 -0
  23. data/lib/event_sourcing/aggregate/message.rb +7 -0
  24. data/lib/event_sourcing/aggregate/wrapper.rb +18 -0
  25. data/lib/event_sourcing/application.rb +37 -0
  26. data/lib/event_sourcing/application/actor.rb +40 -0
  27. data/lib/event_sourcing/application/actor/reference.rb +23 -0
  28. data/lib/event_sourcing/command.rb +30 -0
  29. data/lib/event_sourcing/command/bus.rb +16 -0
  30. data/lib/event_sourcing/event.rb +29 -0
  31. data/lib/event_sourcing/event/bus.rb +29 -0
  32. data/lib/event_sourcing/event/bus/reference.rb +28 -0
  33. data/lib/event_sourcing/event/publisher.rb +47 -0
  34. data/lib/event_sourcing/event/publisher/reference.rb +11 -0
  35. data/lib/event_sourcing/event/store.rb +8 -0
  36. data/lib/event_sourcing/event/store/memory.rb +43 -0
  37. data/lib/event_sourcing/event/stream.rb +23 -0
  38. data/lib/event_sourcing/event/subscriber.rb +16 -0
  39. data/lib/event_sourcing/version.rb +3 -0
  40. data/spec/concurrent_logging.rb +6 -0
  41. data/spec/spec_helper.rb +8 -0
  42. data/spec/support/actor_helpers.rb +11 -0
  43. data/spec/support/shared_examples/a_store_implementation.rb +70 -0
  44. data/spec/unit/aggregate/actor_spec.rb +59 -0
  45. data/spec/unit/aggregate/manager/cache_spec.rb +26 -0
  46. data/spec/unit/aggregate/manager/reference_spec.rb +14 -0
  47. data/spec/unit/aggregate/manager_spec.rb +32 -0
  48. data/spec/unit/aggregate/wrapper_spec.rb +22 -0
  49. data/spec/unit/aggregate_spec.rb +75 -0
  50. data/spec/unit/application/actor/reference_spec.rb +25 -0
  51. data/spec/unit/application/actor_spec.rb +36 -0
  52. data/spec/unit/application_spec.rb +41 -0
  53. data/spec/unit/command/bus_spec.rb +15 -0
  54. data/spec/unit/command_spec.rb +38 -0
  55. data/spec/unit/event/bus/reference_spec.rb +48 -0
  56. data/spec/unit/event/bus_spec.rb +41 -0
  57. data/spec/unit/event/publisher_spec.rb +28 -0
  58. data/spec/unit/event/store/memory_spec.rb +6 -0
  59. data/spec/unit/event/stream_spec.rb +30 -0
  60. data/spec/unit/event/subscriber_spec.rb +27 -0
  61. data/spec/unit/event_spec.rb +27 -0
  62. data/spec/unit_helper.rb +14 -0
  63. metadata +233 -0
@@ -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
@@ -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
@@ -0,0 +1,2 @@
1
+ --color
2
+ --backtrace
data/Gemfile ADDED
@@ -0,0 +1,7 @@
1
+ source 'https://rubygems.org'
2
+ ruby "2.0.0", engine: "jruby", engine_version: "1.7.13"
3
+ #ruby=jruby-1.7.13
4
+
5
+ # Specify your gem's dependencies in event_sourcing.gemspec
6
+ gemspec
7
+ gem "concurrent-ruby", "0.7.0.rc2" #TODO: Move to gemspec once released
@@ -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
@@ -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.
@@ -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
@@ -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,5 @@
1
+ require "rspec"
2
+ require "rspec/expectations"
3
+ require "./spec/concurrent_logging"
4
+
5
+ Concurrent::Actor.i_know_it_is_experimental!
@@ -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,6 @@
1
+ Feature: Whole stack
2
+
3
+ Scenario: Sending commands to aggregates
4
+ Given my app is running
5
+ When I send a command to an aggregate
6
+ Then an event is raised
@@ -0,0 +1,3 @@
1
+ module EventSourcing
2
+ require "event_sourcing/version"
3
+ end
@@ -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,11 @@
1
+ # TODO Remove
2
+ require "values"
3
+ require "event_sourcing/aggregate/manager"
4
+
5
+ module EventSourcing
6
+ module Aggregate
7
+ class Manager
8
+ InstanceOf = Value.new(:aggregate, :id)
9
+ end
10
+ end
11
+ 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,7 @@
1
+ require "values"
2
+
3
+ module EventSourcing
4
+ module Aggregate
5
+ Message = Value.new(:aggregate, :id, :message)
6
+ end
7
+ 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