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
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: feea2aeb3dba28570a36615392e4ecc1037bfdfd1c2d667bcc787113e4c7fdba
4
- data.tar.gz: 79dc60b21109885a56f3f68cf97185afee5692167f54db1934eaa89dc9d030de
3
+ metadata.gz: 24d2ee32c038b179c4386d2df97025af873487c5fe333952e5fedbe5d3ed47d8
4
+ data.tar.gz: 8d0464ac057a6e458ee7f131caba027fbddbd751af5b9d4314907ed248d0734d
5
5
  SHA512:
6
- metadata.gz: bb74d9c6cbb946bab9b00cb2ea73eab80caed4737d12ce2115a6c97348ca0e9452a14280b3d0de9ecba237c201fdd8b2914382751569224c89cae92128d5d3f5
7
- data.tar.gz: 5fe084735eb4df8855b8102e68d027ca828a8dd7540d577b6e0a97616bbd22815432e684f20d48d04ad111db962fe935af4bff50afd8ebc7242f5c15f01b97a7
6
+ metadata.gz: 5b064934ae367d2e57baaa081c0b3f392bb932a1bb12fed00db337a6365f32c0949e5c6c4fe3eae6fa4a1374d931db517cf33efa911fdc265883bb122e8ce6ab
7
+ data.tar.gz: b5570de3a8b9deb24c1006486c37166f6e17d06d23a30d38a9de9b1e0755f1cbf4e57e1a2c1890ff9a68908f2077e90d7f8d1e79240ae3c5943de23f185be6f1
data/bin/sequent CHANGED
@@ -31,7 +31,7 @@ def new_project(args)
31
31
  bundle exec rake sequent:migrate:offline
32
32
 
33
33
  Run the example specs:
34
- RACK_ENV=test bundle exec rake sequent:db:create
34
+ SEQUENT_ENV=test bundle exec rake sequent:db:create
35
35
  bundle exec rspec spec
36
36
 
37
37
  To generate new aggregates use:
data/db/sequent_schema.rb CHANGED
@@ -1,7 +1,7 @@
1
1
  ActiveRecord::Schema.define do
2
2
 
3
3
  create_table "event_records", :force => true do |t|
4
- t.string "aggregate_id", :null => false
4
+ t.uuid "aggregate_id", :null => false
5
5
  t.integer "sequence_number", :null => false
6
6
  t.datetime "created_at", :null => false
7
7
  t.string "event_type", :null => false
@@ -27,7 +27,7 @@ CREATE INDEX snapshot_events ON event_records (aggregate_id, sequence_number DES
27
27
 
28
28
  create_table "command_records", :force => true do |t|
29
29
  t.string "user_id"
30
- t.string "aggregate_id"
30
+ t.uuid "aggregate_id"
31
31
  t.string "command_type", :null => false
32
32
  t.string "event_aggregate_id"
33
33
  t.integer "event_sequence_number"
@@ -40,7 +40,7 @@ CREATE INDEX snapshot_events ON event_records (aggregate_id, sequence_number DES
40
40
  create_table "stream_records", :force => true do |t|
41
41
  t.datetime "created_at", :null => false
42
42
  t.string "aggregate_type", :null => false
43
- t.string "aggregate_id", :null => false
43
+ t.uuid "aggregate_id", :null => false
44
44
  t.integer "snapshot_threshold"
45
45
  end
46
46
 
@@ -32,6 +32,8 @@ module Sequent
32
32
 
33
33
  DEFAULT_ERROR_LOCALE_RESOLVER = -> { I18n.locale || :en }
34
34
 
35
+ DEFAULT_TIME_PRECISION = ActiveSupport::JSON::Encoding.time_precision
36
+
35
37
  attr_accessor :aggregate_repository,
36
38
  :event_store,
37
39
  :command_service,
@@ -43,6 +45,7 @@ module Sequent
43
45
  :event_record_hooks_class,
44
46
  :command_handlers,
45
47
  :command_filters,
48
+ :command_middleware,
46
49
  :event_handlers,
47
50
  :uuid_generator,
48
51
  :disable_event_handlers,
@@ -56,7 +59,11 @@ module Sequent
56
59
  :database_config_directory,
57
60
  :database_schema_directory,
58
61
  :event_store_schema_name,
59
- :strict_check_attributes_on_apply_events
62
+ :strict_check_attributes_on_apply_events,
63
+ :enable_multiple_database_support,
64
+ :primary_database_role,
65
+ :primary_database_key,
66
+ :time_precision
60
67
 
61
68
  attr_reader :migrations_class_name,
62
69
  :versions_table_name,
@@ -78,6 +85,7 @@ module Sequent
78
85
  self.command_handlers = []
79
86
  self.command_filters = []
80
87
  self.event_handlers = []
88
+ self.command_middleware = Sequent::Core::Middleware::Chain.new
81
89
 
82
90
  self.aggregate_repository = Sequent::Core::AggregateRepository.new
83
91
  self.event_store = Sequent::Core::EventStore.new
@@ -107,6 +115,16 @@ module Sequent
107
115
 
108
116
  self.logger = Logger.new(STDOUT).tap { |l| l.level = Logger::INFO }
109
117
  self.error_locale_resolver = DEFAULT_ERROR_LOCALE_RESOLVER
118
+
119
+ self.enable_multiple_database_support = false
120
+ self.primary_database_role = :writing
121
+ self.primary_database_key = :primary
122
+
123
+ self.time_precision = DEFAULT_TIME_PRECISION
124
+ end
125
+
126
+ def can_use_multiple_databases?
127
+ enable_multiple_database_support && ActiveRecord.version > Gem::Version.new('6.1.0')
110
128
  end
111
129
 
112
130
  def replayed_ids_table_name=(table_name)
@@ -39,14 +39,10 @@ module Sequent
39
39
  include Helpers::MessageHandler
40
40
  include Helpers::AutosetAttributes
41
41
  include SnapshotConfiguration
42
+ extend ActiveSupport::DescendantsTracker
42
43
 
43
44
  attr_reader :id, :uncommitted_events, :sequence_number, :event_stream
44
45
 
45
- def self.inherited(subclass)
46
- super
47
- AggregateRoots << subclass
48
- end
49
-
50
46
  def self.load_from_history(stream, events)
51
47
  first, *rest = events
52
48
  if first.is_a? SnapshotEvent
@@ -127,7 +123,7 @@ module Sequent
127
123
  # Provide subclasses nice DSL to 'apply' events via:
128
124
  #
129
125
  # def send_invoice
130
- # apply InvoiceSentEvent, send_date: DateTime.now
126
+ # apply InvoiceSentEvent, send_date: Time.now
131
127
  # end
132
128
  #
133
129
  def apply(event, params = {})
@@ -3,21 +3,17 @@
3
3
  module Sequent
4
4
  module Core
5
5
  #
6
- # Utility class containing all subclasses of AggregateRoot
6
+ # Utility class containing all subclasses of AggregateRoot.
7
7
  #
8
8
  class AggregateRoots
9
9
  class << self
10
10
  def aggregate_roots
11
- @aggregate_roots ||= []
11
+ Sequent::AggregateRoot.descendants
12
12
  end
13
13
 
14
14
  def all
15
15
  aggregate_roots
16
16
  end
17
-
18
- def <<(aggregate_root)
19
- aggregate_roots << aggregate_root
20
- end
21
17
  end
22
18
  end
23
19
  end
@@ -28,17 +28,17 @@ module Sequent
28
28
  include ActiveModel::Validations
29
29
  include ActiveModel::Validations::Callbacks
30
30
  include Sequent::Core::Helpers::TypeConversionSupport
31
+ extend ActiveSupport::DescendantsTracker
31
32
 
32
- attrs created_at: DateTime
33
+ attrs created_at: Time
34
+
35
+ define_model_callbacks :initialize, only: :after
33
36
 
34
37
  def initialize(args = {})
35
38
  update_all_attributes args
36
- @created_at = DateTime.now
37
- end
39
+ @created_at = Time.now
38
40
 
39
- def self.inherited(subclass)
40
- super
41
- Commands << subclass
41
+ _run_initialize_callbacks
42
42
  end
43
43
  end
44
44
 
@@ -56,22 +56,18 @@ module Sequent
56
56
  end
57
57
 
58
58
  #
59
- # Utility class containing all subclasses of BaseCommand
59
+ # Utility class containing all subclasses of BaseCommand.
60
60
  #
61
61
  class Commands
62
62
  class << self
63
63
  def commands
64
- @commands ||= []
64
+ Sequent::Core::BaseCommand.descendants
65
65
  end
66
66
 
67
67
  def all
68
68
  commands
69
69
  end
70
70
 
71
- def <<(command)
72
- commands << command
73
- end
74
-
75
71
  def find(command_name)
76
72
  commands.find { |c| c.name == command_name }
77
73
  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)
@@ -50,7 +50,12 @@ module Sequent
50
50
  def process_commands
51
51
  Sequent::Util.skip_if_already_processing(:command_service_process_commands) do
52
52
  transaction_provider.transactional do
53
- process_command(command_queue.pop) until command_queue.empty?
53
+ until command_queue.empty?
54
+ command = command_queue.pop
55
+ command_middleware.invoke(command) do
56
+ process_command(command)
57
+ end
58
+ end
54
59
  Sequent::Util.done_processing(:command_service_process_commands)
55
60
  end
56
61
  ensure
@@ -66,7 +71,9 @@ module Sequent
66
71
 
67
72
  filters.each { |filter| filter.execute(command) }
68
73
 
69
- fail CommandNotValid, command unless command.valid?
74
+ I18n.with_locale(Sequent.configuration.error_locale_resolver.call) do
75
+ fail CommandNotValid, command unless command.valid?
76
+ end
70
77
 
71
78
  parsed_command = command.parse_attrs_to_correct_types
72
79
  command_handlers.select do |h|
@@ -98,6 +105,10 @@ module Sequent
98
105
  def command_handlers
99
106
  Sequent.configuration.command_handlers
100
107
  end
108
+
109
+ def command_middleware
110
+ Sequent.configuration.command_middleware
111
+ end
101
112
  end
102
113
 
103
114
  # Raised when BaseCommand.valid? returns false
@@ -4,6 +4,7 @@ require_relative 'sequent_oj'
4
4
  require_relative 'helpers/helpers'
5
5
  require_relative 'persistors/persistors'
6
6
  require_relative 'transactions/transactions'
7
+ require_relative 'middleware/middleware'
7
8
  require_relative 'event'
8
9
  require_relative 'aggregate_repository'
9
10
  require_relative 'aggregate_root'
@@ -13,14 +13,14 @@ module Sequent
13
13
  include Sequent::Core::Helpers::AttributeSupport
14
14
  include Sequent::Core::Helpers::EqualSupport
15
15
  include Sequent::Core::Helpers::StringSupport
16
- attrs aggregate_id: String, sequence_number: Integer, created_at: DateTime
16
+ attrs aggregate_id: String, sequence_number: Integer, created_at: Time
17
17
 
18
18
  def initialize(args = {})
19
19
  update_all_attributes args
20
20
  fail 'Missing aggregate_id' unless @aggregate_id
21
21
  fail 'Missing sequence_number' unless @sequence_number
22
22
 
23
- @created_at ||= DateTime.now
23
+ @created_at ||= Time.now
24
24
  end
25
25
 
26
26
  def payload
@@ -26,8 +26,21 @@ module Sequent
26
26
  end
27
27
  end
28
28
 
29
- def initialize
30
- @event_types = ThreadSafe::Cache.new
29
+ ##
30
+ # Disables event type caching (ie. for in development).
31
+ #
32
+ class NoEventTypesCache
33
+ def fetch_or_store(event_type)
34
+ yield(event_type)
35
+ end
36
+ end
37
+
38
+ def initialize(cache_event_types: true)
39
+ @event_types = if cache_event_types
40
+ ThreadSafe::Cache.new
41
+ else
42
+ NoEventTypesCache.new
43
+ end
31
44
  end
32
45
 
33
46
  ##
@@ -68,6 +68,23 @@ class DateTime
68
68
  end
69
69
  end
70
70
 
71
+ class Time
72
+ def self.from_params(value)
73
+ value.blank? ? nil : Time.iso8601(value.dup)
74
+ rescue ArgumentError
75
+ value
76
+ end
77
+
78
+ def self.deserialize_from_json(value)
79
+ value.blank? ? nil : Time.iso8601(value.dup)
80
+ rescue ArgumentError => e
81
+ return Time.parse(value.dup) if e.message =~ /invalid xmlschema format/ # ruby >= 3
82
+ return Time.parse(value.dup) if e.message =~ /invalid date:/ # ruby 2.7
83
+
84
+ raise
85
+ end
86
+ end
87
+
71
88
  class Array
72
89
  def self.deserialize_from_json(value)
73
90
  value
@@ -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
+ )
@@ -6,6 +6,7 @@ require_relative 'array_with_type'
6
6
  require_relative 'default_validators'
7
7
  require_relative 'type_conversion_support'
8
8
  require_relative 'date_time_validator'
9
+ require_relative 'time_validator'
9
10
  require_relative 'association_validator'
10
11
 
11
12
  module Sequent
@@ -40,19 +41,19 @@ module Sequent
40
41
 
41
42
  # module containing class methods to be added
42
43
  module ClassMethods
43
- def types
44
- @types ||= {}
45
- return @merged_types if @merged_types
44
+ attr_reader :types
46
45
 
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
46
+ # Called when this module is included or when a class which includes this module is inherited from.
47
+ #
48
+ # All declared attrs are merged into @types in order to prevent superfluous calculation of types in a class
49
+ # hierarchy.
50
+ def initialize_types
51
+ @types = inherited_types
52
52
  end
53
53
 
54
54
  def attrs(args)
55
- @types ||= {}
55
+ validate_attrs!(args)
56
+
56
57
  @types.merge!(args)
57
58
  associations = []
58
59
  args.each do |attribute, type|
@@ -126,6 +127,18 @@ EOS
126
127
  @upcasters.push(block)
127
128
  end
128
129
 
130
+ private
131
+
132
+ def inherited_types
133
+ merged_types = is_a?(Class) && superclass.respond_to?(:types) ? superclass.types.dup : {}
134
+
135
+ included_modules
136
+ .select { |m| m.include? Sequent::Core::Helpers::AttributeSupport }
137
+ .reduce(merged_types) do |memo, mod|
138
+ memo.merge(mod.types)
139
+ end
140
+ end
141
+
129
142
  def upcast!(hash)
130
143
  return if @upcasters.nil?
131
144
 
@@ -133,11 +146,24 @@ EOS
133
146
  upcaster.call(hash)
134
147
  end
135
148
  end
149
+
150
+ def validate_attrs!(args)
151
+ duplicate_attrs = types.keys & args.keys
152
+
153
+ fail ArgumentError, "Attributes already defined: #{duplicate_attrs.join(', ')}" if duplicate_attrs.any?
154
+ end
155
+
156
+ def inherited(subclass)
157
+ super
158
+
159
+ subclass.initialize_types
160
+ end
136
161
  end
137
162
 
138
163
  # extend host class with class methods when we're included
139
164
  def self.included(host_class)
140
165
  host_class.extend(ClassMethods)
166
+ host_class.initialize_types
141
167
  end
142
168
 
143
169
  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
@@ -17,6 +17,9 @@ module Sequent
17
17
  Date => ->(klass, field) do
18
18
  klass.validates field, 'sequent::Core::Helpers::Date' => true
19
19
  end,
20
+ Time => ->(klass, field) do
21
+ klass.validates field, 'sequent::Core::Helpers::Time' => true
22
+ end,
20
23
  DateTime => ->(klass, field) do
21
24
  klass.validates field, 'sequent::Core::Helpers::DateTime' => true
22
25
  end,