sequent 4.3.0 → 6.0.0

Sign up to get free protection for your applications and to get access to all the features.
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,