sequent 4.1.0 → 5.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (41) hide show
  1. checksums.yaml +4 -4
  2. data/bin/sequent +2 -1
  3. data/lib/sequent/core/aggregate_repository.rb +31 -0
  4. data/lib/sequent/core/aggregate_root.rb +20 -0
  5. data/lib/sequent/core/command_record.rb +1 -1
  6. data/lib/sequent/core/event_store.rb +32 -1
  7. data/lib/sequent/core/helpers/attr_matchers/argument_serializer.rb +35 -0
  8. data/lib/sequent/core/helpers/attr_matchers/attr_matchers.rb +10 -0
  9. data/lib/sequent/core/helpers/attr_matchers/dsl.rb +23 -0
  10. data/lib/sequent/core/helpers/attr_matchers/equals.rb +24 -0
  11. data/lib/sequent/core/helpers/attr_matchers/greater_than.rb +24 -0
  12. data/lib/sequent/core/helpers/attr_matchers/greater_than_equals.rb +24 -0
  13. data/lib/sequent/core/helpers/attr_matchers/less_than.rb +24 -0
  14. data/lib/sequent/core/helpers/attr_matchers/less_than_equals.rb +24 -0
  15. data/lib/sequent/core/helpers/attr_matchers/not_equals.rb +24 -0
  16. data/lib/sequent/core/helpers/attribute_support.rb +34 -9
  17. data/lib/sequent/core/helpers/autoset_attributes.rb +5 -5
  18. data/lib/sequent/core/helpers/message_dispatcher.rb +20 -0
  19. data/lib/sequent/core/helpers/message_handler.rb +62 -8
  20. data/lib/sequent/core/helpers/message_handler_option_registry.rb +59 -0
  21. data/lib/sequent/core/helpers/message_matchers/any.rb +34 -0
  22. data/lib/sequent/core/helpers/message_matchers/argument_coercer.rb +24 -0
  23. data/lib/sequent/core/helpers/message_matchers/argument_serializer.rb +20 -0
  24. data/lib/sequent/core/helpers/message_matchers/dsl.rb +23 -0
  25. data/lib/sequent/core/helpers/message_matchers/except_opt.rb +24 -0
  26. data/lib/sequent/core/helpers/message_matchers/has_attrs.rb +54 -0
  27. data/lib/sequent/core/helpers/message_matchers/instance_of.rb +24 -0
  28. data/lib/sequent/core/helpers/message_matchers/is_a.rb +34 -0
  29. data/lib/sequent/core/helpers/message_matchers/message_matchers.rb +10 -0
  30. data/lib/sequent/core/helpers/message_router.rb +55 -0
  31. data/lib/sequent/core/projector.rb +28 -0
  32. data/lib/sequent/core/transactions/active_record_transaction_provider.rb +25 -0
  33. data/lib/sequent/core/transactions/read_only_active_record_transaction_provider.rb +46 -0
  34. data/lib/sequent/core/transactions/transactions.rb +1 -0
  35. data/lib/sequent/core/workflow.rb +30 -2
  36. data/lib/sequent/generator/project.rb +7 -0
  37. data/lib/sequent/generator/template_project/ruby-version +1 -0
  38. data/lib/sequent/test/command_handler_helpers.rb +2 -2
  39. data/lib/sequent/util/dry_run.rb +18 -13
  40. data/lib/version.rb +1 -1
  41. metadata +36 -13
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 5e0c606e57bb36fdcaeeb102904dd450a0ff9fc2a84fdd5aff7f365a914c45d3
4
- data.tar.gz: ea03e8a0c7ab524ff8c73fded97a726edf3dff240b95db62496496d5a6a6d580
3
+ metadata.gz: 1dea4b53205452eaa68007ed47688b3fa074229677186e9342265cdd7a31d4db
4
+ data.tar.gz: 2d342b9a013372ca99b441c4980432f642a56741746291d814c68ad60cf6c26e
5
5
  SHA512:
6
- metadata.gz: 10f78e12826549dcd0ea3677940b5b2cdbde26fb9ecdf2f5a1bca67d5dd8405447409417b6cd277bd554c3590a2a7a3814a0918ae79ceec2b6da9510d8d1fa3f
7
- data.tar.gz: abde6abb5638e8ad3f4f2081eb1e9eada6588d4d74994a5a62a4ef761b9737277981a5c270e75297a5e508cc611031a2e779df43b3a70b3d5a9bb0b77c81ec34
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, __dir__)}
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.to_sym).deserialize_from_json(args)
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 aggregate ordered by sequence_number
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
- def types
44
- @types ||= {}
45
- return @merged_types if @merged_types
43
+ attr_reader :types
46
44
 
47
- @merged_types = is_a?(Class) && superclass.respond_to?(:types) ? @types.merge(superclass.types) : @types
48
- included_modules.select { |m| m.include? Sequent::Core::Helpers::AttributeSupport }.each do |mod|
49
- @merged_types.merge!(mod.types)
50
- end
51
- @merged_types
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
- @types ||= {}
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(*event_classes)
40
- event_classes.each do |event_class|
41
- on event_class do |event|
42
- self.class.event_attribute_keys(event_class).each do |attribute_name|
43
- instance_variable_set(:"@#{attribute_name}", event.send(attribute_name.to_sym))
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(*message_classes, &block)
40
- message_classes.each do |message_class|
41
- message_mapping[message_class] ||= []
42
- message_mapping[message_class] << block
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
- @message_mapping ||= {}
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
- message_mapping.keys.include? message.class
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
- handlers = self.class.message_mapping[message.class]
61
- handlers&.each { |handler| instance_exec(message, &handler) }
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
+ )