akasha 0.1.0 → 0.2.0

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 1294447dab3ae4202502a232dcc069d06dcdbf5d
4
- data.tar.gz: 296cfc35578c3f15a90d55f0f0ed1db018458541
3
+ metadata.gz: '09372b1685568c249f091ff1bbbf4915bb574b91'
4
+ data.tar.gz: 982ef7993d2087b2bd55f424228ba5bdf31d867a
5
5
  SHA512:
6
- metadata.gz: 0b177423fce21ab067cee6c37f0d50e1d132056611791af3af17c56b2b8a2ba6ea20d76a11462e99459814b1ce24259ea86d01bc794818110da1ea49283fcbbc
7
- data.tar.gz: 3cbe93b9aeafeca8b3541d01feaacfb6bdd0395bf3ed92d9be73a5ee7df3dfdb593deb8ad55e9d9647748f51a069ae881c9fd970aa473fc73f5087f63d012c3e
6
+ metadata.gz: 59730ad9b8f01e86d5fdfd01945fea0586002972571595f03810d7af600f9d864a5d5c22fc1e7092a68067f6fd310c16f825980b8515d71f7933a05f2362dfc1
7
+ data.tar.gz: 1fb02d4091efe00a925af1ec4d15990c0cc0c61aaaf64a1c64de36a63dcc3eea3b73465d5e558880ad46393d90146dfb6a788d5e5544f7afcdf68bda67f38d78
@@ -2,3 +2,5 @@ Style/FrozenStringLiteralComment:
2
2
  Enabled: false
3
3
  Metrics/LineLength:
4
4
  Max: 120
5
+ Metrics/MethodLength:
6
+ Max: 15
@@ -1,5 +1,10 @@
1
1
  # Changelog
2
2
 
3
+ ## Version 0.2.0
4
+
5
+ * Synchronous event listeners (see `examples/sinatra/app.rb`).
6
+ * HTTP-based Eventstore storage.
7
+
3
8
 
4
9
  ## Version 0.1.0
5
10
 
@@ -1,7 +1,8 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- akasha (0.1.0)
4
+ akasha (0.2.0)
5
+ http_event_store
5
6
 
6
7
  GEM
7
8
  remote: https://rubygems.org/
@@ -9,7 +10,19 @@ GEM
9
10
  byebug (10.0.2)
10
11
  coderay (1.1.2)
11
12
  diff-lcs (1.3)
13
+ faraday (0.15.2)
14
+ multipart-post (>= 1.2, < 3)
15
+ faraday_middleware (0.12.2)
16
+ faraday (>= 0.7.4, < 1.0)
17
+ hashie (3.5.7)
18
+ http_event_store (0.2.2)
19
+ faraday
20
+ faraday_middleware
21
+ hashie
22
+ json
23
+ json (2.1.0)
12
24
  method_source (0.9.0)
25
+ multipart-post (2.0.0)
13
26
  pry (0.11.3)
14
27
  coderay (~> 1.1.0)
15
28
  method_source (~> 0.9.0)
data/README.md CHANGED
@@ -29,7 +29,7 @@ require 'sinatra'
29
29
 
30
30
  class User < Akasha::Aggregate
31
31
  def sign_up(email:, password:, admin: false, **)
32
- changeset << Akasha::Event.new(:user_signed_up, email: email, password: password, admin: admin)
32
+ changeset.append(:user_signed_up, email: email, password: password, admin: admin)
33
33
  end
34
34
 
35
35
  def on_user_signed_up(email:, password:, admin:, **)
@@ -73,11 +73,13 @@ end
73
73
  ## Next steps
74
74
 
75
75
  - [x] Command routing (default and user-defined)
76
- - [ ] EventHandler (relying only on Eventstore)
77
- - [ ] HTTP Eventstore storage backend
76
+ - [x] Synchronous EventHandler
77
+ - [x] HTTP Eventstore storage backend
78
+ - [ ] Telemetry (Dogstatsd)
79
+ - [ ] Event#id for better idempotence (validate this claim)
78
80
  - [ ] Namespacing for events and aggregates
79
81
  - [ ] Version-based concurrency
80
- - [ ] Rake task for running EventHandlers
82
+ - [ ] Async EventHandlers (storing cursors in Eventstore, configurable durability guarantees)
81
83
  - [ ] Socket-based Eventstore storage backend
82
84
 
83
85
  ## Development
@@ -23,6 +23,8 @@ Gem::Specification.new do |spec|
23
23
  spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
24
24
  spec.require_paths = ['lib']
25
25
 
26
+ spec.add_dependency 'http_event_store'
27
+
26
28
  spec.add_development_dependency 'bundler', '~> 1.16'
27
29
  spec.add_development_dependency 'rake', '~> 10.0'
28
30
  spec.add_development_dependency 'rspec', '~> 3.7'
@@ -0,0 +1,4 @@
1
+ source "https://rubygems.org"
2
+
3
+ gem 'sinatra'
4
+ gem 'akasha'
@@ -0,0 +1,24 @@
1
+ GEM
2
+ remote: https://rubygems.org/
3
+ specs:
4
+ akasha (0.1.0)
5
+ mustermann (1.0.2)
6
+ rack (2.0.5)
7
+ rack-protection (2.0.1)
8
+ rack
9
+ sinatra (2.0.1)
10
+ mustermann (~> 1.0)
11
+ rack (~> 2.0)
12
+ rack-protection (= 2.0.1)
13
+ tilt (~> 2.0)
14
+ tilt (2.0.8)
15
+
16
+ PLATFORMS
17
+ ruby
18
+
19
+ DEPENDENCIES
20
+ akasha
21
+ sinatra
22
+
23
+ BUNDLED WITH
24
+ 1.16.2
@@ -0,0 +1,67 @@
1
+ require 'akasha'
2
+ require 'sinatra'
3
+
4
+ class User < Akasha::Aggregate
5
+ attr_reader :email
6
+
7
+ def sign_up(email:, password:, admin: false, **)
8
+ changeset.append(:user_signed_up, email: email, password: password, admin: admin)
9
+ end
10
+
11
+ def on_user_signed_up(email:, password:, admin:, **)
12
+ @email = email
13
+ @password = password
14
+ @admin = admin
15
+ end
16
+ end
17
+
18
+ class Notifier < Akasha::EventListener
19
+ def on_user_signed_up(user_id, **)
20
+ notify_about_signup(user_id)
21
+ end
22
+
23
+ private
24
+
25
+ def notify_about_signup(user_id)
26
+ # Here we could just grab email from the event but let's demonstrate
27
+ # how to load an aggregate from events.
28
+ user = User.find_or_create(user_id)
29
+ email = <<~EOS
30
+ User #{user.email} just signed up!
31
+ EOS
32
+ # Let's not send any emails... :)
33
+ puts email
34
+ end
35
+ end
36
+
37
+ before do
38
+ @command_router = Akasha::CommandRouter.new
39
+
40
+ # Aggregates will load from and save to in-memory storage.
41
+ repository = Akasha::Repository.new(Akasha::Storage::MemoryEventStore.new)
42
+ Akasha::Aggregate.connect!(repository)
43
+
44
+ # Set up event listeners.
45
+ @event_router = Akasha::EventRouter.new
46
+ @event_router.register_event_listener(:user_signed_up, Notifier)
47
+ repository.subscribe(@event_router)
48
+
49
+ # This is how you link commands to aggregates.
50
+ @command_router.register_default_route(:sign_up, User)
51
+
52
+ # Nearly identital to the default handling above but we're setting the admin
53
+ # flag to demo custom command handling.
54
+ @command_router.register_route(:sign_up_admin) do |aggregate_id, **data|
55
+ user = User.find_or_create(aggregate_id)
56
+ user.sign_up(email: data[:email], password: data[:password], admin: true)
57
+ user.save!
58
+ end
59
+ end
60
+
61
+ post '/users/:user_id' do # With CQRS client pass unique aggregate ids.
62
+ @command_router.route!(:sign_up,
63
+ params[:user_id],
64
+ email: params[:email],
65
+ password: params[:password])
66
+ 'OK'
67
+ end
@@ -1,5 +1,8 @@
1
1
  require 'akasha/event'
2
2
  require 'akasha/aggregate'
3
3
  require 'akasha/command_router'
4
+ require 'akasha/event_listener'
5
+ require 'akasha/event_router'
4
6
  require 'akasha/repository'
7
+ require 'akasha/storage/http_event_store'
5
8
  require 'akasha/storage/memory_event_store'
@@ -12,6 +12,12 @@ module Akasha
12
12
  @data = data
13
13
  end
14
14
 
15
+ def metadata
16
+ {
17
+ created_at: @created_at
18
+ }
19
+ end
20
+
15
21
  def ==(other)
16
22
  self.class == other.class &&
17
23
  name == other.name &&
@@ -0,0 +1,5 @@
1
+ module Akasha
2
+ # Event listener base class.
3
+ class EventListener
4
+ end
5
+ end
@@ -0,0 +1,38 @@
1
+ module Akasha
2
+ # Routes events to event listeners.
3
+ class EventRouter
4
+ def initialize
5
+ @routes = Hash.new { |hash, key| hash[key] = [] }
6
+ end
7
+
8
+ # Registers a new event listener, derived from
9
+ # `Akasha::EventListener`.
10
+ def register_event_listener(event_name, listener_class)
11
+ @routes[event_name] << listener_class
12
+ end
13
+
14
+ # Routes an event.
15
+ def route(event_name, aggregate_id, **data)
16
+ @routes[event_name].each do |listener_class|
17
+ listener = listener_class.new
18
+ begin
19
+ listener.public_send(:"on_#{event_name}", aggregate_id, **data)
20
+ rescue RuntimeError => e
21
+ log "Error handling event #{event_name.inspect}: #{e}"
22
+ end
23
+ end
24
+ end
25
+
26
+ # Routes an event (an Akasha::Event instance).
27
+ # This is interface allowing subscription via `Akasha::Repository#subscribe`.
28
+ def call(aggregate_id, event)
29
+ route(event.name, aggregate_id, **event.data)
30
+ end
31
+
32
+ private
33
+
34
+ def log(msg)
35
+ puts msg
36
+ end
37
+ end
38
+ end
@@ -5,25 +5,43 @@ module Akasha
5
5
  class Repository
6
6
  STREAM_NAME_SEP = '-'.freeze
7
7
 
8
+ # Creates a new repository using the underlying `store` (e.g. `MemoryEventStore`).
8
9
  def initialize(store)
9
10
  @store = store
11
+ @subscribers = []
10
12
  end
11
13
 
14
+ # Loads an aggregate identified by `id` and `klass` from the repository.
15
+ # Returns an aggregate instance of class `klass` constructed by applying events from the corresponding
16
+ # stream.
12
17
  def load_aggregate(klass, id)
13
18
  agg = klass.new(id)
14
19
 
15
20
  start = 0
16
- chunk_size = 100
17
- stream(klass, id).read_events(start, chunk_size) do |events|
21
+ page_size = 20
22
+ stream(klass, id).read_events(start, page_size) do |events|
18
23
  agg.apply_events(events)
19
24
  end
20
25
 
21
26
  agg
22
27
  end
23
28
 
29
+ # Saves an aggregate to the repository, appending events to the corresponding stream.
24
30
  def save_aggregate(aggregate)
25
31
  changeset = aggregate.changeset
26
32
  stream(aggregate.class, changeset.aggregate_id).write_events(changeset.events)
33
+ notify_subscribers(aggregate)
34
+ end
35
+
36
+ # Subscribes to event streams passing either a lambda or a block.
37
+ # Example:
38
+ #
39
+ # repo.subscribe do |aggregate_id, event|
40
+ # ... handle the event ...
41
+ # end
42
+ def subscribe(lambda = nil, &block)
43
+ callable = lambda || block
44
+ @subscribers << callable
27
45
  end
28
46
 
29
47
  private
@@ -35,5 +53,15 @@ module Akasha
35
53
  def stream(aggregate_klass, aggregate_id)
36
54
  @store.streams[stream_name(aggregate_klass, aggregate_id)]
37
55
  end
56
+
57
+ def notify_subscribers(aggregate)
58
+ id = aggregate.changeset.aggregate_id
59
+ events = aggregate.changeset.events
60
+ @subscribers.each do |subscriber|
61
+ events.each do |event|
62
+ subscriber.call(id, event)
63
+ end
64
+ end
65
+ end
38
66
  end
39
67
  end
@@ -0,0 +1,24 @@
1
+ require_relative 'http_event_store/stream'
2
+ require 'http_event_store'
3
+
4
+ module Akasha
5
+ module Storage
6
+ # HTTP-based interface to Eventstore (https://geteventstore.com)
7
+ class HttpEventStore
8
+ def initialize(host: 'localhost', port: 2113)
9
+ @client = ::HttpEventStore::Connection.new do |config|
10
+ config.endpoint = host
11
+ config.port = port
12
+ end
13
+ end
14
+
15
+ def streams
16
+ self # Use the `[]` method on self.
17
+ end
18
+
19
+ def [](stream_name)
20
+ Stream.new(@client, stream_name)
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,50 @@
1
+ module Akasha
2
+ module Storage
3
+ class HttpEventStore
4
+ # HTTP Eventstore stream.
5
+ class Stream
6
+ def initialize(client, stream_name)
7
+ @client = client
8
+ @stream_name = stream_name
9
+ end
10
+
11
+ # Appends events to the stream.
12
+ def write_events(events)
13
+ event_hashes = events.map do |event|
14
+ {
15
+ event_type: event.name,
16
+ data: event.data,
17
+ metadata: event.metadata
18
+ }
19
+ end
20
+ @client.append_to_stream(@stream_name, event_hashes)
21
+ end
22
+
23
+ # Reads events from the stream starting from `start` inclusive.
24
+ # If block given, reads all events from the position in pages of `page_size`.
25
+ # If block not given, reads `size` events from the position.
26
+ def read_events(start, page_size)
27
+ if block_given?
28
+ position = start
29
+ loop do
30
+ events = read_events(position, page_size)
31
+ return if events.empty?
32
+ yield(events)
33
+ position += events.size
34
+ end
35
+ else
36
+ safe_read_events(start, page_size)
37
+ end
38
+ end
39
+
40
+ private
41
+
42
+ def safe_read_events(start, page_size)
43
+ @client.read_events_forward(@stream_name, start, page_size)
44
+ rescue ::HttpEventStore::StreamNotFound
45
+ []
46
+ end
47
+ end
48
+ end
49
+ end
50
+ end
@@ -12,15 +12,14 @@ module Akasha
12
12
  @events += events
13
13
  end
14
14
 
15
- # Reads events from the stream starting from `position` inclusive.
16
- # If block given, reads all events from the position in chunks
17
- # of `size`.
18
- # If block not given, reads `size` events from the position.
19
- def read_events(position, size, &block)
15
+ # Reads events from the stream starting from `start` inclusive.
16
+ # If block given, reads all events from the start in pages of `page_size`.
17
+ # If block not given, reads `page_size` events from the start.
18
+ def read_events(start, page_size, &block)
20
19
  if block_given?
21
- @events.lazy.drop(position).each_slice(size, &block)
20
+ @events.lazy.drop(start).each_slice(page_size, &block)
22
21
  else
23
- @events[position..position + size]
22
+ @events[start..start + page_size]
24
23
  end
25
24
  end
26
25
  end
@@ -1,3 +1,3 @@
1
1
  module Akasha
2
- VERSION='0.1.0'
2
+ VERSION = '0.2.0'.freeze
3
3
  end
metadata CHANGED
@@ -1,15 +1,29 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: akasha
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Marcin Bilski
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2018-06-04 00:00:00.000000000 Z
11
+ date: 2018-06-05 00:00:00.000000000 Z
12
12
  dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: http_event_store
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
13
27
  - !ruby/object:Gem::Dependency
14
28
  name: bundler
15
29
  requirement: !ruby/object:Gem::Requirement
@@ -101,6 +115,9 @@ files:
101
115
  - akasha.gemspec
102
116
  - bin/console
103
117
  - bin/setup
118
+ - examples/sinatra/Gemfile
119
+ - examples/sinatra/Gemfile.lock
120
+ - examples/sinatra/app.rb
104
121
  - lib/akasha.rb
105
122
  - lib/akasha/aggregate.rb
106
123
  - lib/akasha/aggregate/syntax_helpers.rb
@@ -108,7 +125,11 @@ files:
108
125
  - lib/akasha/command_router.rb
109
126
  - lib/akasha/command_router/default_handler.rb
110
127
  - lib/akasha/event.rb
128
+ - lib/akasha/event_listener.rb
129
+ - lib/akasha/event_router.rb
111
130
  - lib/akasha/repository.rb
131
+ - lib/akasha/storage/http_event_store.rb
132
+ - lib/akasha/storage/http_event_store/stream.rb
112
133
  - lib/akasha/storage/memory_event_store.rb
113
134
  - lib/akasha/storage/memory_event_store/stream.rb
114
135
  - lib/akasha/version.rb