sequent 4.3.0 → 6.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 (64) hide show
  1. checksums.yaml +4 -4
  2. data/bin/sequent +1 -1
  3. data/db/sequent_schema.rb +3 -3
  4. data/lib/sequent/configuration.rb +19 -1
  5. data/lib/sequent/core/aggregate_root.rb +2 -6
  6. data/lib/sequent/core/aggregate_roots.rb +2 -6
  7. data/lib/sequent/core/command.rb +8 -12
  8. data/lib/sequent/core/command_record.rb +1 -1
  9. data/lib/sequent/core/command_service.rb +13 -2
  10. data/lib/sequent/core/core.rb +1 -0
  11. data/lib/sequent/core/event.rb +2 -2
  12. data/lib/sequent/core/event_store.rb +15 -2
  13. data/lib/sequent/core/ext/ext.rb +17 -0
  14. data/lib/sequent/core/helpers/attr_matchers/argument_serializer.rb +35 -0
  15. data/lib/sequent/core/helpers/attr_matchers/attr_matchers.rb +10 -0
  16. data/lib/sequent/core/helpers/attr_matchers/dsl.rb +23 -0
  17. data/lib/sequent/core/helpers/attr_matchers/equals.rb +24 -0
  18. data/lib/sequent/core/helpers/attr_matchers/greater_than.rb +24 -0
  19. data/lib/sequent/core/helpers/attr_matchers/greater_than_equals.rb +24 -0
  20. data/lib/sequent/core/helpers/attr_matchers/less_than.rb +24 -0
  21. data/lib/sequent/core/helpers/attr_matchers/less_than_equals.rb +24 -0
  22. data/lib/sequent/core/helpers/attr_matchers/not_equals.rb +24 -0
  23. data/lib/sequent/core/helpers/attribute_support.rb +35 -9
  24. data/lib/sequent/core/helpers/autoset_attributes.rb +5 -5
  25. data/lib/sequent/core/helpers/default_validators.rb +3 -0
  26. data/lib/sequent/core/helpers/message_dispatcher.rb +20 -0
  27. data/lib/sequent/core/helpers/message_handler.rb +62 -8
  28. data/lib/sequent/core/helpers/message_handler_option_registry.rb +59 -0
  29. data/lib/sequent/core/helpers/message_matchers/any.rb +34 -0
  30. data/lib/sequent/core/helpers/message_matchers/argument_coercer.rb +24 -0
  31. data/lib/sequent/core/helpers/message_matchers/argument_serializer.rb +20 -0
  32. data/lib/sequent/core/helpers/message_matchers/dsl.rb +23 -0
  33. data/lib/sequent/core/helpers/message_matchers/except_opt.rb +24 -0
  34. data/lib/sequent/core/helpers/message_matchers/has_attrs.rb +54 -0
  35. data/lib/sequent/core/helpers/message_matchers/instance_of.rb +24 -0
  36. data/lib/sequent/core/helpers/message_matchers/is_a.rb +34 -0
  37. data/lib/sequent/core/helpers/message_matchers/message_matchers.rb +10 -0
  38. data/lib/sequent/core/helpers/message_router.rb +55 -0
  39. data/lib/sequent/core/helpers/param_support.rb +2 -0
  40. data/lib/sequent/core/helpers/string_to_value_parsers.rb +5 -0
  41. data/lib/sequent/core/helpers/time_validator.rb +23 -0
  42. data/lib/sequent/core/helpers/value_validators.rb +11 -0
  43. data/lib/sequent/core/middleware/chain.rb +37 -0
  44. data/lib/sequent/core/middleware/middleware.rb +3 -0
  45. data/lib/sequent/core/projector.rb +3 -11
  46. data/lib/sequent/core/workflow.rb +5 -13
  47. data/lib/sequent/generator/template_project/Rakefile +2 -2
  48. data/lib/sequent/generator/template_project/db/sequent_schema.rb +3 -3
  49. data/lib/sequent/generator/template_project/spec/spec_helper.rb +1 -1
  50. data/lib/sequent/migrations/migrations.rb +1 -0
  51. data/lib/sequent/migrations/projectors.rb +2 -2
  52. data/lib/sequent/migrations/sequent_schema.rb +40 -0
  53. data/lib/sequent/migrations/view_schema.rb +39 -3
  54. data/lib/sequent/rake/migration_tasks.rb +36 -33
  55. data/lib/sequent/support/database.rb +29 -13
  56. data/lib/sequent/test/command_handler_helpers.rb +3 -3
  57. data/lib/sequent/test/database_helpers.rb +20 -0
  58. data/lib/sequent/test/time_comparison.rb +2 -5
  59. data/lib/sequent/test/{event_handler_helpers.rb → workflow_helpers.rb} +24 -10
  60. data/lib/sequent/test.rb +2 -1
  61. data/lib/sequent/util/dry_run.rb +1 -1
  62. data/lib/version.rb +1 -1
  63. metadata +40 -15
  64. data/lib/sequent/rake/tasks.rb +0 -121
@@ -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
+ )
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sequent
4
+ module Core
5
+ module Helpers
6
+ module MessageMatchers
7
+ class ArgumentCoercer
8
+ class << self
9
+ def coerce_argument(arg)
10
+ fail ArgumentError, 'Cannot coerce nil argument' if arg.nil?
11
+
12
+ return MessageMatchers::InstanceOf.new(arg) if [Class, Module].include?(arg.class)
13
+ return arg if arg.respond_to?(:matches_message?)
14
+
15
+ fail ArgumentError,
16
+ "Can't coerce argument '#{arg}'; " \
17
+ 'must be either a Class, Module or message matcher (respond to :matches_message?)'
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sequent
4
+ module Core
5
+ module Helpers
6
+ module MessageMatchers
7
+ class ArgumentSerializer
8
+ class << self
9
+ def serialize_value(value)
10
+ return value.to_s if value.respond_to?(:matches_message?)
11
+ return %("#{value}") if value.is_a?(String)
12
+
13
+ value
14
+ end
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sequent
4
+ module Core
5
+ module Helpers
6
+ module MessageMatchers
7
+ module DSL
8
+ def register_matcher(name, matcher_class)
9
+ if respond_to?(name)
10
+ fail ArgumentError, "Cannot register message 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 MessageMatchers
7
+ module ExceptOpt
8
+ private
9
+
10
+ def excluded?(message)
11
+ [except]
12
+ .flatten
13
+ .compact
14
+ .any? { |x| message.is_a?(x) }
15
+ end
16
+
17
+ def except
18
+ opts.try(:[], :except)
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sequent
4
+ module Core
5
+ module Helpers
6
+ module MessageMatchers
7
+ HasAttrs = Struct.new(:message_matcher, :expected_attrs) do
8
+ def initialize(message_matcher, expected_attrs)
9
+ super
10
+
11
+ fail ArgumentError, 'Missing required message matcher' if message_matcher.nil?
12
+ fail ArgumentError, 'Missing required expected attrs' if expected_attrs.blank?
13
+
14
+ self.message_matcher = ArgumentCoercer.coerce_argument(message_matcher)
15
+ end
16
+
17
+ def matches_message?(message)
18
+ message_matcher.matches_message?(message) &&
19
+ matches_attrs?(message, expected_attrs)
20
+ end
21
+
22
+ def to_s
23
+ 'has_attrs(' \
24
+ "#{MessageMatchers::ArgumentSerializer.serialize_value(message_matcher)}, " \
25
+ "#{AttrMatchers::ArgumentSerializer.serialize_value(expected_attrs)}" \
26
+ ')'
27
+ end
28
+
29
+ private
30
+
31
+ def matches_attrs?(message, expected_attrs, path = [])
32
+ expected_attrs.all? do |(name, expected_value)|
33
+ matches_attr?(message, expected_value, path.dup.push(name))
34
+ end
35
+ end
36
+
37
+ def matches_attr?(message, expected_value, path)
38
+ return matches_attrs?(message, expected_value, path) if expected_value.is_a?(Hash)
39
+
40
+ expected_value = AttrMatchers::Equals.new(expected_value) unless expected_value.respond_to?(:matches_attr?)
41
+ value = path.reduce(message) { |memo, p| memo.public_send(p) }
42
+
43
+ expected_value.matches_attr?(value)
44
+ end
45
+ end
46
+ end
47
+ end
48
+ end
49
+ end
50
+
51
+ Sequent::Core::Helpers::MessageMatchers.register_matcher(
52
+ :has_attrs,
53
+ Sequent::Core::Helpers::MessageMatchers::HasAttrs,
54
+ )
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sequent
4
+ module Core
5
+ module Helpers
6
+ module MessageMatchers
7
+ InstanceOf = Struct.new(:expected_class) do
8
+ def matches_message?(message)
9
+ message.instance_of?(expected_class)
10
+ end
11
+
12
+ def to_s
13
+ expected_class
14
+ end
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
20
+
21
+ Sequent::Core::Helpers::MessageMatchers.register_matcher(
22
+ :instance_of,
23
+ Sequent::Core::Helpers::MessageMatchers::InstanceOf,
24
+ )
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sequent
4
+ module Core
5
+ module Helpers
6
+ module MessageMatchers
7
+ IsA = Struct.new(:expected_class, :opts) do
8
+ include ExceptOpt
9
+
10
+ def matches_message?(message)
11
+ message.is_a?(expected_class) unless excluded?(message)
12
+ end
13
+
14
+ def to_s
15
+ "is_a(#{matcher_arguments})"
16
+ end
17
+
18
+ private
19
+
20
+ def matcher_arguments
21
+ arguments = expected_class.to_s
22
+ arguments += ", except: #{except}" if except
23
+ arguments
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
30
+
31
+ Sequent::Core::Helpers::MessageMatchers.register_matcher(
32
+ :is_a,
33
+ Sequent::Core::Helpers::MessageMatchers::IsA,
34
+ )
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'dsl'
4
+ require_relative 'argument_coercer'
5
+ require_relative 'argument_serializer'
6
+ require_relative 'except_opt'
7
+ require_relative 'instance_of'
8
+ require_relative 'is_a'
9
+ require_relative 'has_attrs'
10
+ require_relative 'any'
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative './attr_matchers/attr_matchers'
4
+ require_relative './message_matchers/message_matchers'
5
+
6
+ module Sequent
7
+ module Core
8
+ module Helpers
9
+ class MessageRouter
10
+ attr_reader :routes
11
+
12
+ def initialize
13
+ clear_routes
14
+ end
15
+
16
+ ##
17
+ # Registers a handler for the given matchers.
18
+ #
19
+ # A matcher must implement #matches_message?(message) and return a truthy value when it matches,
20
+ # or a falsey value otherwise.
21
+ #
22
+ def register_matchers(*matchers, handler)
23
+ matchers.each do |matcher|
24
+ @routes[matcher] << handler
25
+ end
26
+ end
27
+
28
+ ##
29
+ # Returns a set of handlers that match the given message, or an empty set when none match.
30
+ #
31
+ def match_message(message)
32
+ @routes
33
+ .reduce(Set.new) do |memo, (matcher, handlers)|
34
+ memo = memo.merge(handlers) if matcher.matches_message?(message)
35
+ memo
36
+ end
37
+ end
38
+
39
+ ##
40
+ # Returns true when there is at least one handler for the given message, or false otherwise.
41
+ #
42
+ def matches_message?(message)
43
+ match_message(message).any?
44
+ end
45
+
46
+ ##
47
+ # Removes all routes from the router.
48
+ #
49
+ def clear_routes
50
+ @routes = Hash.new { |h, k| h[k] = Set.new }
51
+ end
52
+ end
53
+ end
54
+ end
55
+ end
@@ -83,6 +83,8 @@ module Sequent
83
83
  val.iso8601
84
84
  elsif val.is_a? Date
85
85
  val.iso8601
86
+ elsif val.is_a? Time
87
+ val.iso8601(Sequent.configuration.time_precision)
86
88
  else
87
89
  val
88
90
  end
@@ -16,6 +16,7 @@ module Sequent
16
16
  ::Boolean => ->(value) { parse_to_bool(value) },
17
17
  ::Date => ->(value) { parse_to_date(value) },
18
18
  ::DateTime => ->(value) { parse_to_date_time(value) },
19
+ ::Time => ->(value) { parse_to_time(value) },
19
20
  ::Sequent::Core::Helpers::ArrayWithType => ->(values, type_in_array) { parse_array(values, type_in_array) },
20
21
  ::Sequent::Core::Helpers::Secret => ->(value) { Sequent::Core::Helpers::Secret.new(value).encrypt },
21
22
  }.freeze
@@ -52,6 +53,10 @@ module Sequent
52
53
  value.is_a?(DateTime) ? value : DateTime.deserialize_from_json(value)
53
54
  end
54
55
 
56
+ def self.parse_to_time(value)
57
+ value.is_a?(Time) ? value : Time.deserialize_from_json(value)
58
+ end
59
+
55
60
  def self.parse_array(values, type_in_array)
56
61
  fail "invalid value for array(): \"#{values}\"" unless values.is_a?(Array)
57
62
 
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'active_model'
4
+
5
+ module Sequent
6
+ module Core
7
+ module Helpers
8
+ # Validates Time
9
+ # Automatically included when using a
10
+ #
11
+ # attrs value: Time
12
+ class TimeValidator < ActiveModel::EachValidator
13
+ def validate_each(subject, attribute, value)
14
+ return if value.is_a?(Time)
15
+
16
+ Time.deserialize_from_json(value)
17
+ rescue StandardError
18
+ subject.errors.add attribute, :invalid_time
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
@@ -16,6 +16,7 @@ module Sequent
16
16
  ::Integer => ->(value) { valid_integer?(value) },
17
17
  ::Boolean => ->(value) { valid_bool?(value) },
18
18
  ::Date => ->(value) { valid_date?(value) },
19
+ ::Time => ->(value) { valid_time?(value) },
19
20
  ::DateTime => ->(value) { valid_date_time?(value) },
20
21
  }.freeze
21
22
 
@@ -53,6 +54,16 @@ module Sequent
53
54
  end
54
55
  end
55
56
 
57
+ def self.valid_time?(value)
58
+ return true if value.blank?
59
+
60
+ begin
61
+ value.is_a?(Time) || !!Time.iso8601(value.dup)
62
+ rescue StandardError
63
+ false
64
+ end
65
+ end
66
+
56
67
  def self.valid_string?(value)
57
68
  return true if value.nil?
58
69
 
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sequent
4
+ module Core
5
+ module Middleware
6
+ class Chain
7
+ attr_reader :entries
8
+
9
+ def initialize
10
+ clear
11
+ end
12
+
13
+ def add(middleware)
14
+ @entries.push(middleware)
15
+ end
16
+
17
+ def clear
18
+ @entries = []
19
+ end
20
+
21
+ def invoke(*args, &invoker)
22
+ chain = @entries.dup
23
+
24
+ traverse_chain = -> do
25
+ if chain.empty?
26
+ invoker.call
27
+ else
28
+ chain.shift.call(*args, &traverse_chain)
29
+ end
30
+ end
31
+
32
+ traverse_chain.call
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,3 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'chain'
@@ -86,17 +86,13 @@ module Sequent
86
86
  extend Forwardable
87
87
  include Helpers::MessageHandler
88
88
  include Migratable
89
+ extend ActiveSupport::DescendantsTracker
89
90
 
90
91
  def initialize(persistor = Sequent::Core::Persistors::ActiveRecordPersistor.new)
91
92
  ensure_valid!
92
93
  @persistor = persistor
93
94
  end
94
95
 
95
- def self.inherited(subclass)
96
- super
97
- Projectors << subclass
98
- end
99
-
100
96
  def self.replay_persistor
101
97
  nil
102
98
  end
@@ -132,22 +128,18 @@ module Sequent
132
128
  end
133
129
 
134
130
  #
135
- # Utility class containing all subclasses of Projector
131
+ # Utility class containing all subclasses of Projector.
136
132
  #
137
133
  class Projectors
138
134
  class << self
139
135
  def projectors
140
- @projectors ||= []
136
+ Sequent::Projector.descendants
141
137
  end
142
138
 
143
139
  def all
144
140
  projectors
145
141
  end
146
142
 
147
- def <<(projector)
148
- projectors << projector
149
- end
150
-
151
143
  def find(projector_name)
152
144
  projectors.find { |c| c.name == projector_name }
153
145
  end