event_sorcerer 0.1.0 → 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 58f7bb946b104e7e64a07a78e3bcb5271295aae6
4
- data.tar.gz: b6fe6643e1f836722cdb917d24be90af205352a3
3
+ metadata.gz: 204474547eba7a0e9289b8dda4f55548ba225fa6
4
+ data.tar.gz: 84fb8dfbd0220067c63bfe526899bb33ac9b4982
5
5
  SHA512:
6
- metadata.gz: 31129950df220d9672037b3bf205a4f3450e0ee81735024736058f40b12a1e00ba209d9cadec8f2003f66e281ae7fc724b0d99b668e3d0ab2e8969239061a624
7
- data.tar.gz: 798db082c7343649a0c529a1ec3d91880cf00af0e9b87785d7f4277912f2b76d0154cccc42bb28a64c7bd2e4e6a1286a46c6233b458f691b7975e6eb77fa691c
6
+ metadata.gz: bd80f51216f11bd01f9a6d4eace6accfa0f703bbd0c9a8fff5e0265862a1e66f749e7592fd5d20b91ecb8ff980c30ba582f023d8118242d75256897bef9abb8a
7
+ data.tar.gz: d13b3e23dc632637ce415212b60873a038a443fc13a585a46072afeb944f5d803df62bf835d4ac35695bc99720d0731b900968797cfa784b88d971ab1f696f97
data/README.md CHANGED
@@ -2,8 +2,145 @@
2
2
 
3
3
  Generic event-sourcing scaffold.
4
4
 
5
+ Disclaimer: This is still a work-in-progress, is not feature complete and _is_ subject to change.
6
+
5
7
  [![Code Climate](https://codeclimate.com/github/SebastianEdwards/event_sorcerer/badges/gpa.svg)](https://codeclimate.com/github/SebastianEdwards/event_sorcerer)
6
8
 
9
+ ## What is event-sourcing?
10
+
11
+ Event-sourcing means using events as the primary source of truth for your domain models. Rather than storing the current state of your domain ala ActiveRecord or any other ORM, you append all mutating events to a log. To restore the current state of a domain object you initialize a new instance and replay the stored events against it.
12
+
13
+ ## That sounds unconventional. Why would I want to do that?
14
+
15
+ Event-sourcing captures the intent of user's interacting with your system, gives you an audit log for free and allows for painless creation of new projections of your data in the future.
16
+
17
+ ## New projections of your data?
18
+
19
+ Imagine replaying a domain model's events into objects that prepare it for being loaded into a relational store. Then, using those same events preparing it for a graph database or a full-text search engine. Use the right tool (read model) for the job.
20
+
21
+ ## I still don't really understand...
22
+
23
+ Greg Young gave a talk on the subject which will probably explain ES concepts much better than I can. It's available here: [CQRS and Event Sourcing - Code on the Beach 2014](https://www.youtube.com/watch?v=JHGkaShoyNs).
24
+
25
+ ## So what does this gem give me?
26
+
27
+ You can mixin `EventSorcerer::Aggregate` to your domain model and get a DSL for defining your events, plus an ActiveRecord-like interface for creating, finding and saving. It also gives you a unit-of-work, event-bus hooks and time-shifts your system during event replay.
28
+
29
+ ## What's the catch?
30
+
31
+ This gem is like a coloring book. You get an outline but you have to color it in with your own storage engine and event bus.
32
+
33
+ ## That sounds scary.
34
+
35
+ It isn't; you just need to subclass a couple of classes and implement a few methods. I'll add some examples at some point showing how to use it with a couple of different datastores.
36
+
37
+ ## Example
38
+
39
+ Here we have a domain model representing a good ol' game of rugby. It allows the game to be started and stopped and points to be scored. Things to notice:
40
+
41
+ - Event definition is very simple. Just wrap the event methods in an `events` block. Note: as it stands in the current version your arguments must all be JSON serializable. Keyword arguments are supported.
42
+ - Validation is done with exceptions, a conceptually simple model, give invalid input and it blows up (for you to rescue and give a reasonable response to the user of course).
43
+ - Rather than keeping a current score in the database, we use events to track score-increasing events. Now, we not only know the score at any point in the game but we also know the context (the why) around the score (tries vs penalties, etc.)
44
+ - We could use fat events to allow interesting projections in the future. Fat events means storing more context than we currently need (storage is cheap!). For example we could track the scoring player for each try and create a projection which shows how many tries each player made during the entire game. We could even replay multiple games into one projection to find a player's total tally for a season.
45
+
46
+ ```ruby
47
+ class RugbyGame
48
+ class Team < Struct.new(:name, :score)
49
+ def add_points(points)
50
+ @score += points
51
+ end
52
+
53
+ def to_s
54
+ "#{name}: #{score}"
55
+ end
56
+ end
57
+
58
+ class DuplicateTeamName < RuntimeError; end
59
+ class GameNotInProgress < RuntimeError; end
60
+ class TeamNotPlaying < RuntimeError; end
61
+
62
+ attr_reader :team_one
63
+ attr_reader :team_two
64
+
65
+ def game_in_progress?
66
+ @game_in_progress == true
67
+ end
68
+
69
+ def scores
70
+ "#{team_one} - #{team_two}"
71
+ end
72
+
73
+ def team_by_name(name)
74
+ return team_one if name == team_one.name
75
+ return team_two if name == team_two.name
76
+
77
+ fail TeamNotPlaying
78
+ end
79
+
80
+ events do
81
+ def game_started(first_team, second_team)
82
+ fail DuplicateTeamName if first_team_name == second_team_name
83
+
84
+ @team_one = Team.new(first_team_name, 0)
85
+ @team_two = Team.new(second_team_name, 0)
86
+ @game_in_progress = true
87
+
88
+ self
89
+ end
90
+
91
+ def game_ended
92
+ @game_in_progress = false
93
+
94
+ self
95
+ end
96
+
97
+ def try_scored(scoring_team)
98
+ fail GameNotInProgress unless game_in_progress?
99
+
100
+ team_by_name(scoring_team).add_points 5
101
+
102
+ self
103
+ end
104
+
105
+ def try_converted(scoring_team)
106
+ fail GameNotInProgress unless game_in_progress?
107
+
108
+ team_by_name(scoring_team).add_points 2
109
+
110
+ self
111
+ end
112
+
113
+ def drop_goal_scored(scoring_team)
114
+ ...
115
+ end
116
+
117
+ def penalty_kick_scored(scoring_team)
118
+ ...
119
+ end
120
+ end
121
+ end
122
+ ```
123
+
124
+ Here's how you'd use the above class:
125
+
126
+ ```ruby
127
+ game = RugbyGame.new
128
+ game.game_started('All Blacks', 'Wallabies')
129
+ game.try_scored('All Blacks')
130
+ game.try_converted('All Blacks')
131
+ game.drop_goal_scored('Wallabies')
132
+
133
+ ...
134
+
135
+ game.game_ended
136
+ game.scores => "All Blacks: 29 - Wallabies: 28"
137
+ game.save
138
+
139
+ ... later ...
140
+ game = RugbyGame.find(6)
141
+ game.scores => "All Blacks: 29 - Wallabies: 28"
142
+ ```
143
+
7
144
  ## Installation
8
145
 
9
146
  Add this line to your application's Gemfile:
@@ -23,6 +23,6 @@ Gem::Specification.new do |spec|
23
23
  spec.add_development_dependency 'rubocop'
24
24
  spec.add_development_dependency 'rake'
25
25
 
26
- spec.add_runtime_dependency 'invokr', '0.0.5'
26
+ spec.add_runtime_dependency 'invokr', '~> 0.9.6'
27
27
  spec.add_runtime_dependency 'timecop', '0.7.1'
28
28
  end
@@ -52,17 +52,25 @@ module EventSorcerer
52
52
  @message_bus || fail(UnsetMessageBus)
53
53
  end
54
54
 
55
- # Public: Mutex to synchronize usage of non-threadsafe time-traveling gems.
56
- def time_travel_lock
57
- @time_travel_lock ||= Mutex.new
58
- end
59
-
60
55
  # Public: Returns the unit_of_work for the current thread. Defaults to
61
56
  # NoUnitOfWork.
62
57
  def unit_of_work
63
58
  Thread.current[:unit_of_work] || NoUnitOfWork
64
59
  end
65
60
 
61
+ # Public: Executes a block with time frozen to a given moment.
62
+ #
63
+ # time - Time to freeze time at.
64
+ #
65
+ # Returns value of block.
66
+ def with_time(time)
67
+ time_travel_lock.synchronize do
68
+ Timecop.freeze(time) do
69
+ yield
70
+ end
71
+ end
72
+ end
73
+
66
74
  # Public: Creates a new UnitOfWork and sets it for the current thread
67
75
  # within the block. Executes the work after block completion.
68
76
  #
@@ -79,5 +87,12 @@ module EventSorcerer
79
87
 
80
88
  result
81
89
  end
90
+
91
+ private
92
+
93
+ # Private: Mutex to synchronize usage of non-threadsafe time-traveling gem.
94
+ def time_travel_lock
95
+ @time_travel_lock ||= Mutex.new
96
+ end
82
97
  end
83
98
  end
@@ -111,7 +111,7 @@ module EventSorcerer
111
111
  # block - the block to yield the event stream to.
112
112
  #
113
113
  # Returns the return value of the given block.
114
- def with_all_event_streams_for_type(&block)
114
+ def with_all_event_streams_for_type()
115
115
  yield event_store.read_event_streams_for_type(name)
116
116
  end
117
117
 
@@ -122,7 +122,7 @@ module EventSorcerer
122
122
  # block - the block to yield the event stream to.
123
123
  #
124
124
  # Returns the return value of the given block.
125
- def with_all_loaders_for_type(prohibit_new = true, &block)
125
+ def with_all_loaders_for_type(prohibit_new = true)
126
126
  with_all_event_streams_for_type do |streams|
127
127
  loaders = streams.map do |stream|
128
128
  AggregateLoader.new(self, stream.id, stream.events,
@@ -140,7 +140,7 @@ module EventSorcerer
140
140
  # block - the block to yield the event stream to.
141
141
  #
142
142
  # Returns the return value of the given block.
143
- def with_event_stream_for_id(id, &block)
143
+ def with_event_stream_for_id(id)
144
144
  yield event_store.read_event_stream(id)
145
145
  end
146
146
 
@@ -151,7 +151,7 @@ module EventSorcerer
151
151
  # block - the block to yield the event stream to.
152
152
  #
153
153
  # Returns the return value of the given block.
154
- def with_loader_for_id(id, prohibit_new = true, &block)
154
+ def with_loader_for_id(id, prohibit_new = true)
155
155
  with_event_stream_for_id(id) do |stream|
156
156
  yield AggregateLoader.new(self, stream.id, stream.events,
157
157
  stream.current_version, prohibit_new)
@@ -60,14 +60,14 @@ module EventSorcerer
60
60
  # Private: Handles the serialization of event arguments and pushes the
61
61
  # Event object onto the _dirty_events array. Increments the local
62
62
  # version number for the aggregate.
63
- def add_dirty_event!(method_sym, *arguments)
63
+ def add_dirty_event!(time, method_sym, *arguments)
64
64
  increment_version!
65
65
 
66
66
  method = @_aggregate.method(method_sym)
67
67
  method.parameters.each.with_index.select { |(type, _), _| type == :req }
68
68
 
69
69
  details = ArgumentHashifier.hashify(method.parameters, arguments.dup)
70
- @_dirty_events << Event.new(method_sym, Time.now, details)
70
+ @_dirty_events << Event.new(method_sym, time, details)
71
71
 
72
72
  self
73
73
  end
@@ -103,9 +103,14 @@ module EventSorcerer
103
103
  def send_event_to_aggregate(method_sym, *arguments, &block)
104
104
  fail EventArgumentError if block
105
105
 
106
- add_dirty_event!(method_sym, *arguments)
106
+ time = Time.now
107
+ add_dirty_event!(time, method_sym, *arguments)
107
108
 
108
- @_aggregate.send method_sym, *arguments
109
+ expected_args = arguments.slice(0, @_aggregate.method(method_sym).arity)
110
+
111
+ EventSorcerer.with_time(time) do
112
+ @_aggregate.send method_sym, *expected_args
113
+ end
109
114
  rescue StandardError => e
110
115
  undo_dirty_event!
111
116
  raise e
@@ -12,10 +12,8 @@ module EventSorcerer
12
12
  #
13
13
  # Returns self.
14
14
  def self.apply_event!(aggregate, event)
15
- EventSorcerer.time_travel_lock.synchronize do
16
- Timecop.freeze(event.created_at) do
17
- Invokr.invoke method: event.name, on: aggregate, with: event.details
18
- end
15
+ EventSorcerer.with_time(event.created_at) do
16
+ Invokr.invoke method: event.name, on: aggregate, using: event.details
19
17
  end
20
18
 
21
19
  self
@@ -1,4 +1,4 @@
1
1
  # Public: Defines a constant for the current version number.
2
2
  module EventSorcerer
3
- VERSION = '0.1.0'
3
+ VERSION = '0.1.1'
4
4
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: event_sorcerer
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.1.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Sebastian Edwards
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2014-11-17 00:00:00.000000000 Z
11
+ date: 2014-12-03 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -84,16 +84,16 @@ dependencies:
84
84
  name: invokr
85
85
  requirement: !ruby/object:Gem::Requirement
86
86
  requirements:
87
- - - '='
87
+ - - "~>"
88
88
  - !ruby/object:Gem::Version
89
- version: 0.0.5
89
+ version: 0.9.6
90
90
  type: :runtime
91
91
  prerelease: false
92
92
  version_requirements: !ruby/object:Gem::Requirement
93
93
  requirements:
94
- - - '='
94
+ - - "~>"
95
95
  - !ruby/object:Gem::Version
96
- version: 0.0.5
96
+ version: 0.9.6
97
97
  - !ruby/object:Gem::Dependency
98
98
  name: timecop
99
99
  requirement: !ruby/object:Gem::Requirement