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.
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
+ )