sequent 4.0.0 → 4.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (89) hide show
  1. checksums.yaml +4 -4
  2. data/bin/sequent +31 -25
  3. data/lib/notices.rb +2 -0
  4. data/lib/sequent/application_record.rb +2 -0
  5. data/lib/sequent/configuration.rb +24 -31
  6. data/lib/sequent/core/aggregate_repository.rb +17 -13
  7. data/lib/sequent/core/aggregate_root.rb +16 -7
  8. data/lib/sequent/core/aggregate_roots.rb +24 -0
  9. data/lib/sequent/core/aggregate_snapshotter.rb +8 -5
  10. data/lib/sequent/core/base_command_handler.rb +4 -2
  11. data/lib/sequent/core/command.rb +17 -9
  12. data/lib/sequent/core/command_record.rb +8 -3
  13. data/lib/sequent/core/command_service.rb +18 -18
  14. data/lib/sequent/core/core.rb +2 -0
  15. data/lib/sequent/core/current_event.rb +2 -0
  16. data/lib/sequent/core/event.rb +16 -11
  17. data/lib/sequent/core/event_publisher.rb +16 -15
  18. data/lib/sequent/core/event_record.rb +7 -7
  19. data/lib/sequent/core/event_store.rb +57 -50
  20. data/lib/sequent/core/ext/ext.rb +9 -1
  21. data/lib/sequent/core/helpers/array_with_type.rb +4 -1
  22. data/lib/sequent/core/helpers/association_validator.rb +9 -7
  23. data/lib/sequent/core/helpers/attribute_support.rb +45 -28
  24. data/lib/sequent/core/helpers/autoset_attributes.rb +4 -4
  25. data/lib/sequent/core/helpers/boolean_validator.rb +6 -1
  26. data/lib/sequent/core/helpers/copyable.rb +2 -2
  27. data/lib/sequent/core/helpers/date_time_validator.rb +4 -1
  28. data/lib/sequent/core/helpers/date_validator.rb +6 -1
  29. data/lib/sequent/core/helpers/default_validators.rb +12 -10
  30. data/lib/sequent/core/helpers/equal_support.rb +8 -6
  31. data/lib/sequent/core/helpers/helpers.rb +2 -0
  32. data/lib/sequent/core/helpers/mergable.rb +6 -5
  33. data/lib/sequent/core/helpers/message_handler.rb +3 -1
  34. data/lib/sequent/core/helpers/param_support.rb +19 -15
  35. data/lib/sequent/core/helpers/secret.rb +14 -12
  36. data/lib/sequent/core/helpers/string_support.rb +5 -4
  37. data/lib/sequent/core/helpers/string_to_value_parsers.rb +7 -2
  38. data/lib/sequent/core/helpers/string_validator.rb +6 -1
  39. data/lib/sequent/core/helpers/type_conversion_support.rb +5 -3
  40. data/lib/sequent/core/helpers/uuid_helper.rb +5 -2
  41. data/lib/sequent/core/helpers/value_validators.rb +23 -9
  42. data/lib/sequent/core/persistors/active_record_persistor.rb +19 -9
  43. data/lib/sequent/core/persistors/persistor.rb +16 -14
  44. data/lib/sequent/core/persistors/persistors.rb +2 -0
  45. data/lib/sequent/core/persistors/replay_optimized_postgres_persistor.rb +70 -47
  46. data/lib/sequent/core/projector.rb +25 -22
  47. data/lib/sequent/core/random_uuid_generator.rb +2 -0
  48. data/lib/sequent/core/sequent_oj.rb +2 -0
  49. data/lib/sequent/core/stream_record.rb +9 -3
  50. data/lib/sequent/core/transactions/active_record_transaction_provider.rb +5 -9
  51. data/lib/sequent/core/transactions/no_transactions.rb +2 -1
  52. data/lib/sequent/core/transactions/transactions.rb +2 -0
  53. data/lib/sequent/core/value_object.rb +8 -10
  54. data/lib/sequent/core/workflow.rb +7 -5
  55. data/lib/sequent/generator/aggregate.rb +16 -10
  56. data/lib/sequent/generator/command.rb +26 -19
  57. data/lib/sequent/generator/event.rb +19 -17
  58. data/lib/sequent/generator/generator.rb +2 -0
  59. data/lib/sequent/generator/project.rb +2 -0
  60. data/lib/sequent/generator/template_project/Gemfile +1 -1
  61. data/lib/sequent/generator.rb +2 -0
  62. data/lib/sequent/migrations/executor.rb +22 -13
  63. data/lib/sequent/migrations/functions.rb +5 -6
  64. data/lib/sequent/migrations/migrate_events.rb +12 -9
  65. data/lib/sequent/migrations/migrations.rb +2 -1
  66. data/lib/sequent/migrations/planner.rb +33 -23
  67. data/lib/sequent/migrations/projectors.rb +4 -3
  68. data/lib/sequent/migrations/sql.rb +2 -0
  69. data/lib/sequent/migrations/view_schema.rb +84 -45
  70. data/lib/sequent/rake/migration_tasks.rb +58 -22
  71. data/lib/sequent/rake/tasks.rb +5 -2
  72. data/lib/sequent/sequent.rb +2 -0
  73. data/lib/sequent/support/database.rb +30 -15
  74. data/lib/sequent/support/view_projection.rb +6 -3
  75. data/lib/sequent/support/view_schema.rb +2 -0
  76. data/lib/sequent/support.rb +2 -0
  77. data/lib/sequent/test/command_handler_helpers.rb +35 -17
  78. data/lib/sequent/test/event_handler_helpers.rb +10 -4
  79. data/lib/sequent/test/event_stream_helpers.rb +7 -3
  80. data/lib/sequent/test/time_comparison.rb +12 -5
  81. data/lib/sequent/test.rb +2 -0
  82. data/lib/sequent/util/dry_run.rb +11 -8
  83. data/lib/sequent/util/printer.rb +6 -5
  84. data/lib/sequent/util/skip_if_already_processing.rb +3 -1
  85. data/lib/sequent/util/timer.rb +2 -0
  86. data/lib/sequent/util/util.rb +2 -0
  87. data/lib/sequent.rb +2 -0
  88. data/lib/version.rb +3 -1
  89. metadata +81 -66
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'active_model'
2
4
  require_relative 'helpers/string_support'
3
5
  require_relative 'helpers/equal_support'
@@ -7,16 +9,17 @@ require_relative 'helpers/copyable'
7
9
  module Sequent
8
10
  module Core
9
11
  class Event
10
- include Sequent::Core::Helpers::StringSupport,
11
- Sequent::Core::Helpers::EqualSupport,
12
- Sequent::Core::Helpers::AttributeSupport,
13
- Sequent::Core::Helpers::Copyable
12
+ include Sequent::Core::Helpers::Copyable
13
+ include Sequent::Core::Helpers::AttributeSupport
14
+ include Sequent::Core::Helpers::EqualSupport
15
+ include Sequent::Core::Helpers::StringSupport
14
16
  attrs aggregate_id: String, sequence_number: Integer, created_at: DateTime
15
17
 
16
18
  def initialize(args = {})
17
19
  update_all_attributes args
18
- raise "Missing aggregate_id" unless @aggregate_id
19
- raise "Missing sequence_number" unless @sequence_number
20
+ fail 'Missing aggregate_id' unless @aggregate_id
21
+ fail 'Missing sequence_number' unless @sequence_number
22
+
20
23
  @created_at ||= DateTime.now
21
24
  end
22
25
 
@@ -24,22 +27,24 @@ module Sequent
24
27
  result = {}
25
28
  instance_variables
26
29
  .reject { |k| payload_variables.include?(k) }
27
- .select { |k| self.class.types.keys.include?(to_attribute_name(k))}
30
+ .select { |k| self.class.types.keys.include?(to_attribute_name(k)) }
28
31
  .each do |k|
29
- result[k.to_s[1 .. -1].to_sym] = instance_variable_get(k)
32
+ result[k.to_s[1..-1].to_sym] = instance_variable_get(k)
30
33
  end
31
34
  result
32
35
  end
36
+
33
37
  protected
38
+
34
39
  def payload_variables
35
- %i{@aggregate_id @sequence_number @created_at}
40
+ %i[@aggregate_id @sequence_number @created_at]
36
41
  end
37
42
 
38
43
  private
44
+
39
45
  def to_attribute_name(instance_variable_name)
40
- instance_variable_name[1 .. -1].to_sym
46
+ instance_variable_name[1..-1].to_sym
41
47
  end
42
-
43
48
  end
44
49
 
45
50
  class SnapshotEvent < Event
@@ -1,17 +1,23 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Sequent
2
4
  module Core
3
5
  #
4
- # EventPublisher ensures that, for every thread, events will be published in the order in which they are queued for publishing.
6
+ # EventPublisher ensures that, for every thread, events will be published
7
+ # in the order in which they are queued for publishing.
5
8
  #
6
- # This potentially introduces a wrinkle into your plans: You therefore should not split a "unit of work" across multiple threads.
9
+ # This potentially introduces a wrinkle into your plans:
10
+ # You therefore should not split a "unit of work" across multiple threads.
7
11
  #
8
- # If you want other behaviour, you are free to implement your own version of EventPublisher and configure Sequent to use it.
12
+ # If you want other behaviour, you are free to implement your own version of EventPublisher
13
+ # and configure Sequent to use it.
9
14
  #
10
15
  class EventPublisher
11
16
  class PublishEventError < RuntimeError
12
17
  attr_reader :event_handler_class, :event
13
18
 
14
19
  def initialize(event_handler_class, event)
20
+ super()
15
21
  @event_handler_class = event_handler_class
16
22
  @event = event
17
23
  end
@@ -23,6 +29,7 @@ module Sequent
23
29
 
24
30
  def publish_events(events)
25
31
  return if configuration.disable_event_handlers
32
+
26
33
  events.each { |event| events_queue.push(event) }
27
34
  process_events
28
35
  end
@@ -35,13 +42,9 @@ module Sequent
35
42
 
36
43
  def process_events
37
44
  Sequent::Util.skip_if_already_processing(:events_queue_lock) do
38
- begin
39
- while(!events_queue.empty?) do
40
- process_event(events_queue.pop)
41
- end
42
- ensure
43
- events_queue.clear
44
- end
45
+ process_event(events_queue.pop) until events_queue.empty?
46
+ ensure
47
+ events_queue.clear
45
48
  end
46
49
  end
47
50
 
@@ -51,11 +54,9 @@ module Sequent
51
54
  Sequent.logger.debug("[EventPublisher] Publishing event #{event.class}")
52
55
 
53
56
  configuration.event_handlers.each do |handler|
54
- begin
55
- handler.handle_message event
56
- rescue
57
- raise PublishEventError.new(handler.class, event)
58
- end
57
+ handler.handle_message event
58
+ rescue StandardError
59
+ raise PublishEventError.new(handler.class, event)
59
60
  end
60
61
  end
61
62
 
@@ -1,10 +1,11 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'active_record'
2
4
  require_relative 'sequent_oj'
3
- require_relative '../application_record.rb'
5
+ require_relative '../application_record'
4
6
 
5
7
  module Sequent
6
8
  module Core
7
-
8
9
  # == Event Record Hooks
9
10
  #
10
11
  # These hooks are called during the life cycle of
@@ -27,7 +28,6 @@ module Sequent
27
28
  # end
28
29
  # end
29
30
  class EventRecordHooks
30
-
31
31
  # Called after assigning Sequent's event attributes to the +event_record+.
32
32
  #
33
33
  # *Params*
@@ -42,13 +42,12 @@ module Sequent
42
42
  def self.after_serialization(event_record, event)
43
43
  # noop
44
44
  end
45
-
46
45
  end
47
46
 
48
47
  module SerializesEvent
49
48
  def event
50
- payload = Sequent::Core::Oj.strict_load(self.event_json)
51
- Class.const_get(self.event_type).deserialize_from_json(payload)
49
+ payload = Sequent::Core::Oj.strict_load(event_json)
50
+ Class.const_get(event_type).deserialize_from_json(payload)
52
51
  end
53
52
 
54
53
  def event=(event)
@@ -76,7 +75,7 @@ module Sequent
76
75
  class EventRecord < Sequent::ApplicationRecord
77
76
  include SerializesEvent
78
77
 
79
- self.table_name = "event_records"
78
+ self.table_name = 'event_records'
80
79
 
81
80
  belongs_to :stream_record
82
81
  belongs_to :command_record
@@ -98,6 +97,7 @@ module Sequent
98
97
 
99
98
  def find_origin(record)
100
99
  return find_origin(record.parent) if record.parent.present?
100
+
101
101
  record
102
102
  end
103
103
  end
@@ -1,10 +1,11 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'forwardable'
2
4
  require_relative 'event_record'
3
5
  require_relative 'sequent_oj'
4
6
 
5
7
  module Sequent
6
8
  module Core
7
-
8
9
  class EventStore
9
10
  include ActiveRecord::ConnectionAdapters::Quoting
10
11
  extend Forwardable
@@ -16,13 +17,13 @@ module Sequent
16
17
  attr_reader :event_hash
17
18
 
18
19
  def initialize(event_hash)
20
+ super()
19
21
  @event_hash = event_hash
20
22
  end
21
23
 
22
24
  def message
23
25
  "Event hash: #{event_hash.inspect}\nCause: #{cause.inspect}"
24
26
  end
25
-
26
27
  end
27
28
 
28
29
  def initialize
@@ -41,7 +42,7 @@ module Sequent
41
42
  # `StreamRecord` to arrays ordered uncommitted `Event`s.
42
43
  #
43
44
  def commit_events(command, streams_with_events)
44
- fail ArgumentError, "command is required" if command.nil?
45
+ fail ArgumentError, 'command is required' if command.nil?
45
46
 
46
47
  Sequent.logger.debug("[EventStore] Committing events for command #{command.class}")
47
48
 
@@ -61,27 +62,36 @@ module Sequent
61
62
 
62
63
  streams = Sequent.configuration.stream_record_class.where(aggregate_id: aggregate_ids)
63
64
 
64
- query = aggregate_ids.uniq.map { |aggregate_id| aggregate_query(aggregate_id) }.join(" UNION ALL ")
65
- events = Sequent.configuration.event_record_class.connection.select_all(query).map! do |event_hash|
65
+ query = aggregate_ids.uniq.map { |aggregate_id| aggregate_query(aggregate_id) }.join(' UNION ALL ')
66
+ events = Sequent.configuration.event_record_class.connection.select_all(query).map do |event_hash|
66
67
  deserialize_event(event_hash)
67
68
  end
68
69
 
69
70
  events
70
- .group_by { |event| event.aggregate_id }
71
- .map { |aggregate_id, _events| [streams.find { |stream_record| stream_record.aggregate_id == aggregate_id }.event_stream, _events] }
71
+ .group_by(&:aggregate_id)
72
+ .map do |aggregate_id, es|
73
+ [
74
+ streams.find do |stream_record|
75
+ stream_record.aggregate_id == aggregate_id
76
+ end.event_stream,
77
+ es,
78
+ ]
79
+ end
72
80
  end
73
81
 
74
82
  def aggregate_query(aggregate_id)
75
- %Q{(
76
- SELECT event_type, event_json
77
- FROM #{quote_table_name Sequent.configuration.event_record_class.table_name} AS o
78
- WHERE aggregate_id = #{quote(aggregate_id)}
79
- AND sequence_number >= COALESCE((SELECT MAX(sequence_number)
80
- FROM #{quote_table_name Sequent.configuration.event_record_class.table_name} AS i
81
- WHERE event_type = #{quote Sequent.configuration.snapshot_event_class.name}
82
- AND i.aggregate_id = #{quote(aggregate_id)}), 0)
83
- ORDER BY sequence_number ASC, (CASE event_type WHEN #{quote Sequent.configuration.snapshot_event_class.name} THEN 0 ELSE 1 END) ASC
84
- )}
83
+ <<~SQL.chomp
84
+ (
85
+ SELECT event_type, event_json
86
+ FROM #{quote_table_name Sequent.configuration.event_record_class.table_name} AS o
87
+ WHERE aggregate_id = #{quote(aggregate_id)}
88
+ AND sequence_number >= COALESCE((SELECT MAX(sequence_number)
89
+ FROM #{quote_table_name Sequent.configuration.event_record_class.table_name} AS i
90
+ WHERE event_type = #{quote Sequent.configuration.snapshot_event_class.name}
91
+ AND i.aggregate_id = #{quote(aggregate_id)}), 0)
92
+ ORDER BY sequence_number ASC, (CASE event_type WHEN #{quote Sequent.configuration.snapshot_event_class.name} THEN 0 ELSE 1 END) ASC
93
+ )
94
+ SQL
85
95
  end
86
96
 
87
97
  def stream_exists?(aggregate_id)
@@ -97,7 +107,7 @@ ORDER BY sequence_number ASC, (CASE event_type WHEN #{quote Sequent.configuratio
97
107
  # @param block that returns the events.
98
108
  # <b>DEPRECATED:</b> use <tt>replay_events_from_cursor</tt> instead.
99
109
  def replay_events
100
- warn "[DEPRECATION] `replay_events` is deprecated in favor of `replay_events_from_cursor`"
110
+ warn '[DEPRECATION] `replay_events` is deprecated in favor of `replay_events_from_cursor`'
101
111
  events = yield.map { |event_hash| deserialize_event(event_hash) }
102
112
  publish_events(events)
103
113
  end
@@ -109,8 +119,7 @@ ORDER BY sequence_number ASC, (CASE event_type WHEN #{quote Sequent.configuratio
109
119
  #
110
120
  # @param get_events lambda that returns the events cursor
111
121
  # @param on_progress lambda that gets called on substantial progress
112
- def replay_events_from_cursor(block_size: 2000,
113
- get_events:,
122
+ def replay_events_from_cursor(get_events:, block_size: 2000,
114
123
  on_progress: PRINT_PROGRESS)
115
124
  progress = 0
116
125
  cursor = get_events.call
@@ -128,7 +137,7 @@ ORDER BY sequence_number ASC, (CASE event_type WHEN #{quote Sequent.configuratio
128
137
  on_progress[progress, true, ids_replayed]
129
138
  end
130
139
 
131
- PRINT_PROGRESS = lambda do |progress, done, _|
140
+ PRINT_PROGRESS = ->(progress, done, _) do
132
141
  if done
133
142
  Sequent.logger.debug "Done replaying #{progress} events"
134
143
  else
@@ -142,38 +151,34 @@ ORDER BY sequence_number ASC, (CASE event_type WHEN #{quote Sequent.configuratio
142
151
  def aggregates_that_need_snapshots(last_aggregate_id, limit = 10)
143
152
  stream_table = quote_table_name Sequent.configuration.stream_record_class.table_name
144
153
  event_table = quote_table_name Sequent.configuration.event_record_class.table_name
145
- query = %Q{
146
- SELECT aggregate_id
147
- FROM #{stream_table} stream
148
- WHERE aggregate_id::varchar > COALESCE(#{quote last_aggregate_id}, '')
149
- AND snapshot_threshold IS NOT NULL
150
- AND snapshot_threshold <= (
151
- (SELECT MAX(events.sequence_number) FROM #{event_table} events WHERE events.event_type <> #{quote Sequent.configuration.snapshot_event_class.name} AND stream.aggregate_id = events.aggregate_id) -
152
- COALESCE((SELECT MAX(snapshots.sequence_number) FROM #{event_table} snapshots WHERE snapshots.event_type = #{quote Sequent.configuration.snapshot_event_class.name} AND stream.aggregate_id = snapshots.aggregate_id), 0))
153
- ORDER BY aggregate_id
154
- LIMIT #{quote limit}
155
- FOR UPDATE
156
- }
154
+ query = <<~SQL.chomp
155
+ SELECT aggregate_id
156
+ FROM #{stream_table} stream
157
+ WHERE aggregate_id::varchar > COALESCE(#{quote last_aggregate_id}, '')
158
+ AND snapshot_threshold IS NOT NULL
159
+ AND snapshot_threshold <= (
160
+ (SELECT MAX(events.sequence_number) FROM #{event_table} events WHERE events.event_type <> #{quote Sequent.configuration.snapshot_event_class.name} AND stream.aggregate_id = events.aggregate_id) -
161
+ COALESCE((SELECT MAX(snapshots.sequence_number) FROM #{event_table} snapshots WHERE snapshots.event_type = #{quote Sequent.configuration.snapshot_event_class.name} AND stream.aggregate_id = snapshots.aggregate_id), 0))
162
+ ORDER BY aggregate_id
163
+ LIMIT #{quote limit}
164
+ FOR UPDATE
165
+ SQL
157
166
  Sequent.configuration.event_record_class.connection.select_all(query).map { |x| x['aggregate_id'] }
158
167
  end
159
168
 
160
169
  def find_event_stream(aggregate_id)
161
170
  record = Sequent.configuration.stream_record_class.where(aggregate_id: aggregate_id).first
162
- if record
163
- record.event_stream
164
- else
165
- nil
166
- end
171
+ record&.event_stream
167
172
  end
168
173
 
169
174
  private
170
175
 
171
176
  def column_names
172
177
  @column_names ||= Sequent
173
- .configuration
174
- .event_record_class
175
- .column_names
176
- .reject { |c| c == primary_key_event_records }
178
+ .configuration
179
+ .event_record_class
180
+ .column_names
181
+ .reject { |c| c == primary_key_event_records }
177
182
  end
178
183
 
179
184
  def primary_key_event_records
@@ -181,11 +186,11 @@ SELECT aggregate_id
181
186
  end
182
187
 
183
188
  def deserialize_event(event_hash)
184
- event_type = event_hash.fetch("event_type")
185
- event_json = Sequent::Core::Oj.strict_load(event_hash.fetch("event_json"))
189
+ event_type = event_hash.fetch('event_type')
190
+ event_json = Sequent::Core::Oj.strict_load(event_hash.fetch('event_json'))
186
191
  resolve_event_type(event_type).deserialize_from_json(event_json)
187
- rescue
188
- raise DeserializeEventError.new(event_hash)
192
+ rescue StandardError
193
+ raise DeserializeEventError, event_hash
189
194
  end
190
195
 
191
196
  def resolve_event_type(event_type)
@@ -215,13 +220,15 @@ SELECT aggregate_id
215
220
  end
216
221
  connection = Sequent.configuration.event_record_class.connection
217
222
  values = event_records
218
- .map { |r| "(#{column_names.map { |c| connection.quote(r[c.to_sym]) }.join(',')})" }
219
- .join(',')
223
+ .map { |r| "(#{column_names.map { |c| connection.quote(r[c.to_sym]) }.join(',')})" }
224
+ .join(',')
220
225
  columns = column_names.map { |c| connection.quote_column_name(c) }.join(',')
221
- sql = %Q{insert into #{connection.quote_table_name(Sequent.configuration.event_record_class.table_name)} (#{columns}) values #{values}}
226
+ sql = <<~SQL.chomp
227
+ insert into #{connection.quote_table_name(Sequent.configuration.event_record_class.table_name)} (#{columns}) values #{values}
228
+ SQL
222
229
  Sequent.configuration.event_record_class.connection.insert(sql, nil, primary_key_event_records)
223
230
  rescue ActiveRecord::RecordNotUnique
224
- fail OptimisticLockingError.new
231
+ raise OptimisticLockingError
225
232
  end
226
233
  end
227
234
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  class Symbol
2
4
  def self.deserialize_from_json(value)
3
5
  value.blank? ? nil : value.try(:to_sym)
@@ -25,19 +27,25 @@ end
25
27
  class BigDecimal
26
28
  def self.deserialize_from_json(value)
27
29
  return nil if value.nil?
30
+
28
31
  BigDecimal(value)
29
32
  end
30
33
  end
31
34
 
32
35
  module Boolean
33
36
  def self.deserialize_from_json(value)
34
- value.nil? ? nil : (value.present? ? value : false)
37
+ if value.nil?
38
+ nil
39
+ else
40
+ (value.present? ? value : false)
41
+ end
35
42
  end
36
43
  end
37
44
 
38
45
  class Date
39
46
  def self.from_params(value)
40
47
  return value if value.is_a?(Date)
48
+
41
49
  value.blank? ? nil : Date.iso8601(value.dup)
42
50
  rescue ArgumentError
43
51
  value
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Sequent
2
4
  module Core
3
5
  module Helpers
@@ -5,7 +7,8 @@ module Sequent
5
7
  attr_accessor :item_type
6
8
 
7
9
  def initialize(item_type)
8
- raise "needs a item_type" unless item_type
10
+ fail 'needs a item_type' unless item_type
11
+
9
12
  @item_type = item_type
10
13
  end
11
14
 
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'active_model/validator'
2
4
 
3
5
  module Sequent
@@ -21,24 +23,23 @@ module Sequent
21
23
  # validates_with Sequent::Core::AssociationValidator, associations: [:trainee]
22
24
  #
23
25
  class AssociationValidator < ActiveModel::Validator
24
-
25
26
  def initialize(options = {})
26
27
  super
27
- raise "Must provide ':associations' to validate" unless options[:associations].present?
28
+ fail "Must provide ':associations' to validate" unless options[:associations].present?
28
29
  end
29
30
 
30
31
  def validate(record)
31
32
  associations = options[:associations]
32
33
  associations = [associations] unless associations.instance_of?(Array)
33
34
  associations.each do |association|
34
- value = record.instance_variable_get("@#{association.to_s}")
35
+ value = record.instance_variable_get("@#{association}")
35
36
  if value && incorrect_type?(value, record, association)
36
37
  record.errors.add(association, "is not of type #{describe_type(record.class.types[association])}")
37
- elsif value && value.is_a?(Array)
38
+ elsif value&.is_a?(Array)
38
39
  item_type = record.class.types.fetch(association).item_type
39
- record.errors.add(association, "is invalid") unless validate_all(value, item_type).all?
40
- else
41
- record.errors.add(association, "is invalid") if value && value.invalid?
40
+ record.errors.add(association, 'is invalid') unless validate_all(value, item_type).all?
41
+ elsif value&.invalid?
42
+ record.errors.add(association, 'is invalid')
42
43
  end
43
44
  end
44
45
  end
@@ -47,6 +48,7 @@ module Sequent
47
48
 
48
49
  def incorrect_type?(value, record, association)
49
50
  return unless record.class.respond_to?(:types)
51
+
50
52
  type = record.class.types[association]
51
53
  if type.respond_to?(:candidate?)
52
54
  !type.candidate?(value)
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'active_support'
2
4
  require_relative '../ext/ext'
3
5
  require_relative 'array_with_type'
@@ -38,18 +40,15 @@ module Sequent
38
40
 
39
41
  # module containing class methods to be added
40
42
  module ClassMethods
41
-
42
43
  def types
43
44
  @types ||= {}
44
- if @merged_types
45
- @merged_types
46
- else
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
+ return @merged_types if @merged_types
46
+
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)
52
50
  end
51
+ @merged_types
53
52
  end
54
53
 
55
54
  def attrs(args)
@@ -58,14 +57,15 @@ module Sequent
58
57
  associations = []
59
58
  args.each do |attribute, type|
60
59
  attr_accessor attribute
60
+
61
61
  if included_modules.include?(Sequent::Core::Helpers::TypeConversionSupport)
62
62
  Sequent::Core::Helpers::DefaultValidators.for(type).add_validations_for(self, attribute)
63
63
  end
64
64
 
65
- if type.class == Sequent::Core::Helpers::ArrayWithType
65
+ if type.instance_of?(Sequent::Core::Helpers::ArrayWithType)
66
66
  associations << attribute
67
67
  elsif included_modules.include?(ActiveModel::Validations) &&
68
- type.included_modules.include?(Sequent::Core::Helpers::AttributeSupport)
68
+ type.included_modules.include?(Sequent::Core::Helpers::AttributeSupport)
69
69
  associations << attribute
70
70
  end
71
71
  end
@@ -77,9 +77,9 @@ module Sequent
77
77
  def update_all_attributes(attrs)
78
78
  super if defined?(super)
79
79
  ensure_known_attributes(attrs)
80
- #{@types.map { |attribute, _|
81
- "@#{attribute} = attrs[:#{attribute}]"
82
- }.join("\n ")}
80
+ #{@types.map do |attribute, _|
81
+ "@#{attribute} = attrs[:#{attribute}]"
82
+ end.join("\n ")}
83
83
  self
84
84
  end
85
85
  EOS
@@ -87,9 +87,9 @@ EOS
87
87
  class_eval <<EOS
88
88
  def update_all_attributes_from_json(attrs)
89
89
  super if defined?(super)
90
- #{@types.map { |attribute, type|
91
- "@#{attribute} = #{type}.deserialize_from_json(attrs['#{attribute}'])"
92
- }.join("\n ")}
90
+ #{@types.map do |attribute, type|
91
+ "@#{attribute} = #{type}.deserialize_from_json(attrs['#{attribute}'])"
92
+ end.join("\n ")}
93
93
  end
94
94
  EOS
95
95
  end
@@ -106,17 +106,33 @@ EOS
106
106
 
107
107
  def deserialize_from_json(args)
108
108
  unless args.nil?
109
- obj = allocate()
109
+ obj = allocate
110
+
111
+ upcast!(args)
112
+
110
113
  obj.update_all_attributes_from_json(args)
111
114
  obj
112
115
  end
113
116
  end
114
117
 
115
-
116
118
  def numeric?(object)
117
- true if Float(object) rescue false
119
+ true if Float(object)
120
+ rescue StandardError
121
+ false
118
122
  end
119
123
 
124
+ def upcast(&block)
125
+ @upcasters ||= []
126
+ @upcasters.push(block)
127
+ end
128
+
129
+ def upcast!(hash)
130
+ return if @upcasters.nil?
131
+
132
+ @upcasters.each do |upcaster|
133
+ upcaster.call(hash)
134
+ end
135
+ end
120
136
  end
121
137
 
122
138
  # extend host class with class methods when we're included
@@ -124,11 +140,10 @@ EOS
124
140
  host_class.extend(ClassMethods)
125
141
  end
126
142
 
127
-
128
143
  def attributes
129
144
  hash = HashWithIndifferentAccess.new
130
145
  self.class.types.each do |name, _|
131
- value = self.instance_variable_get("@#{name}")
146
+ value = instance_variable_get("@#{name}")
132
147
  hash[name] = if value.respond_to?(:attributes)
133
148
  value.attributes
134
149
  else
@@ -141,7 +156,7 @@ EOS
141
156
  def as_json(opts = {})
142
157
  hash = HashWithIndifferentAccess.new
143
158
  self.class.types.each do |name, _|
144
- value = self.instance_variable_get("@#{name}")
159
+ value = instance_variable_get("@#{name}")
145
160
  hash[name] = if value.respond_to?(:as_json)
146
161
  value.as_json(opts)
147
162
  else
@@ -158,15 +173,15 @@ EOS
158
173
  def validation_errors(prefix = nil)
159
174
  result = errors.to_hash
160
175
  self.class.types.each do |field|
161
- value = self.instance_variable_get("@#{field[0]}")
176
+ value = instance_variable_get("@#{field[0]}")
162
177
  if value.respond_to? :validation_errors
163
- value.validation_errors.each { |k, v| result["#{field[0].to_s}_#{k.to_s}".to_sym] = v }
164
- elsif field[1].class == ArrayWithType and value.present?
178
+ value.validation_errors.each { |k, v| result["#{field[0]}_#{k}".to_sym] = v }
179
+ elsif field[1].instance_of?(ArrayWithType) && value.present?
165
180
  value
166
181
  .select { |val| val.respond_to?(:validation_errors) }
167
182
  .each_with_index do |val, index|
168
183
  val.validation_errors.each do |k, v|
169
- result["#{field[0].to_s}_#{index}_#{k.to_s}".to_sym] = v
184
+ result["#{field[0]}_#{index}_#{k}".to_sym] = v
170
185
  end
171
186
  end
172
187
  end
@@ -178,7 +193,9 @@ EOS
178
193
  return unless Sequent.configuration.strict_check_attributes_on_apply_events
179
194
 
180
195
  unknowns = attrs.keys.map(&:to_s) - self.class.types.keys.map(&:to_s)
181
- raise UnknownAttributeError.new("#{self.class.name} does not specify attrs: #{unknowns.join(", ")}") if unknowns.any?
196
+ if unknowns.any?
197
+ fail UnknownAttributeError, "#{self.class.name} does not specify attrs: #{unknowns.join(', ')}"
198
+ end
182
199
  end
183
200
  end
184
201
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Sequent
2
4
  module Core
3
5
  module Helpers
@@ -24,8 +26,7 @@ module Sequent
24
26
  #
25
27
  module AutosetAttributes
26
28
  module ClassMethods
27
-
28
- @@autoset_ignore_attributes = %w{aggregate_id sequence_number created_at}
29
+ @@autoset_ignore_attributes = %w[aggregate_id sequence_number created_at]
29
30
 
30
31
  def set_autoset_ignore_attributes(attribute_names)
31
32
  @@autoset_ignore_attributes = attribute_names
@@ -39,7 +40,7 @@ module Sequent
39
40
  event_classes.each do |event_class|
40
41
  on event_class do |event|
41
42
  self.class.event_attribute_keys(event_class).each do |attribute_name|
42
- instance_variable_set(:"@#{attribute_name.to_s}", event.send(attribute_name.to_sym))
43
+ instance_variable_set(:"@#{attribute_name}", event.send(attribute_name.to_sym))
43
44
  end
44
45
  end
45
46
  end
@@ -53,4 +54,3 @@ module Sequent
53
54
  end
54
55
  end
55
56
  end
56
-