sandthorn_sequel_projection 0.0.1
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 +7 -0
- data/.autotest +3 -0
- data/.gitignore +23 -0
- data/.rspec +2 -0
- data/.ruby-gemset +1 -0
- data/.ruby-version +1 -0
- data/.travis.yml +9 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +130 -0
- data/Rakefile +16 -0
- data/lib/sandthorn_sequel_projection.rb +57 -0
- data/lib/sandthorn_sequel_projection/cursor.rb +39 -0
- data/lib/sandthorn_sequel_projection/errors.rb +14 -0
- data/lib/sandthorn_sequel_projection/event_handler.rb +49 -0
- data/lib/sandthorn_sequel_projection/event_handler_collection.rb +27 -0
- data/lib/sandthorn_sequel_projection/lock.rb +83 -0
- data/lib/sandthorn_sequel_projection/manifest.rb +10 -0
- data/lib/sandthorn_sequel_projection/processed_events_tracker.rb +94 -0
- data/lib/sandthorn_sequel_projection/projection.rb +65 -0
- data/lib/sandthorn_sequel_projection/runner.rb +62 -0
- data/lib/sandthorn_sequel_projection/tasks.rb +14 -0
- data/lib/sandthorn_sequel_projection/utilities.rb +5 -0
- data/lib/sandthorn_sequel_projection/utilities/core_extensions/array_wrap.rb +13 -0
- data/lib/sandthorn_sequel_projection/utilities/null_proc.rb +9 -0
- data/lib/sandthorn_sequel_projection/version.rb +3 -0
- data/sandthorn_sequel_projection.gemspec +37 -0
- data/spec/cursor_spec.rb +44 -0
- data/spec/event_handler_collection_spec.rb +39 -0
- data/spec/event_handler_spec.rb +82 -0
- data/spec/integration/projection_spec.rb +126 -0
- data/spec/lock_spec.rb +141 -0
- data/spec/processed_events_tracker_spec.rb +79 -0
- data/spec/projection_spec.rb +91 -0
- data/spec/runner_spec.rb +32 -0
- data/spec/spec_helper.rb +23 -0
- data/spec/support/mock_event_store.rb +39 -0
- data/spec/test_data/event_data.json +1876 -0
- data/spec/utilities/null_proc_spec.rb +14 -0
- metadata +262 -0
| @@ -0,0 +1,126 @@ | |
| 1 | 
            +
            require 'spec_helper'
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            module SandthornSequelProjection
         | 
| 4 | 
            +
             | 
| 5 | 
            +
              class MyProjection < Projection
         | 
| 6 | 
            +
             | 
| 7 | 
            +
                migration("20150303-1") do |db_con|
         | 
| 8 | 
            +
                  db_con.create_table?(table_name) do
         | 
| 9 | 
            +
                    primary_key :id
         | 
| 10 | 
            +
                    String :aggregate_id
         | 
| 11 | 
            +
                    TrueClass :on_sale, default: false
         | 
| 12 | 
            +
                  end
         | 
| 13 | 
            +
                end
         | 
| 14 | 
            +
             | 
| 15 | 
            +
                define_event_handlers do |handlers|
         | 
| 16 | 
            +
                  handlers.define product_added: {
         | 
| 17 | 
            +
                      aggregate_type: "SandthornProduct",
         | 
| 18 | 
            +
                      event_name: "new"
         | 
| 19 | 
            +
                  }
         | 
| 20 | 
            +
                  handlers.define on_sale: {
         | 
| 21 | 
            +
                      aggregate_type: "SandthornProduct",
         | 
| 22 | 
            +
                      event_name: "product_on_sale"
         | 
| 23 | 
            +
                  }
         | 
| 24 | 
            +
                  handlers.define removed_from_sale: {
         | 
| 25 | 
            +
                      aggregate_type: "SandthornProduct",
         | 
| 26 | 
            +
                      event_names: ["removed_from_sale", "destroyed"]
         | 
| 27 | 
            +
                  }
         | 
| 28 | 
            +
                end
         | 
| 29 | 
            +
             | 
| 30 | 
            +
                def on_sale(event)
         | 
| 31 | 
            +
                  aggregate_id = event[:aggregate_id]
         | 
| 32 | 
            +
                  add_aggregate(aggregate_id)
         | 
| 33 | 
            +
                  table.where(aggregate_id: aggregate_id).update(on_sale: true)
         | 
| 34 | 
            +
                end
         | 
| 35 | 
            +
             | 
| 36 | 
            +
                def add_aggregate(aggregate_id)
         | 
| 37 | 
            +
                  db_connection.transaction do
         | 
| 38 | 
            +
                    exists = table.where(aggregate_id: aggregate_id).any?
         | 
| 39 | 
            +
                    return exists || table.insert(aggregate_id: aggregate_id)
         | 
| 40 | 
            +
                  end
         | 
| 41 | 
            +
                end
         | 
| 42 | 
            +
             | 
| 43 | 
            +
                def product_added(event)
         | 
| 44 | 
            +
                  add_aggregate(event[:aggregate_id])
         | 
| 45 | 
            +
                end
         | 
| 46 | 
            +
             | 
| 47 | 
            +
                def removed_from_sale(event)
         | 
| 48 | 
            +
                  aggregate_id = event[:aggregate_id]
         | 
| 49 | 
            +
                  table.where(aggregate_id: aggregate_id).update(on_sale: false)
         | 
| 50 | 
            +
                end
         | 
| 51 | 
            +
             | 
| 52 | 
            +
                def table
         | 
| 53 | 
            +
                  db_connection[table_name]
         | 
| 54 | 
            +
                end
         | 
| 55 | 
            +
             | 
| 56 | 
            +
                def table_name
         | 
| 57 | 
            +
                  :products_on_sale
         | 
| 58 | 
            +
                end
         | 
| 59 | 
            +
             | 
| 60 | 
            +
                def aggregates_on_sale_ids
         | 
| 61 | 
            +
                  table.where(on_sale: true).select_map(:aggregate_id)
         | 
| 62 | 
            +
                end
         | 
| 63 | 
            +
             | 
| 64 | 
            +
                def aggregate_ids
         | 
| 65 | 
            +
                  table.select_map(:aggregate_id)
         | 
| 66 | 
            +
                end
         | 
| 67 | 
            +
              end
         | 
| 68 | 
            +
             | 
| 69 | 
            +
              describe MyProjection do
         | 
| 70 | 
            +
             | 
| 71 | 
            +
                def db_connection
         | 
| 72 | 
            +
                  SandthornSequelProjection.configuration.db_connection
         | 
| 73 | 
            +
                end
         | 
| 74 | 
            +
             | 
| 75 | 
            +
                def table
         | 
| 76 | 
            +
                  db_connection[projection.table_name]
         | 
| 77 | 
            +
                end
         | 
| 78 | 
            +
             | 
| 79 | 
            +
                let(:projection) { MyProjection.new }
         | 
| 80 | 
            +
             | 
| 81 | 
            +
                before do
         | 
| 82 | 
            +
                  Sandthorn.default_event_store = MockEventStore.with_data
         | 
| 83 | 
            +
                end
         | 
| 84 | 
            +
             | 
| 85 | 
            +
                describe "#migrate!" do
         | 
| 86 | 
            +
                  it "creates the wanted table" do
         | 
| 87 | 
            +
                    projection.migrate!
         | 
| 88 | 
            +
                    expect(table.all).to eq([])
         | 
| 89 | 
            +
                  end
         | 
| 90 | 
            +
                end
         | 
| 91 | 
            +
             | 
| 92 | 
            +
                describe "#update" do
         | 
| 93 | 
            +
                  before do
         | 
| 94 | 
            +
                    projection.migrate!
         | 
| 95 | 
            +
                  end
         | 
| 96 | 
            +
                  methods = [:product_added, :removed_from_sale]
         | 
| 97 | 
            +
                  methods.each do |method|
         | 
| 98 | 
            +
                    it "calls #{method}" do
         | 
| 99 | 
            +
                      expect(projection).to receive(method).at_least(:once).and_call_original
         | 
| 100 | 
            +
                      projection.update!
         | 
| 101 | 
            +
                    end
         | 
| 102 | 
            +
                  end
         | 
| 103 | 
            +
             | 
| 104 | 
            +
                  describe "data" do
         | 
| 105 | 
            +
                    before { projection.update! }
         | 
| 106 | 
            +
                    it "sets the on_sale boolean correctly" do
         | 
| 107 | 
            +
                      expected = %w[
         | 
| 108 | 
            +
                      ac1be457-b6b9-4dad-900b-acb400f810df
         | 
| 109 | 
            +
                    ]
         | 
| 110 | 
            +
                      expect(projection.aggregates_on_sale_ids).to eq(expected)
         | 
| 111 | 
            +
                    end
         | 
| 112 | 
            +
             | 
| 113 | 
            +
                    it "sets the correct last processed sequence number" do
         | 
| 114 | 
            +
                      expect(projection.last_processed_sequence_number).to eq(128)
         | 
| 115 | 
            +
                    end
         | 
| 116 | 
            +
             | 
| 117 | 
            +
                    let(:json_events) { JSON.parse(File.read("./spec/test_data/event_data.json"), symbolize_names: true) }
         | 
| 118 | 
            +
                    let(:aggregate_ids) { json_events.map{|event| event[:aggregate_id] }.uniq }
         | 
| 119 | 
            +
             | 
| 120 | 
            +
                    it "records all aggregate ids" do
         | 
| 121 | 
            +
                      expect(projection.aggregate_ids).to contain_exactly(*aggregate_ids)
         | 
| 122 | 
            +
                    end
         | 
| 123 | 
            +
                  end
         | 
| 124 | 
            +
                end
         | 
| 125 | 
            +
              end
         | 
| 126 | 
            +
            end
         | 
    
        data/spec/lock_spec.rb
    ADDED
    
    | @@ -0,0 +1,141 @@ | |
| 1 | 
            +
            module SandthornSequelProjection
         | 
| 2 | 
            +
             | 
| 3 | 
            +
              describe Lock do
         | 
| 4 | 
            +
                let(:db_connection) { Sequel.sqlite }
         | 
| 5 | 
            +
                let(:table_name) { ProcessedEventsTracker::DEFAULT_TABLE_NAME }
         | 
| 6 | 
            +
                let(:lock) { SandthornSequelProjection::Lock.new("foo", db_connection) }
         | 
| 7 | 
            +
             | 
| 8 | 
            +
                before(:each) do
         | 
| 9 | 
            +
                  db_connection.create_table?(table_name) do
         | 
| 10 | 
            +
                    String    :identifier
         | 
| 11 | 
            +
                    DateTime  :locked_at, null: true
         | 
| 12 | 
            +
                  end
         | 
| 13 | 
            +
                  db_connection[table_name].insert(identifier: lock.identifier)
         | 
| 14 | 
            +
                end
         | 
| 15 | 
            +
             | 
| 16 | 
            +
                def null_lock
         | 
| 17 | 
            +
                  db_connection[table_name].where(identifier: lock.identifier).update(locked_at: nil)
         | 
| 18 | 
            +
                end
         | 
| 19 | 
            +
             | 
| 20 | 
            +
                def set_lock(time = Time.now)
         | 
| 21 | 
            +
                  db_connection[table_name].where(identifier: lock.identifier).update(locked_at: time)
         | 
| 22 | 
            +
                end
         | 
| 23 | 
            +
             | 
| 24 | 
            +
                def lock_row
         | 
| 25 | 
            +
                  db_connection[table_name].where(identifier: lock.identifier).first
         | 
| 26 | 
            +
                end
         | 
| 27 | 
            +
             | 
| 28 | 
            +
                def lock_column
         | 
| 29 | 
            +
                  lock_row[:locked_at]
         | 
| 30 | 
            +
                end
         | 
| 31 | 
            +
             | 
| 32 | 
            +
                describe "#locked?" do
         | 
| 33 | 
            +
                  context "when the lock column is nulled" do
         | 
| 34 | 
            +
                    it "should return false" do
         | 
| 35 | 
            +
                      null_lock
         | 
| 36 | 
            +
                      expect(lock.locked?).to be_falsey
         | 
| 37 | 
            +
                    end
         | 
| 38 | 
            +
                  end
         | 
| 39 | 
            +
             | 
| 40 | 
            +
                  context "when the lock is locked" do
         | 
| 41 | 
            +
                    it "should return true" do
         | 
| 42 | 
            +
                      set_lock
         | 
| 43 | 
            +
                      expect(lock.locked?).to be_truthy
         | 
| 44 | 
            +
                    end
         | 
| 45 | 
            +
                  end
         | 
| 46 | 
            +
                end
         | 
| 47 | 
            +
             | 
| 48 | 
            +
                describe "#unlocked" do
         | 
| 49 | 
            +
                  context "when the lock column is nulled" do
         | 
| 50 | 
            +
                    it "should return true" do
         | 
| 51 | 
            +
                      expect(lock.unlocked?).to be_truthy
         | 
| 52 | 
            +
                    end
         | 
| 53 | 
            +
                  end
         | 
| 54 | 
            +
                end
         | 
| 55 | 
            +
             | 
| 56 | 
            +
                describe "#attempt_lock" do
         | 
| 57 | 
            +
                  context "when there is no previous lock" do
         | 
| 58 | 
            +
                    it "creates the lock" do
         | 
| 59 | 
            +
                      null_lock
         | 
| 60 | 
            +
                      expect(lock.attempt_lock).to be_truthy
         | 
| 61 | 
            +
                      expect(lock.locked?).to be_truthy
         | 
| 62 | 
            +
                    end
         | 
| 63 | 
            +
                  end
         | 
| 64 | 
            +
             | 
| 65 | 
            +
                  context "when there is a previous lock" do
         | 
| 66 | 
            +
                    context "and it has not expired" do
         | 
| 67 | 
            +
                      it "returns false and doesn't set the lock" do
         | 
| 68 | 
            +
                        set_lock
         | 
| 69 | 
            +
                        locked_at = lock_column
         | 
| 70 | 
            +
                        expect(lock.attempt_lock).to be_falsey
         | 
| 71 | 
            +
                        locked_at_after = lock_column
         | 
| 72 | 
            +
                        expect(locked_at_after).to eq(locked_at)
         | 
| 73 | 
            +
                      end
         | 
| 74 | 
            +
                    end
         | 
| 75 | 
            +
             | 
| 76 | 
            +
                    context "and it has expired" do
         | 
| 77 | 
            +
                      it "returns true at sets a new lock" do
         | 
| 78 | 
            +
                        set_lock(Time.now - lock.timeout)
         | 
| 79 | 
            +
                        locked_at = lock_column
         | 
| 80 | 
            +
                        expect(lock.attempt_lock).to be_truthy
         | 
| 81 | 
            +
                        locked_at_after = lock_column
         | 
| 82 | 
            +
                        expect(locked_at_after).to_not eq(locked_at)
         | 
| 83 | 
            +
             | 
| 84 | 
            +
                      end
         | 
| 85 | 
            +
                    end
         | 
| 86 | 
            +
                  end
         | 
| 87 | 
            +
                end
         | 
| 88 | 
            +
             | 
| 89 | 
            +
                describe "#release" do
         | 
| 90 | 
            +
                  it "releases the lock" do
         | 
| 91 | 
            +
                    set_lock
         | 
| 92 | 
            +
                    expect(lock.release).to be_truthy
         | 
| 93 | 
            +
                    expect(lock_column).to be_nil
         | 
| 94 | 
            +
                  end
         | 
| 95 | 
            +
                end
         | 
| 96 | 
            +
             | 
| 97 | 
            +
                describe "#aqcuire" do
         | 
| 98 | 
            +
                  context "when the lock is unlocked" do
         | 
| 99 | 
            +
                    it "executes the block" do
         | 
| 100 | 
            +
                      null_lock
         | 
| 101 | 
            +
                      expect { |b| lock.acquire(&b) }.to yield_control
         | 
| 102 | 
            +
                    end
         | 
| 103 | 
            +
             | 
| 104 | 
            +
                    it "has the lock during execution, and releases the lock afterwards" do
         | 
| 105 | 
            +
                      null_lock
         | 
| 106 | 
            +
                      lock.acquire do
         | 
| 107 | 
            +
                        expect(lock.locked?).to be_truthy
         | 
| 108 | 
            +
                      end
         | 
| 109 | 
            +
                      expect(lock_column).to be_nil
         | 
| 110 | 
            +
                    end
         | 
| 111 | 
            +
                  end
         | 
| 112 | 
            +
             | 
| 113 | 
            +
                  context "when the lock is locked" do
         | 
| 114 | 
            +
                    it "doesn't yield" do
         | 
| 115 | 
            +
                      set_lock
         | 
| 116 | 
            +
                      expect { |b| lock.acquire(&b) }.to_not yield_control
         | 
| 117 | 
            +
                    end
         | 
| 118 | 
            +
                  end
         | 
| 119 | 
            +
             | 
| 120 | 
            +
                  context "when an exception is raised" do
         | 
| 121 | 
            +
                    MyMegaException = Class.new(StandardError)
         | 
| 122 | 
            +
             | 
| 123 | 
            +
                    it "releases the lock" do
         | 
| 124 | 
            +
                      begin
         | 
| 125 | 
            +
                        lock.acquire do
         | 
| 126 | 
            +
                          expect(lock.locked?).to be_truthy
         | 
| 127 | 
            +
                          raise MyMegaException
         | 
| 128 | 
            +
                        end
         | 
| 129 | 
            +
                      rescue
         | 
| 130 | 
            +
                      ensure
         | 
| 131 | 
            +
                        expect(lock.locked?).to be_falsey
         | 
| 132 | 
            +
                      end
         | 
| 133 | 
            +
                    end
         | 
| 134 | 
            +
             | 
| 135 | 
            +
                    it "reraises the exception" do
         | 
| 136 | 
            +
                      expect { lock.acquire { raise MyMegaException } }.to raise_exception(MyMegaException)
         | 
| 137 | 
            +
                    end
         | 
| 138 | 
            +
                  end
         | 
| 139 | 
            +
                end
         | 
| 140 | 
            +
              end
         | 
| 141 | 
            +
            end
         | 
| @@ -0,0 +1,79 @@ | |
| 1 | 
            +
            require "spec_helper"
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            module SandthornSequelProjection
         | 
| 4 | 
            +
              describe ProcessedEventsTracker do
         | 
| 5 | 
            +
             | 
| 6 | 
            +
                describe "migrated specs" do
         | 
| 7 | 
            +
                  let(:event_store) { Sandthorn.default_event_store }
         | 
| 8 | 
            +
                  let(:db_connection) { SandthornSequelProjection.configuration.db_connection }
         | 
| 9 | 
            +
                  let(:tracker) { ProcessedEventsTracker.new(identifier: :foo, event_store: event_store) }
         | 
| 10 | 
            +
                  describe "::initialize" do
         | 
| 11 | 
            +
                    it "ensures that the tracker row is present" do
         | 
| 12 | 
            +
                      tracker_row = db_connection[tracker.table_name].where(identifier: tracker.identifier).first
         | 
| 13 | 
            +
                      expect(tracker_row).to_not be_nil
         | 
| 14 | 
            +
                      expect(tracker_row[:identifier]).to eq(tracker.identifier)
         | 
| 15 | 
            +
                      expect(tracker_row[:last_processed_sequence_number]).to eq(0)
         | 
| 16 | 
            +
                    end
         | 
| 17 | 
            +
                  end
         | 
| 18 | 
            +
             | 
| 19 | 
            +
                  describe '#last_processed_sequence_number' do
         | 
| 20 | 
            +
                    it "returns the integer in the db" do
         | 
| 21 | 
            +
                      db_connection[tracker.table_name].
         | 
| 22 | 
            +
                        where(identifier: tracker.identifier).
         | 
| 23 | 
            +
                        update(last_processed_sequence_number: 12)
         | 
| 24 | 
            +
                      expect(tracker.last_processed_sequence_number).to eq(12)
         | 
| 25 | 
            +
                    end
         | 
| 26 | 
            +
                  end
         | 
| 27 | 
            +
             | 
| 28 | 
            +
                  describe "#process_events" do
         | 
| 29 | 
            +
                    let(:events) { [{sequence_number: 1}, {sequence_number: 2}] }
         | 
| 30 | 
            +
                    around do |example|
         | 
| 31 | 
            +
                      old_batch_size = SandthornSequelProjection.batch_size
         | 
| 32 | 
            +
                      SandthornSequelProjection.configuration.batch_size = 1
         | 
| 33 | 
            +
                      example.run
         | 
| 34 | 
            +
                      SandthornSequelProjection.configuration.batch_size = old_batch_size
         | 
| 35 | 
            +
                    end
         | 
| 36 | 
            +
             | 
| 37 | 
            +
                    before do
         | 
| 38 | 
            +
                      tracker.reset
         | 
| 39 | 
            +
                      Sandthorn.default_event_store.reset
         | 
| 40 | 
            +
                      events.each { |e| event_store.add(e) }
         | 
| 41 | 
            +
                    end
         | 
| 42 | 
            +
             | 
| 43 | 
            +
                    it "yields events" do
         | 
| 44 | 
            +
                      expect { |b| tracker.process_events(&b) }.to yield_successive_args([events.first], [events.last])
         | 
| 45 | 
            +
                    end
         | 
| 46 | 
            +
             | 
| 47 | 
            +
                    it "sets the last processed number" do
         | 
| 48 | 
            +
                      tracker.process_events { |*| }
         | 
| 49 | 
            +
                      expect(tracker.last_processed_sequence_number).to eq(2)
         | 
| 50 | 
            +
                    end
         | 
| 51 | 
            +
             | 
| 52 | 
            +
                    it "has the lock during the yield" do
         | 
| 53 | 
            +
                      tracker.process_events do |*|
         | 
| 54 | 
            +
                        expect(tracker.lock.locked?).to be_truthy
         | 
| 55 | 
            +
                      end
         | 
| 56 | 
            +
                    end
         | 
| 57 | 
            +
             | 
| 58 | 
            +
                    it "has released the lock afterwards" do
         | 
| 59 | 
            +
                      tracker.process_events { |*| }
         | 
| 60 | 
            +
                      expect(tracker.lock.locked?).to be_falsey
         | 
| 61 | 
            +
                    end
         | 
| 62 | 
            +
             | 
| 63 | 
            +
                  end
         | 
| 64 | 
            +
                end
         | 
| 65 | 
            +
             | 
| 66 | 
            +
                describe "non-migrated specs" do
         | 
| 67 | 
            +
                  let(:db_connection) { Sequel.sqlite }
         | 
| 68 | 
            +
                  describe "::migrate!" do
         | 
| 69 | 
            +
                    it "creates the requisite database table" do
         | 
| 70 | 
            +
                      expect { ProcessedEventsTracker.migrate!(db_connection) }.to_not raise_error
         | 
| 71 | 
            +
                      expect(db_connection.table_exists?(ProcessedEventsTracker.table_name)).to be_truthy
         | 
| 72 | 
            +
                      expected_columns = :identifier, :last_processed_sequence_number, :locked_at
         | 
| 73 | 
            +
                      expect(db_connection[ProcessedEventsTracker.table_name].columns).to include(*expected_columns)
         | 
| 74 | 
            +
                    end
         | 
| 75 | 
            +
                  end
         | 
| 76 | 
            +
                end
         | 
| 77 | 
            +
             | 
| 78 | 
            +
              end
         | 
| 79 | 
            +
            end
         | 
| @@ -0,0 +1,91 @@ | |
| 1 | 
            +
            require 'spec_helper'
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            module SandthornSequelProjection
         | 
| 4 | 
            +
             | 
| 5 | 
            +
              module MyModule
         | 
| 6 | 
            +
                class TestProjection < Projection
         | 
| 7 | 
            +
             | 
| 8 | 
            +
                  def foo
         | 
| 9 | 
            +
             | 
| 10 | 
            +
                  end
         | 
| 11 | 
            +
             | 
| 12 | 
            +
                  def bar
         | 
| 13 | 
            +
             | 
| 14 | 
            +
                  end
         | 
| 15 | 
            +
             | 
| 16 | 
            +
                end
         | 
| 17 | 
            +
             | 
| 18 | 
            +
                class WithHandlers < TestProjection
         | 
| 19 | 
            +
             | 
| 20 | 
            +
                  define_event_handlers do |handlers|
         | 
| 21 | 
            +
                    handlers.define(:foo)
         | 
| 22 | 
            +
                    handlers.define(:bar)
         | 
| 23 | 
            +
                  end
         | 
| 24 | 
            +
             | 
| 25 | 
            +
                end
         | 
| 26 | 
            +
              end
         | 
| 27 | 
            +
             | 
| 28 | 
            +
              describe Projection do
         | 
| 29 | 
            +
                let(:projection) { MyModule::WithHandlers.new }
         | 
| 30 | 
            +
             | 
| 31 | 
            +
                describe "::initialize" do
         | 
| 32 | 
            +
                  it "sets the handlers on the instance" do
         | 
| 33 | 
            +
                    handlers = projection.event_handlers
         | 
| 34 | 
            +
                    expect(handlers.length).to eq(2)
         | 
| 35 | 
            +
                  end
         | 
| 36 | 
            +
                end
         | 
| 37 | 
            +
             | 
| 38 | 
            +
                describe '::identifier' do
         | 
| 39 | 
            +
                  it "snake cases the class identifier" do
         | 
| 40 | 
            +
                    expect(MyModule::TestProjection.identifier).to eq("sandthorn_sequel_projection_my_module_test_projection")
         | 
| 41 | 
            +
                  end
         | 
| 42 | 
            +
                end
         | 
| 43 | 
            +
             | 
| 44 | 
            +
                describe "#update!" do
         | 
| 45 | 
            +
             | 
| 46 | 
            +
                  before do
         | 
| 47 | 
            +
                    event_store = Sandthorn.default_event_store
         | 
| 48 | 
            +
                    event_store.reset
         | 
| 49 | 
            +
                    event_store.add_event({sequence_number: 1})
         | 
| 50 | 
            +
                    event_store.add_event({sequence_number: 2})
         | 
| 51 | 
            +
                  end
         | 
| 52 | 
            +
             | 
| 53 | 
            +
                  it "fetches events and passes them on to the handlers" do
         | 
| 54 | 
            +
                    projection = MyModule::WithHandlers.new
         | 
| 55 | 
            +
                    handlers = projection.event_handlers
         | 
| 56 | 
            +
                    handlers.each do |handler|
         | 
| 57 | 
            +
                      expect(handler).to receive(:handle).twice
         | 
| 58 | 
            +
                    end
         | 
| 59 | 
            +
                    projection.update!
         | 
| 60 | 
            +
                  end
         | 
| 61 | 
            +
                end
         | 
| 62 | 
            +
             | 
| 63 | 
            +
                describe "::event_store" do
         | 
| 64 | 
            +
                  let(:klass) { Class.new(Projection) }
         | 
| 65 | 
            +
                  context "when given an event store name" do
         | 
| 66 | 
            +
                    it "sets the event store" do
         | 
| 67 | 
            +
                      klass.event_store(:foo)
         | 
| 68 | 
            +
                      expect(klass.event_store_name).to eq(:foo)
         | 
| 69 | 
            +
                    end
         | 
| 70 | 
            +
                  end
         | 
| 71 | 
            +
             | 
| 72 | 
            +
                  context "when given no argument" do
         | 
| 73 | 
            +
                    context "when an event store has been configured" do
         | 
| 74 | 
            +
                      before do
         | 
| 75 | 
            +
                        klass.event_store(:foo)
         | 
| 76 | 
            +
                      end
         | 
| 77 | 
            +
                      it "fetches the event store" do
         | 
| 78 | 
            +
                        expect(SandthornSequelProjection).to receive(:find_event_store).with(:foo)
         | 
| 79 | 
            +
                        klass.event_store
         | 
| 80 | 
            +
                      end 
         | 
| 81 | 
            +
                    end
         | 
| 82 | 
            +
                  end
         | 
| 83 | 
            +
                end
         | 
| 84 | 
            +
             | 
| 85 | 
            +
                describe "::define_event_handlers" do
         | 
| 86 | 
            +
                  it "yields an EventHandlerCollection" do
         | 
| 87 | 
            +
                    expect { |b| MyModule::TestProjection.define_event_handlers(&b) }.to yield_with_args(EventHandlerCollection)
         | 
| 88 | 
            +
                  end
         | 
| 89 | 
            +
                end
         | 
| 90 | 
            +
              end
         | 
| 91 | 
            +
            end
         | 
    
        data/spec/runner_spec.rb
    ADDED
    
    | @@ -0,0 +1,32 @@ | |
| 1 | 
            +
            module SandthornSequelProjection
         | 
| 2 | 
            +
              describe Runner do
         | 
| 3 | 
            +
             | 
| 4 | 
            +
                module FakeProjection
         | 
| 5 | 
            +
                  def initialize(*); end
         | 
| 6 | 
            +
                  def migrate!; end
         | 
| 7 | 
            +
                  def update!; end
         | 
| 8 | 
            +
                end
         | 
| 9 | 
            +
             | 
| 10 | 
            +
                class Projection1; include FakeProjection; end
         | 
| 11 | 
            +
                class Projection2; include FakeProjection; end
         | 
| 12 | 
            +
             | 
| 13 | 
            +
                let(:manifest) { Manifest.new(Projection1, Projection2) }
         | 
| 14 | 
            +
             | 
| 15 | 
            +
                let(:runner) { SandthornSequelProjection::Runner.new(manifest) }
         | 
| 16 | 
            +
             | 
| 17 | 
            +
                describe "#run" do
         | 
| 18 | 
            +
                  it "migrates all projections" do
         | 
| 19 | 
            +
                    expect_any_instance_of(Projection1).to receive(:migrate!).once
         | 
| 20 | 
            +
                    expect_any_instance_of(Projection2).to receive(:migrate!).once
         | 
| 21 | 
            +
                    runner.run(false)
         | 
| 22 | 
            +
                  end
         | 
| 23 | 
            +
             | 
| 24 | 
            +
                  it "updates all projections" do
         | 
| 25 | 
            +
                    expect_any_instance_of(Projection1).to receive(:update!).once
         | 
| 26 | 
            +
                    expect_any_instance_of(Projection2).to receive(:update!).once
         | 
| 27 | 
            +
                    runner.run(false)
         | 
| 28 | 
            +
                  end
         | 
| 29 | 
            +
                end
         | 
| 30 | 
            +
             | 
| 31 | 
            +
              end
         | 
| 32 | 
            +
            end
         |