aggregate_root 2.6.0 → 2.8.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.
    
        checksums.yaml
    CHANGED
    
    | @@ -1,7 +1,7 @@ | |
| 1 1 | 
             
            ---
         | 
| 2 2 | 
             
            SHA256:
         | 
| 3 | 
            -
              metadata.gz:  | 
| 4 | 
            -
              data.tar.gz:  | 
| 3 | 
            +
              metadata.gz: d826b6ed4e196ffabc267132bae45d6a42040f16006c9c0feb3eed559f1046b9
         | 
| 4 | 
            +
              data.tar.gz: d89c1cbd5a35e0c7d2f1b8921671de56d428e0af8a5fae42a144dd444b0e5fc6
         | 
| 5 5 | 
             
            SHA512:
         | 
| 6 | 
            -
              metadata.gz:  | 
| 7 | 
            -
              data.tar.gz:  | 
| 6 | 
            +
              metadata.gz: e758436e49345ea06fdb8236bcfbcf0a644878113c74cc3a2adf9284b4d1d8c8f2492a7a43cd83ad02646b78cb4b8623c6df67e1440c2f7291afc546e601c472
         | 
| 7 | 
            +
              data.tar.gz: 127b7d1fdb8966b027d5c55897de6d96d915d94a9307f56b664a80a6044e03495eae19fd1e869fe94a33bbe9bc84be7da013a39424b4bbca4164b1375913631e
         | 
| @@ -1,10 +1,11 @@ | |
| 1 1 | 
             
            # frozen_string_literal: true
         | 
| 2 | 
            -
             | 
| 2 | 
            +
            require 'delegate'
         | 
| 3 3 | 
             
            module AggregateRoot
         | 
| 4 4 | 
             
              class InstrumentedRepository
         | 
| 5 5 | 
             
                def initialize(repository, instrumentation)
         | 
| 6 6 | 
             
                  @repository = repository
         | 
| 7 7 | 
             
                  @instrumentation = instrumentation
         | 
| 8 | 
            +
                  self.error_handler = method(:handle_error) if respond_to?(:error_handler=)
         | 
| 8 9 | 
             
                end
         | 
| 9 10 |  | 
| 10 11 | 
             
                def load(aggregate, stream_name)
         | 
| @@ -42,6 +43,14 @@ module AggregateRoot | |
| 42 43 |  | 
| 43 44 | 
             
                private
         | 
| 44 45 |  | 
| 46 | 
            +
                def handle_error(error)
         | 
| 47 | 
            +
                  instrumentation.instrument(
         | 
| 48 | 
            +
                    "error_occured.repository.aggregate_root",
         | 
| 49 | 
            +
                    exception: [error.class.name, error.message],
         | 
| 50 | 
            +
                    exception_object: error,
         | 
| 51 | 
            +
                  )
         | 
| 52 | 
            +
                end
         | 
| 53 | 
            +
             | 
| 45 54 | 
             
                attr_reader :instrumentation, :repository
         | 
| 46 55 | 
             
              end
         | 
| 47 56 | 
             
            end
         | 
| @@ -0,0 +1,98 @@ | |
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 | 
            +
            require 'base64'
         | 
| 3 | 
            +
            require 'ruby_event_store/event'
         | 
| 4 | 
            +
             | 
| 5 | 
            +
            module AggregateRoot
         | 
| 6 | 
            +
              class SnapshotRepository
         | 
| 7 | 
            +
                DEFAULT_SNAPSHOT_INTERVAL = 100.freeze
         | 
| 8 | 
            +
                SNAPSHOT_STREAM_PATTERN = ->(base_stream_name) { "#{base_stream_name}_snapshots" }
         | 
| 9 | 
            +
                NotRestorableSnapshot = Class.new(StandardError)
         | 
| 10 | 
            +
                NotDumpableAggregateRoot = Class.new(StandardError)
         | 
| 11 | 
            +
             | 
| 12 | 
            +
                def initialize(event_store, interval = DEFAULT_SNAPSHOT_INTERVAL)
         | 
| 13 | 
            +
                  raise ArgumentError, 'interval must be an Integer' unless interval.instance_of?(Integer)
         | 
| 14 | 
            +
                  raise ArgumentError, 'interval must be greater than 0' unless interval > 0
         | 
| 15 | 
            +
                  @event_store = event_store
         | 
| 16 | 
            +
                  @interval = interval
         | 
| 17 | 
            +
                  @error_handler = ->(_) { }
         | 
| 18 | 
            +
                end
         | 
| 19 | 
            +
             | 
| 20 | 
            +
                attr_writer :error_handler
         | 
| 21 | 
            +
             | 
| 22 | 
            +
                Snapshot = Class.new(RubyEventStore::Event)
         | 
| 23 | 
            +
             | 
| 24 | 
            +
                def load(aggregate, stream_name)
         | 
| 25 | 
            +
                  last_snapshot = load_snapshot_event(stream_name)
         | 
| 26 | 
            +
                  query = event_store.read.stream(stream_name)
         | 
| 27 | 
            +
                  if last_snapshot
         | 
| 28 | 
            +
                    begin
         | 
| 29 | 
            +
                      aggregate = load_marshal(last_snapshot)
         | 
| 30 | 
            +
                    rescue NotRestorableSnapshot => e
         | 
| 31 | 
            +
                      error_handler.(e)
         | 
| 32 | 
            +
                    else
         | 
| 33 | 
            +
                      aggregate.version = last_snapshot.data.fetch(:version)
         | 
| 34 | 
            +
                      query = query.from(last_snapshot.data.fetch(:last_event_id))
         | 
| 35 | 
            +
                    end
         | 
| 36 | 
            +
                  end
         | 
| 37 | 
            +
                  query.reduce { |_, ev| aggregate.apply(ev) }
         | 
| 38 | 
            +
                  aggregate.version = aggregate.version + aggregate.unpublished_events.count
         | 
| 39 | 
            +
                  aggregate
         | 
| 40 | 
            +
                end
         | 
| 41 | 
            +
             | 
| 42 | 
            +
                def store(aggregate, stream_name)
         | 
| 43 | 
            +
                  events = aggregate.unpublished_events.to_a
         | 
| 44 | 
            +
                  event_store.publish(events,
         | 
| 45 | 
            +
                                      stream_name: stream_name,
         | 
| 46 | 
            +
                                      expected_version: aggregate.version)
         | 
| 47 | 
            +
             | 
| 48 | 
            +
                  aggregate.version = aggregate.version + events.count
         | 
| 49 | 
            +
             | 
| 50 | 
            +
                  if time_for_snapshot?(aggregate.version, events.size)
         | 
| 51 | 
            +
                    begin
         | 
| 52 | 
            +
                      publish_snapshot_event(aggregate, stream_name, events.last.event_id)
         | 
| 53 | 
            +
                    rescue NotDumpableAggregateRoot => e
         | 
| 54 | 
            +
                      error_handler.(e)
         | 
| 55 | 
            +
                    end
         | 
| 56 | 
            +
                  end
         | 
| 57 | 
            +
                end
         | 
| 58 | 
            +
             | 
| 59 | 
            +
                private
         | 
| 60 | 
            +
             | 
| 61 | 
            +
                attr_reader :event_store, :interval, :error_handler
         | 
| 62 | 
            +
             | 
| 63 | 
            +
                def publish_snapshot_event(aggregate, stream_name, last_event_id)
         | 
| 64 | 
            +
                  event_store.publish(
         | 
| 65 | 
            +
                    Snapshot.new(
         | 
| 66 | 
            +
                      data: { marshal: build_marshal(aggregate), last_event_id: last_event_id, version: aggregate.version }
         | 
| 67 | 
            +
                    ),
         | 
| 68 | 
            +
                    stream_name: SNAPSHOT_STREAM_PATTERN.(stream_name)
         | 
| 69 | 
            +
                  )
         | 
| 70 | 
            +
                end
         | 
| 71 | 
            +
             | 
| 72 | 
            +
                def build_marshal(aggregate)
         | 
| 73 | 
            +
                  Marshal.dump(aggregate)
         | 
| 74 | 
            +
                rescue TypeError
         | 
| 75 | 
            +
                  raise NotDumpableAggregateRoot, "#{aggregate.class} cannot be dumped.
         | 
| 76 | 
            +
            It may be caused by instance variables being: bindings, procedure or method objects, instances of class IO, or singleton objects.
         | 
| 77 | 
            +
            Snapshot skipped."
         | 
| 78 | 
            +
                end
         | 
| 79 | 
            +
             | 
| 80 | 
            +
                def load_snapshot_event(stream_name)
         | 
| 81 | 
            +
                  event_store.read.stream(SNAPSHOT_STREAM_PATTERN.(stream_name)).last
         | 
| 82 | 
            +
                end
         | 
| 83 | 
            +
             | 
| 84 | 
            +
                def load_marshal(snpashot_event)
         | 
| 85 | 
            +
                  Marshal.load(snpashot_event.data.fetch(:marshal))
         | 
| 86 | 
            +
                rescue TypeError, ArgumentError
         | 
| 87 | 
            +
                  raise NotRestorableSnapshot, "Aggregate root cannot be restored from the last snapshot (event id: #{snpashot_event.event_id}).
         | 
| 88 | 
            +
            It may be caused by aggregate class rename or Marshal version mismatch.
         | 
| 89 | 
            +
            Loading aggregate based on the whole stream."
         | 
| 90 | 
            +
                end
         | 
| 91 | 
            +
             | 
| 92 | 
            +
                def time_for_snapshot?(aggregate_version, just_published_events)
         | 
| 93 | 
            +
                  events_in_stream = aggregate_version + 1
         | 
| 94 | 
            +
                  events_since_time_for_snapshot = events_in_stream % interval
         | 
| 95 | 
            +
                  just_published_events > events_since_time_for_snapshot
         | 
| 96 | 
            +
                end
         | 
| 97 | 
            +
              end
         | 
| 98 | 
            +
            end
         | 
    
        data/lib/aggregate_root.rb
    CHANGED
    
    | @@ -7,6 +7,7 @@ require_relative "aggregate_root/default_apply_strategy" | |
| 7 7 | 
             
            require_relative "aggregate_root/repository"
         | 
| 8 8 | 
             
            require_relative "aggregate_root/instrumented_repository"
         | 
| 9 9 | 
             
            require_relative "aggregate_root/instrumented_apply_strategy"
         | 
| 10 | 
            +
            require_relative 'aggregate_root/snapshot_repository'
         | 
| 10 11 |  | 
| 11 12 | 
             
            module AggregateRoot
         | 
| 12 13 | 
             
              module OnDSL
         | 
| @@ -60,6 +61,21 @@ module AggregateRoot | |
| 60 61 | 
             
                def unpublished_events
         | 
| 61 62 | 
             
                  @unpublished_events.each
         | 
| 62 63 | 
             
                end
         | 
| 64 | 
            +
             | 
| 65 | 
            +
                UNMARSHALED_VARIABLES = [:@version, :@unpublished_events]
         | 
| 66 | 
            +
             | 
| 67 | 
            +
                def marshal_dump
         | 
| 68 | 
            +
                  instance_variables.reject{|m| UNMARSHALED_VARIABLES.include? m}.inject({}) do |vars, attr|
         | 
| 69 | 
            +
                    vars[attr] = instance_variable_get(attr)
         | 
| 70 | 
            +
                    vars
         | 
| 71 | 
            +
                  end
         | 
| 72 | 
            +
                end
         | 
| 73 | 
            +
             | 
| 74 | 
            +
                def marshal_load(vars)
         | 
| 75 | 
            +
                  vars.each do |attr, value|
         | 
| 76 | 
            +
                    instance_variable_set(attr, value) unless UNMARSHALED_VARIABLES.include?(attr)
         | 
| 77 | 
            +
                  end
         | 
| 78 | 
            +
                end
         | 
| 63 79 | 
             
              end
         | 
| 64 80 |  | 
| 65 81 | 
             
              def self.with_default_apply_strategy
         | 
    
        metadata
    CHANGED
    
    | @@ -1,14 +1,14 @@ | |
| 1 1 | 
             
            --- !ruby/object:Gem::Specification
         | 
| 2 2 | 
             
            name: aggregate_root
         | 
| 3 3 | 
             
            version: !ruby/object:Gem::Version
         | 
| 4 | 
            -
              version: 2. | 
| 4 | 
            +
              version: 2.8.0
         | 
| 5 5 | 
             
            platform: ruby
         | 
| 6 6 | 
             
            authors:
         | 
| 7 7 | 
             
            - Arkency
         | 
| 8 8 | 
             
            autorequire:
         | 
| 9 9 | 
             
            bindir: bin
         | 
| 10 10 | 
             
            cert_chain: []
         | 
| 11 | 
            -
            date:  | 
| 11 | 
            +
            date: 2023-01-13 00:00:00.000000000 Z
         | 
| 12 12 | 
             
            dependencies:
         | 
| 13 13 | 
             
            - !ruby/object:Gem::Dependency
         | 
| 14 14 | 
             
              name: ruby_event_store
         | 
| @@ -16,14 +16,14 @@ dependencies: | |
| 16 16 | 
             
                requirements:
         | 
| 17 17 | 
             
                - - '='
         | 
| 18 18 | 
             
                  - !ruby/object:Gem::Version
         | 
| 19 | 
            -
                    version: 2. | 
| 19 | 
            +
                    version: 2.8.0
         | 
| 20 20 | 
             
              type: :runtime
         | 
| 21 21 | 
             
              prerelease: false
         | 
| 22 22 | 
             
              version_requirements: !ruby/object:Gem::Requirement
         | 
| 23 23 | 
             
                requirements:
         | 
| 24 24 | 
             
                - - '='
         | 
| 25 25 | 
             
                  - !ruby/object:Gem::Version
         | 
| 26 | 
            -
                    version: 2. | 
| 26 | 
            +
                    version: 2.8.0
         | 
| 27 27 | 
             
            description:
         | 
| 28 28 | 
             
            email: dev@arkency.com
         | 
| 29 29 | 
             
            executables: []
         | 
| @@ -38,6 +38,7 @@ files: | |
| 38 38 | 
             
            - lib/aggregate_root/instrumented_apply_strategy.rb
         | 
| 39 39 | 
             
            - lib/aggregate_root/instrumented_repository.rb
         | 
| 40 40 | 
             
            - lib/aggregate_root/repository.rb
         | 
| 41 | 
            +
            - lib/aggregate_root/snapshot_repository.rb
         | 
| 41 42 | 
             
            - lib/aggregate_root/transform.rb
         | 
| 42 43 | 
             
            - lib/aggregate_root/version.rb
         | 
| 43 44 | 
             
            homepage: https://railseventstore.org
         |