sequent 0.1.1 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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