event_source 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: f482b09497aecd4c9d777e9044129236d63492e4
4
+ data.tar.gz: cd32bd6e04351a71e77aaf1c22e51e14f437dca1
5
+ SHA512:
6
+ metadata.gz: bef50c5d9b5fa8e3691da030a59cc002f1c3ea62794a5510b5c8318252d7704c6a07893077902073b02312f6ace48e2078b351dab3e009ed447847da72be7625
7
+ data.tar.gz: 09fb34fd611b0a9146e913f3a512cac0afb31334d744bb31024db007889debc1c72382c8f53e5729c957006428ac76751b19d2fadfbcb25bb65221b90c7d7c0e
data/CHANGELOG.md ADDED
@@ -0,0 +1,5 @@
1
+ # changes
2
+
3
+ ## version 0.1.0
4
+
5
+ nothing yet!
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2013 Louis P. Salin
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,101 @@
1
+ # Event Sourcing
2
+
3
+ This library is an implementation of the Event Sourcing pattern, where instead of persisting the state of your objects in a data store, it is the sequence of events that led to the objects' state that is stored. In order to rebuild objects, the events must be replayed on each object.
4
+
5
+ ## The event repository
6
+
7
+ An event store must be initialized. Currently, only the in_memory SQLlite3 type is available.
8
+
9
+ ```ruby
10
+ EventSource::EventRepository.create(in_memory: true)
11
+ ```
12
+
13
+ ## Your entities
14
+
15
+ An entity is an object that you intend to persist. You must extend and include some class methods and instance methods. Let's create an entity called BankAccount.
16
+
17
+ ```ruby
18
+ class BankAccount
19
+ extend EventSource::Entity::ClassMethods
20
+ include EventSource::Entity
21
+
22
+ end
23
+ ```
24
+
25
+ For each of the entity's members that you want to persist, you must create an accessor. Then you must create events for your entity. For example, 'deposit' could be an event on the bank account entity and balance would be something we want to persist.
26
+
27
+ ```ruby
28
+ class BankAccount
29
+ extend EventSource::Entity::ClassMethods
30
+ include EventSource::Entity
31
+
32
+ attr_accessor :balance
33
+
34
+ on_event :deposit do |e, amount|
35
+ end
36
+ end
37
+ ```
38
+
39
+ Your event handler will receive at least one parameter, which is the instance of the entity being modified. It is used to set the state of your entity by calling the method 'set' on it. All the other parameters will be arguments that must be giving when calling the event. In this case, when calling the deposit event, the caller will have to supply the amount.
40
+
41
+ Here's what could go into event handler:
42
+
43
+ ```ruby
44
+ on_event :deposit do |e, amount|
45
+ e.set(:balance) {@balance + amount}
46
+ end
47
+ ```
48
+
49
+ The 'set' method does more than simply set the @balance attribute. It also saves the state of your entity into the event so that when the event eventually gets replayed to rebuild this entity, it will know how to set the balance of the account.
50
+
51
+ To call the event, simply call the 'deposit' method that was created for you:
52
+
53
+ ```ruby
54
+ account = BankAccount.create
55
+ account.deposit(100)
56
+ ```
57
+
58
+ Note that calling the event outside the context of a Entity Repository transaction won't persist the event. (see below)
59
+
60
+ ### unique identifiers
61
+
62
+ Each entity gets a unique identifier when created the first time. This id is important. You will need it to recreate the entity. You can access it with the 'uid' attribute.
63
+
64
+ ```ruby
65
+ account.uid
66
+ ```
67
+
68
+ ## Creating entities
69
+
70
+ Call the 'create' class method to create a new instance of your entity.
71
+
72
+ ```ruby
73
+ account = BankAccount.create
74
+ ```
75
+
76
+ ## The Entity Repository
77
+
78
+ This repository is used to store and load entities. It is also used to monitor changes to entities. When a transaction is created, all changes to all entities and all events created will be saved in the event repository.
79
+
80
+ ### Transactions
81
+
82
+ ```ruby
83
+ EventSource::EntityRepository.transaction do
84
+ account.deposit(100)
85
+ end
86
+ ```
87
+
88
+ The code above will store one event in the event store: a deposit event that adds 100 to the balance of the account.
89
+
90
+ ### Loading an entity
91
+
92
+ To load an entity, you must create an entity repository and pass it an instance of your event repository, then call 'find'. I know, this isn't the simplest thing yet. It'll improve.
93
+
94
+ The 'find' method expects an entity type and the entity's unique identifier. The type, by convention, is simply a lowercase undescored string of the entity class name.
95
+
96
+ ```ruby
97
+ event_repo = EventSource::EventRepository.current
98
+ entity_repo = EventSource::EntityRepository.new(event_repo)
99
+ entity = entity_repo.find(:bank_account, uid)
100
+ ```
101
+
@@ -0,0 +1,71 @@
1
+ module EventSource
2
+ module Entity
3
+ module ClassMethods
4
+ def create(uid = EventSource::UIDGenerator.generate_id)
5
+ entity = self.new
6
+ entity.send(:uid=, uid)
7
+
8
+ yield entity if block_given?
9
+ entity
10
+ end
11
+
12
+ def rebuild(events)
13
+ return self.create() if events.length == 0
14
+
15
+ entity = self.new
16
+
17
+ events.each do |e|
18
+ data = JSON.parse(e.data)
19
+ data.keys.each do |attr|
20
+ entity.send("#{attr}=", data[attr])
21
+ end
22
+ end
23
+
24
+ entity
25
+ end
26
+
27
+ def on_event(name, &block)
28
+ self.send(:define_method, name) do |*args|
29
+ returnValue = block.call(args.unshift(self))
30
+ entity_events << EventSource::Event.create(name, self)
31
+
32
+ # if repo is nil, that's because this isn't being executed in the context of a
33
+ # transaction and the result won't be saved
34
+ repo = EventSource::EntityRepository.current
35
+ repo.entities << self if repo
36
+
37
+ returnValue
38
+ end
39
+ end
40
+ end
41
+
42
+ attr_reader :uid,
43
+ :entity_changes
44
+
45
+ def set(attr_name, &block)
46
+ val = block.call
47
+ @entity_changes[attr_name.to_sym] = val
48
+ self.send("#{attr_name}=", val)
49
+ end
50
+
51
+ def entity_events
52
+ @events ||= Array.new
53
+ @events
54
+ end
55
+
56
+ def save
57
+ entity_events.each {|e| e.save}
58
+ end
59
+
60
+ private
61
+
62
+ def initialize
63
+ @entity_changes = Hash.new
64
+ end
65
+
66
+ def uid=(new_uid)
67
+ @uid = new_uid
68
+ end
69
+ end
70
+ end
71
+
@@ -0,0 +1,57 @@
1
+ # the repository should be thread safe. This is accomplished by forcing
2
+ # every transaction done (anything from newing up the repo to commiting it) to
3
+ # create their own repo. That way, changes aren't shared.
4
+
5
+ require 'set'
6
+ require 'active_support/inflector'
7
+
8
+ module EventSource
9
+ class EntityRepository
10
+ attr_reader :entities
11
+
12
+ class << self
13
+ @@current = nil
14
+
15
+ def transaction
16
+ @@current = self.new
17
+ yield
18
+ @@current.commit
19
+ @@current = nil
20
+ end
21
+
22
+ def current
23
+ @@current
24
+ end
25
+ end
26
+
27
+ def initialize(event_repo = nil)
28
+ @entities = Set.new
29
+ @event_repo = event_repo
30
+ end
31
+
32
+ def add(entity)
33
+ @entities << entity
34
+ end
35
+
36
+ def commit
37
+ # TODO: gather all events of all entities, maintain order and save in batch
38
+ @entities.each {|e| e.save}
39
+ end
40
+
41
+ def find(type, uid)
42
+ entity = @entities.select {|e| e.uid == uid}[0]
43
+ return entity if entity
44
+
45
+ events = @event_repo.get_events(type, uid)
46
+
47
+ entity_class = type.to_s.camelize.constantize
48
+ if events.count > 0
49
+ entity = entity_class.rebuild(events)
50
+ else
51
+ entity = entity_class.create(uid)
52
+ end
53
+
54
+ entity
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,49 @@
1
+ require 'active_support/inflector'
2
+ require 'json'
3
+
4
+ module EventSource
5
+ class Event
6
+ attr_reader :name, :entity_type, :entity_id, :data, :created_at
7
+
8
+ class << self
9
+ def build_from_data(data)
10
+ event = self.new
11
+ event.send(:new_from_data, data)
12
+ event
13
+ end
14
+
15
+ def create(name, entity)
16
+ event = self.new
17
+ event.send(:new_from_entity, name, entity)
18
+ event
19
+ end
20
+ end
21
+
22
+ def save
23
+ raise CannotSaveRebuiltEvent if @is_rebuilt
24
+ EventSource::EventRepository.current.save(self)
25
+ end
26
+
27
+ private
28
+
29
+ def new_from_entity(name, entity)
30
+ @name = name.to_s
31
+ @entity_id = entity.uid
32
+ @data = entity.entity_changes.to_json
33
+ @created_at = Time.now
34
+ @entity_type = entity.class.to_s.underscore
35
+
36
+ @is_rebuilt = false
37
+ end
38
+
39
+ def new_from_data(data)
40
+ @name = data[:name]
41
+ @entity_id = data[:entity_id]
42
+ @data = data[:data]
43
+ @created_at = data[:created_at]
44
+ @entity_type = data[:entity_type]
45
+
46
+ @is_rebuilt = true
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,57 @@
1
+ require 'sequel'
2
+
3
+ module EventSource
4
+ class EventRepository
5
+ attr_reader :db
6
+
7
+ class << self
8
+ def current
9
+ @@instance ||= self.new(in_memory: true)
10
+ end
11
+
12
+ def create(options)
13
+ @@instance = self.new(options)
14
+ end
15
+ end
16
+
17
+ def save(event)
18
+ count = @db[:events].exclude_where(entity_type: event.entity_type).
19
+ where(entity_id: event.entity_id).count
20
+
21
+ raise InvalidEntityID if count > 0
22
+ @db[:events].insert(name: event.name, entity_type: event.entity_type,
23
+ entity_id: event.entity_id, data: event.data,
24
+ created_at: event.created_at)
25
+ end
26
+
27
+ def get_events(type, uid)
28
+ data = @db[:events].where(entity_type: type.to_s, entity_id: uid).order(:created_at)
29
+ data.map {|d| create_event(d)}
30
+ end
31
+
32
+ private
33
+
34
+ def initialize(options)
35
+ if options[:in_memory]
36
+ @db = Sequel.sqlite
37
+ init_in_memory_schema
38
+ end
39
+ end
40
+
41
+ def init_in_memory_schema
42
+ @db.create_table :events do
43
+ primary_key :id
44
+ String :name
45
+ String :entity_id
46
+ String :entity_type
47
+ Time :created_at
48
+ String :data
49
+ end
50
+ end
51
+
52
+ def create_event(data)
53
+ EventSource::Event.build_from_data(data)
54
+ end
55
+ end
56
+ end
57
+
@@ -0,0 +1,2 @@
1
+ class InvalidEntityID < Exception; end
2
+ class CannotSaveRebuiltEvent < Exception; end
@@ -0,0 +1,9 @@
1
+ require 'uuidtools'
2
+
3
+ module EventSource
4
+ class UIDGenerator
5
+ def self.generate_id
6
+ UUIDTools::UUID.timestamp_create.to_s
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,3 @@
1
+ module EventSource
2
+ Version = '0.1.0'
3
+ end
@@ -0,0 +1,9 @@
1
+ require './lib/event_source/exceptions'
2
+ require './lib/event_source/uid_generator'
3
+ require './lib/event_source/event'
4
+ require './lib/event_source/entity'
5
+ require './lib/event_source/entity_repository'
6
+ require './lib/event_source/event_repository'
7
+
8
+ module EventSource
9
+ end
metadata ADDED
@@ -0,0 +1,112 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: event_source
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Louis Salin
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2013-06-06 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: rspec
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - '>='
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - '>='
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: uuidtools
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - '>='
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - '>='
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: sequel
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - '>='
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - '>='
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: activesupport
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - '>='
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - '>='
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ description: Event sourcing allows you to persist changes to your domain instead of
70
+ the state of your domain
71
+ email:
72
+ - louis.phil@gmail.com
73
+ executables: []
74
+ extensions: []
75
+ extra_rdoc_files: []
76
+ files:
77
+ - lib/event_source.rb
78
+ - lib/event_source/uid_generator.rb
79
+ - lib/event_source/event.rb
80
+ - lib/event_source/entity.rb
81
+ - lib/event_source/event_repository.rb
82
+ - lib/event_source/entity_repository.rb
83
+ - lib/event_source/version.rb
84
+ - lib/event_source/exceptions.rb
85
+ - LICENSE
86
+ - README.md
87
+ - CHANGELOG.md
88
+ homepage: http://github.com/louissalin/event_source
89
+ licenses:
90
+ - MIT
91
+ metadata: {}
92
+ post_install_message:
93
+ rdoc_options: []
94
+ require_paths:
95
+ - lib
96
+ required_ruby_version: !ruby/object:Gem::Requirement
97
+ requirements:
98
+ - - '>='
99
+ - !ruby/object:Gem::Version
100
+ version: '0'
101
+ required_rubygems_version: !ruby/object:Gem::Requirement
102
+ requirements:
103
+ - - '>='
104
+ - !ruby/object:Gem::Version
105
+ version: '0'
106
+ requirements: []
107
+ rubyforge_project:
108
+ rubygems_version: 2.0.0.rc.2
109
+ signing_key:
110
+ specification_version: 4
111
+ summary: Event sourcing framework
112
+ test_files: []