sequent 4.1.0 → 5.0.0
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.
- 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
|
+
)
|