frp-eventsourcing 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (39) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +15 -0
  3. data/.rspec +2 -0
  4. data/.travis.yml +5 -0
  5. data/Gemfile +4 -0
  6. data/LICENSE.txt +25 -0
  7. data/README.md +187 -0
  8. data/Rakefile +6 -0
  9. data/bin/console +14 -0
  10. data/bin/setup +8 -0
  11. data/frp-eventsourcing.gemspec +43 -0
  12. data/lib/frp-eventsourcing.rb +12 -0
  13. data/lib/frp-eventsourcing/event.rb +37 -0
  14. data/lib/frp-eventsourcing/event_repository.rb +95 -0
  15. data/lib/frp-eventsourcing/generators/migration_generator.rb +19 -0
  16. data/lib/frp-eventsourcing/generators/templates/migration_template.rb +16 -0
  17. data/lib/frp-eventsourcing/stream.rb +114 -0
  18. data/lib/frp-eventsourcing/stream/each.rb +29 -0
  19. data/lib/frp-eventsourcing/stream/filter.rb +14 -0
  20. data/lib/frp-eventsourcing/stream/init.rb +24 -0
  21. data/lib/frp-eventsourcing/stream/map.rb +15 -0
  22. data/lib/frp-eventsourcing/stream/when.rb +31 -0
  23. data/lib/frp-eventsourcing/version.rb +3 -0
  24. data/sources/All_About_Monads.pdf +0 -0
  25. data/sources/GregoryMeredith.pdf +54530 -72
  26. data/sources/RetroactiveComputing_Mueller2016.pdf +0 -0
  27. data/sources/a_survey_on_reactive_programming.pdf +0 -0
  28. data/sources/aw.implementing.domain-driven.design.0321834577/AW.Implementing.Domain-Driven.Design.0321834577.epub +0 -0
  29. data/sources/aw.implementing.domain-driven.design.0321834577/AW.Implementing.Domain-Driven.Design.0321834577.mobi +0 -0
  30. data/sources/conal_elliott_push_pull_frp.pdf +0 -0
  31. data/sources/cqrs_documents.pdf +0 -0
  32. data/sources/event_driven_frp.pdf +0 -0
  33. data/sources/flapjax.pdf +0 -0
  34. data/sources/functional_reactive_animation.pdf +0 -0
  35. data/sources/reactive_programming_with_events.pdf +0 -0
  36. data/sources/reactiveruby.pdf +0 -0
  37. data/sources/tool-support-for-reactive-programming.pdf +0 -0
  38. data/sources/xx735.Eric.Evans.Domaindriven.Design.Tackling.Complexity.in.the.Heart.of.Software.pdf +0 -0
  39. metadata +210 -0
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: b89e010d2caf9f64fc4a66718e79111e0e0d3a30
4
+ data.tar.gz: cf6ce8fbc999d409ae9a454cf06c9e843f24fc8d
5
+ SHA512:
6
+ metadata.gz: 8d097c4b0c2813a039ed35815c3ee3d837eb15b7722120617393131e7b5c0ea035473d0e25fdab5034d013aa8ff644f9f6f91b440519b96463b907ff8230ad66
7
+ data.tar.gz: 7e78cc3362d3b8d8b2003cf8553bf362455ebf85b3b3a01c7b40707fdddded9011b59f54ca3092afd6e05333b1b79960c5ba1ab007623d4a7e226e8282eebaa2
@@ -0,0 +1,15 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
10
+
11
+ # rspec failure tracking
12
+ .rspec_status
13
+
14
+ # Mac files
15
+ .DS_Store
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --format documentation
2
+ --color
@@ -0,0 +1,5 @@
1
+ sudo: false
2
+ language: ruby
3
+ rvm:
4
+ - 2.4.0
5
+ before_install: gem install bundler -v 1.15.1
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source "https://rubygems.org"
2
+
3
+ # Specify your gem's dependencies in frp-eventsourcing.gemspec
4
+ gemspec
@@ -0,0 +1,25 @@
1
+ The MIT License (MIT)
2
+ =====================
3
+
4
+ Copyright © 2017 Žilvinas Kučinskas
5
+
6
+ Permission is hereby granted, free of charge, to any person
7
+ obtaining a copy of this software and associated documentation
8
+ files (the “Software”), to deal in the Software without
9
+ restriction, including without limitation the rights to use,
10
+ copy, modify, merge, publish, distribute, sublicense, and/or sell
11
+ copies of the Software, and to permit persons to whom the
12
+ Software is furnished to do so, subject to the following
13
+ conditions:
14
+
15
+ The above copyright notice and this permission notice shall be
16
+ included in all copies or substantial portions of the Software.
17
+
18
+ THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND,
19
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
20
+ OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
21
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
22
+ HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
23
+ WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
24
+ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
25
+ OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,187 @@
1
+ [![Build Status](https://travis-ci.org/ZilvinasKucinskas/FRP-EventSourcing.svg?branch=master)](https://travis-ci.org/ZilvinasKucinskas/FRP-EventSourcing)
2
+
3
+ Will be released soon (2017 June) after testing out in sample application.
4
+
5
+ # Frp-Eventsourcing
6
+
7
+ EventSourcing describes current state as series of events that occurred in a system. Events hold all information that is needed to recreate current state. This method allows to achieve high volume of transactions, and enables efficient replication. Whereas reactive programming lets implement reactive systems in declarative style, decomposing logic into smaller, easier to understand components. The goal is to create reactive programming program interface, incorporating both principles. Applying reactive programming in event-sourcing systems enables modelling not only instantaneous events, but also have their history. Furthermore, it enables focus on the solvable problem, regardless of low level realization details. Reactive operators enable read model creation without exposing realization details of operations with data storage.
8
+
9
+ ## Sources to learn more about
10
+
11
+ ### (Functional) Reactive programming
12
+
13
+ * [The introduction to Reactive Programming you've been missing](https://gist.github.com/staltz/868e7e9bc2a7b8c1f754)
14
+ (by [@andrestaltz](https://twitter.com/andrestaltz))
15
+ * [Lambda Jam 2015 - Conal Elliott - The Essence and Origins of Functional Reactive Programming](https://youtu.be/j3Q32brCUAI)
16
+ * More [here](https://github.com/ZilvinasKucinskas/FRP-EventSourcing/tree/master/sources)
17
+
18
+ ### EventSourcing
19
+
20
+ * [Martin Fowler - EventSourcing](http://martinfowler.com/eaaDev/EventSourcing.html)
21
+ * [Greg Young - CQRS documents](https://github.com/ZilvinasKucinskas/FRP-EventSourcing/blob/master/sources/cqrs_documents.pdf)
22
+ * GetEventStore - The open-source, functional database with Complex Event Processing in JavaScript. [Event Sourcing Basics](http://docs.geteventstore.com/introduction/3.9.0/event-sourcing-basics/)
23
+ * [Martin Fowler - Retroactive Event](https://martinfowler.com/eaaDev/RetroactiveEvent.html)
24
+ * [Akka Persistence module uses EventSourcing](http://doc.akka.io/docs/akka/snapshot/scala/persistence.html#event-sourcing)
25
+ * More [here](https://github.com/ZilvinasKucinskas/FRP-EventSourcing/tree/master/sources)
26
+
27
+ ## Installation
28
+
29
+ Add this line to your application's Gemfile:
30
+
31
+ ```ruby
32
+ gem 'frp-eventsourcing'
33
+ ```
34
+
35
+ And then execute:
36
+
37
+ $ bundle
38
+
39
+ Or install it yourself as:
40
+
41
+ $ gem install frp-eventsourcing
42
+
43
+ ## Usage
44
+
45
+ ### Generate EventStore event model
46
+
47
+ Use provided task to generate a table to store events in your database.
48
+
49
+ ```
50
+ rails generate frp_eventsourcing:migration
51
+ rake db:migrate
52
+ ```
53
+
54
+ ### Event definitions
55
+
56
+ ```
57
+ # Define events
58
+ AccountCreated = Class.new(FrpEventsourcing::Event)
59
+ MoneyDeposited = Class.new(FrpEventsourcing::Event)
60
+ MoneyWithdrawn = Class.new(FrpEventsourcing::Event)
61
+ ```
62
+
63
+ Alternative definition:
64
+
65
+ ```
66
+ class AccountCreated < FrpEventsourcing::Event; end
67
+ class MoneyDeposited < FrpEventsourcing::Event; end
68
+ class MoneyWithdrawn < FrpEventsourcing::Event; end
69
+ ```
70
+
71
+ ### Create stream
72
+
73
+ We can define stream that is creating read model once in our app. Keep in mind that no database operations are present here.
74
+
75
+ ```
76
+ account_stream = FrpEventsourcing::Stream.new(AccountCreated, MoneyDeposited, MoneyWithdrawn).
77
+ as_persistent_type(Account, %i(account_id)).
78
+ init(-> (state) { state.balance = 0 }).
79
+ when(MoneyDeposited, -> (state, event) { state.balance += event[:data][:amount] }).
80
+ when(MoneyWithdrawn, -> (state, event) { state.balance -= event[:data][:amount] })
81
+ ```
82
+
83
+ Instead of passing `lambda` directly, we can also use a variable to save and reuse `lambda`:
84
+
85
+ ```
86
+ account_initial_state_change_function = -> (state) { state.balance = 0 }
87
+ ```
88
+
89
+ or even use a class that implements `call` method. We can structure our code with some kind of denormalizer for example:
90
+
91
+ ```
92
+ class Denormalizers::ReadModelType::InitialState::Account
93
+ def call(state)
94
+ state.balance = 0
95
+ end
96
+ end
97
+ ```
98
+
99
+ ### Publish events
100
+
101
+ We can create an account:
102
+
103
+ ```
104
+ stream_name = "account"
105
+ event = AccountCreated.new(data: {
106
+ account_id: 'LT121000011101001000'
107
+ })
108
+ FrpEventsourcing::EventRepository.new.create(event, stream_name)
109
+ ```
110
+
111
+ Transfer some money (100$ for example) to the account:
112
+
113
+ ```
114
+ stream_name = "account"
115
+ event = MoneyDeposited.new(data: {
116
+ account_id: 'LT121000011101001000',
117
+ amount: 100
118
+ })
119
+ FrpEventsourcing::EventRepository.new.create(event, stream_name)
120
+ ```
121
+
122
+ Withdraw some money (25$ for example) from the account:
123
+
124
+ ```
125
+ stream_name = "account"
126
+ event = MoneyWithdrawn.new(data: {
127
+ account_id: 'LT121000011101001000',
128
+ amount: 25
129
+ })
130
+ FrpEventsourcing::EventRepository.new.create(event, stream_name)
131
+ ```
132
+
133
+ Now we can query read model like:
134
+
135
+ ```
136
+ account = Account.find_by(account_id: 'LT121000011101001000')
137
+ puts account.balance # prints 75
138
+ ```
139
+
140
+ ## Available reactive operators
141
+
142
+ * `merge(another_stream)` - merge one stream to another.
143
+ * `filter(predicate_function)` - if predicate function returns false, event won't get propogated through the chain any more.
144
+ * `map(transform_function)` - applies transformation function and propogates event through the chain.
145
+ * `init(initial_state_change_function)` - applies initial state change function for the first event.
146
+ * `when(event_type, state_change_function)` - if event matches event type, record is being created or loaded, state change function is being applied for the record and transition state saved to database.
147
+ * `each(state_change_function)` - same as `when` operator, just does not check event type and applies state change function for each event.
148
+
149
+ ## Implementation
150
+
151
+ Reactive operators were implemented using Observer design pattern, object oriented programming principles and introspection.
152
+
153
+ Transition state is being solved by applying metaprogramming (introspection, reflection) and using method chaining.
154
+
155
+ ## Development
156
+
157
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
158
+
159
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
160
+
161
+ ## Possible improvements
162
+
163
+ Easy-medium difficulty:
164
+
165
+ * Make demo application with UI. Controller should publish events. Event types and streams should be defined in some kind of initializer file for example.
166
+ * Improve error handling - could be bugs, because it's just prototype version.
167
+ * Refactor event publishing mechanics. We can borrow optimistic locking from [RailsEventStore](https://github.com/arkency/rails_event_store)
168
+ * More tests
169
+
170
+ Challenging:
171
+
172
+ * Make mechanics for recreating read models after changes. We should reapply all associated events.
173
+ * Use custom events repository + instructions how to change adapter for `FrpEventsourcing::EventRepository`
174
+
175
+ ## Contributing
176
+
177
+ Bug reports and pull requests are welcome on GitHub at https://github.com/ZilvinasKucinskas/FRP-EventSourcing.
178
+
179
+ 1. Fork it
180
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
181
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
182
+ 4. Push to the branch (`git push origin my-new-feature`)
183
+ 5. Create new Pull Request
184
+
185
+ ## License
186
+
187
+ The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
@@ -0,0 +1,6 @@
1
+ require "bundler/gem_tasks"
2
+ require "rspec/core/rake_task"
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ task :default => :spec
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "frp-eventsourcing"
5
+
6
+ # You can add fixtures and/or initialization code here to make experimenting
7
+ # with your gem easier. You can also use a different console, if you like.
8
+
9
+ # (If you use this, don't forget to add pry to your Gemfile!)
10
+ # require "pry"
11
+ # Pry.start
12
+
13
+ require "irb"
14
+ IRB.start(__FILE__)
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
@@ -0,0 +1,43 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'frp-eventsourcing/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = 'frp-eventsourcing'
8
+ spec.version = FrpEventsourcing::VERSION
9
+ spec.authors = ['Zilvinas']
10
+ spec.email = ['zil.kucinskas@gmail.com']
11
+
12
+ spec.summary = 'A library to do Functional Reactive Programming with EventSourcing in Ruby'
13
+ spec.description = 'Functional Reactive Programming with EventSourcing in Ruby'
14
+ spec.homepage = 'https://github.com/ZilvinasKucinskas/FRP-EventSourcing'
15
+ spec.license = 'MIT'
16
+
17
+ # Prevent pushing this gem to RubyGems.org. To allow pushes either set the 'allowed_push_host'
18
+ # to allow pushing to a single host or delete this section to allow pushing to any host.
19
+ if spec.respond_to?(:metadata)
20
+ spec.metadata['allowed_push_host'] = 'https://rubygems.org'
21
+ else
22
+ raise 'RubyGems 2.0 or newer is required to protect against ' \
23
+ 'public gem pushes.'
24
+ end
25
+
26
+ spec.files = `git ls-files -z`.split("\x0").reject do |f|
27
+ f.match(%r{^(test|spec|features)/})
28
+ end
29
+ spec.bindir = 'exe'
30
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
31
+ spec.require_paths = ['lib']
32
+
33
+ spec.add_runtime_dependency 'activerecord', '>= 4.0.0'
34
+
35
+ spec.add_development_dependency 'bundler', '~> 1.15'
36
+ spec.add_development_dependency 'rake', '~> 10.0'
37
+ spec.add_development_dependency 'rspec', '~> 3.0'
38
+ spec.add_development_dependency 'rails', '~> 4.2'
39
+ spec.add_development_dependency 'pry', '~> 0.10'
40
+ spec.add_development_dependency 'activerecord', '~> 4.0'
41
+ spec.add_development_dependency 'sqlite3', '~> 1.3'
42
+ spec.add_development_dependency 'database_cleaner', '~> 1.6'
43
+ end
@@ -0,0 +1,12 @@
1
+ require 'frp-eventsourcing/version'
2
+ require 'frp-eventsourcing/generators/migration_generator'
3
+ require 'frp-eventsourcing/event'
4
+ require 'frp-eventsourcing/event_repository'
5
+ require 'frp-eventsourcing/stream'
6
+
7
+ class EventStoreEvent < ::ActiveRecord::Base
8
+ self.primary_key = :id
9
+
10
+ serialize :metadata
11
+ serialize :data
12
+ end
@@ -0,0 +1,37 @@
1
+ require 'securerandom'
2
+ require 'observer'
3
+
4
+ module FrpEventsourcing
5
+ class Event
6
+ extend Observable
7
+
8
+ def initialize(event_id: SecureRandom.uuid, metadata: nil, data: nil)
9
+ @event_id = event_id.to_s
10
+ @metadata = metadata.to_h
11
+ @data = data.to_h
12
+ end
13
+ attr_reader :event_id, :metadata, :data
14
+
15
+ def to_h
16
+ {
17
+ event_id: event_id,
18
+ event_type: self.class,
19
+ metadata: metadata,
20
+ data: data
21
+ }
22
+ end
23
+
24
+ def ==(other_event)
25
+ other_event.instance_of?(self.class) &&
26
+ other_event.event_id.eql?(event_id) &&
27
+ other_event.data.eql?(data)
28
+ end
29
+
30
+ alias_method :eql?, :==
31
+
32
+ def emit
33
+ self.class.changed
34
+ self.class.notify_observers(self.to_h)
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,95 @@
1
+ module FrpEventsourcing
2
+ class EventRepository
3
+ def initialize(adapter: ::EventStoreEvent)
4
+ @adapter = adapter
5
+ end
6
+ attr_reader :adapter
7
+
8
+ def create(event, stream_name)
9
+ data = event.to_h.merge!(stream: stream_name)
10
+ adapter.create(data)
11
+
12
+ event.emit if event.respond_to?(:emit)
13
+
14
+ event
15
+ end
16
+
17
+ def delete_stream(stream_name)
18
+ condition = {stream: stream_name}
19
+ adapter.destroy_all condition
20
+ end
21
+
22
+ def has_event?(event_id)
23
+ adapter.exists?(event_id: event_id)
24
+ end
25
+
26
+ def last_stream_event(stream_name)
27
+ build_event_entity(adapter.where(stream: stream_name).last)
28
+ end
29
+
30
+ def read_events_forward(stream_name, start_event_id, count)
31
+ stream = adapter.where(stream: stream_name)
32
+ unless start_event_id.equal?(:head)
33
+ starting_event = adapter.find_by(event_id: start_event_id)
34
+ stream = stream.where('id > ?', starting_event)
35
+ end
36
+
37
+ stream.limit(count)
38
+ .map(&method(:build_event_entity))
39
+ end
40
+
41
+ def read_events_backward(stream_name, start_event_id, count)
42
+ stream = adapter.where(stream: stream_name)
43
+ unless start_event_id.equal?(:head)
44
+ starting_event = adapter.find_by(event_id: start_event_id)
45
+ stream = stream.where('id < ?', starting_event)
46
+ end
47
+
48
+ stream.order('id DESC').limit(count)
49
+ .map(&method(:build_event_entity))
50
+ end
51
+
52
+ def read_stream_events_forward(stream_name)
53
+ adapter.where(stream: stream_name)
54
+ .map(&method(:build_event_entity))
55
+ end
56
+
57
+ def read_stream_events_backward(stream_name)
58
+ adapter.where(stream: stream_name).order('id DESC')
59
+ .map(&method(:build_event_entity))
60
+ end
61
+
62
+ def read_all_streams_forward(start_event_id, count)
63
+ stream = adapter
64
+ unless start_event_id.equal?(:head)
65
+ starting_event = adapter.find_by(event_id: start_event_id)
66
+ stream = stream.where('id > ?', starting_event)
67
+ end
68
+
69
+ stream.limit(count)
70
+ .map(&method(:build_event_entity))
71
+ end
72
+
73
+ def read_all_streams_backward(start_event_id, count)
74
+ stream = adapter
75
+ unless start_event_id.equal?(:head)
76
+ starting_event = adapter.find_by(event_id: start_event_id)
77
+ stream = stream.where('id < ?', starting_event)
78
+ end
79
+
80
+ stream.order('id DESC').limit(count)
81
+ .map(&method(:build_event_entity))
82
+ end
83
+
84
+ private
85
+
86
+ def build_event_entity(record)
87
+ return nil unless record
88
+ record.event_type.constantize.new(
89
+ event_id: record.event_id,
90
+ metadata: record.metadata,
91
+ data: record.data
92
+ )
93
+ end
94
+ end
95
+ end