eventsimple 1.8.1 → 2.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.
@@ -17,22 +17,22 @@ module Eventsimple
17
17
  case value
18
18
  when String
19
19
  decoded = ActiveSupport::JSON.decode(value)
20
- return event_klass::Message.new(decoded) if event_klass.const_defined?(:Message)
21
-
22
- decoded
20
+ prepare_data(decoded)
23
21
  when Hash
24
- return event_klass::Message.new(value) if event_klass.const_defined?(:Message)
25
-
26
- value
27
- when event_klass::Message
22
+ prepare_data(value)
23
+ when message_class
28
24
  value
29
25
  end
30
26
  end
31
27
 
32
28
  def serialize(value)
33
29
  case value
34
- when Hash, event_klass::Message
35
- ActiveSupport::JSON.encode(value)
30
+ when Hash
31
+ encrypted_data = encrypt_message_data(value)
32
+ ActiveSupport::JSON.encode(encrypted_data)
33
+ when event_klass::Message
34
+ encrypted_data = encrypt_message_data(value.attributes)
35
+ ActiveSupport::JSON.encode(encrypted_data)
36
36
  else
37
37
  super
38
38
  end
@@ -40,9 +40,62 @@ module Eventsimple
40
40
 
41
41
  def deserialize(value)
42
42
  decoded = ActiveSupport::JSON.decode(value)
43
- return event_klass::Message.new(decoded) if event_klass.const_defined?(:Message)
43
+ decrypted_data = decrypt_message_data(decoded)
44
+ message_class ? message_class.new(decrypted_data) : decrypted_data
45
+ end
46
+
47
+ def changed_in_place?(raw_old_value, new_value)
48
+ old_value = deserialize(raw_old_value)
49
+ old_value != new_value
50
+ end
51
+
52
+ def mutable?
53
+ true
54
+ end
55
+
56
+ private
57
+
58
+ def message_class
59
+ event_klass.const_defined?(:Message) ? event_klass::Message : nil
60
+ end
61
+
62
+ def prepare_data(data_hash)
63
+ decrypted_data = decrypt_message_data(data_hash)
64
+ message_class ? message_class.new(decrypted_data) : decrypted_data
65
+ end
66
+
67
+ def encrypt_message_data(data_hash)
68
+ transform_message_data(data_hash, :encrypt)
69
+ end
70
+
71
+ def decrypt_message_data(data_hash)
72
+ transform_message_data(data_hash, :decrypt)
73
+ end
74
+
75
+ def transform_message_data(data_hash, operation)
76
+ return data_hash unless message_class
77
+
78
+ schema = message_class.schema
79
+ result = data_hash.transform_keys(&:to_sym).dup
80
+
81
+ schema.keys.each do |key| # rubocop:disable Style/HashEachMethods
82
+ attr_name = key.name
83
+ type = find_type_with_operation(key.type, operation)
84
+ value = result[attr_name]
85
+
86
+ next if value.nil? || type.nil?
87
+
88
+ result[attr_name] = type.public_send(operation, value.to_s)
89
+ end
90
+
91
+ result
92
+ end
93
+
94
+ def find_type_with_operation(type, operation)
95
+ return type if type.respond_to?(operation)
44
96
 
45
- decoded
97
+ # For Sum types (optional), check if the right side has the operation
98
+ type.respond_to?(:right) && type.right.respond_to?(operation) ? type.right : nil
46
99
  end
47
100
  end
48
101
  end
@@ -51,10 +51,13 @@ module Eventsimple
51
51
 
52
52
  default_scope { order(:created_at) }
53
53
 
54
+ after_initialize :readonly!, if: :persisted?
55
+
54
56
  before_validation :extend_validation
55
- after_validation :perform_transition_checks
57
+ after_validation :perform_transition_checks, on: :create
56
58
  before_create :apply_and_persist
57
59
  after_create :dispatch
60
+ after_create :readonly!
58
61
 
59
62
  include InstanceMethods
60
63
  extend ClassMethods
@@ -73,7 +76,6 @@ module Eventsimple
73
76
  false
74
77
  end
75
78
 
76
- # Apply the event to the aggregate passed in. The default behaviour is a no-op
77
79
  def apply(aggregate); end
78
80
 
79
81
  def can_apply?(_aggregate)
@@ -101,12 +103,10 @@ module Eventsimple
101
103
  self.aggregate = aggregate.extend(validate_form) if validate_form
102
104
  end
103
105
 
104
- # Apply the transformation to the aggregate and save it.
105
106
  def apply_and_persist
106
107
  apply_timestamps(aggregate)
107
108
  apply(aggregate)
108
109
 
109
- # Persist!
110
110
  aggregate.enable_writes!
111
111
  aggregate.save!
112
112
  aggregate.readonly!
@@ -125,6 +125,16 @@ module Eventsimple
125
125
  def aggregate=(aggregate)
126
126
  public_send(:"#{_aggregate_klass.model_name.element}=", aggregate)
127
127
  end
128
+
129
+ def enable_writes!(&block)
130
+ was_readonly = @readonly
131
+ @readonly = false
132
+
133
+ return unless block
134
+
135
+ yield self
136
+ @readonly = was_readonly
137
+ end
128
138
  end
129
139
 
130
140
  module ClassMethods
@@ -169,8 +179,7 @@ module Eventsimple
169
179
 
170
180
  # We want to automatically retry writes on concurrency failures. However events with sync
171
181
  # reactors may have multiple nested events that are written within the same transaction.
172
- # We can only catch and retry writes when they the outermost event encapsulating the whole
173
- # transaction.
182
+ # We can only catch and retry writes when we are in the outermost transaction.
174
183
  def create(*args, &block)
175
184
  with_locks do
176
185
  with_retries(args) { super }
@@ -185,7 +194,7 @@ module Eventsimple
185
194
 
186
195
  def with_locks(&)
187
196
  if _outbox_enabled
188
- base_class.with_advisory_lock(base_class.name, { transaction: true }, &)
197
+ base_class.with_advisory_lock(base_class.name, { transaction: existing_transaction_in_progress? }, &)
189
198
  else
190
199
  yield
191
200
  end
@@ -1,23 +1,193 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Eventsimple
4
- class Message < Dry::Struct
5
- transform_keys(&:to_sym)
4
+ class Message # rubocop:disable Metrics/ClassLength
5
+ class << self
6
+ def attribute(name, type)
7
+ validate_type(type)
8
+ optional = type.respond_to?(:optional?) ? type.optional? : false
9
+ define_attribute(name.to_sym, type, optional: optional, required: true)
10
+ end
11
+
12
+ def attribute?(name, type)
13
+ validate_type(type)
14
+ optional = type.respond_to?(:optional?) ? type.optional? : false
15
+ define_attribute(name.to_sym, type, optional: optional, required: false)
16
+ end
17
+
18
+ def schema
19
+ @schema ||= if superclass.respond_to?(:schema)
20
+ Schema.new(superclass.schema)
21
+ else
22
+ Schema.new
23
+ end
24
+ end
25
+
26
+ private
27
+
28
+ def validate_type(type)
29
+ return if valid_eventsimple_type?(type)
30
+
31
+ deprecator = ActiveSupport::Deprecation.new
32
+ deprecator.behavior = Rails.application.config.active_support.deprecation
33
+
34
+ deprecator.warn(
35
+ "Only Eventsimple::Types are allowed in Message attributes. " \
36
+ "Received: #{type.inspect}. " \
37
+ "Use Eventsimple::Types::String, Eventsimple::Types::Integer, etc.",
38
+ )
39
+ end
40
+
41
+ def valid_eventsimple_type?(type)
42
+ return true if Eventsimple::Types.constants(false).any? { |const_name|
43
+ next if const_name == :BuilderExtension
44
+
45
+ const = Eventsimple::Types.const_get(const_name, false)
46
+ const == type || const.equal?(type)
47
+ }
48
+
49
+ # For Default types, check the underlying type
50
+ if type.respond_to?(:default?) && type.default? && type.respond_to?(:type)
51
+ underlying_type = type.type
52
+ return valid_eventsimple_type?(underlying_type)
53
+ end
54
+
55
+ # For Sum types (optional), check the right side
56
+ if type.respond_to?(:right)
57
+ return valid_eventsimple_type?(type.right)
58
+ end
59
+
60
+ # For EncryptedType, check the wrapped type
61
+ if type.is_a?(Eventsimple::Types::EncryptedType)
62
+ return valid_eventsimple_type?(type.instance_variable_get(:@type))
63
+ end
6
64
 
7
- # dry types will apply default values only on missing keys
8
- # modify the behaviour so the default is used even when the key is present but nil
9
- transform_types do |type|
10
- if type.default?
11
- type.constructor do |value|
12
- value.nil? ? Dry::Types::Undefined : value
65
+ # For Enum types, check the underlying type
66
+ if type.respond_to?(:type) && type.respond_to?(:values)
67
+ return valid_eventsimple_type?(type.type)
68
+ end
69
+
70
+ # Check if the type's primitive matches any base type's primitive
71
+ # This allows Enum, Constrained, and other derived types
72
+ if type.respond_to?(:primitive)
73
+ type_primitive = type.primitive
74
+ return true if Eventsimple::Types.constants(false).any? { |const_name|
75
+ next if const_name == :BuilderExtension
76
+
77
+ base_type = Eventsimple::Types.const_get(const_name, false)
78
+ base_type.respond_to?(:primitive) && base_type.primitive == type_primitive
79
+ }
80
+ end
81
+
82
+ false
83
+ end
84
+
85
+ def define_attribute(name, type, optional:, required:)
86
+ schema.register(name, type, optional: optional, required: required)
87
+ define_method(name) { @attributes[name] }
88
+ define_method("#{name}=") do |value|
89
+ new_value = coerce(name, value, type)
90
+ @attributes[name] = new_value
13
91
  end
14
- else
15
- type
16
92
  end
17
93
  end
18
94
 
95
+ class Schema
96
+ def initialize(parent_schema = nil)
97
+ @definitions = parent_schema ? parent_schema.definitions.dup : {}
98
+ end
99
+
100
+ def register(name, type, optional:, required:)
101
+ @definitions[name] = { type: type, optional: optional, required: required }
102
+ end
103
+
104
+ def keys
105
+ @definitions.map { |name, definition| AttributeKey.new(name, definition[:type]) }
106
+ end
107
+
108
+ def [](name) # rubocop:disable Rails/Delegate
109
+ @definitions[name]
110
+ end
111
+
112
+ attr_reader :definitions
113
+
114
+ class AttributeKey
115
+ attr_reader :name, :type
116
+
117
+ def initialize(name, type)
118
+ @name = name
119
+ @type = type
120
+ end
121
+ end
122
+ end
123
+
124
+ def initialize(attributes = {})
125
+ @attributes = {}
126
+ attributes = attributes.transform_keys(&:to_sym)
127
+
128
+ self.class.schema.keys.each do |key| # rubocop:disable Style/HashEachMethods
129
+ name = key.name
130
+ definition = self.class.schema[name]
131
+
132
+ if !definition[:required] && !attributes.key?(name)
133
+ next unless default?(definition[:type])
134
+
135
+ value = nil
136
+ else
137
+ value = attributes[name]
138
+ end
139
+
140
+ @attributes[name] = coerce(name, value, definition[:type])
141
+ end
142
+ end
143
+
144
+ def attributes
145
+ @attributes.dup
146
+ end
147
+
148
+ def to_h
149
+ attributes
150
+ end
151
+
152
+ def as_json(*)
153
+ attributes.transform_keys(&:to_s)
154
+ end
155
+
19
156
  def inspect
20
- as_json
157
+ "#<#{self.class.name} #{as_json}>"
158
+ end
159
+
160
+ def ==(other)
161
+ return false unless other.is_a?(self.class)
162
+
163
+ attributes == other.attributes
164
+ end
165
+
166
+ private
167
+
168
+ def coerce(name, value, type)
169
+ return get_default_value(type) if value.nil? && default?(type)
170
+ return nil if value.nil? && optional?(type)
171
+ raise ArgumentError, "Missing required attribute: #{name}" if value.nil?
172
+
173
+ type.call(value)
174
+ rescue StandardError => e
175
+ raise ArgumentError, "Invalid value for #{name}: #{e.message}"
176
+ end
177
+
178
+ def get_default_value(type)
179
+ return type.default_value if type.respond_to?(:default_value)
180
+
181
+ default_val = type.value
182
+ default_val.respond_to?(:call) ? default_val.call : default_val
183
+ end
184
+
185
+ def default?(type)
186
+ type.respond_to?(:default?) && type.default?
187
+ end
188
+
189
+ def optional?(type)
190
+ type.respond_to?(:optional?) && type.optional?
21
191
  end
22
192
  end
23
193
  end
@@ -3,9 +3,7 @@
3
3
  # Event metadata store information on the event, for example the user who triggered the event.
4
4
  module Eventsimple
5
5
  class Metadata < Eventsimple::Message
6
- schema schema.strict
7
-
8
- attribute? :actor_id, DryTypes::Strict::String
9
- attribute? :reason, DryTypes::Strict::String
6
+ attribute :actor_id, Eventsimple::Types::String.optional
7
+ attribute :reason, Eventsimple::Types::String.optional
10
8
  end
11
9
  end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ # require 'active_support/core_ext/object/blank'
4
+
5
+ module Eventsimple
6
+ module Types
7
+ # Encrypted decorator type that wraps any type and adds encryption methods
8
+ class EncryptedType
9
+ include Dry::Types::Type
10
+ include Dry::Types::Decorator
11
+ include Dry::Types::Builder
12
+
13
+ def initialize(type, **options)
14
+ super
15
+ @type = type
16
+ freeze
17
+ end
18
+
19
+ def encrypt(value)
20
+ return value if value.blank?
21
+
22
+ return value if ActiveRecord::Encryption::Encryptor.new.encrypted?(value)
23
+
24
+ ActiveRecord::Encryption::Encryptor.new.encrypt(value)
25
+ end
26
+
27
+ def decrypt(value)
28
+ return value if value.blank?
29
+
30
+ ActiveRecord::Encryption::Encryptor.new.decrypt(value)
31
+ rescue StandardError
32
+ value
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'eventsimple/types/encrypted_type'
4
+
5
+ module Eventsimple
6
+ module DryTypes
7
+ include Dry.Types(:strict, :json)
8
+ end
9
+
10
+ module Types
11
+ Bool = DryTypes::Strict::Bool
12
+ Array = DryTypes::Strict::Array
13
+ Hash = DryTypes::Strict::Hash
14
+ Integer = DryTypes::Strict::Integer
15
+ String = DryTypes::Strict::String
16
+
17
+ Decimal = DryTypes::JSON::Decimal
18
+ Date = DryTypes::JSON::Date
19
+ DateTime = DryTypes::JSON::DateTime
20
+ Time = DryTypes::JSON::Time
21
+
22
+ module BuilderExtension
23
+ def encrypted
24
+ raise ArgumentError, "encrypted is only supported for String types" unless self == String
25
+
26
+ EncryptedType.new(self)
27
+ end
28
+ end
29
+
30
+ Dry::Types::Builder.include(BuilderExtension)
31
+ end
32
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Eventsimple
4
- VERSION = '1.8.1'
4
+ VERSION = '2.0.0'
5
5
  end
data/lib/eventsimple.rb CHANGED
@@ -7,13 +7,11 @@ require 'active_model'
7
7
  require 'active_job'
8
8
  require 'active_support'
9
9
  require 'dry-types'
10
- require 'dry-struct'
11
10
  require 'retriable'
12
11
 
13
- require 'dry_types'
14
-
15
12
  require 'eventsimple/active_job/arguments'
16
13
  require 'eventsimple/configuration'
14
+ require 'eventsimple/types'
17
15
  require 'eventsimple/message'
18
16
  require 'eventsimple/data_type'
19
17
  require 'eventsimple/metadata_type'
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: eventsimple
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.8.1
4
+ version: 2.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Zulfiqar Ali
@@ -23,34 +23,20 @@ dependencies:
23
23
  - - ">="
24
24
  - !ruby/object:Gem::Version
25
25
  version: 1.2.3
26
- - !ruby/object:Gem::Dependency
27
- name: dry-struct
28
- requirement: !ruby/object:Gem::Requirement
29
- requirements:
30
- - - "~>"
31
- - !ruby/object:Gem::Version
32
- version: '1.6'
33
- type: :runtime
34
- prerelease: false
35
- version_requirements: !ruby/object:Gem::Requirement
36
- requirements:
37
- - - "~>"
38
- - !ruby/object:Gem::Version
39
- version: '1.6'
40
26
  - !ruby/object:Gem::Dependency
41
27
  name: dry-types
42
28
  requirement: !ruby/object:Gem::Requirement
43
29
  requirements:
44
- - - "~>"
30
+ - - ">="
45
31
  - !ruby/object:Gem::Version
46
- version: '1.7'
32
+ version: 1.7.0
47
33
  type: :runtime
48
34
  prerelease: false
49
35
  version_requirements: !ruby/object:Gem::Requirement
50
36
  requirements:
51
- - - "~>"
37
+ - - ">="
52
38
  - !ruby/object:Gem::Version
53
- version: '1.7'
39
+ version: 1.7.0
54
40
  - !ruby/object:Gem::Dependency
55
41
  name: pg
56
42
  requirement: !ruby/object:Gem::Requirement
@@ -340,7 +326,6 @@ files:
340
326
  - catalog-info.yaml
341
327
  - config/routes.rb
342
328
  - eventsimple.gemspec
343
- - lib/dry_types.rb
344
329
  - lib/eventsimple.rb
345
330
  - lib/eventsimple/active_job/arguments.rb
346
331
  - lib/eventsimple/configuration.rb
@@ -364,6 +349,8 @@ files:
364
349
  - lib/eventsimple/reactor.rb
365
350
  - lib/eventsimple/reactor_worker.rb
366
351
  - lib/eventsimple/support/spec_helpers.rb
352
+ - lib/eventsimple/types.rb
353
+ - lib/eventsimple/types/encrypted_type.rb
367
354
  - lib/eventsimple/version.rb
368
355
  - log/development.log
369
356
  - sonar-project.properties
data/lib/dry_types.rb DELETED
@@ -1,5 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module DryTypes
4
- include Dry.Types()
5
- end