replay 0.0.1 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (53) hide show
  1. checksums.yaml +7 -0
  2. data/.rvmrc +1 -1
  3. data/Gemfile +7 -3
  4. data/Guardfile +10 -0
  5. data/LICENSE +21 -0
  6. data/README.md +153 -0
  7. data/Rakefile +15 -0
  8. data/lib/replay.rb +39 -10
  9. data/lib/replay/backends.rb +50 -0
  10. data/lib/replay/event_declarations.rb +36 -0
  11. data/lib/replay/event_decorator.rb +13 -0
  12. data/lib/replay/events.rb +24 -0
  13. data/lib/replay/inflector.rb +55 -0
  14. data/lib/replay/observer.rb +18 -0
  15. data/lib/replay/publisher.rb +72 -0
  16. data/lib/replay/repository.rb +61 -0
  17. data/lib/replay/repository/configuration.rb +30 -0
  18. data/lib/replay/repository/identity_map.rb +25 -0
  19. data/lib/replay/router.rb +5 -0
  20. data/lib/replay/router/default_router.rb +21 -0
  21. data/lib/replay/rspec.rb +50 -0
  22. data/lib/replay/subscription_manager.rb +28 -0
  23. data/lib/replay/test.rb +64 -0
  24. data/lib/replay/test/test_event_stream.rb +19 -0
  25. data/lib/replay/version.rb +1 -1
  26. data/proofs/all.rb +7 -0
  27. data/proofs/proofs_init.rb +10 -0
  28. data/proofs/replay/inflector_proof.rb +32 -0
  29. data/proofs/replay/publisher_proof.rb +170 -0
  30. data/proofs/replay/repository_configuration_proof.rb +67 -0
  31. data/proofs/replay/repository_proof.rb +46 -0
  32. data/proofs/replay/subscriber_manager_proof.rb +39 -0
  33. data/proofs/replay/test_proof.rb +28 -0
  34. data/replay.gemspec +5 -4
  35. data/test/replay/observer_spec.rb +37 -0
  36. data/test/replay/router/default_router_spec.rb +43 -0
  37. data/test/test_helper.rb +10 -0
  38. metadata +65 -48
  39. data/README +0 -27
  40. data/lib/replay/active_record_event_store.rb +0 -32
  41. data/lib/replay/domain.rb +0 -33
  42. data/lib/replay/event.rb +0 -27
  43. data/lib/replay/event_store.rb +0 -55
  44. data/lib/replay/projector.rb +0 -19
  45. data/lib/replay/test_storage.rb +0 -8
  46. data/lib/replay/unknown_event_error.rb +0 -2
  47. data/test/spec_helper.rb +0 -6
  48. data/test/test_events.sqlite3 +0 -0
  49. data/test/unit/active_record_event_store_spec.rb +0 -24
  50. data/test/unit/domain_spec.rb +0 -53
  51. data/test/unit/event_spec.rb +0 -13
  52. data/test/unit/event_store_spec.rb +0 -28
  53. data/test/unit/projector_spec.rb +0 -19
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 1c2e196bd8e54aa363a8ad762f41797aa99c989e
4
+ data.tar.gz: cda39f5a62f384de3ba509f291407a54035b007b
5
+ SHA512:
6
+ metadata.gz: af511f51f8dbec4f12fc284bebf4232786081e6832089d1bd7209f7ab88b001e02a40bb85a7182acb562c19715c037c797c0e5be4d54b4bfd51228e9bb853c94
7
+ data.tar.gz: 25ff65cea036543f63088c7ff299acbd1128cbbfd70c74aa6fbfefa4fa4c9312a0b034ac687cecff2e4ef041c063aac18ec5956d897d9f2d63da3c71c8351a57
data/.rvmrc CHANGED
@@ -1 +1 @@
1
- rvm use 1.9.2@replay
1
+ rvm use 2.0.0@replay --create
data/Gemfile CHANGED
@@ -3,8 +3,12 @@ source "http://rubygems.org"
3
3
  # Specify your gem's dependencies in replay.gemspec
4
4
  gemspec
5
5
 
6
+ group :development do
7
+ gem 'guard'
8
+ gem 'guard-shell'
9
+ end
10
+
6
11
  group :test do
7
- gem "ruby-debug19", :platforms => :ruby_19
8
- gem "minitest"
9
- gem "mocha"
12
+ gem "proof", :git => 'https://github.com/Sans/proof.git'
13
+ gem 'byebug'
10
14
  end
data/Guardfile ADDED
@@ -0,0 +1,10 @@
1
+ # A sample Guardfile
2
+ # More info at https://github.com/guard/guard#readme
3
+
4
+ # Add files and commands to this file, like the example:
5
+ # watch(%r{file/path}) { `command(s)` }
6
+ #
7
+ guard :shell, :all_on_start => false do
8
+ watch(/lib\/(.*).rb/) {|m| `ruby proofs/all.rb` }
9
+ watch(/proofs\/(.*).rb/) {|m| `ruby #{m[0]}`}
10
+ end
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2014 Keith Gaddis
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,153 @@
1
+ #Replay
2
+ Replay is a gem to support event sourcing data within domain boundaries. With event sourced data, the application stores data as a series of domain events, which are applied to the domain object in order to mutate state.
3
+
4
+ ###CQRS/ES 30 second intro
5
+ [Command Query Responsibility Segregation](http://codebetter.com/gregyoung/2010/02/16/cqrs-task-based-uis-event-sourcing-agh/) (and [Fowler's explanation](http://martinfowler.com/bliki/CQRS.html) is a pattern popularized by Greg Young and Udi Dahan from within the sphere of Domain Driven Design. The general idea is that within domain models, objects are rarely good at both representing truth and being purposeful for queries and reporting, and therefore we should separate the responsibilities.
6
+
7
+ [Event Sourcing](http://martinfowler.com/eaaDev/EventSourcing.html) is a pattern that is not required by (but pairs extremely well with) CQRS. However, by embracing this pattern a system can adapt to new reporting and query requirements at any time with a great deal of flexibility, and the use of messaging/pub-sub along with events creates an easy path to breaking apart monolithic applications and separating domains.
8
+
9
+ ###A short example
10
+
11
+ class ReplayExample
12
+ include Replay::Publisher
13
+
14
+ #define events
15
+ events do
16
+ SomethingHappened(name: String, pid: Integer)
17
+ SomethingElseHappened(pid: Integer)
18
+ #....
19
+ end
20
+
21
+ #applying events (changing state)
22
+ apply SomethingHappened do |event|
23
+ @name = event.name
24
+ @pid = event.pid
25
+ end
26
+
27
+ apply SomethingElseHappened do |event|
28
+ @state = :happened_again
29
+ @pid = nil if event.pid == @pid
30
+ end
31
+
32
+ def do_something(pid = nil)
33
+ #the command validates inputs
34
+ #InvalidCommand is defined by the application
35
+ raise InvalidCommand.new("parameters were invalid") unless pid
36
+
37
+ #publish events
38
+ publish SomethingHappened.new(:name = "foo", :pid => pid)
39
+
40
+ #publish with method syntax
41
+ publish SomethingElseHappened(pid: pid)
42
+ end
43
+ end
44
+
45
+ There's a couple of things to note about the above example. ReplayExample is a domain object. (Clearly this example is a bit contrived.) [Domain objects](http://martinfowler.com/eaaCatalog/domainModel.html) represent and encapsulate domain logic in its purest sense. No application code should make its way into a domain object, nor should concerns from another bounded context.
46
+
47
+ Domain objects publish events in order to mutate state. The events published by this domain object are defined within the `events` block; `ReplayExample::SomethingHappened` is a class defined there, which has two attributes `name` and `pid`, which are `String` and `Integer` respectively. Events may also be defined manually, like any other class. Because they're essentially value objects, with zero behavior, the shorthand form above is usually going to be easier.
48
+
49
+ `ReplayExample` instances change state by applying events. These events are handled in the `apply` blocks in the above example (you see what I did there?) This part is, mostly, really simple. You probably did a lot of this state thing in your freshman programming class. More on that later.
50
+
51
+ So if we've got the events defined, and we know what events change state in which ways, where do they come from? Commands, of course. The role of a command is to validate its inputs and publish the events if the command is valid. That's it. No changing state allowed there—seriously, none. Ever heard the term [snowflake server](http://martinfowler.com/bliki/SnowflakeServer.html)? Break the state rule and you're going to have snowflake instances and weird bugs.
52
+
53
+ Commands are the art and science of CQRS. In the above example, I've implemented it as a method on the domain object (which is also called an aggregate root in the language of DDD.) Its just as frequently done as a class, e.g.
54
+
55
+ class ReplayExample::DoSomething
56
+ include Replay::Publisher
57
+
58
+ def initialize(name, pid=nil)
59
+ raise InvalidCommand.new unless pid
60
+ @name = name
61
+ @pid = pid
62
+ end
63
+
64
+ def perform
65
+ #the publish the event, but don't raise an error if an application block can't be found
66
+ publish ReplayExample::SomethingHappened.new(name: @name, pid: @pid), false
67
+ end
68
+ end
69
+
70
+ ReplayExample::DoSomething.new("foo", 123).perform
71
+
72
+ The above command class performs the same function, but has some advantages. In a Rails application, you can mix in ActiveModel::Validations to get ActiveRecord-style validators on it. You can also use Virtus (recommended) or ActiveModel to make it ActiveModel compliant and use it as a form object. This pattern is especially useful when you're dealing with non-domain services (e.g. credit card processors.) You can publish events from any model; there's nothing special about that (though its best if you don't do it without good reason, or you'll subvert one of the great advantages of DDD—separation of bounded contexts).
73
+
74
+ ##Digging deeper
75
+
76
+ ###The Repository
77
+ The Repository is an application-defined object (replay will generate one for you) which will load your domain objects from storage. The repository's job is to find the event stream requested and apply the events from the event stream to a newly created object of the supplied type. Every application has at least one repository, and may have several.
78
+
79
+ Use it like so:
80
+
81
+ example = Repository.load(ReplayExample, some_guid)
82
+
83
+ What you'll get back is a newly initialized instance of your object, with all events from the stream applied in sequence. By default, if it doesn't find any events for that stream identifier, it will raise an exception; you can change this behavior by supplying `:create => false` or `:create => true` to `load`. When false, the Repository will not attempt to create the instance. If true, and the object defines a `create` method that takes no parameters, the default implementation will call `create`. (Its standard practice for that method to publish a `Created` event.)
84
+
85
+ Your application's repository will look something like this:
86
+
87
+ class Repository
88
+ include Replay::Repository
89
+
90
+ configure do |config|
91
+ config.store = :active_record
92
+ config.add_default_subscriber EventLogger
93
+ end
94
+ end
95
+
96
+ You can also create a repository for your test environment (though for unit tests its typically unnecessary and for higher levels adding a subscriber will suffice. For example, in Cucumber or its analogues:
97
+
98
+ #features/env.rb
99
+ Repository.configuration.add_default_listener EventMonitor.new
100
+
101
+ ###Observers
102
+ Replay provides a default message router for observers of events.
103
+
104
+ In your repository implementation, add :replay_router to the configuration's default subscribers:
105
+
106
+ class Repository
107
+ include Replay::Repository
108
+
109
+ configure do |config|
110
+ config.add_default_subscriber :replay_router
111
+ end
112
+ end
113
+
114
+ In your application or domain services:
115
+
116
+ class MailService
117
+ include Replay::Observer
118
+
119
+ observe Model::EventHappened do |event|
120
+ #handle the event
121
+ end
122
+ end
123
+
124
+ It may be advantageous in some situations to create multiple routers:
125
+
126
+ class InternalRouter
127
+ include Replay::Router
128
+ end
129
+
130
+ class Repository
131
+ include Replay::Repository
132
+
133
+ configure do |config|
134
+ config.add_default_subscriber InternalRouter
135
+ end
136
+ end
137
+
138
+ class MailService
139
+ include Replay::Observer
140
+ router InternalRouter
141
+
142
+ #observations...
143
+ end
144
+
145
+ ##Additional gems
146
+
147
+ [replay-rails](http://github.com/karmajunkie/replay-rails) provides a very basic ActiveRecord-based event store. Its a good template for building your own event store and light duties in an application in which aggregates don't receive hundreds or thousands of events.
148
+
149
+
150
+ ##TODO
151
+ * Implement snapshots for efficient load from repository
152
+ * Better documentation
153
+ * Build a demonstration app
data/Rakefile CHANGED
@@ -1 +1,16 @@
1
1
  require "bundler/gem_tasks"
2
+ require 'rake/testtask'
3
+
4
+ task :prove_all do
5
+ Bundler.setup
6
+ Bundler.require :default
7
+ require_relative "proofs/all"
8
+ end
9
+ Rake::TestTask.new do |t|
10
+ t.libs.push "lib"
11
+ t.libs.push "test"
12
+ t.test_files = FileList['test/**/*_spec.rb']
13
+ t.verbose = true
14
+ end
15
+
16
+ task :default => [:test, :prove_all]
data/lib/replay.rb CHANGED
@@ -1,14 +1,43 @@
1
- require "replay/version"
2
- require 'active_support/concern'
3
- require 'replay/unknown_event_error'
4
- require 'replay/test_storage'
5
- require 'replay/configuration'
6
- require 'replay/event'
7
- require 'replay/event_store'
8
- require 'replay/active_record_event_store'
9
- require 'replay/domain'
10
- require 'replay/projector'
1
+ require 'virtus'
11
2
 
12
3
  module Replay
4
+ def self.logger=(logger)
5
+ @logger = logger
6
+ end
13
7
 
8
+ def self.logger
9
+ @logger
10
+ end
11
+
12
+ class ReplayError < StandardError; end
13
+ class UndefinedKeyError < ReplayError; end
14
+ class UnhandledEventError < ReplayError; end
15
+ class UnknownEventError < ReplayError; end
16
+ class InvalidStorageError < ReplayError;
17
+ def initialize(*args)
18
+ klass = args.shift
19
+ super( "Storage #{klass.to_s} does not implement #event_stream(stream, event)", *args)
20
+ end
21
+ end
22
+ class InvalidSubscriberError < ReplayError;
23
+ def initialize(*args)
24
+ obj = args.shift
25
+ super( "Subscriber#{obj.to_s} does not implement #published(stream, event)", *args)
26
+ end
27
+ end
14
28
  end
29
+
30
+ require 'replay/inflector'
31
+ require 'replay/events'
32
+ require 'replay/event_decorator'
33
+ require 'replay/event_declarations'
34
+ require 'replay/publisher'
35
+ require 'replay/subscription_manager'
36
+ require 'replay/backends'
37
+ require 'replay/repository'
38
+ require 'replay/repository/identity_map'
39
+ require 'replay/repository/configuration'
40
+ require 'replay/observer'
41
+ require 'replay/router'
42
+ require 'replay/router/default_router'
43
+
@@ -0,0 +1,50 @@
1
+ require 'singleton'
2
+ module Replay
3
+ class Backends
4
+ def self.register(shorthand, klass)
5
+ @backends ||= {}
6
+ @backends[shorthand] = klass
7
+ return klass
8
+ end
9
+ def self.resolve(shorthand)
10
+ @backends[shorthand] || shorthand
11
+ end
12
+
13
+ class MemoryStore
14
+ include Singleton
15
+ def initialize
16
+ @store = {}
17
+ end
18
+ def self.published(stream_id, event)
19
+ instance.published(stream_id, event)
20
+ end
21
+
22
+ def self.clear
23
+ instance.clear
24
+ end
25
+
26
+ def clear
27
+ @store = {}
28
+ end
29
+
30
+ def published(stream_id, event)
31
+ @store[stream_id] ||= []
32
+ @store[stream_id] << event
33
+ end
34
+
35
+ def event_stream(stream_id)
36
+ @store[stream_id] || []
37
+ end
38
+
39
+ def self.event_stream(stream_id)
40
+ instance.event_stream(stream_id)
41
+ end
42
+ def self.[](stream_id)
43
+ instance.event_stream(stream_id)
44
+ end
45
+ end
46
+ register :memory, MemoryStore
47
+ end
48
+ end
49
+
50
+
@@ -0,0 +1,36 @@
1
+ module Replay
2
+ module EventDeclarations
3
+ def self.included(base)
4
+ base.extend(Replay::Events)
5
+ end
6
+
7
+ def included(base)
8
+ self.constants.each do |c|
9
+ base.const_set(c, const_get(c).dup)
10
+ klass = base.const_get(c)
11
+ base.class_eval do
12
+ define_method c do |props|
13
+ klass.new props
14
+ end
15
+ end
16
+ end
17
+ end
18
+ def method_missing(name, *args)
19
+ declare_event(self, name, args.first)
20
+ end
21
+
22
+ def declare_event(base, name, props)
23
+ klass = Class.new do
24
+ include Replay::EventDecorator
25
+ attribute :published_at, Time, default: lambda{|p,a| Time.now}
26
+ values do
27
+ props.keys.each do |prop|
28
+ attribute prop, props[prop]
29
+ end
30
+ end
31
+ include Virtus::Equalizer.new("#{name.to_s} equalizer", (self.attribute_set.map(&:name) - [:published_at]).map(&:to_s))
32
+ end
33
+ base.const_set name, klass
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,13 @@
1
+ module Replay
2
+ #hook class to apply global decorators to events
3
+ module EventDecorator
4
+ def self.included(base)
5
+ base.class_eval do
6
+ include Virtus.value_object
7
+ def inspect
8
+ "#{self.class.to_s}: #{self.attributes.map{|k, v| "#{k.to_s} = #{v.to_s}"}.join(", ")}"
9
+ end
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,24 @@
1
+
2
+ module Replay
3
+ module Events
4
+ def self.extended(base)
5
+ base.extend(ClassMethods)
6
+ end
7
+ def self.included(base)
8
+ base.extend(ClassMethods)
9
+ #self.constants.each{|c| base.const_set(c, const_get(c))}
10
+ end
11
+ module ClassMethods
12
+ def events(mod = nil, &block)
13
+ unless mod
14
+ mod = Module.new do
15
+ extend Replay::EventDeclarations
16
+ module_eval &block
17
+ end
18
+ self.const_set(:Events, mod)
19
+ end
20
+ include mod
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,55 @@
1
+ #This class substantially copied from ActiveSupport, licensed under MIT
2
+ #Their version is generally better, so use that unless you've got a good reason
3
+ #not to do so. also, i've changed certain aspects of behavior.
4
+ class Replay::Inflector
5
+
6
+ # By default, +camelize+ converts strings to UpperCamelCase. If the argument
7
+ # to +camelize+ is set to <tt>:lower</tt> then +camelize+ produces
8
+ # lowerCamelCase.
9
+ #
10
+ # +camelize+ will also convert '/' to '::' which is useful for converting
11
+ # paths to namespaces.
12
+ #
13
+ # 'active_model'.camelize # => "ActiveModel"
14
+ # 'active_model'.camelize(:lower) # => "activeModel"
15
+ # 'active_model/errors'.camelize # => "ActiveModel::Errors"
16
+ # 'active_model/errors'.camelize(:lower) # => "activeModel::Errors"
17
+ #
18
+ # As a rule of thumb you can think of +camelize+ as the inverse of
19
+ # +underscore+, though there are cases where that does not hold:
20
+ #
21
+ # 'SSLError'.underscore.camelize # => "SslError"
22
+ def self.camelize(term, uppercase_first_letter = false)
23
+ string = term.to_s
24
+ string = string.sub(/^[A-Z_]/) { $&.downcase }
25
+ string.gsub!(/(?:_|(\/))([a-z\d]*)/i) { "#{$1}#{$2.capitalize}" }
26
+ string.gsub!('/', '::')
27
+ string.gsub!('.', '::')
28
+ string
29
+ end
30
+ # Makes an underscored, lowercase form from the expression in the string.
31
+ #
32
+ # Changes '::' to '/' to convert namespaces to paths.
33
+ #
34
+ # 'ActiveModel'.underscore # => "active_model"
35
+ # 'ActiveModel::Errors'.underscore # => "active_model/errors"
36
+ #
37
+ # As a rule of thumb you can think of +underscore+ as the inverse of
38
+ # +camelize+, though there are cases where that does not hold:
39
+ #
40
+ # 'SSLError'.underscore.camelize # => "SslError"
41
+ def self.underscore(camel_cased_word)
42
+ return camel_cased_word unless camel_cased_word =~ /[A-Z-]|::/
43
+ word = camel_cased_word.to_s.gsub('::', '.')
44
+ word.gsub!(/(?:([A-Za-z\d])^)(?=\b|[^a-z])/) { "#{$1}#{$2 && '_'}#{$2.downcase}" }
45
+ word.gsub!(/([A-Z\d]+)([A-Z][a-z])/,'\1_\2')
46
+ word.gsub!(/([a-z\d])([A-Z])/,'\1_\2')
47
+ word.tr!("-", "_")
48
+ word.downcase!
49
+ word
50
+ end
51
+
52
+ def self.constantize(class_name)
53
+ class_name.to_s.split("::").inject(Kernel){|parent, mod| parent.const_get(mod)}
54
+ end
55
+ end