sequent 0.1.1 → 0.1.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (38) hide show
  1. checksums.yaml +4 -4
  2. data/lib/sequent/core/aggregate_repository.rb +94 -0
  3. data/lib/sequent/core/aggregate_root.rb +87 -0
  4. data/lib/sequent/core/base_command_handler.rb +39 -0
  5. data/lib/sequent/core/base_event_handler.rb +51 -0
  6. data/lib/sequent/core/command.rb +79 -0
  7. data/lib/sequent/core/command_record.rb +26 -0
  8. data/lib/sequent/core/command_service.rb +118 -0
  9. data/lib/sequent/core/core.rb +15 -0
  10. data/lib/sequent/core/event.rb +62 -0
  11. data/lib/sequent/core/event_record.rb +34 -0
  12. data/lib/sequent/core/event_store.rb +110 -0
  13. data/lib/sequent/core/helpers/association_validator.rb +39 -0
  14. data/lib/sequent/core/helpers/attribute_support.rb +207 -0
  15. data/lib/sequent/core/helpers/boolean_support.rb +36 -0
  16. data/lib/sequent/core/helpers/copyable.rb +25 -0
  17. data/lib/sequent/core/helpers/equal_support.rb +41 -0
  18. data/lib/sequent/core/helpers/helpers.rb +9 -0
  19. data/lib/sequent/core/helpers/mergable.rb +21 -0
  20. data/lib/sequent/core/helpers/param_support.rb +80 -0
  21. data/lib/sequent/core/helpers/self_applier.rb +45 -0
  22. data/lib/sequent/core/helpers/string_support.rb +22 -0
  23. data/lib/sequent/core/helpers/uuid_helper.rb +17 -0
  24. data/lib/sequent/core/record_sessions/active_record_session.rb +92 -0
  25. data/lib/sequent/core/record_sessions/record_sessions.rb +2 -0
  26. data/lib/sequent/core/record_sessions/replay_events_session.rb +306 -0
  27. data/lib/sequent/core/tenant_event_store.rb +18 -0
  28. data/lib/sequent/core/transactions/active_record_transaction_provider.rb +16 -0
  29. data/lib/sequent/core/transactions/no_transactions.rb +13 -0
  30. data/lib/sequent/core/transactions/transactions.rb +2 -0
  31. data/lib/sequent/core/value_object.rb +48 -0
  32. data/lib/sequent/migrations/migrate_events.rb +53 -0
  33. data/lib/sequent/migrations/migrations.rb +7 -0
  34. data/lib/sequent/sequent.rb +3 -0
  35. data/lib/sequent/test/command_handler_helpers.rb +101 -0
  36. data/lib/sequent/test/test.rb +1 -0
  37. data/lib/version.rb +3 -0
  38. metadata +38 -3
@@ -0,0 +1,110 @@
1
+ require 'oj'
2
+ require_relative 'event_record'
3
+
4
+ module Sequent
5
+ module Core
6
+ class EventStoreConfiguration
7
+ attr_accessor :record_class, :event_handlers
8
+
9
+ def initialize(record_class = Sequent::Core::EventRecord, event_handlers = [])
10
+ @record_class = record_class
11
+ @event_handlers = event_handlers
12
+ end
13
+ end
14
+
15
+ class EventStore
16
+
17
+ class << self
18
+ attr_accessor :configuration,
19
+ :instance
20
+ end
21
+
22
+ # Creates a new EventStore and overwrites all existing config.
23
+ # The new EventStore can be retrieved via the +EventStore.instance+ method.
24
+ #
25
+ # If you don't want a singleton you can always instantiate it yourself using the +EventStore.new+.
26
+ def self.configure
27
+ self.configuration = EventStoreConfiguration.new
28
+ yield(configuration) if block_given?
29
+ EventStore.instance = EventStore.new(configuration)
30
+ end
31
+
32
+ def initialize(configuration = EventStoreConfiguration.new)
33
+ @record_class = configuration.record_class
34
+ @event_handlers = configuration.event_handlers
35
+ end
36
+
37
+ ##
38
+ # Stores the events in the EventStore and publishes the events
39
+ # to the registered event_handlers.
40
+ #
41
+ def commit_events(command, events)
42
+ store_events(command, events)
43
+ publish_events(events, @event_handlers)
44
+ end
45
+
46
+ ##
47
+ # Returns all events for the aggregate ordered by sequence_number
48
+ #
49
+ def load_events(aggregate_id)
50
+ event_types = {}
51
+ @record_class.connection.select_all("select event_type, event_json from #{@record_class.table_name} where aggregate_id = '#{aggregate_id}' order by sequence_number asc").map! do |event_hash|
52
+ event_type = event_hash["event_type"]
53
+ event_json = Oj.strict_load(event_hash["event_json"])
54
+ unless event_types.has_key?(event_type)
55
+ event_types[event_type] = Class.const_get(event_type.to_sym)
56
+ end
57
+ event_types[event_type].deserialize_from_json(event_json)
58
+ end
59
+ end
60
+
61
+ ##
62
+ # Replays all events in the event store to the registered event_handlers.
63
+ #
64
+ # @param block that returns the event stream.
65
+ def replay_events
66
+ event_stream = yield
67
+ event_types = {}
68
+ event_stream.each do |event_hash|
69
+ event_type = event_hash["event_type"]
70
+ payload = Oj.strict_load(event_hash["event_json"])
71
+ unless event_types.has_key?(event_type)
72
+ event_types[event_type] = Class.const_get(event_type.to_sym)
73
+ end
74
+ event = event_types[event_type].deserialize_from_json(payload)
75
+ @event_handlers.each do |handler|
76
+ handler.handle_message event
77
+ end
78
+ end
79
+ end
80
+
81
+ protected
82
+ def record_class
83
+ @record_class
84
+ end
85
+
86
+ private
87
+
88
+ def publish_events(events, event_handlers)
89
+ events.each do |event|
90
+ event_handlers.each do |handler|
91
+ handler.handle_message event
92
+ end
93
+ end
94
+ end
95
+
96
+ def to_events(event_records)
97
+ event_records.map(&:event)
98
+ end
99
+
100
+ def store_events(command, events = [])
101
+ command_record = Sequent::Core::CommandRecord.create!(:command => command)
102
+ events.each do |event|
103
+ @record_class.create!(:command_record => command_record, :event => event)
104
+ end
105
+ end
106
+
107
+ end
108
+
109
+ end
110
+ end
@@ -0,0 +1,39 @@
1
+ require 'active_model'
2
+ module Sequent
3
+ module Core
4
+ module Helpers
5
+ #
6
+ # Validator for associations. Typically used in Sequent::Core::Command,
7
+ # Sequent::Core::Event and Sequent::Core::ValueObjects.
8
+ #
9
+ # Example:
10
+ #
11
+ # class RegisterForTrainingCommand < UpdateCommand
12
+ # attrs trainee: Person
13
+ #
14
+ # validates_presence_of :trainee
15
+ # validates_with Sequent::Core::Helpers::AssociationValidator, associations: [:trainee]
16
+ #
17
+ # end
18
+ class AssociationValidator < ActiveModel::Validator
19
+
20
+ def validate(record)
21
+ associations = options[:associations]
22
+ associations = [associations] unless associations.instance_of?(Array)
23
+ associations.each do |association|
24
+ next unless association # since ruby 2.0...?
25
+ value = record.instance_variable_get("@#{association.to_s}")
26
+ if value && !value.kind_of?(Array) && record.respond_to?(:attributes) && !value.kind_of?(record.attributes[association])
27
+ record.errors[association] = "is not of type #{record.attributes[association]}"
28
+ elsif value && value.kind_of?(Array)
29
+ record.errors[association] = "is invalid" if value.any? { |v| not v.valid? }
30
+ else
31
+ record.errors[association] = "is invalid" if value && value.invalid?
32
+
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,207 @@
1
+ require 'active_support'
2
+ # TODO: Move this into a separate core_ext folder like for instance Rails does.
3
+ # WARNING: Monkey patches below...
4
+ class Symbol
5
+ def self.deserialize_from_json(value)
6
+ value.try(:to_sym)
7
+ end
8
+ end
9
+
10
+ class String
11
+ def self.deserialize_from_json(value)
12
+ value
13
+ end
14
+ end
15
+
16
+ class Integer
17
+ def self.deserialize_from_json(value)
18
+ value.blank? ? nil : value.to_i
19
+ end
20
+ end
21
+
22
+ class Boolean
23
+ def self.deserialize_from_json(value)
24
+ value.nil? ? nil : (value.present? ? value : false)
25
+ end
26
+ end
27
+
28
+ class Date
29
+ def self.deserialize_from_json(value)
30
+ value.nil? ? nil : Date.iso8601(value.dup)
31
+ end
32
+ end
33
+
34
+ class DateTime
35
+ def self.deserialize_from_json(value)
36
+ value.nil? ? nil : DateTime.iso8601(value.dup)
37
+ end
38
+ end
39
+
40
+ class Array
41
+ def self.deserialize_from_json(value)
42
+ value
43
+ end
44
+ end
45
+
46
+ module Sequent
47
+ module Core
48
+ module Helpers
49
+ # Provides functionality for defining attributes with their types
50
+ #
51
+ # Since our Commands and ValueObjects are not backed by a database like e.g. rails
52
+ # we can not infer their types. We need the types to be able to parse from and to json.
53
+ # We could have stored te type information in the json, but we didn't.
54
+ #
55
+ # You typically do not need to include this module in your classes. If you extend from
56
+ # Sequent::Core::ValueObject, Sequent::Core::Event or Sequent::Core::BaseCommand you will
57
+ # get this functionality for free.
58
+ #
59
+ module AttributeSupport
60
+ # module containing class methods to be added
61
+ module ClassMethods
62
+
63
+ def types
64
+ @types ||= {}
65
+ if @merged_types
66
+ @merged_types
67
+ else
68
+ @merged_types = is_a?(Class) && superclass.respond_to?(:types) ? @types.merge(superclass.types) : @types
69
+ included_modules.select { |m| m.include? Sequent::Core::Helpers::AttributeSupport }.each do |mod|
70
+ @merged_types.merge!(mod.types)
71
+ end
72
+ @merged_types
73
+ end
74
+ end
75
+
76
+ def attrs(args)
77
+ @types ||= {}
78
+ @types.merge!(args)
79
+ args.each do |attribute, _|
80
+ attr_accessor attribute
81
+ end
82
+
83
+ # Generate method that sets all defined attributes based on the attrs hash.
84
+ class_eval <<EOS
85
+ def update_all_attributes(attrs)
86
+ super if defined?(super)
87
+ #{@types.map { |attribute, _|
88
+ "@#{attribute} = attrs[:#{attribute}]"
89
+ }.join("\n ")}
90
+ self
91
+ end
92
+ EOS
93
+
94
+ class_eval <<EOS
95
+ def update_all_attributes_from_json(attrs)
96
+ super if defined?(super)
97
+ #{@types.map { |attribute, type|
98
+ "@#{attribute} = #{type}.deserialize_from_json(attrs['#{attribute}'])"
99
+ }.join("\n ")}
100
+ end
101
+ EOS
102
+ end
103
+
104
+ #
105
+ # Allows you to define something is an array of a type
106
+ # Example:
107
+ #
108
+ # attrs trainees: array(Person)
109
+ #
110
+ def array(type)
111
+ ArrayWithType.new(type)
112
+ end
113
+
114
+ def deserialize_from_json(args)
115
+ unless args.nil?
116
+ obj = allocate()
117
+ obj.update_all_attributes_from_json(args)
118
+ obj
119
+ end
120
+ end
121
+
122
+
123
+ def numeric?(object)
124
+ true if Float(object) rescue false
125
+ end
126
+
127
+ end
128
+
129
+ # extend host class with class methods when we're included
130
+ def self.included(host_class)
131
+ host_class.extend(ClassMethods)
132
+ end
133
+
134
+
135
+ # needed for active module JSON serialization
136
+ def attributes
137
+ self.class.types
138
+ end
139
+
140
+ def validation_errors(prefix = nil)
141
+ result = errors.to_hash
142
+ self.class.types.each do |field|
143
+ value = self.instance_variable_get("@#{field[0]}")
144
+ if value.respond_to? :validation_errors
145
+ value.validation_errors.each { |k, v| result["#{field[0].to_s}_#{k.to_s}".to_sym] = v }
146
+ end
147
+ end
148
+ prefix ? HashWithIndifferentAccess[result.map { |k, v| ["#{prefix}_#{k}", v] }] : result
149
+ end
150
+
151
+ # If you have a Date object validate it with this method when the unparsed input is a String
152
+ # This scenario is typically when a date is posted from the web.
153
+ #
154
+ # Example
155
+ #
156
+ # class Person < Sequent::Core::ValueObject
157
+ # attrs date_of_birth: Date
158
+ #
159
+ # validate :valid_date
160
+ # end
161
+ #
162
+ # If the date_of_birth is a valid date it will be parsed into a proper Date object.
163
+ # This implementation currently only support dd-mm-yyyy format.
164
+ def valid_date
165
+ self.class.types.each do |name, clazz|
166
+ if clazz == Date
167
+ return if self.instance_variable_get("@#{name}").kind_of? Date
168
+ unless self.instance_variable_get("@#{name}").blank?
169
+ if (/\d{2}-\d{2}-\d{4}/ =~ self.instance_variable_get("@#{name}")).nil?
170
+ @errors.add(name.to_s, :invalid_date) if (/\d{2}-\d{2}-\d{4}/ =~ self.instance_variable_get("@#{name}")).nil?
171
+ else
172
+ begin
173
+ self.instance_variable_set "@#{name}", Date.strptime(self.instance_variable_get("@#{name}"), "%d-%m-%Y")
174
+ rescue
175
+ @errors.add(name.to_s, :invalid_date)
176
+ end
177
+ end
178
+ end
179
+
180
+ end
181
+ end
182
+ end
183
+
184
+ end
185
+
186
+ class ArrayWithType
187
+ attr_accessor :item_type
188
+
189
+ def initialize(item_type)
190
+ raise "needs a item_type" unless item_type
191
+ @item_type = item_type
192
+ end
193
+
194
+ def deserialize_from_json(value)
195
+ value.nil? ? nil : value.map { |item| item_type.deserialize_from_json(item) }
196
+ end
197
+
198
+ def to_s
199
+ "Sequent::Core::Helpers::ArrayWithType.new(#{item_type})"
200
+ end
201
+ end
202
+
203
+ end
204
+ end
205
+ end
206
+
207
+
@@ -0,0 +1,36 @@
1
+ module Sequent
2
+ module Core
3
+ module Helpers
4
+ #
5
+ # Parses the strings "true", "false" and nil to true, false, false
6
+ #
7
+ # You need to include this module explicitly when working with booleans.
8
+ #
9
+ # Example:
10
+ #
11
+ # class Registration < Sequent::Core::ValueObject
12
+ # include Sequent::Core::Helpers::BooleanSupport
13
+ # attrs accepted_terms: Boolean
14
+ # end
15
+ module BooleanSupport
16
+
17
+ def self.included(base)
18
+ base.before_validation :parse_booleans
19
+ end
20
+
21
+ def parse_booleans
22
+ attributes.each do |name, type|
23
+ if type == Boolean
24
+ raw_value = self.instance_variable_get("@#{name}")
25
+ return if raw_value.kind_of? Boolean
26
+ return unless [nil, "true", "false"].include?(raw_value)
27
+ bool_value = raw_value == "true" ? true : false
28
+ self.instance_variable_set "@#{name}", bool_value
29
+ end
30
+ end
31
+ end
32
+ end
33
+
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,25 @@
1
+ module Sequent
2
+ module Core
3
+ module Helpers
4
+ # Make a deep clone of an object that include AttributeSupport
5
+ #
6
+ # person = Person.new(name: 'Ben').copy(name: 'Kim')
7
+ #
8
+ # You typically do not need to include this module in your classes. If you extend from
9
+ # Sequent::Core::ValueObject, Sequent::Core::Event or Sequent::Core::BaseCommand you will
10
+ # get this functionality for free.
11
+ #
12
+ module Copyable
13
+ def copy(attrs = {})
14
+ the_copy = Marshal.load(Marshal.dump(self))
15
+ attrs.each do |name, value|
16
+ the_copy.send("#{name}=", value)
17
+ end
18
+ the_copy
19
+ end
20
+
21
+ end
22
+ end
23
+ end
24
+ end
25
+
@@ -0,0 +1,41 @@
1
+ module Sequent
2
+ module Core
3
+ module Helpers
4
+ #
5
+ # You typically do not need to include this module in your classes. If you extend from
6
+ # Sequent::Core::ValueObject, Sequent::Core::Event or Sequent::Core::BaseCommand you will
7
+ # get this functionality for free.
8
+ #
9
+ module EqualSupport
10
+ def ==(other)
11
+ return false if other == nil
12
+ return false if self.class != other.class
13
+ self.class.types.each do |name, _|
14
+ self_value = self.send(name)
15
+ other_value = other.send(name)
16
+ if self_value.class == DateTime && other_value.class == DateTime
17
+ # we don't care about milliseconds. If you know a better way of checking for equality please improve.
18
+ return false unless (self_value.iso8601 == other_value.iso8601)
19
+ else
20
+ return false unless (self_value == other_value)
21
+ end
22
+ end
23
+ true
24
+ end
25
+
26
+ def hash
27
+ hash = 17
28
+ self.class.types.each do |name, _|
29
+ hash = hash * 31 + self.send(name).hash
30
+ end
31
+ hash
32
+ end
33
+
34
+ def eql?(other)
35
+ self == other
36
+ end
37
+ end
38
+
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,9 @@
1
+ require_relative 'uuid_helper'
2
+ require_relative 'copyable'
3
+ require_relative 'attribute_support'
4
+ require_relative 'equal_support'
5
+ require_relative 'param_support'
6
+ require_relative 'mergable'
7
+ require_relative 'association_validator'
8
+ require_relative 'string_support'
9
+ require_relative 'boolean_support'
@@ -0,0 +1,21 @@
1
+ module Sequent
2
+ module Core
3
+ module Helpers
4
+ # Looks like Copyable but changes this instance
5
+ #
6
+ # ben = Person.new(name: 'Ben').merge!(name: 'Ben Vonk')
7
+ #
8
+ module Mergable
9
+
10
+ def merge!(attrs = {})
11
+ attrs.each do |name, value|
12
+ self.send("#{name}=", value)
13
+ end
14
+ self
15
+ end
16
+
17
+ end
18
+ end
19
+ end
20
+ end
21
+
@@ -0,0 +1,80 @@
1
+ require 'active_support'
2
+
3
+ module Sequent
4
+ module Core
5
+ module Helpers
6
+ # Class to support binding from a params hash like the one from Sinatra
7
+ #
8
+ # You typically do not need to include this module in your classes. If you extend from
9
+ # Sequent::Core::ValueObject, Sequent::Core::Event or Sequent::Core::BaseCommand you will
10
+ # get this functionality for free.
11
+ #
12
+ module ParamSupport
13
+ module ClassMethods
14
+ def from_params(params = {})
15
+ result = allocate
16
+ params = HashWithIndifferentAccess.new(params)
17
+ result.attributes.each do |attribute, type|
18
+ value = params[attribute]
19
+
20
+ next if value.blank?
21
+ if type.respond_to? :from_params
22
+ value = type.from_params(value)
23
+ elsif type.is_a? Sequent::Core::Helpers::ArrayWithType
24
+ value = value.map { |v| type.item_type.from_params(v) }
25
+ elsif type <= Date
26
+ value = Date.strptime(value, "%d-%m-%Y") if value
27
+ end
28
+ result.instance_variable_set(:"@#{attribute}", value)
29
+ end
30
+ result
31
+ end
32
+
33
+ end
34
+ # extend host class with class methods when we're included
35
+ def self.included(host_class)
36
+ host_class.extend(ClassMethods)
37
+ end
38
+
39
+ def to_params(root)
40
+ make_params root, as_params
41
+ end
42
+
43
+ def as_params
44
+ hash = HashWithIndifferentAccess.new
45
+ self.class.types.each do |field|
46
+ value = self.instance_variable_get("@#{field[0]}")
47
+ next if field[0] == "errors"
48
+ if value.respond_to?(:as_params) && value.kind_of?(ValueObject)
49
+ value = value.as_params
50
+ elsif value.kind_of?(Array)
51
+ value = value.map { |val| val.kind_of?(ValueObject) ? val.as_params : val }
52
+ elsif value.kind_of? Date
53
+ value = value.strftime("%d-%m-%Y") if value
54
+ end
55
+ hash[field[0]] = value
56
+ end
57
+ hash
58
+ end
59
+
60
+ private
61
+ def make_params(root, hash)
62
+ result={}
63
+ hash.each do |k, v|
64
+ key = "#{root}[#{k}]"
65
+ if v.is_a? Hash
66
+ make_params(key, v).each do |k, v|
67
+ result[k] = v.nil? ? "" : v.to_s
68
+ end
69
+ elsif v.is_a? Array
70
+ result[key] = v
71
+ else
72
+ result[key] = v.nil? ? "" : v.to_s
73
+ end
74
+ end
75
+ result
76
+ end
77
+ end
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,45 @@
1
+ module Sequent
2
+ module Core
3
+ module Helpers
4
+ ##
5
+ # Creates ability to use DSL like:
6
+ # class MyEventHandler < Sequent::Core::BaseEventHandler
7
+ #
8
+ # on MyEvent do |event|
9
+ # do_some_logic
10
+ # end
11
+ # end
12
+ #
13
+ # You typically do not need to include this module in your classes. If you extend from
14
+ # Sequent::Core::AggregateRoot, Sequent::Core::BaseEventHandler or Sequent::Core::BaseCommandHandler
15
+ # you will get this functionality for free.
16
+ #
17
+ module SelfApplier
18
+
19
+ module ClassMethods
20
+
21
+ def on(*message_classes, &block)
22
+ @message_mapping ||= {}
23
+ message_classes.each { |message_class| @message_mapping[message_class] = block }
24
+ end
25
+
26
+ def message_mapping
27
+ @message_mapping || {}
28
+ end
29
+ end
30
+
31
+ def self.included(host_class)
32
+ host_class.extend(ClassMethods)
33
+ end
34
+
35
+ def handle_message(message)
36
+ handler = self.class.message_mapping[message.class]
37
+ self.instance_exec(message, &handler) if handler
38
+ end
39
+
40
+ end
41
+
42
+ end
43
+ end
44
+ end
45
+
@@ -0,0 +1,22 @@
1
+ module Sequent
2
+ module Core
3
+ module Helpers
4
+ #
5
+ # You typically do not need to include this module in your classes. If you extend from
6
+ # Sequent::Core::ValueObject, Sequent::Core::Event or Sequent::Core::BaseCommand you will
7
+ # get this functionality for free.
8
+ #
9
+ module StringSupport
10
+ def to_s
11
+ s = "#{self.class.name}: "
12
+ self.instance_variables.each do |name|
13
+ value = self.instance_variable_get("#{name}")
14
+ s += "#{name}=[#{value}], "
15
+ end
16
+ "{" + s.chomp(", ") + "}"
17
+ end
18
+ end
19
+
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,17 @@
1
+ require 'securerandom'
2
+
3
+ module Sequent
4
+ module Core
5
+ module Helpers
6
+ module UuidHelper
7
+
8
+ def new_uuid
9
+ SecureRandom.uuid
10
+ end
11
+
12
+ module_function :new_uuid
13
+
14
+ end
15
+ end
16
+ end
17
+ end