sequent 4.1.0 → 5.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/bin/sequent +2 -1
- data/lib/sequent/core/aggregate_repository.rb +31 -0
- data/lib/sequent/core/aggregate_root.rb +20 -0
- data/lib/sequent/core/command_record.rb +1 -1
- data/lib/sequent/core/event_store.rb +32 -1
- data/lib/sequent/core/helpers/attr_matchers/argument_serializer.rb +35 -0
- data/lib/sequent/core/helpers/attr_matchers/attr_matchers.rb +10 -0
- data/lib/sequent/core/helpers/attr_matchers/dsl.rb +23 -0
- data/lib/sequent/core/helpers/attr_matchers/equals.rb +24 -0
- data/lib/sequent/core/helpers/attr_matchers/greater_than.rb +24 -0
- data/lib/sequent/core/helpers/attr_matchers/greater_than_equals.rb +24 -0
- data/lib/sequent/core/helpers/attr_matchers/less_than.rb +24 -0
- data/lib/sequent/core/helpers/attr_matchers/less_than_equals.rb +24 -0
- data/lib/sequent/core/helpers/attr_matchers/not_equals.rb +24 -0
- data/lib/sequent/core/helpers/attribute_support.rb +34 -9
- data/lib/sequent/core/helpers/autoset_attributes.rb +5 -5
- data/lib/sequent/core/helpers/message_dispatcher.rb +20 -0
- data/lib/sequent/core/helpers/message_handler.rb +62 -8
- data/lib/sequent/core/helpers/message_handler_option_registry.rb +59 -0
- data/lib/sequent/core/helpers/message_matchers/any.rb +34 -0
- data/lib/sequent/core/helpers/message_matchers/argument_coercer.rb +24 -0
- data/lib/sequent/core/helpers/message_matchers/argument_serializer.rb +20 -0
- data/lib/sequent/core/helpers/message_matchers/dsl.rb +23 -0
- data/lib/sequent/core/helpers/message_matchers/except_opt.rb +24 -0
- data/lib/sequent/core/helpers/message_matchers/has_attrs.rb +54 -0
- data/lib/sequent/core/helpers/message_matchers/instance_of.rb +24 -0
- data/lib/sequent/core/helpers/message_matchers/is_a.rb +34 -0
- data/lib/sequent/core/helpers/message_matchers/message_matchers.rb +10 -0
- data/lib/sequent/core/helpers/message_router.rb +55 -0
- data/lib/sequent/core/projector.rb +28 -0
- data/lib/sequent/core/transactions/active_record_transaction_provider.rb +25 -0
- data/lib/sequent/core/transactions/read_only_active_record_transaction_provider.rb +46 -0
- data/lib/sequent/core/transactions/transactions.rb +1 -0
- data/lib/sequent/core/workflow.rb +30 -2
- data/lib/sequent/generator/project.rb +7 -0
- data/lib/sequent/generator/template_project/ruby-version +1 -0
- data/lib/sequent/test/command_handler_helpers.rb +2 -2
- data/lib/sequent/util/dry_run.rb +18 -13
- data/lib/version.rb +1 -1
- metadata +36 -13
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 1dea4b53205452eaa68007ed47688b3fa074229677186e9342265cdd7a31d4db
|
4
|
+
data.tar.gz: 2d342b9a013372ca99b441c4980432f642a56741746291d814c68ad60cf6c26e
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: cea76a7429f29dcff3e73789f608c968030ade74446493effcf6cf6b51f39c3815da7e5ccc040cf0392a797a6978fa773b5f8f2791f907ebbb31830ab7ae56bc
|
7
|
+
data.tar.gz: e03569382132ffefabd44ebc973d78a2c1745291cf64ef17c0dc744bd513d5fe72c816a0cfb69da7297ddd076df81139a53058816f02bc215f58ad48c0b7bc67
|
data/bin/sequent
CHANGED
@@ -20,10 +20,11 @@ def new_project(args)
|
|
20
20
|
Success!
|
21
21
|
|
22
22
|
Your brand spanking new sequent app is waiting for you in:
|
23
|
-
#{File.expand_path(name,
|
23
|
+
#{File.expand_path(name, Dir.pwd)}
|
24
24
|
|
25
25
|
To finish setting up your app:
|
26
26
|
cd #{name}
|
27
|
+
bundle install
|
27
28
|
bundle exec rake sequent:db:create
|
28
29
|
bundle exec rake sequent:db:create_view_schema
|
29
30
|
bundle exec rake sequent:migrate:online
|
@@ -57,6 +57,37 @@ module Sequent
|
|
57
57
|
load_aggregates([aggregate_id], clazz)[0]
|
58
58
|
end
|
59
59
|
|
60
|
+
# Optimised for loading lots of events and ignore snapshot events. To get the correct historical state of an
|
61
|
+
# AggregateRoot it is necessary to be able to ignore snapshots. For a nested AggregateRoot, there will not be a
|
62
|
+
# sequence number known, so a load_until timestamp can be used instead.
|
63
|
+
#
|
64
|
+
# +aggregate_id+ The id of the aggregate to be loaded
|
65
|
+
#
|
66
|
+
# +clazz+ Optional argument that checks if aggregate is of type +clazz+
|
67
|
+
#
|
68
|
+
# +load_until+ Optional argument that defines up until what point in time the AggregateRoot will be rebuilt.
|
69
|
+
def load_aggregate_for_snapshotting(aggregate_id, clazz = nil, load_until: nil)
|
70
|
+
fail ArgumentError, 'aggregate_id is required' if aggregate_id.blank?
|
71
|
+
|
72
|
+
stream = Sequent
|
73
|
+
.configuration
|
74
|
+
.event_store
|
75
|
+
.find_event_stream(aggregate_id)
|
76
|
+
aggregate = Class.const_get(stream.aggregate_type).stream_from_history(stream)
|
77
|
+
|
78
|
+
Sequent
|
79
|
+
.configuration
|
80
|
+
.event_store
|
81
|
+
.stream_events_for_aggregate(aggregate_id, load_until: load_until) do |event_stream|
|
82
|
+
aggregate.stream_from_history(event_stream)
|
83
|
+
end
|
84
|
+
|
85
|
+
if clazz
|
86
|
+
fail TypeError, "#{aggregate.class} is not a #{clazz}" unless aggregate.class <= clazz
|
87
|
+
end
|
88
|
+
aggregate
|
89
|
+
end
|
90
|
+
|
60
91
|
##
|
61
92
|
# Loads multiple aggregates at once.
|
62
93
|
# Returns the ones in the current Unit Of Work otherwise loads it from history.
|
@@ -80,6 +80,26 @@ module Sequent
|
|
80
80
|
events.each { |event| apply_event(event) }
|
81
81
|
end
|
82
82
|
|
83
|
+
def initialize_for_streaming(stream)
|
84
|
+
@uncommitted_events = []
|
85
|
+
@sequence_number = 1
|
86
|
+
@event_stream = stream
|
87
|
+
end
|
88
|
+
|
89
|
+
def stream_from_history(stream_events)
|
90
|
+
_stream, event = stream_events
|
91
|
+
fail 'Empty history' if event.blank?
|
92
|
+
|
93
|
+
@id ||= event.aggregate_id
|
94
|
+
apply_event(event)
|
95
|
+
end
|
96
|
+
|
97
|
+
def self.stream_from_history(stream)
|
98
|
+
aggregate_root = allocate
|
99
|
+
aggregate_root.initialize_for_streaming(stream)
|
100
|
+
aggregate_root
|
101
|
+
end
|
102
|
+
|
83
103
|
def to_s
|
84
104
|
"#{self.class.name}: #{@id}"
|
85
105
|
end
|
@@ -8,7 +8,7 @@ module Sequent
|
|
8
8
|
module SerializesCommand
|
9
9
|
def command
|
10
10
|
args = Sequent::Core::Oj.strict_load(command_json)
|
11
|
-
Class.const_get(command_type
|
11
|
+
Class.const_get(command_type).deserialize_from_json(args)
|
12
12
|
end
|
13
13
|
|
14
14
|
def command=(command)
|
@@ -51,7 +51,38 @@ module Sequent
|
|
51
51
|
end
|
52
52
|
|
53
53
|
##
|
54
|
-
# Returns all events for the
|
54
|
+
# Returns all events for the AggregateRoot ordered by sequence_number, disregarding snapshot events.
|
55
|
+
#
|
56
|
+
# This streaming is done in batches to prevent loading many events in memory all at once. A usecase for ignoring
|
57
|
+
# the snapshots is when events of a nested AggregateRoot need to be loaded up until a certain moment in time.
|
58
|
+
#
|
59
|
+
# @param aggregate_id Aggregate id of the AggregateRoot
|
60
|
+
# @param load_until The timestamp up until which you want to built the aggregate. Optional.
|
61
|
+
# @param &block Block that should be passed to handle the batches returned from this method
|
62
|
+
def stream_events_for_aggregate(aggregate_id, load_until: nil, &block)
|
63
|
+
stream = find_event_stream(aggregate_id)
|
64
|
+
fail ArgumentError, 'no stream found for this aggregate' if stream.blank?
|
65
|
+
|
66
|
+
q = Sequent
|
67
|
+
.configuration
|
68
|
+
.event_record_class
|
69
|
+
.where(aggregate_id: aggregate_id)
|
70
|
+
.where.not(event_type: Sequent.configuration.snapshot_event_class.name)
|
71
|
+
.order(:sequence_number)
|
72
|
+
q = q.where('created_at < ?', load_until) if load_until.present?
|
73
|
+
has_events = false
|
74
|
+
|
75
|
+
q.select('event_type, event_json').each_row do |event_hash|
|
76
|
+
has_events = true
|
77
|
+
event = deserialize_event(event_hash)
|
78
|
+
block.call([stream, event])
|
79
|
+
end
|
80
|
+
fail ArgumentError, 'no events for this aggregate' unless has_events
|
81
|
+
end
|
82
|
+
|
83
|
+
##
|
84
|
+
# Returns all events for the aggregate ordered by sequence_number, loading them from the latest snapshot
|
85
|
+
# event onwards, if a snapshot is present
|
55
86
|
#
|
56
87
|
def load_events(aggregate_id)
|
57
88
|
load_events_for_aggregates([aggregate_id])[0]
|
@@ -0,0 +1,35 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Sequent
|
4
|
+
module Core
|
5
|
+
module Helpers
|
6
|
+
module AttrMatchers
|
7
|
+
class ArgumentSerializer
|
8
|
+
class << self
|
9
|
+
def serialize_value(value, enclose_hash: false)
|
10
|
+
return value.to_s if value.respond_to?(:matches_attr?)
|
11
|
+
return %("#{value}") if value.is_a?(String)
|
12
|
+
return serialize_hash(value, enclose_hash: enclose_hash) if value.is_a?(Hash)
|
13
|
+
|
14
|
+
value
|
15
|
+
end
|
16
|
+
|
17
|
+
private
|
18
|
+
|
19
|
+
def serialize_hash(hash, enclose_hash:)
|
20
|
+
serialized = hash
|
21
|
+
.map do |(name, value)|
|
22
|
+
"#{name}: #{serialize_value(value, enclose_hash: true)}"
|
23
|
+
end
|
24
|
+
.join(', ')
|
25
|
+
|
26
|
+
return "{#{serialized}}" if enclose_hash
|
27
|
+
|
28
|
+
serialized
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,10 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'dsl'
|
4
|
+
require_relative 'argument_serializer'
|
5
|
+
require_relative 'equals'
|
6
|
+
require_relative 'not_equals'
|
7
|
+
require_relative 'greater_than_equals'
|
8
|
+
require_relative 'greater_than'
|
9
|
+
require_relative 'less_than_equals'
|
10
|
+
require_relative 'less_than'
|
@@ -0,0 +1,23 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Sequent
|
4
|
+
module Core
|
5
|
+
module Helpers
|
6
|
+
module AttrMatchers
|
7
|
+
module DSL
|
8
|
+
def register_matcher(name, matcher_class)
|
9
|
+
if respond_to?(name)
|
10
|
+
fail ArgumentError, "Cannot register attr matcher because it would overwrite existing method '#{name}'"
|
11
|
+
end
|
12
|
+
|
13
|
+
define_method(name) do |*expected|
|
14
|
+
matcher_class.new(*expected)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
extend DSL
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Sequent
|
4
|
+
module Core
|
5
|
+
module Helpers
|
6
|
+
module AttrMatchers
|
7
|
+
Equals = Struct.new(:expected_value) do
|
8
|
+
def matches_attr?(actual_value)
|
9
|
+
actual_value == expected_value
|
10
|
+
end
|
11
|
+
|
12
|
+
def to_s
|
13
|
+
"eq(#{ArgumentSerializer.serialize_value(expected_value)})"
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
Sequent::Core::Helpers::AttrMatchers.register_matcher(
|
22
|
+
:eq,
|
23
|
+
Sequent::Core::Helpers::AttrMatchers::Equals,
|
24
|
+
)
|
@@ -0,0 +1,24 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Sequent
|
4
|
+
module Core
|
5
|
+
module Helpers
|
6
|
+
module AttrMatchers
|
7
|
+
GreaterThan = Struct.new(:expected_value) do
|
8
|
+
def matches_attr?(actual_value)
|
9
|
+
actual_value > expected_value
|
10
|
+
end
|
11
|
+
|
12
|
+
def to_s
|
13
|
+
"gt(#{ArgumentSerializer.serialize_value(expected_value)})"
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
Sequent::Core::Helpers::AttrMatchers.register_matcher(
|
22
|
+
:gt,
|
23
|
+
Sequent::Core::Helpers::AttrMatchers::GreaterThan,
|
24
|
+
)
|
@@ -0,0 +1,24 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Sequent
|
4
|
+
module Core
|
5
|
+
module Helpers
|
6
|
+
module AttrMatchers
|
7
|
+
GreaterThanEquals = Struct.new(:expected_value) do
|
8
|
+
def matches_attr?(actual_value)
|
9
|
+
actual_value >= expected_value
|
10
|
+
end
|
11
|
+
|
12
|
+
def to_s
|
13
|
+
"gte(#{ArgumentSerializer.serialize_value(expected_value)})"
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
Sequent::Core::Helpers::AttrMatchers.register_matcher(
|
22
|
+
:gte,
|
23
|
+
Sequent::Core::Helpers::AttrMatchers::GreaterThanEquals,
|
24
|
+
)
|
@@ -0,0 +1,24 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Sequent
|
4
|
+
module Core
|
5
|
+
module Helpers
|
6
|
+
module AttrMatchers
|
7
|
+
LessThan = Struct.new(:expected_value) do
|
8
|
+
def matches_attr?(actual_value)
|
9
|
+
actual_value < expected_value
|
10
|
+
end
|
11
|
+
|
12
|
+
def to_s
|
13
|
+
"lt(#{ArgumentSerializer.serialize_value(expected_value)})"
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
Sequent::Core::Helpers::AttrMatchers.register_matcher(
|
22
|
+
:lt,
|
23
|
+
Sequent::Core::Helpers::AttrMatchers::LessThan,
|
24
|
+
)
|
@@ -0,0 +1,24 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Sequent
|
4
|
+
module Core
|
5
|
+
module Helpers
|
6
|
+
module AttrMatchers
|
7
|
+
LessThanEquals = Struct.new(:expected_value) do
|
8
|
+
def matches_attr?(actual_value)
|
9
|
+
actual_value <= expected_value
|
10
|
+
end
|
11
|
+
|
12
|
+
def to_s
|
13
|
+
"lte(#{ArgumentSerializer.serialize_value(expected_value)})"
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
Sequent::Core::Helpers::AttrMatchers.register_matcher(
|
22
|
+
:lte,
|
23
|
+
Sequent::Core::Helpers::AttrMatchers::LessThanEquals,
|
24
|
+
)
|
@@ -0,0 +1,24 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Sequent
|
4
|
+
module Core
|
5
|
+
module Helpers
|
6
|
+
module AttrMatchers
|
7
|
+
NotEquals = Struct.new(:expected_value) do
|
8
|
+
def matches_attr?(actual_value)
|
9
|
+
actual_value != expected_value
|
10
|
+
end
|
11
|
+
|
12
|
+
def to_s
|
13
|
+
"neq(#{ArgumentSerializer.serialize_value(expected_value)})"
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
Sequent::Core::Helpers::AttrMatchers.register_matcher(
|
22
|
+
:neq,
|
23
|
+
Sequent::Core::Helpers::AttrMatchers::NotEquals,
|
24
|
+
)
|
@@ -40,19 +40,19 @@ module Sequent
|
|
40
40
|
|
41
41
|
# module containing class methods to be added
|
42
42
|
module ClassMethods
|
43
|
-
|
44
|
-
@types ||= {}
|
45
|
-
return @merged_types if @merged_types
|
43
|
+
attr_reader :types
|
46
44
|
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
45
|
+
# Called when this module is included or when a class which includes this module is inherited from.
|
46
|
+
#
|
47
|
+
# All declared attrs are merged into @types in order to prevent superfluous calculation of types in a class
|
48
|
+
# hierarchy.
|
49
|
+
def initialize_types
|
50
|
+
@types = inherited_types
|
52
51
|
end
|
53
52
|
|
54
53
|
def attrs(args)
|
55
|
-
|
54
|
+
validate_attrs!(args)
|
55
|
+
|
56
56
|
@types.merge!(args)
|
57
57
|
associations = []
|
58
58
|
args.each do |attribute, type|
|
@@ -126,6 +126,18 @@ EOS
|
|
126
126
|
@upcasters.push(block)
|
127
127
|
end
|
128
128
|
|
129
|
+
private
|
130
|
+
|
131
|
+
def inherited_types
|
132
|
+
merged_types = is_a?(Class) && superclass.respond_to?(:types) ? superclass.types.dup : {}
|
133
|
+
|
134
|
+
included_modules
|
135
|
+
.select { |m| m.include? Sequent::Core::Helpers::AttributeSupport }
|
136
|
+
.reduce(merged_types) do |memo, mod|
|
137
|
+
memo.merge(mod.types)
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
129
141
|
def upcast!(hash)
|
130
142
|
return if @upcasters.nil?
|
131
143
|
|
@@ -133,11 +145,24 @@ EOS
|
|
133
145
|
upcaster.call(hash)
|
134
146
|
end
|
135
147
|
end
|
148
|
+
|
149
|
+
def validate_attrs!(args)
|
150
|
+
duplicate_attrs = types.keys & args.keys
|
151
|
+
|
152
|
+
fail ArgumentError, "Attributes already defined: #{duplicate_attrs.join(', ')}" if duplicate_attrs.any?
|
153
|
+
end
|
154
|
+
|
155
|
+
def inherited(subclass)
|
156
|
+
super
|
157
|
+
|
158
|
+
subclass.initialize_types
|
159
|
+
end
|
136
160
|
end
|
137
161
|
|
138
162
|
# extend host class with class methods when we're included
|
139
163
|
def self.included(host_class)
|
140
164
|
host_class.extend(ClassMethods)
|
165
|
+
host_class.initialize_types
|
141
166
|
end
|
142
167
|
|
143
168
|
def attributes
|
@@ -36,11 +36,11 @@ module Sequent
|
|
36
36
|
event_class.types.keys.reject { |k| @@autoset_ignore_attributes.include?(k.to_s) }
|
37
37
|
end
|
38
38
|
|
39
|
-
def autoset_attributes_for_events(*
|
40
|
-
|
41
|
-
on
|
42
|
-
self.class.event_attribute_keys(
|
43
|
-
instance_variable_set(:"@#{attribute_name}", event.
|
39
|
+
def autoset_attributes_for_events(*args)
|
40
|
+
args.each do |arg|
|
41
|
+
on arg do |event|
|
42
|
+
self.class.event_attribute_keys(event.class).each do |attribute_name|
|
43
|
+
instance_variable_set(:"@#{attribute_name}", event.public_send(attribute_name.to_sym))
|
44
44
|
end
|
45
45
|
end
|
46
46
|
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Sequent
|
4
|
+
module Core
|
5
|
+
module Helpers
|
6
|
+
class MessageDispatcher
|
7
|
+
def initialize(message_router, context)
|
8
|
+
@message_router = message_router
|
9
|
+
@context = context
|
10
|
+
end
|
11
|
+
|
12
|
+
def dispatch_message(message)
|
13
|
+
@message_router
|
14
|
+
.match_message(message)
|
15
|
+
.each { |handler| @context.instance_exec(message, &handler) }
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -1,5 +1,9 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require_relative 'message_handler_option_registry'
|
4
|
+
require_relative 'message_router'
|
5
|
+
require_relative 'message_dispatcher'
|
6
|
+
|
3
7
|
module Sequent
|
4
8
|
module Core
|
5
9
|
module Helpers
|
@@ -36,29 +40,79 @@ module Sequent
|
|
36
40
|
#
|
37
41
|
module MessageHandler
|
38
42
|
module ClassMethods
|
39
|
-
def on(*
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
+
def on(*args, **opts, &block)
|
44
|
+
OnArgumentsValidator.validate_arguments!(*args)
|
45
|
+
|
46
|
+
message_matchers = args.map { |arg| MessageMatchers::ArgumentCoercer.coerce_argument(arg) }
|
47
|
+
|
48
|
+
message_router.register_matchers(
|
49
|
+
*message_matchers,
|
50
|
+
block,
|
51
|
+
)
|
52
|
+
|
53
|
+
opts.each do |name, value|
|
54
|
+
option_registry.call_option(self, name, message_matchers, value)
|
43
55
|
end
|
44
56
|
end
|
45
57
|
|
58
|
+
def option(name, &block)
|
59
|
+
option_registry.register_option(name, block)
|
60
|
+
end
|
61
|
+
|
46
62
|
def message_mapping
|
47
|
-
|
63
|
+
message_router
|
64
|
+
.routes
|
65
|
+
.select { |matcher, _handlers| matcher.is_a?(MessageMatchers::InstanceOf) }
|
66
|
+
.map { |k, v| [k.expected_class, v] }
|
67
|
+
.to_h
|
48
68
|
end
|
49
69
|
|
50
70
|
def handles_message?(message)
|
51
|
-
|
71
|
+
message_router.matches_message?(message)
|
72
|
+
end
|
73
|
+
|
74
|
+
def message_router
|
75
|
+
@message_router ||= MessageRouter.new
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
class OnArgumentsValidator
|
80
|
+
class << self
|
81
|
+
def validate_arguments!(*args)
|
82
|
+
fail ArgumentError, "Must provide at least one argument to 'on'" if args.empty?
|
83
|
+
|
84
|
+
duplicates = args
|
85
|
+
.select { |arg| args.count(arg) > 1 }
|
86
|
+
.uniq
|
87
|
+
|
88
|
+
if duplicates.any?
|
89
|
+
humanized_duplicates = duplicates
|
90
|
+
.map { |x| MessageMatchers::ArgumentSerializer.serialize_value(x) }
|
91
|
+
.join(', ')
|
92
|
+
|
93
|
+
fail ArgumentError,
|
94
|
+
"Arguments to 'on' must be unique, duplicates: #{humanized_duplicates}"
|
95
|
+
end
|
96
|
+
end
|
52
97
|
end
|
53
98
|
end
|
54
99
|
|
55
100
|
def self.included(host_class)
|
56
101
|
host_class.extend(ClassMethods)
|
102
|
+
host_class.extend(MessageMatchers)
|
103
|
+
host_class.extend(AttrMatchers)
|
104
|
+
|
105
|
+
host_class.class_attribute :option_registry, default: MessageHandlerOptionRegistry.new
|
57
106
|
end
|
58
107
|
|
59
108
|
def handle_message(message)
|
60
|
-
|
61
|
-
|
109
|
+
message_dispatcher.dispatch_message(message)
|
110
|
+
end
|
111
|
+
|
112
|
+
private
|
113
|
+
|
114
|
+
def message_dispatcher
|
115
|
+
MessageDispatcher.new(self.class.message_router, self)
|
62
116
|
end
|
63
117
|
end
|
64
118
|
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Sequent
|
4
|
+
module Core
|
5
|
+
module Helpers
|
6
|
+
class MessageHandlerOptionRegistry
|
7
|
+
attr_reader :entries
|
8
|
+
|
9
|
+
def initialize
|
10
|
+
clear_options
|
11
|
+
end
|
12
|
+
|
13
|
+
##
|
14
|
+
# Registers a handler for the given option.
|
15
|
+
#
|
16
|
+
def register_option(name, handler)
|
17
|
+
fail ArgumentError, "Option with name '#{name}' already registered" if option_registered?(name)
|
18
|
+
|
19
|
+
@entries[name] = handler
|
20
|
+
end
|
21
|
+
|
22
|
+
##
|
23
|
+
# Calls the options with the given arguments with `self` bound to the given context.
|
24
|
+
#
|
25
|
+
def call_option(context, name, *args)
|
26
|
+
handler = find_option(name)
|
27
|
+
context.instance_exec(*args, &handler)
|
28
|
+
end
|
29
|
+
|
30
|
+
##
|
31
|
+
# Removes all options from the registry.
|
32
|
+
#
|
33
|
+
def clear_options
|
34
|
+
@entries = {}
|
35
|
+
end
|
36
|
+
|
37
|
+
private
|
38
|
+
|
39
|
+
##
|
40
|
+
# Returns the handler for given option.
|
41
|
+
#
|
42
|
+
def find_option(name)
|
43
|
+
@entries[name] || fail(
|
44
|
+
ArgumentError,
|
45
|
+
"Unsupported option: '#{name}'; " \
|
46
|
+
"#{@entries.keys.any? ? "registered options: #{@entries.keys.join(', ')}" : 'no registered options'}",
|
47
|
+
)
|
48
|
+
end
|
49
|
+
|
50
|
+
##
|
51
|
+
# Returns true when an option for the given name is registered, or false otherwise.
|
52
|
+
#
|
53
|
+
def option_registered?(name)
|
54
|
+
@entries.key?(name)
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Sequent
|
4
|
+
module Core
|
5
|
+
module Helpers
|
6
|
+
module MessageMatchers
|
7
|
+
Any = Struct.new(:opts) do
|
8
|
+
include ExceptOpt
|
9
|
+
|
10
|
+
def matches_message?(message)
|
11
|
+
return false if excluded?(message)
|
12
|
+
|
13
|
+
true
|
14
|
+
end
|
15
|
+
|
16
|
+
def to_s
|
17
|
+
"any#{matcher_arguments}"
|
18
|
+
end
|
19
|
+
|
20
|
+
private
|
21
|
+
|
22
|
+
def matcher_arguments
|
23
|
+
"(except: #{except})" if except
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
Sequent::Core::Helpers::MessageMatchers.register_matcher(
|
32
|
+
:any,
|
33
|
+
Sequent::Core::Helpers::MessageMatchers::Any,
|
34
|
+
)
|