eventsimple 2.0.0 → 2.0.2

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: acb33cb7729c0927eb994c24269a1c265cc323972ff29e146acc8e9c9593a718
4
- data.tar.gz: b2dd04a605cb7f7df941bf71ede2ab6287ad2816d020ee3de5e4cba9c147561d
3
+ metadata.gz: a07018ecb4b9ced577a8cceb4f762817d4656334cbac6385470facda5900cd2e
4
+ data.tar.gz: df635875cbbb2868adf0e6f9068d70a0a67deb18625afa34de4867b595c2dd6e
5
5
  SHA512:
6
- metadata.gz: f9f44bd3ef7ec08202f586442cacf900cd7cb2f4ebcad9566c202ae0b9c06e0f7dbf0cfd1626fc43c39cea6d4a8b681dcc601375be55011d4f5e63a37fc56485
7
- data.tar.gz: 2ffd06cbeb50c9f79c879678ca94902bf0f6e63e1762fbd3f0c22afc6bb1e97e6fccc8cb6df6ca797d807d1dc5f9996a607fb8b5e95fbf5d67e1ea85770adbae
6
+ metadata.gz: 9cee9758c6c43828ee16812b115b9bc122125be49f51844a00dca73e01d0936434b5847ffd2a7c91990c5cfcf031cd09ebb043ea34ec4c47ddea76213d510960
7
+ data.tar.gz: 7a401cbfd71252fc2d204b9df8d50b6e78994098cba026da149307a22595f987fa774a67e137a39fb3c60c47ed2364ebb6f7e5e19cca78162c5be553d1ef8f7f
data/CHANGELOG.md CHANGED
@@ -6,6 +6,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
6
6
 
7
7
  ## Unreleased
8
8
 
9
+ ## 2.0.2 - 2025-12-12
10
+ ### Changed
11
+ - Fix UI event loading issue.
12
+
13
+ ## 2.0.1 - 2025-12-11
14
+ ### Changed
15
+ - Switch back to using dry-struct for Message class, for better compatibility with existing code.
16
+ - Fix Eventsimple type enforcement in Message class.
17
+ - Fix wiki links in README.md
18
+
9
19
  ## 2.0.0 - 2025-12-07
10
20
  ### Changed
11
21
  - Remove dry-types dependency and replace with custom type system.
data/Gemfile.lock CHANGED
@@ -1,8 +1,9 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- eventsimple (2.0.0)
4
+ eventsimple (2.0.2)
5
5
  concurrent-ruby (>= 1.2.3)
6
+ dry-struct (>= 1.8.0)
6
7
  dry-types (>= 1.7.0)
7
8
  pg (~> 1.4)
8
9
  rails (>= 7.0, < 9.0)
@@ -114,6 +115,11 @@ GEM
114
115
  concurrent-ruby (~> 1.0)
115
116
  dry-core (~> 1.1)
116
117
  zeitwerk (~> 2.6)
118
+ dry-struct (1.8.0)
119
+ dry-core (~> 1.1)
120
+ dry-types (~> 1.8, >= 1.8.2)
121
+ ice_nine (~> 0.11)
122
+ zeitwerk (~> 2.6)
117
123
  dry-types (1.8.3)
118
124
  bigdecimal (~> 3.0)
119
125
  concurrent-ruby (~> 1.0)
@@ -160,6 +166,7 @@ GEM
160
166
  rspec (>= 2.99.0, < 4.0)
161
167
  i18n (1.14.7)
162
168
  concurrent-ruby (~> 1.0)
169
+ ice_nine (0.11.2)
163
170
  io-console (0.8.1)
164
171
  irb (1.15.3)
165
172
  pp (>= 0.6.0)
data/README.md CHANGED
@@ -54,14 +54,14 @@ end
54
54
 
55
55
  ## Quick Start
56
56
 
57
- - **[Home](Home)** - Installation, configuration, and getting started
58
- - **[Usage-Events](Usage-Events)** - How to create and use events
59
- - **[Usage-Reactors](Usage-Reactors)** - Handle side effects with sync and async reactors
60
- - **[Encryption](Encryption)** - Encrypt sensitive data in event messages
61
- - **[Outbox-Pattern](Outbox-Pattern)** - Implement ordered event processing
62
- - **[Best-Practices](Best-Practices)** - Development guidelines and best practices
63
- - **[Testing](Testing)** - Testing best practices for events and reactors
64
- - **[Helper-Methods](Helper-Methods)** - Convenience methods for common tasks
65
- - **[Data-Migrations](Data-Migrations)** - Migrating event data
66
- - **[Existing-Models](Existing-Models)** - Adding Eventsimple to existing models
67
- - **[Factory-Bot-Compatibility](Factory-Bot-Compatibility)** - Using Factory Bot with Eventsimple
57
+ - **[Home](https://github.com/wealthsimple/eventsimple/wiki/Home)** - Installation, configuration, and getting started
58
+ - **[Usage-Events](https://github.com/wealthsimple/eventsimple/wiki/Usage-Events)** - How to create and use events
59
+ - **[Usage-Reactors](https://github.com/wealthsimple/eventsimple/wiki/Usage-Reactors)** - Handle side effects with sync and async reactors
60
+ - **[Encryption](https://github.com/wealthsimple/eventsimple/wiki/Encryption)** - Encrypt sensitive data in event messages
61
+ - **[Outbox-Pattern](https://github.com/wealthsimple/eventsimple/wiki/Outbox-Pattern)** - Implement ordered event processing
62
+ - **[Best-Practices](https://github.com/wealthsimple/eventsimple/wiki/Best-Practices)** - Development guidelines and best practices
63
+ - **[Testing](https://github.com/wealthsimple/eventsimple/wiki/Testing)** - Testing best practices for events and reactors
64
+ - **[Helper-Methods](https://github.com/wealthsimple/eventsimple/wiki/Helper-Methods)** - Convenience methods for common tasks
65
+ - **[Data-Migrations](https://github.com/wealthsimple/eventsimple/wiki/Data-Migrations)** - Migrating event data
66
+ - **[Existing-Models](https://github.com/wealthsimple/eventsimple/wiki/Existing-Models)** - Adding Eventsimple to existing models
67
+ - **[Factory-Bot-Compatibility](https://github.com/wealthsimple/eventsimple/wiki/Factory-Bot-Compatibility)** - Using Factory Bot with Eventsimple
@@ -10,7 +10,7 @@ module Eventsimple
10
10
  @tab_id = params[:t] == 'event' ? 'event' : 'entity'
11
11
 
12
12
  filter_columns = @model_class._filter_attributes
13
- params_filters = params.permit(filters: {})[:filters] || {}
13
+ params_filters = params.permit(filters: filter_columns).fetch(:filters, {})
14
14
  @filters = filter_columns.index_with { |column| params_filters[column] }
15
15
 
16
16
  primary_key = @model_class.event_class._aggregate_id
@@ -19,7 +19,7 @@ module Eventsimple
19
19
  def apply_filter(model_class, model_event_class)
20
20
  filter_columns = model_class._filter_attributes
21
21
 
22
- params_filters = params.permit(filters: {})[:filters] || {}
22
+ params_filters = params.permit(filters: filter_columns).fetch(:filters, {})
23
23
  @filters = filter_columns.index_with { |column| params_filters[column] }
24
24
 
25
25
  return model_event_class unless @filters.values.any?(&:present?)
@@ -61,7 +61,7 @@
61
61
  <th scope="row" colspan="2">Data</th>
62
62
  </tr>
63
63
  <% if @selected_event.data.present? %>
64
- <% @selected_event.data.attributes.each do |attr_name, attr_value| %>
64
+ <% @selected_event.data.to_h.each do |attr_name, attr_value| %>
65
65
  <tr>
66
66
  <td>&nbsp;&nbsp;&nbsp;&nbsp;<%= attr_name %></td>
67
67
  <td><code class="entity-property"><%= attr_value %></code></td>
@@ -72,7 +72,7 @@
72
72
  <tr>
73
73
  <th scope="row" colspan="2">Metadata</th>
74
74
  </tr>
75
- <% @selected_event.metadata.attributes.each do |attr_name, attr_value| %>
75
+ <% @selected_event.metadata.to_h.each do |attr_name, attr_value| %>
76
76
  <tr>
77
77
  <td>&nbsp;&nbsp;&nbsp;&nbsp;<%= attr_name %></td>
78
78
  <td><code class="entity-property">: <%= attr_value %></code></td>
data/eventsimple.gemspec CHANGED
@@ -24,6 +24,7 @@ Gem::Specification.new do |spec|
24
24
  spec.require_paths = ['lib']
25
25
 
26
26
  spec.add_runtime_dependency 'concurrent-ruby', '>= 1.2.3'
27
+ spec.add_runtime_dependency 'dry-struct', '>= 1.8.0'
27
28
  spec.add_runtime_dependency 'dry-types', '>= 1.7.0'
28
29
  spec.add_runtime_dependency 'pg', '~> 1.4'
29
30
  spec.add_runtime_dependency 'rails', '>= 7.0', '< 9.0'
@@ -4,7 +4,7 @@ module Eventsimple
4
4
  class Configuration
5
5
  attr_reader :max_concurrency_retries
6
6
  attr_writer :metadata_klass, :parent_record_klass
7
- attr_accessor :retry_reactor_on_record_not_found, :ui_visible_models
7
+ attr_accessor :retry_reactor_on_record_not_found, :ui_visible_model_names
8
8
 
9
9
  def initialize
10
10
  @dispatchers = []
@@ -13,7 +13,13 @@ module Eventsimple
13
13
  @parent_record_klass = 'ApplicationRecord'
14
14
  @retry_reactor_on_record_not_found = false
15
15
 
16
- @ui_visible_models = [] # internal use only
16
+ @ui_visible_model_names = [] # internal use only - stores class names as strings
17
+ end
18
+
19
+ # Returns fresh class objects by constantizing stored names
20
+ # This avoids stale class references after code reload in development
21
+ def ui_visible_models
22
+ @ui_visible_model_names.map(&:constantize)
17
23
  end
18
24
 
19
25
  def max_concurrency_retries=(value)
@@ -43,7 +43,7 @@ module Eventsimple
43
43
  # disable automatic timestamp updates
44
44
  self.record_timestamps = false
45
45
 
46
- Eventsimple.configuration.ui_visible_models |= [self]
46
+ Eventsimple.configuration.ui_visible_model_names |= [name]
47
47
 
48
48
  include InstanceMethods
49
49
  extend ClassMethods
@@ -1,193 +1,66 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'dry-struct'
4
+
3
5
  module Eventsimple
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)
6
+ class Message < Dry::Struct
7
+ transform_keys(&:to_sym)
8
+
9
+ # dry types will apply default values only on missing keys
10
+ # modify the behaviour so the default is used even when the key is present but nil
11
+ transform_types do |type|
12
+ if type.default?
13
+ type.constructor do |value|
14
+ value.nil? ? Dry::Types::Undefined : value
15
+ end
16
+ else
17
+ type
10
18
  end
19
+ end
11
20
 
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
21
+ def inspect
22
+ as_json
23
+ end
17
24
 
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
+ def self.attribute(name, type = Dry::Types::Any, **options)
26
+ validate_type(type)
27
+ super
28
+ end
25
29
 
26
- private
30
+ def self.attribute?(name, type = Dry::Types::Any, **options)
31
+ validate_type(type)
32
+ super
33
+ end
27
34
 
35
+ class << self
28
36
  def validate_type(type)
29
37
  return if valid_eventsimple_type?(type)
30
38
 
39
+ source_location = begin
40
+ source = const_source_location(name)&.first if respond_to?(:const_source_location) && name
41
+ source || caller_locations.find { |loc| !loc.path.include?('eventsimple') }&.path
42
+ rescue StandardError
43
+ 'unknown'
44
+ end
45
+
46
+ message = [
47
+ "Only Eventsimple::Types are allowed in Message attributes. \n",
48
+ "File: #{source_location} \n",
49
+ "Received: #{type.inspect} \n",
50
+ "Use Eventsimple::Types::String, Eventsimple::Types::Integer, etc. \n",
51
+ ].join(' ')
52
+
31
53
  deprecator = ActiveSupport::Deprecation.new
32
54
  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
- )
55
+ deprecator.warn(message)
39
56
  end
40
57
 
41
58
  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
64
-
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
59
+ return true if type.respond_to?(:meta) && type.meta[:eventsimple] == true
60
+ return true if type.is_a?(Class) && type < Eventsimple::Message
81
61
 
82
62
  false
83
63
  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
91
- end
92
- end
93
- end
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
-
156
- def inspect
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?
191
64
  end
192
65
  end
193
66
  end
@@ -3,7 +3,9 @@
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
- attribute :actor_id, Eventsimple::Types::String.optional
7
- attribute :reason, Eventsimple::Types::String.optional
6
+ schema schema.strict
7
+
8
+ attribute? :actor_id, Eventsimple::Types::String.optional
9
+ attribute? :reason, Eventsimple::Types::String.optional
8
10
  end
9
11
  end
@@ -16,6 +16,12 @@ module Eventsimple
16
16
  freeze
17
17
  end
18
18
 
19
+ def meta(data = nil)
20
+ return { eventsimple: true } if data.nil?
21
+
22
+ self.class.new(@type.meta(data))
23
+ end
24
+
19
25
  def encrypt(value)
20
26
  return value if value.blank?
21
27
 
@@ -8,25 +8,44 @@ module Eventsimple
8
8
  end
9
9
 
10
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
11
+ EVENTSIMPLE_META = { eventsimple: true }.freeze
12
+
13
+ # Extension that preserves eventsimple meta through all type transformations
14
+ module MetaPreservingBuilder
15
+ CHAINABLE_METHODS = %i[optional default enum constrained of].freeze
16
+
17
+ CHAINABLE_METHODS.each do |method_name|
18
+ define_method(method_name) do |*args, &block|
19
+ result = super(*args, &block)
20
+ # Re-apply eventsimple meta if the original type had it
21
+ if respond_to?(:meta) && meta[:eventsimple]
22
+ result.meta(EVENTSIMPLE_META)
23
+ else
24
+ result
25
+ end
26
+ end
27
+ end
28
+
23
29
  def encrypted
24
- raise ArgumentError, "encrypted is only supported for String types" unless self == String
30
+ is_string_type = respond_to?(:primitive) && primitive == ::String
31
+ raise ArgumentError, "encrypted is only supported for String types" unless is_string_type
25
32
 
26
33
  EncryptedType.new(self)
27
34
  end
28
35
  end
29
36
 
30
- Dry::Types::Builder.include(BuilderExtension)
37
+ # Include the meta-preserving builder in Dry::Types
38
+ Dry::Types::Builder.prepend(MetaPreservingBuilder)
39
+
40
+ Bool = DryTypes::Strict::Bool.meta(EVENTSIMPLE_META)
41
+ Array = DryTypes::Strict::Array.meta(EVENTSIMPLE_META)
42
+ Hash = DryTypes::Strict::Hash.meta(EVENTSIMPLE_META)
43
+ Integer = DryTypes::Strict::Integer.meta(EVENTSIMPLE_META)
44
+ String = DryTypes::Strict::String.meta(EVENTSIMPLE_META)
45
+
46
+ Decimal = DryTypes::JSON::Decimal.meta(EVENTSIMPLE_META)
47
+ Date = DryTypes::JSON::Date.meta(EVENTSIMPLE_META)
48
+ DateTime = DryTypes::JSON::DateTime.meta(EVENTSIMPLE_META)
49
+ Time = DryTypes::JSON::Time.meta(EVENTSIMPLE_META)
31
50
  end
32
51
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Eventsimple
4
- VERSION = '2.0.0'
4
+ VERSION = '2.0.2'
5
5
  end
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: 2.0.0
4
+ version: 2.0.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Zulfiqar Ali
@@ -23,6 +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.8.0
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - ">="
38
+ - !ruby/object:Gem::Version
39
+ version: 1.8.0
26
40
  - !ruby/object:Gem::Dependency
27
41
  name: dry-types
28
42
  requirement: !ruby/object:Gem::Requirement