event_sourced_record 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +25 -0
- data/Appraisals +8 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +131 -0
- data/Rakefile +10 -0
- data/event_sourced_record.gemspec +37 -0
- data/lib/event_sourced_record/calculator.rb +101 -0
- data/lib/event_sourced_record/event/event_type_config.rb +30 -0
- data/lib/event_sourced_record/event.rb +94 -0
- data/lib/event_sourced_record/version.rb +3 -0
- data/lib/event_sourced_record.rb +7 -0
- data/lib/generators/event_sourced_record/USAGE +8 -0
- data/lib/generators/event_sourced_record/calculator_generator.rb +19 -0
- data/lib/generators/event_sourced_record/event_generator.rb +40 -0
- data/lib/generators/event_sourced_record/event_sourced_record_generator.rb +41 -0
- data/lib/generators/event_sourced_record/observer_generator.rb +34 -0
- data/lib/generators/event_sourced_record/projection_generator.rb +50 -0
- data/lib/generators/event_sourced_record/templates/calculator.rb +7 -0
- data/lib/generators/event_sourced_record/templates/event_model.rb +13 -0
- data/lib/generators/event_sourced_record/templates/observer.rb +8 -0
- data/lib/generators/event_sourced_record/templates/projection_model.rb +15 -0
- data/lib/generators/rspec/service_generator.rb +15 -0
- data/lib/generators/rspec/templates/service_spec.rb +6 -0
- data/lib/generators/test_unit/service_generator.rb +15 -0
- data/lib/generators/test_unit/templates/service_test.rb +9 -0
- data/test/event_sourced_record/calculator_test.rb +111 -0
- data/test/event_sourced_record/event_test.rb +112 -0
- data/test/generators/calculator_generator_test.rb +25 -0
- data/test/generators/event_generator_test.rb +35 -0
- data/test/generators/event_sourced_record_generator_test.rb +38 -0
- data/test/generators/observer_generator_test.rb +19 -0
- data/test/generators/projection_generator_test.rb +36 -0
- data/test/generators/templates/application.rb +12 -0
- data/test/test_helper.rb +82 -0
- metadata +217 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 092ed18da567347e5ac8f364e1d0f462f53e017d
|
4
|
+
data.tar.gz: ee8bd1282829f2b7f4f0f67a52415ef5d2d2b433
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 5453d5b029fc488d6ac88d910b5c200bc9fe435b30616256c1058aa7dc686024c263ec8f8b66bf3db3427031d2d105d288760bbad91005efdc32096429ac1a68
|
7
|
+
data.tar.gz: c8a38090c8720b7fd0d5476ccd56dec501bebe5503077eafb63da0a7c91834a5af9d4d7911d73f10228619cff5c531f43ab8f9ff00f9efb24ad060ef8fdbd8e7
|
data/.gitignore
ADDED
@@ -0,0 +1,25 @@
|
|
1
|
+
*.gem
|
2
|
+
*.rbc
|
3
|
+
.bundle
|
4
|
+
.config
|
5
|
+
.yardoc
|
6
|
+
Gemfile.lock
|
7
|
+
InstalledFiles
|
8
|
+
_yardoc
|
9
|
+
coverage
|
10
|
+
doc/
|
11
|
+
lib/bundler/man
|
12
|
+
pkg
|
13
|
+
rdoc
|
14
|
+
spec/reports
|
15
|
+
test/tmp
|
16
|
+
test/version_tmp
|
17
|
+
tmp
|
18
|
+
*.bundle
|
19
|
+
*.so
|
20
|
+
*.o
|
21
|
+
*.a
|
22
|
+
mkmf.log
|
23
|
+
.ruby-version
|
24
|
+
gemfiles/*.gemfile
|
25
|
+
gemfiles/*.gemfile.lock
|
data/Appraisals
ADDED
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2014 Francis Hwang
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,131 @@
|
|
1
|
+
# Event Sourced Record
|
2
|
+
|
3
|
+
Event Sourced Record offers an idiomatic way to use the Event Sourcing pattern in Rails code.
|
4
|
+
|
5
|
+
With Event Sourcing, every change to the state of an object is recorded as an immutable event in a replayable sequence. The result is decoupled code that simplifies state debugging and retrospective reporting.
|
6
|
+
|
7
|
+
For more, see Martin Fowler's writeup of the pattern: http://martinfowler.com/eaaDev/EventSourcing.html
|
8
|
+
|
9
|
+
## Installation
|
10
|
+
|
11
|
+
Add this line to your application's Gemfile:
|
12
|
+
|
13
|
+
gem 'event_sourced_record'
|
14
|
+
|
15
|
+
And then execute:
|
16
|
+
|
17
|
+
$ bundle
|
18
|
+
|
19
|
+
Or install it yourself as:
|
20
|
+
|
21
|
+
$ gem install event_sourced_record
|
22
|
+
|
23
|
+
Event Sourced Record uses observers, so you'll need to add them to your Gemfile:
|
24
|
+
|
25
|
+
gem 'rails-observers'
|
26
|
+
|
27
|
+
Note that only Rails 4 is supported as of this writing. Rails 3 support is coming soon.
|
28
|
+
|
29
|
+
## Usage
|
30
|
+
|
31
|
+
Generate the required classes with `rails generate event_sourced_record`:
|
32
|
+
|
33
|
+
rails generate event_sourced_record Subscription \
|
34
|
+
user_id:integer bottles_per_shipment:integer \
|
35
|
+
bottles_left:integer
|
36
|
+
|
37
|
+
The argument list is the same as with `rails generate model`. This will generate the event model, the projection, the calculator, and the observer.
|
38
|
+
|
39
|
+
### Event model
|
40
|
+
|
41
|
+
The event model is an ActiveRecord model but it can act significantly different based on what type of event it is. Use `event_type` to configure these types:
|
42
|
+
|
43
|
+
class SubscriptionEvent < ActiveRecord::Base
|
44
|
+
include EventSourcedRecord::Event
|
45
|
+
|
46
|
+
event_type :creation do
|
47
|
+
attributes :bottles_per_shipment, :bottles_purchased, :user_id
|
48
|
+
|
49
|
+
validates :bottles_per_shipment, presence: true, numericality: true
|
50
|
+
validates :bottles_purchased, presence: true, numericality: true
|
51
|
+
validates :user_id, presence: true
|
52
|
+
end
|
53
|
+
|
54
|
+
event_type :change_settings do
|
55
|
+
attributes :bottles_per_shipment
|
56
|
+
|
57
|
+
validates :bottles_per_shipment, numericality: true
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
The easiest way to create these records is with the scopes that are automatically generated by `event_type`:
|
62
|
+
|
63
|
+
SubscriptionEvent.creation.create!(
|
64
|
+
bottles_per_shipment: 1, bottles_purchased: 6, user_id: current_user.id
|
65
|
+
)
|
66
|
+
|
67
|
+
## Projection
|
68
|
+
|
69
|
+
The projection is the ActiveRecord model that is generated deterministically with the data in the timestamped events combined with the logic in the calculator. Projections shouldn't have any code for modifying themselves, as that will be done externally. Accordingly, projections end up being fairly small classes:
|
70
|
+
|
71
|
+
class Subscription < ActiveRecord::Base
|
72
|
+
has_many :subscription_events
|
73
|
+
|
74
|
+
validates :uuid, uniqueness: true
|
75
|
+
end
|
76
|
+
|
77
|
+
(`uuid` is core to Event Sourced Model, so please don't remove its validations.)
|
78
|
+
|
79
|
+
## Calculator
|
80
|
+
|
81
|
+
The calculator is a service class that idempotently calculates the state of the projection by running one method for each event, in order. Name the methods `advance_[event_type]`.
|
82
|
+
|
83
|
+
class SubscriptionCalculator < EventSourcedRecord::Calculator
|
84
|
+
events :subscription_events
|
85
|
+
|
86
|
+
def advance_creation(event)
|
87
|
+
@subscription.user_id = event.user_id
|
88
|
+
@subscription.bottles_per_shipment = event.bottles_per_shipment
|
89
|
+
@subscription.bottles_left = event.bottles_purchased
|
90
|
+
end
|
91
|
+
|
92
|
+
def advance_change_settings(event)
|
93
|
+
@subscription.bottles_per_shipment = event.bottles_per_shipment
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
Calculators can also include other associated models, which can come in handy (as long as those models don't change significantly after creation). Add that class to `events` and name the advance method `advance_[underscored class name]`:
|
98
|
+
|
99
|
+
class SubscriptionCalculator < EventSourcedRecord::Calculator
|
100
|
+
events :subscription_events, :shipments
|
101
|
+
|
102
|
+
def advance_shipment(shipment)
|
103
|
+
@subscription.bottles_left -= shipment.num_bottles
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
## Observer
|
108
|
+
|
109
|
+
The observer simply tracks creations of events related to the projection and runs the calculator every time. You may never have to modify what the generator creates for you:
|
110
|
+
|
111
|
+
class SubscriptionEventObserver < ActiveRecord::Observer
|
112
|
+
observe :subscription_event
|
113
|
+
|
114
|
+
def after_create(event)
|
115
|
+
SubscriptionCalculator.new(event).run.save!
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
If you use other models as events, simply add them to the `observe` method:
|
120
|
+
|
121
|
+
class SubscriptionEventObserver < ActiveRecord::Observer
|
122
|
+
observe :subscription_event, :shipment
|
123
|
+
|
124
|
+
## Contributing
|
125
|
+
|
126
|
+
1. Fork it ( https://github.com/[my-github-username]/event_sourced_record/fork )
|
127
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
128
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
129
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
130
|
+
5. Create a new Pull Request
|
131
|
+
|
data/Rakefile
ADDED
@@ -0,0 +1,37 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'event_sourced_record/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "event_sourced_record"
|
8
|
+
spec.version = EventSourcedRecord::VERSION
|
9
|
+
spec.authors = ["Francis Hwang"]
|
10
|
+
spec.email = ["sera@fhwang.net"]
|
11
|
+
spec.summary = %q{Event Sourcing with ActiveRecord.}
|
12
|
+
spec.description = %q{Event Sourcing with ActiveRecord.}
|
13
|
+
spec.homepage = ""
|
14
|
+
spec.license = "MIT"
|
15
|
+
|
16
|
+
spec.files = `git ls-files -z`.split("\x0")
|
17
|
+
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
18
|
+
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
19
|
+
spec.require_paths = ["lib"]
|
20
|
+
|
21
|
+
spec.post_install_message = <<-MESSAGE
|
22
|
+
Thanks for installing!
|
23
|
+
|
24
|
+
EventSourcedRecord uses Rails observers. If you are using Rails 4.0 or greater, add `rails-observers` to your Gemfile.
|
25
|
+
MESSAGE
|
26
|
+
|
27
|
+
spec.add_dependency 'activemodel'
|
28
|
+
|
29
|
+
spec.add_development_dependency "bundler", "~> 1.6"
|
30
|
+
spec.add_development_dependency "rake"
|
31
|
+
spec.add_development_dependency 'activerecord'
|
32
|
+
spec.add_development_dependency 'appraisal'
|
33
|
+
spec.add_development_dependency 'mocha'
|
34
|
+
spec.add_development_dependency 'railties'
|
35
|
+
spec.add_development_dependency 'sqlite3'
|
36
|
+
spec.add_development_dependency 'pry'
|
37
|
+
end
|
@@ -0,0 +1,101 @@
|
|
1
|
+
class EventSourcedRecord::Calculator
|
2
|
+
def self.events(*event_symbols)
|
3
|
+
@@event_symbols = event_symbols
|
4
|
+
end
|
5
|
+
|
6
|
+
def self.event_classes
|
7
|
+
@@event_symbols.map { |sym|
|
8
|
+
Module.const_get(sym.to_s.singularize.camelize)
|
9
|
+
}
|
10
|
+
end
|
11
|
+
|
12
|
+
def initialize(lookup)
|
13
|
+
@lookup = lookup
|
14
|
+
end
|
15
|
+
|
16
|
+
def run(options = {})
|
17
|
+
instance_variable_set(projection_variable_name, find_or_build_record)
|
18
|
+
sorted_events(options[:last_event_time]).each do |event|
|
19
|
+
advance_event(event)
|
20
|
+
end
|
21
|
+
instance_variable_get projection_variable_name
|
22
|
+
end
|
23
|
+
|
24
|
+
private
|
25
|
+
|
26
|
+
def advance_event(event)
|
27
|
+
if event.respond_to?(:event_type)
|
28
|
+
method = "advance_#{event.event_type}"
|
29
|
+
else
|
30
|
+
method = "advance_#{event.class.name.underscore}"
|
31
|
+
end
|
32
|
+
self.send(method, event)
|
33
|
+
end
|
34
|
+
|
35
|
+
def event_class
|
36
|
+
Module.const_get(projection_class_name + 'Event')
|
37
|
+
end
|
38
|
+
|
39
|
+
def find_or_build_record
|
40
|
+
case @lookup
|
41
|
+
when Integer
|
42
|
+
projection_class.where(id: @lookup).first
|
43
|
+
when projection_class
|
44
|
+
projection_class.where(id: @lookup.id).first
|
45
|
+
when *self.class.event_classes
|
46
|
+
find_or_build_record_by_event_instance(@lookup)
|
47
|
+
else
|
48
|
+
find_or_build_record_by_uuid(@lookup)
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
def find_or_build_record_by_event_instance(event)
|
53
|
+
if event.respond_to?(uuid_field)
|
54
|
+
find_or_build_record_by_uuid(event.send(uuid_field))
|
55
|
+
else
|
56
|
+
conditions = {id: event.send(projection_name + '_id')}
|
57
|
+
projection_class.where(conditions).first
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
def find_or_build_record_by_uuid(uuid)
|
62
|
+
projection_class.where(uuid: uuid).first || projection_class.new(uuid: uuid)
|
63
|
+
end
|
64
|
+
|
65
|
+
def projection_class
|
66
|
+
Module.const_get(projection_class_name)
|
67
|
+
end
|
68
|
+
|
69
|
+
def projection_class_name
|
70
|
+
self.class.name.gsub(/Calculator$/, '')
|
71
|
+
end
|
72
|
+
|
73
|
+
def projection_name
|
74
|
+
projection_class_name.underscore
|
75
|
+
end
|
76
|
+
|
77
|
+
def projection_variable_name
|
78
|
+
"@#{projection_name}".to_sym
|
79
|
+
end
|
80
|
+
|
81
|
+
def sorted_events(last_event_time)
|
82
|
+
projection = instance_variable_get projection_variable_name
|
83
|
+
self.class.event_classes.map { |event_class|
|
84
|
+
conditions = nil
|
85
|
+
if event_class.column_names.include?(uuid_field)
|
86
|
+
conditions = {uuid_field => projection.uuid}
|
87
|
+
else
|
88
|
+
conditions = {projection_name + '_id' => projection.id} if projection
|
89
|
+
end
|
90
|
+
conditions ? event_class.where(conditions).to_a : []
|
91
|
+
}.flatten.sort_by(&:created_at).reject { |evt|
|
92
|
+
if last_event_time
|
93
|
+
evt.created_at > last_event_time
|
94
|
+
end
|
95
|
+
}
|
96
|
+
end
|
97
|
+
|
98
|
+
def uuid_field
|
99
|
+
projection_name + '_uuid'
|
100
|
+
end
|
101
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
require 'active_model'
|
2
|
+
|
3
|
+
class EventSourcedRecord::Event::EventTypeConfig
|
4
|
+
include ::ActiveModel::Validations::ClassMethods
|
5
|
+
|
6
|
+
attr_reader :_validators
|
7
|
+
|
8
|
+
def initialize
|
9
|
+
@_validators = Hash.new { |h,k| h[k] = [] }
|
10
|
+
end
|
11
|
+
|
12
|
+
def attributes(*attrs)
|
13
|
+
attrs.present? ? @attributes = attrs : @attributes
|
14
|
+
end
|
15
|
+
|
16
|
+
def const_get(sym_or_str, inherit=true)
|
17
|
+
ActiveModel::Validations.const_get(sym_or_str, inherit)
|
18
|
+
end
|
19
|
+
|
20
|
+
# Don't do anything; the interesting work has already been done in
|
21
|
+
# `validate_with` adding validators to @_validators
|
22
|
+
def validate(*args, &block)
|
23
|
+
end
|
24
|
+
|
25
|
+
def validate_record(record)
|
26
|
+
_validators.values.flatten.each do |validator|
|
27
|
+
validator.validate(record)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,94 @@
|
|
1
|
+
module EventSourcedRecord::Event
|
2
|
+
def self.included(model)
|
3
|
+
model.cattr_accessor :_event_type_configs
|
4
|
+
model.extend ClassMethods
|
5
|
+
model.after_initialize :ensure_data
|
6
|
+
model.after_initialize :ensure_projection_uuid
|
7
|
+
model.after_initialize :lock_event_type
|
8
|
+
model.validates :event_type, presence: true
|
9
|
+
model.validate :validate_corrent_event_type
|
10
|
+
model.validate :validate_by_event_type
|
11
|
+
model.serialize :data
|
12
|
+
end
|
13
|
+
|
14
|
+
def event_type
|
15
|
+
attributes["event_type"]
|
16
|
+
end
|
17
|
+
|
18
|
+
def event_type=(value)
|
19
|
+
if @event_type_locked
|
20
|
+
raise EventTypeImmutableError, "Event types can't be changed"
|
21
|
+
else
|
22
|
+
write_attribute(:event_type, value)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
private
|
27
|
+
|
28
|
+
def ensure_data
|
29
|
+
self.data ||= {}
|
30
|
+
end
|
31
|
+
|
32
|
+
def ensure_projection_uuid
|
33
|
+
unless self.send(projection_uuid_name)
|
34
|
+
self.send("#{projection_uuid_name}=", SecureRandom.uuid)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def event_type_config
|
39
|
+
self.class.event_type_config(event_type)
|
40
|
+
end
|
41
|
+
|
42
|
+
def lock_event_type
|
43
|
+
@event_type_locked = true
|
44
|
+
end
|
45
|
+
|
46
|
+
def method_missing(meth, *args, &block)
|
47
|
+
if event_type_config && event_type_config.attributes.include?(meth)
|
48
|
+
ensure_data
|
49
|
+
self.data[meth.to_s]
|
50
|
+
elsif event_type_config && event_type_config.attributes.any? { |a| "#{a}=".to_sym == meth }
|
51
|
+
ensure_data
|
52
|
+
attr = meth.to_s.gsub(/=$/, '')
|
53
|
+
self.data[attr] = args.first
|
54
|
+
else
|
55
|
+
super
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
def projection_uuid_name
|
60
|
+
self.class.name.underscore.gsub(/_event$/, '') + '_uuid'
|
61
|
+
end
|
62
|
+
|
63
|
+
def validate_corrent_event_type
|
64
|
+
unless self.class.event_types.include?(event_type.to_s)
|
65
|
+
errors.add(:event_type, "is not a valid event type")
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
def validate_by_event_type
|
70
|
+
event_type_config.validate_record(self) if event_type_config
|
71
|
+
end
|
72
|
+
|
73
|
+
module ClassMethods
|
74
|
+
def event_type(event_type, &block)
|
75
|
+
scope event_type, -> { where(event_type: event_type) }
|
76
|
+
self._event_type_configs ||= HashWithIndifferentAccess.new
|
77
|
+
config = EventTypeConfig.new
|
78
|
+
self._event_type_configs[event_type] = config
|
79
|
+
config.instance_eval(&block)
|
80
|
+
end
|
81
|
+
|
82
|
+
def event_type_config(event_type)
|
83
|
+
self._event_type_configs[event_type]
|
84
|
+
end
|
85
|
+
|
86
|
+
def event_types
|
87
|
+
self._event_type_configs.keys
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
class EventTypeImmutableError < StandardError; end
|
92
|
+
end
|
93
|
+
|
94
|
+
require 'event_sourced_record/event/event_type_config'
|
@@ -0,0 +1,19 @@
|
|
1
|
+
class EventSourcedRecord::CalculatorGenerator < Rails::Generators::NamedBase
|
2
|
+
source_root File.expand_path('../templates', __FILE__)
|
3
|
+
argument :attributes,
|
4
|
+
:type => :array, :default => []
|
5
|
+
|
6
|
+
def create_calculator_file
|
7
|
+
template(
|
8
|
+
'calculator.rb', File.join('app/services', class_path, "#{file_name}.rb")
|
9
|
+
)
|
10
|
+
end
|
11
|
+
|
12
|
+
hook_for :test_framework, as: :service
|
13
|
+
|
14
|
+
private
|
15
|
+
|
16
|
+
def event_name
|
17
|
+
file_name.gsub(/_calculator$/, '') + '_event'
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
class EventSourcedRecord::EventGenerator < Rails::Generators::NamedBase
|
2
|
+
source_root File.expand_path('../templates', __FILE__)
|
3
|
+
argument :attributes,
|
4
|
+
:type => :array, :default => []
|
5
|
+
|
6
|
+
def create_migration_file
|
7
|
+
attributes_str = attributes.map { |attr|
|
8
|
+
attr_banner = attr.name
|
9
|
+
attr_banner << ":#{attr.type}" if attr.type
|
10
|
+
attr_banner << ':index' if attr.has_index?
|
11
|
+
attr_banner
|
12
|
+
}.join(' ')
|
13
|
+
generate(
|
14
|
+
"migration", "create_#{event_table_name} #{attributes_str}"
|
15
|
+
)
|
16
|
+
end
|
17
|
+
|
18
|
+
def create_model_file
|
19
|
+
template(
|
20
|
+
'event_model.rb',
|
21
|
+
File.join('app/models', class_path, "#{event_file_name}.rb")
|
22
|
+
)
|
23
|
+
end
|
24
|
+
|
25
|
+
hook_for :test_framework, as: :model
|
26
|
+
|
27
|
+
private
|
28
|
+
|
29
|
+
def event_class_name
|
30
|
+
class_name
|
31
|
+
end
|
32
|
+
|
33
|
+
def event_file_name
|
34
|
+
file_name
|
35
|
+
end
|
36
|
+
|
37
|
+
def event_table_name
|
38
|
+
table_name
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
class EventSourcedRecord::EventSourcedRecordGenerator < Rails::Generators::NamedBase
|
2
|
+
source_root File.expand_path('../templates', __FILE__)
|
3
|
+
argument :attributes,
|
4
|
+
:type => :array, :default => [],
|
5
|
+
:banner => "field[:type][:index] field[:type][:index]"
|
6
|
+
|
7
|
+
check_class_collision
|
8
|
+
|
9
|
+
def create_calculator_class
|
10
|
+
generate "event_sourced_record:calculator", "#{file_name}_calculator"
|
11
|
+
end
|
12
|
+
|
13
|
+
def create_event
|
14
|
+
arguments = [
|
15
|
+
"#{file_name}_uuid:string:index", "event_type:string",
|
16
|
+
"data:text", "created_at:datetime"
|
17
|
+
].join(' ')
|
18
|
+
generate "event_sourced_record:event", "#{file_name}_event #{arguments}"
|
19
|
+
end
|
20
|
+
|
21
|
+
def create_observer
|
22
|
+
generate "event_sourced_record:observer", "#{file_name}_event_observer"
|
23
|
+
end
|
24
|
+
|
25
|
+
def create_projection
|
26
|
+
attr_strings = attributes.map { |attr|
|
27
|
+
attr_string = attr.name
|
28
|
+
attr_string << ":#{attr.type}" if attr.type
|
29
|
+
attr_string << ':index' if attr.has_index?
|
30
|
+
attr_string
|
31
|
+
}
|
32
|
+
projection_attributes = attr_strings.join(' ')
|
33
|
+
generate "event_sourced_record:projection", "#{file_name} #{projection_attributes}"
|
34
|
+
end
|
35
|
+
|
36
|
+
protected
|
37
|
+
|
38
|
+
def calculator_class_name
|
39
|
+
class_name + 'Calculator'
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
class EventSourcedRecord::ObserverGenerator < Rails::Generators::NamedBase
|
2
|
+
source_root File.expand_path('../templates', __FILE__)
|
3
|
+
|
4
|
+
def create_observer_file
|
5
|
+
template(
|
6
|
+
'observer.rb',
|
7
|
+
File.join('app/observers', class_path, "#{file_name}.rb")
|
8
|
+
)
|
9
|
+
end
|
10
|
+
|
11
|
+
def create_application_observer_hook
|
12
|
+
application do
|
13
|
+
"config.active_record.observers = :#{file_name}"
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
private
|
18
|
+
|
19
|
+
def calculator_class_name
|
20
|
+
(projection_name + '_calculator').camelize
|
21
|
+
end
|
22
|
+
|
23
|
+
def event_name
|
24
|
+
projection_name + '_event'
|
25
|
+
end
|
26
|
+
|
27
|
+
def event_uuid_field
|
28
|
+
projection_name + '_uuid'
|
29
|
+
end
|
30
|
+
|
31
|
+
def projection_name
|
32
|
+
file_name.gsub(/_event_observer$/, '')
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
class EventSourcedRecord::ProjectionGenerator < Rails::Generators::NamedBase
|
2
|
+
source_root File.expand_path('../templates', __FILE__)
|
3
|
+
argument :attributes,
|
4
|
+
:type => :array, :default => []
|
5
|
+
|
6
|
+
def create_migration_file
|
7
|
+
generate(
|
8
|
+
"migration", "create_#{projection_table_name} #{migration_attributes}"
|
9
|
+
)
|
10
|
+
end
|
11
|
+
|
12
|
+
def create_model_file
|
13
|
+
template(
|
14
|
+
'projection_model.rb',
|
15
|
+
File.join('app/models', class_path, "#{projection_file_name}.rb")
|
16
|
+
)
|
17
|
+
end
|
18
|
+
|
19
|
+
hook_for :test_framework, as: :model
|
20
|
+
|
21
|
+
private
|
22
|
+
|
23
|
+
def migration_attributes
|
24
|
+
attr_strings = attributes.map { |attr|
|
25
|
+
attr_string = attr.name
|
26
|
+
attr_string << ":#{attr.type}" if attr.type
|
27
|
+
attr_string << ':index' if attr.has_index?
|
28
|
+
attr_string
|
29
|
+
}
|
30
|
+
attr_strings << "uuid:string:uniq"
|
31
|
+
attr_strings.join(' ')
|
32
|
+
end
|
33
|
+
|
34
|
+
|
35
|
+
def projection_class_name
|
36
|
+
class_name
|
37
|
+
end
|
38
|
+
|
39
|
+
def projection_file_name
|
40
|
+
file_name
|
41
|
+
end
|
42
|
+
|
43
|
+
def projection_parent_class_name
|
44
|
+
"ActiveRecord::Base"
|
45
|
+
end
|
46
|
+
|
47
|
+
def projection_table_name
|
48
|
+
table_name
|
49
|
+
end
|
50
|
+
end
|