rails_simple_event_sourcing 1.0.7 → 1.0.9

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: 9415504de52817629c90a90ad0fff708ad8f829c768ed88485b8eae53f241c18
4
- data.tar.gz: 2344f7a06e5c3b9dca5d0b6c98b1526ad73088c59cde9a0c64f2b5cb6a227ec6
3
+ metadata.gz: 2cdc3924ccf5cb03de4c576e06e36516187ab5743fdeacb7c4b543e1733af6ed
4
+ data.tar.gz: e8a6bcce2a7b918a9a55ffd37dcf11e31c4692c53678ae083a59213fdd39e29e
5
5
  SHA512:
6
- metadata.gz: 3f2ec958c3dab2f9b398ed1bd0bfddec38fb13c720762ab57cc45c59091d9116f07af133601a19dae083ba5c40b32177cbfaa4809cb6af1a37cf569ed941693d
7
- data.tar.gz: 32b8d673ca804d1ef03e5e55a561011fd052d70789afc078b9f8c7becff373d310bb2d24da411e6fc8a2fa869720a3aad487fbc4a93c5290cc2ce9a4f4d81c4d
6
+ metadata.gz: 2825950884885d9366e2586ddada26b272fc9551ba92caefe9be08c78569ad5df7243bef7b6fcf51f3e6529fc93dba1b455aacb7e7ed61b94ce9c40bd9ac0868
7
+ data.tar.gz: b8b2ec4ceca8ace0ec9f51185ac373008c73a11c0aa536d650d844464822c2e4bf6aa919d797aff36807ceca23f366ebbadaf94dcd78230b0ace007d0efd33c6
data/README.md CHANGED
@@ -173,15 +173,45 @@ Handlers can be discovered in two ways:
173
173
  2. **Convention-based** - Using naming convention mapping (can be disabled via configuration)
174
174
 
175
175
  **Result Object:**
176
- The `Result` struct has three fields:
176
+ The `Result` class has three fields:
177
177
  - `success?` - Boolean indicating if the operation succeeded
178
178
  - `data` - Data to return (usually the aggregate/model instance)
179
179
  - `errors` - Array or hash of error messages when `success?` is false
180
180
 
181
- **Helper Methods:**
182
- The base class provides convenience methods:
183
- - `success_result(data:)` - Creates a successful result
184
- - `failure_result(errors:)` - Creates a failed result
181
+ Use the factory methods to build results:
182
+
183
+ ```ruby
184
+ # Successful result
185
+ RailsSimpleEventSourcing::Result.success(data: customer)
186
+
187
+ # Failed result
188
+ RailsSimpleEventSourcing::Result.failure(errors: { email: ["already taken"] })
189
+ ```
190
+
191
+ It supports a chainable API for use in controllers:
192
+ - `on_success { |data| }` - Executes the block (yielding `data`) if the result is successful
193
+ - `on_failure { |errors| }` - Executes the block (yielding `errors`) if the result failed
194
+
195
+ Both methods return `self`, so they can be chained. The predicate `success?` remains available for use in conditionals and tests.
196
+
197
+ ```ruby
198
+ result = RailsSimpleEventSourcing::Result.success(data: customer)
199
+
200
+ # Chainable (preferred in controllers)
201
+ result
202
+ .on_success { |data| render json: data }
203
+ .on_failure { |errors| render json: { errors: }, status: :unprocessable_entity }
204
+
205
+ # Predicate (preferred in tests)
206
+ result.success? # => true
207
+ result.data # => #<Customer ...>
208
+ result.errors # => nil
209
+ ```
210
+
211
+ **Helper Methods in Handlers:**
212
+ Inside a command handler, `success` and `failure` are delegated to `RailsSimpleEventSourcing::Result`:
213
+ - `success(data:)` - delegates to `RailsSimpleEventSourcing::Result.success`
214
+ - `failure(errors:)` - delegates to `RailsSimpleEventSourcing::Result.failure`
185
215
 
186
216
  **Example - Basic Handler:**
187
217
 
@@ -198,11 +228,7 @@ class Customer
198
228
  updated_at: Time.zone.now
199
229
  )
200
230
 
201
- # Using helper method (recommended)
202
- success_result(data: event.aggregate)
203
-
204
- # Or create Result directly
205
- # RailsSimpleEventSourcing::Result.new(success?: true, data: event.aggregate)
231
+ success(data: event.aggregate)
206
232
  end
207
233
  end
208
234
  end
@@ -224,11 +250,11 @@ class Customer
224
250
  updated_at: Time.zone.now
225
251
  )
226
252
 
227
- success_result(data: event.aggregate)
253
+ success(data: event.aggregate)
228
254
  rescue ActiveRecord::RecordNotUnique
229
- failure_result(errors: ["Email has already been taken"])
255
+ failure(errors: ["Email has already been taken"])
230
256
  rescue StandardError => e
231
- failure_result(errors: ["An error occurred: #{e.message}"])
257
+ failure(errors: ["An error occurred: #{e.message}"])
232
258
  end
233
259
  end
234
260
  end
@@ -358,13 +384,10 @@ class CustomersController < ApplicationController
358
384
  last_name: params[:last_name],
359
385
  email: params[:email]
360
386
  )
361
- handler = RailsSimpleEventSourcing::CommandHandler.new(cmd).call
362
387
 
363
- if handler.success?
364
- render json: handler.data
365
- else
366
- render json: { errors: handler.errors }, status: :unprocessable_entity
367
- end
388
+ RailsSimpleEventSourcing::CommandHandler.new(cmd).call
389
+ .on_success { |data| render json: data }
390
+ .on_failure { |errors| render json: { errors: }, status: :unprocessable_entity }
368
391
  end
369
392
  end
370
393
  ```
@@ -382,13 +405,10 @@ class CustomersController < ApplicationController
382
405
  last_name: params[:last_name],
383
406
  email: params[:email]
384
407
  )
385
- handler = RailsSimpleEventSourcing::CommandHandler.new(cmd).call
386
408
 
387
- if handler.success?
388
- render json: handler.data
389
- else
390
- render json: { errors: handler.errors }, status: :unprocessable_entity
391
- end
409
+ RailsSimpleEventSourcing::CommandHandler.new(cmd).call
410
+ .on_success { |data| render json: data }
411
+ .on_failure { |errors| render json: { errors: }, status: :unprocessable_entity }
392
412
  end
393
413
  end
394
414
  ```
@@ -399,13 +419,10 @@ end
399
419
  class CustomersController < ApplicationController
400
420
  def destroy
401
421
  cmd = Customer::Commands::Delete.new(aggregate_id: params[:id])
402
- handler = RailsSimpleEventSourcing::CommandHandler.new(cmd).call
403
422
 
404
- if handler.success?
405
- head :no_content
406
- else
407
- render json: { errors: handler.errors }, status: :unprocessable_entity
408
- end
423
+ RailsSimpleEventSourcing::CommandHandler.new(cmd).call
424
+ .on_success { head :no_content }
425
+ .on_failure { |errors| render json: { errors: }, status: :unprocessable_entity }
409
426
  end
410
427
  end
411
428
  ```
@@ -548,11 +565,11 @@ Mount the engine in your `config/routes.rb`:
548
565
 
549
566
  ```ruby
550
567
  Rails.application.routes.draw do
551
- mount RailsSimpleEventSourcing::Engine, at: "/event-store"
568
+ mount RailsSimpleEventSourcing::Engine, at: "/events"
552
569
  end
553
570
  ```
554
571
 
555
- Then navigate to `/event-store` in your browser to access the viewer.
572
+ Then navigate to `/events` in your browser to access the viewer.
556
573
 
557
574
  **Password Protection:**
558
575
 
@@ -564,11 +581,11 @@ In production you will likely want to restrict access to the events viewer.
564
581
  Rails.application.routes.draw do
565
582
  mount Rack::Auth::Basic.new(
566
583
  RailsSimpleEventSourcing::Engine,
567
- "Event Store"
584
+ "Events"
568
585
  ) { |username, password|
569
586
  ActiveSupport::SecurityUtils.secure_compare(username, "admin") &
570
587
  ActiveSupport::SecurityUtils.secure_compare(password, Rails.application.credentials.event_sourcing_password || "secret")
571
- }, at: "/event-store"
588
+ }, at: "/events"
572
589
  end
573
590
  ```
574
591
 
@@ -577,7 +594,7 @@ end
577
594
  ```ruby
578
595
  Rails.application.routes.draw do
579
596
  authenticate :user, ->(user) { user.admin? } do
580
- mount RailsSimpleEventSourcing::Engine, at: "/event-store"
597
+ mount RailsSimpleEventSourcing::Engine, at: "/events"
581
598
  end
582
599
  end
583
600
  ```
@@ -639,10 +656,6 @@ class Customer < ApplicationRecord
639
656
 
640
657
  scope :active, -> { where(deleted_at: nil) }
641
658
  scope :deleted, -> { where.not(deleted_at: nil) }
642
-
643
- def soft_delete
644
- update(deleted_at: Time.current)
645
- end
646
659
  end
647
660
 
648
661
  # Event
@@ -13,6 +13,10 @@ module RailsSimpleEventSourcing
13
13
  @write_access_enabled = true
14
14
  end
15
15
 
16
+ def disable_write_access!
17
+ @write_access_enabled = false
18
+ end
19
+
16
20
  private
17
21
 
18
22
  def write_access_enabled
@@ -14,10 +14,13 @@ module RailsSimpleEventSourcing
14
14
 
15
15
  before_validation :setup_for_create, on: :create
16
16
  before_save :persist_aggregate, if: :aggregate_defined?
17
+ after_create :disable_write_access!
17
18
 
18
19
  def apply(aggregate)
19
20
  payload.each do |key, value|
20
- aggregate.send("#{key}=", value) if aggregate.respond_to?("#{key}=")
21
+ raise "Unknown attribute '#{key}' on #{aggregate.class}" unless aggregate.respond_to?("#{key}=")
22
+
23
+ aggregate.send("#{key}=", value)
21
24
  end
22
25
  end
23
26
 
@@ -67,6 +70,7 @@ module RailsSimpleEventSourcing
67
70
 
68
71
  aggregate_repository.save!(@aggregate)
69
72
  self.aggregate_id = @aggregate.id
73
+ @aggregate.disable_write_access!
70
74
  end
71
75
 
72
76
  def aggregate_repository
data/config/routes.rb CHANGED
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  RailsSimpleEventSourcing::Engine.routes.draw do
4
- resources :events, only: [:index, :show]
5
- root to: "events#index"
4
+ root to: "events#index", as: :events
5
+ get ":id", to: "events#show", as: :event
6
6
  end
@@ -9,7 +9,7 @@ module RailsSimpleEventSourcing
9
9
  end
10
10
 
11
11
  def call
12
- return Result.new(success?: false, errors: @command.errors) unless @command.valid?
12
+ return Result.failure(errors: @command.errors) unless @command.valid?
13
13
 
14
14
  initialize_command_handler.call
15
15
  end
@@ -3,20 +3,14 @@
3
3
  module RailsSimpleEventSourcing
4
4
  module CommandHandlers
5
5
  class Base
6
+ delegate :success, :failure, to: 'RailsSimpleEventSourcing::Result'
7
+
6
8
  def initialize(command:)
7
9
  @command = command
8
10
  end
9
11
 
10
12
  def call
11
- raise NoMethodError, "You must implement #{self.class}#call"
12
- end
13
-
14
- def success_result(data: nil)
15
- RailsSimpleEventSourcing::Result.new(success?: true, data:)
16
- end
17
-
18
- def failure_result(errors:)
19
- RailsSimpleEventSourcing::Result.new(success?: false, errors:)
13
+ raise NotImplementedError, "You must implement #{self.class}#call"
20
14
  end
21
15
  end
22
16
  end
@@ -4,7 +4,6 @@ module RailsSimpleEventSourcing
4
4
  module Commands
5
5
  class Base
6
6
  include ActiveModel::Model
7
- include ActiveModel::Validations
8
7
 
9
8
  attr_accessor :aggregate_id
10
9
  end
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'concurrent'
3
4
  require_relative 'aggregate_repository'
4
5
  require_relative 'command_handler'
5
6
  require_relative 'command_handlers/base'
@@ -1,5 +1,42 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module RailsSimpleEventSourcing
4
- Result = Struct.new(:success?, :data, :errors, keyword_init: true)
4
+ class Result
5
+ attr_reader :data, :errors
6
+
7
+ def self.success(data: nil)
8
+ new(success: true, data:)
9
+ end
10
+
11
+ def self.failure(errors:)
12
+ new(success: false, errors:)
13
+ end
14
+
15
+ def initialize(success:, data: nil, errors: nil)
16
+ raise ArgumentError, 'Successful result cannot have errors' if success && errors
17
+ raise ArgumentError, 'Failed result cannot have data' if !success && data
18
+
19
+ @success = success
20
+ @data = data
21
+ @errors = errors
22
+ end
23
+
24
+ def success?
25
+ @success
26
+ end
27
+
28
+ def on_success(&block)
29
+ raise ArgumentError, 'Block required' unless block
30
+
31
+ block.call(data) if success?
32
+ self
33
+ end
34
+
35
+ def on_failure(&block)
36
+ raise ArgumentError, 'Block required' unless block
37
+
38
+ block.call(errors) unless success?
39
+ self
40
+ end
41
+ end
5
42
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module RailsSimpleEventSourcing
4
- VERSION = '1.0.7'
4
+ VERSION = '1.0.9'
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rails_simple_event_sourcing
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.7
4
+ version: 1.0.9
5
5
  platform: ruby
6
6
  authors:
7
7
  - Damian Baćkowski
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-02-22 00:00:00.000000000 Z
11
+ date: 2026-03-01 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: pg
@@ -38,6 +38,20 @@ dependencies:
38
38
  - - ">="
39
39
  - !ruby/object:Gem::Version
40
40
  version: 7.1.2
41
+ - !ruby/object:Gem::Dependency
42
+ name: concurrent-ruby
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '1.1'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '1.1'
41
55
  - !ruby/object:Gem::Dependency
42
56
  name: rubocop
43
57
  requirement: !ruby/object:Gem::Requirement