event_store_client 0.1.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.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: d3deeba909c8bf46f05e7994ae5ebef841e622c17032f0259387871335f80e98
4
+ data.tar.gz: 0bc3e13528fa696506b3d34ca2b00004a736f30bb62d379f41ef50e4a2b180ab
5
+ SHA512:
6
+ metadata.gz: 2606efc97b2695dc52d358a9c0bc967d0583c2b58b95b27289c6f9e4dbac04a87bb134dfc8a1e0b251ca11b08c5f0c28841c25f48fdbd4a33db2dd37a0369727
7
+ data.tar.gz: f1b690148757d9f316127b272c03d70558fc53a8868dbf0058d7669bcdd702b92849d44002dd6eb2367982525fdf25299931d8c8563c5c7ed7d3527f24d9ddc3
@@ -0,0 +1,20 @@
1
+ Copyright 2019 Sebastian Wilgosz
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,148 @@
1
+ # EventStoreClient
2
+
3
+ An easy-to use API client for connecting ruby applications with https://eventstore.org/
4
+
5
+ ## Installation
6
+ Add this line to your application's Gemfile:
7
+
8
+ ```ruby
9
+ gem 'event_store_client'
10
+ ```
11
+
12
+ And then execute:
13
+ ```bash
14
+ $ bundle
15
+ ```
16
+
17
+ Or install it yourself as:
18
+ ```bash
19
+ $ gem install event_store_client
20
+ ```
21
+
22
+ ## Usage
23
+
24
+ ### EventStore engine setup
25
+
26
+ 1. Download Event Store From https://eventstore.org/downloads/ or docker
27
+
28
+ ` docker pull eventstore/eventstore`
29
+
30
+ 2. Run the Event Store server
31
+
32
+ `docker run --name eventstore -it -p 2113:2113 -p 1113:1113 eventstore/eventstore`
33
+
34
+ 3. Set Basic HTTP auth enviornment variables #below are defaults
35
+ - export EVENT_STORE_USER=admin
36
+ - export EVENT_STORE_PASSWORD=changeit
37
+
38
+ Ref: https://eventstore.org/docs/http-api/security
39
+
40
+ 4. Login to admin panel http://localhost:2113 and enable Projections for Event-Types
41
+
42
+ ### Configure EventStoreClient
43
+
44
+ Before you start, add this to the `initializer` or to the top of your script:
45
+
46
+ `EventStoreClient.configure`
47
+
48
+ ### Create Dummy event and dummy Handler
49
+
50
+ To test out the behavior, you'll need a sample event and handler to work with:
51
+
52
+ ```ruby
53
+ # Sample Event using dry-struct (recommended)
54
+ require 'dry-struct'
55
+ class SomethingHappened < Dry::Struct
56
+ attribute :data, EventStoreClient::Types::Strict::Hash
57
+ attribute :metadata, EventStoreClient::Types::Strict::Hash
58
+ end
59
+
60
+ # Sample Event without types check (not recommended)
61
+
62
+ class SomethingHappened < Dry::Struct
63
+ attr_reader :data, :metadata
64
+
65
+ private
66
+
67
+ def initialize(data: {}, metadata: {})
68
+ @data = data
69
+ @metadata = metadata
70
+ end
71
+ end
72
+
73
+ event = SomethingHappened.new(
74
+ data: { user_id: SecureRandom.uuid, title: "Something happened" },
75
+ metadata: {}
76
+ )
77
+ ```
78
+
79
+ Now create a handler. It can be anything, which responds to a `call` method
80
+ with an event being passed as an argument.
81
+
82
+ ```ruby
83
+ class DummyHandler
84
+ def self.call(event)
85
+ puts "Handled #{event.class.name}"
86
+ end
87
+ end
88
+ ```
89
+ ## Usage
90
+
91
+ ```ruby
92
+ # initialize the client
93
+ client = EventStoreClient::Client.new
94
+ ```
95
+
96
+ ### Publishing events
97
+
98
+ ```ruby
99
+ client.publish(stream: 'newstream', events: [event])
100
+ ```
101
+
102
+ ### Reading from a stream
103
+
104
+ ```ruby
105
+ events = client.read('newstream')
106
+ ```
107
+
108
+ **Changing reading direction
109
+
110
+ ```ruby
111
+ events = client.read('newstream', direction: 'backward') #default 'forward'
112
+ ```
113
+
114
+ ### Subscribing to events
115
+
116
+ # Using automatic pooling
117
+
118
+ ```ruby
119
+ client.subscribe(DummyHandler, to: [SomethingHappened])
120
+
121
+ # now try to publish several events
122
+ 10.times { client.publish(stream: 'newstream', events: [event]) }
123
+
124
+ You can also publish multiple events at once
125
+
126
+ events = (1..10).map { event }
127
+ client.publish(stream: 'newstream', events: events)
128
+
129
+ # .... wait a little bit ... Your handler should be called for every single event you publish
130
+ ```
131
+
132
+ ### Stop pooling
133
+
134
+ ```ruby
135
+ client.stop_pooling
136
+ ```
137
+
138
+ ## Contributing
139
+
140
+ Do you want to contribute? Welcome!
141
+
142
+ 1. Fork repository
143
+ 2. Create Issue
144
+ 3. Create PR ;)
145
+
146
+ ## License
147
+
148
+ The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ begin
4
+ require 'bundler/setup'
5
+ rescue LoadError
6
+ puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
7
+ end
8
+
9
+ require 'rdoc/task'
10
+
11
+ RDoc::Task.new(:rdoc) do |rdoc|
12
+ rdoc.rdoc_dir = 'rdoc'
13
+ rdoc.title = 'EventStoreClient'
14
+ rdoc.options << '--line-numbers'
15
+ rdoc.rdoc_files.include('README.md')
16
+ rdoc.rdoc_files.include('lib/**/*.rb')
17
+ end
18
+
19
+ require 'bundler/gem_tasks'
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module EventStoreClient
4
+ end
5
+
6
+ require 'event_store_client/configuration'
7
+ require 'event_store_client/types'
8
+ require 'event_store_client/event'
9
+
10
+ require 'event_store_client/serializer/json'
11
+
12
+ require 'event_store_client/mapper'
13
+
14
+ require 'event_store_client/endpoint'
15
+
16
+ require 'event_store_client/store_adapter'
17
+
18
+ require 'event_store_client/connection'
19
+
20
+ require 'event_store_client/subscription'
21
+ require 'event_store_client/subscriptions'
22
+ require 'event_store_client/broker'
23
+ require 'event_store_client/client'
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module EventStoreClient
4
+ class Broker
5
+ def call(subscriptions)
6
+ subscriptions.each do |subscription|
7
+ new_events = connection.consume_feed(subscription.stream, subscription.name)
8
+ next if new_events.none?
9
+ new_events.each do |event|
10
+ subscription.subscribers.each do |subscriber|
11
+ subscriber.call(event)
12
+ end
13
+ end
14
+ end
15
+ end
16
+
17
+ private
18
+
19
+ attr_reader :connection
20
+
21
+ def initialize(connection:)
22
+ @connection = connection
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,90 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'dry-struct'
4
+
5
+ module EventStoreClient
6
+ class Client
7
+ NoCallMethodOnSubscriber = Class.new(StandardError)
8
+
9
+ def publish(stream:, events:, expected_version: nil)
10
+ connection.publish(stream: stream, events: events, expected_version: expected_version)
11
+ end
12
+
13
+ def read(stream, direction: 'forward')
14
+ connection.read(stream, direction: direction)
15
+ end
16
+
17
+ def subscribe(subscriber, to: [], pooling: true)
18
+ raise NoCallMethodOnSubscriber unless subscriber.respond_to?(:call)
19
+ @subscriptions.create(subscriber, to)
20
+ pool if pooling
21
+ end
22
+
23
+ def pool(interval: 5)
24
+ return if @pooling_started
25
+ @pooling_started = true
26
+ thread1 = Thread.new do
27
+ loop do
28
+ create_pid_file
29
+ Thread.handle_interrupt(Interrupt => :never) {
30
+ begin
31
+ Thread.handle_interrupt(Interrupt => :immediate) {
32
+ broker.call(subscriptions)
33
+ }
34
+ rescue Exception => e
35
+ # When the thread had been interrupted or broker.call returned an error
36
+ sleep(interval) # wait for events to be processed
37
+ delete_pid_file
38
+ error_handler.call(e) if error_handler
39
+ ensure
40
+ # this code is run always
41
+ Thread.stop
42
+ end
43
+ }
44
+ end
45
+ end
46
+ thread2 = Thread.new do
47
+ loop { sleep 1; break unless thread1.alive?; thread1.run }
48
+ end
49
+ @threads = [thread1, thread2]
50
+ nil
51
+ end
52
+
53
+ def stop_pooling
54
+ return if @threads.none?
55
+ @threads.each do |thread|
56
+ thread.kill
57
+ end
58
+ @pooling_started = false
59
+ nil
60
+ end
61
+
62
+ attr_accessor :connection, :service_name
63
+
64
+ private
65
+
66
+ attr_reader :subscriptions, :broker, :error_handler
67
+
68
+ def config
69
+ EventStoreClient::Configuration.instance
70
+ end
71
+
72
+ def initialize
73
+ @threads = []
74
+ @connection ||= Connection.new
75
+ @error_handler ||= config.error_handler
76
+ @service_name ||= 'default'
77
+ @broker ||= Broker.new(connection: connection)
78
+ @subscriptions ||= Subscriptions.new(connection: connection, service: config.service_name)
79
+ end
80
+
81
+ def create_pid_file
82
+ return unless File.exists?(config.pid_path)
83
+ File.open(config.pid_path, 'w') { |file| file.write(SecureRandom.uuid) }
84
+ end
85
+
86
+ def delete_pid_file
87
+ File.delete(config.pid_path)
88
+ end
89
+ end
90
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'dry-struct'
4
+ require 'singleton'
5
+
6
+ module EventStoreClient
7
+ class Configuration
8
+ include Singleton
9
+
10
+ attr_accessor :host, :port, :per_page, :service_name, :mapper, :error_handler, :pid_path
11
+
12
+ def configure
13
+ yield(self) if block_given?
14
+ end
15
+
16
+ private
17
+
18
+ def initialize
19
+ @host = 'http://localhost'
20
+ @port = 2113
21
+ @per_page = 20
22
+ @pid_path = 'tmp/pool.pid'
23
+ @mapper = Mapper::Default.new
24
+ @service_name = 'default'
25
+ @error_handler = nil
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,76 @@
1
+ # frozen_string_literal: true
2
+
3
+ module EventStoreClient
4
+ class Connection
5
+ def publish(stream:, events:, expected_version: nil)
6
+ serialized_events = events.map { |event| mapper.serialize(event) }
7
+ client.append_to_stream(
8
+ stream, serialized_events, expected_version: expected_version
9
+ )
10
+ serialized_events
11
+ end
12
+
13
+ def read(stream, direction: 'forward')
14
+ response =
15
+ client.read(stream, start: 0, direction: direction)
16
+ return [] unless response.body&.present?
17
+ JSON.parse(response.body)['entries'].map do |entry|
18
+ event = EventStoreClient::Event.new(
19
+ id: entry['eventId'],
20
+ type: entry['eventType'],
21
+ data: entry['data'],
22
+ metadata: entry['metaData']
23
+ )
24
+ mapper.deserialize(event)
25
+ end
26
+ end
27
+
28
+ def delete_stream(stream); end
29
+
30
+ def subscribe(stream, name:)
31
+ client.subscribe_to_stream(stream, name)
32
+ end
33
+
34
+ def consume_feed(stream, subscription)
35
+ response = client.consume_feed(stream, subscription)
36
+ return [] unless response.body
37
+ body = JSON.parse(response.body)
38
+ ack_uri =
39
+ body['links'].find { |link| link['relation'] == 'ackAll' }.
40
+ try(:[], 'uri')
41
+ events = body['entries'].map do |entry|
42
+ event = EventStoreClient::Event.new(
43
+ id: entry['eventId'],
44
+ type: entry['eventType'],
45
+ data: entry['data'] || '{}',
46
+ metadata: entry['isMetaData'] ? entry['metaData'] : '{}'
47
+ )
48
+ mapper.deserialize(event)
49
+ end
50
+ client.ack(ack_uri)
51
+ events
52
+ end
53
+
54
+ private
55
+
56
+ attr_reader :host, :port, :mapper, :per_page
57
+
58
+ def config
59
+ EventStoreClient::Configuration.instance
60
+ end
61
+
62
+ def initialize
63
+ @host = config.host
64
+ @port = config.port
65
+ @per_page = config.per_page
66
+ @mapper = config.mapper
67
+ end
68
+
69
+ def client
70
+ @client ||=
71
+ EventStoreClient::StoreAdapter::Api::Client.new(
72
+ host: host, port: port, per_page: per_page
73
+ )
74
+ end
75
+ end
76
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'dry-struct'
4
+
5
+ module EventStoreClient
6
+ class Endpoint < Dry::Struct
7
+ def url
8
+ "#{host}:#{port}"
9
+ end
10
+
11
+ private
12
+
13
+ attribute :host, Types::String
14
+ attribute :port, Types::Coercible::Integer
15
+ end
16
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'dry-struct'
4
+ require 'securerandom'
5
+ require 'json'
6
+
7
+ module EventStoreClient
8
+ class Event < Dry::Struct
9
+ attr_reader :id
10
+
11
+ attribute :data, Types::Strict::String.default('{}')
12
+ attribute :metadata, Types::Strict::String.default('{}')
13
+ attribute :type, Types::Strict::String
14
+
15
+ private
16
+
17
+ def initialize(**args)
18
+ @id = SecureRandom.uuid
19
+ hash_meta =
20
+ JSON.parse(args[:metadata] || '{}').merge(created_at: Time.now)
21
+ args[:metadata] = JSON.generate(hash_meta)
22
+ super(args)
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ module EventStoreClient
4
+ module Mapper
5
+ end
6
+ end
7
+
8
+ require 'event_store_client/mapper/default'
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ module EventStoreClient
4
+ module Mapper
5
+ class Default
6
+ def serialize(event)
7
+ Event.new(
8
+ metadata: serializer.serialize(event.metadata),
9
+ data: serializer.serialize(event.data),
10
+ type: event.class.to_s
11
+ )
12
+ end
13
+
14
+ def deserialize(event)
15
+ metadata = serializer.deserialize(event.metadata)
16
+ data = serializer.deserialize(event.data)
17
+
18
+ Object.const_get(event.type).new(
19
+ metadata: metadata,
20
+ data: data
21
+ )
22
+ end
23
+
24
+ private
25
+
26
+ attr_reader :serializer
27
+
28
+ def initialize(serializer: Serializer::Json)
29
+ @serializer = serializer
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Serializer
4
+ module Json
5
+ def self.deserialize(data)
6
+ JSON.parse(data)
7
+ end
8
+
9
+ def self.serialize(data)
10
+ JSON.generate(data)
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ module EventStoreClient
4
+ module StoreAdapter
5
+ end
6
+ end
7
+
8
+ require 'event_store_client/store_adapter/api/request_method'
9
+ require 'event_store_client/store_adapter/api/connection'
10
+ require 'event_store_client/store_adapter/api/client'
@@ -0,0 +1,96 @@
1
+ # frozen_string_literal: true
2
+
3
+ module EventStoreClient
4
+ module StoreAdapter
5
+ module Api
6
+ class Client
7
+ def append_to_stream(stream_name, events, expected_version: nil)
8
+ headers = {
9
+ 'ES-ExpectedVersion' => expected_version.to_s
10
+ }.reject { |_key, val| val.empty? }
11
+
12
+ data = [events].flatten.map do |event|
13
+ {
14
+ eventId: event.id,
15
+ eventType: event.type,
16
+ data: event.data,
17
+ metadata: event.metadata
18
+ }
19
+ end
20
+
21
+ make_request(:post, "/streams/#{stream_name}", body: data, headers: headers)
22
+ end
23
+
24
+ def delete_stream(stream_name, hard_delete)
25
+ headers = JSON_HEADERS.merge('ES-HardDelete' => hard_delete.to_s)
26
+ make_request(:delete, "/streams/#{stream_name}", {}, headers)
27
+ end
28
+
29
+ def read(stream_name, direction: 'forward', start: 0, count: per_page)
30
+ make_request(:get, "/streams/#{stream_name}/#{start}/#{direction}/#{count}")
31
+ end
32
+
33
+ def subscribe_to_stream(
34
+ stream_name, subscription_name, stats: true, start_from: 0, retries: 5
35
+ )
36
+ make_request(
37
+ :put,
38
+ "/subscriptions/#{stream_name}/#{subscription_name}",
39
+ body: {
40
+ extraStatistics: stats,
41
+ startFrom: start_from,
42
+ maxRetryCount: retries,
43
+ resolveLinkTos: true
44
+ },
45
+ headers: {
46
+ "Content-Type" => "application/json"
47
+ }
48
+ )
49
+ end
50
+
51
+ def consume_feed(
52
+ stream_name,
53
+ subscription_name,
54
+ start: 0,
55
+ count: 1,
56
+ long_pool: 0
57
+ )
58
+ headers = long_pool > 0 ? { "ES-LongPoll" => "#{long_pool}" } : {}
59
+ headers['Content-Type'] = 'application/vnd.eventstore.competingatom+json'
60
+ headers['Accept'] = 'application/vnd.eventstore.competingatom+json'
61
+ make_request(
62
+ :get,
63
+ "/subscriptions/#{stream_name}/#{subscription_name}/#{count}",
64
+ headers: headers
65
+ )
66
+ end
67
+
68
+ def ack(url)
69
+ make_request(:post, url)
70
+ end
71
+
72
+ private
73
+
74
+ attr_reader :endpoint, :per_page
75
+
76
+ def initialize(host:, port:, per_page: 20)
77
+ @endpoint = Endpoint.new(host: host, port: port)
78
+ @per_page = per_page
79
+ end
80
+
81
+ def make_request(method_name, path, body: {}, headers: {})
82
+ method = RequestMethod.new(method_name)
83
+ connection.send(method.to_s, path) do |req|
84
+ req.headers = req.headers.merge(headers)
85
+ req.body = body.to_json
86
+ req.params['embed'] = 'body' if method == :get
87
+ end
88
+ end
89
+
90
+ def connection
91
+ @connection ||= Api::Connection.new(endpoint).call
92
+ end
93
+ end
94
+ end
95
+ end
96
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'faraday'
4
+
5
+ module EventStoreClient
6
+ module StoreAdapter
7
+ module Api
8
+ class Connection
9
+ def call
10
+ Faraday.new(
11
+ url: endpoint.url,
12
+ headers: DEFAULT_HEADERS
13
+ ) do |conn|
14
+ conn.basic_auth(ENV['EVENT_STORE_USER'], ENV['EVENT_STORE_PASSWORD'])
15
+ conn.adapter Faraday.default_adapter
16
+ end
17
+ end
18
+
19
+ private
20
+
21
+ def initialize(endpoint)
22
+ @endpoint = endpoint
23
+ end
24
+
25
+ attr_reader :endpoint
26
+
27
+ DEFAULT_HEADERS = {
28
+ 'Content-Type' => 'application/vnd.eventstore.events+json'
29
+ # 'Accept' => 'application/vnd.eventstore.atom+json',
30
+ # 'ES-EventType' => 'UserRegistered',
31
+ # 'ES-EventId' => SecureRandom.uuid
32
+ }.freeze
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module EventStoreClient
4
+ module StoreAdapter
5
+ module Api
6
+ class RequestMethod
7
+ InvalidMethodError = Class.new(StandardError)
8
+ def ==(other)
9
+ name == other.to_s
10
+ end
11
+
12
+ def to_s
13
+ name
14
+ end
15
+
16
+ private
17
+
18
+ attr_reader :name
19
+
20
+ def initialize(name)
21
+ raise InvalidMethodError unless name.to_s.in?(SUPPORTED_METHODS)
22
+
23
+ @name = name.to_s
24
+ end
25
+
26
+ SUPPORTED_METHODS = %w[get post put].freeze
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,77 @@
1
+ # frozen_string_literal: true
2
+
3
+ module EventStoreClient
4
+ module StoreAdapter
5
+ class InMemory
6
+ attr_reader :event_store
7
+
8
+ def append_to_stream(stream_name, events, expected_version: nil)
9
+ event_store[stream_name] = [] unless event_store.key?(stream_name)
10
+
11
+ [events].flatten.each do |event|
12
+ event_store[stream_name].unshift(
13
+ 'eventId' => event.id,
14
+ 'data' => event.data,
15
+ 'eventType' => event.type,
16
+ 'metadata' => event.metadata,
17
+ 'positionEventNumber' => event_store[stream_name].length
18
+ )
19
+ end
20
+ end
21
+
22
+ def delete_stream(stream_name, hard_delete: false)
23
+ event_store.delete(stream_name)
24
+ end
25
+
26
+ def read_stream_backward(stream_name, start: 0, count: per_page)
27
+ return [] unless event_store.key?(stream_name)
28
+
29
+ start = start == 0 ? event_store[stream_name].length - 1 : start
30
+ last_index = start - count
31
+ entries = event_store[stream_name].select do |event|
32
+ event['positionEventNumber'] > last_index &&
33
+ event['positionEventNumber'] <= start
34
+ end
35
+ {
36
+ 'entries' => entries,
37
+ 'links' => links(stream_name, last_index, 'next', entries, count)
38
+ }
39
+ end
40
+
41
+ def read_stream_forward(stream_name, start: 0, count: per_page)
42
+ return [] unless event_store.key?(stream_name)
43
+
44
+ last_index = start + count
45
+ entries = event_store[stream_name].reverse.select do |event|
46
+ event['positionEventNumber'] < last_index &&
47
+ event['positionEventNumber'] >= start
48
+ end
49
+ {
50
+ 'entries' => entries,
51
+ 'links' => links(stream_name, last_index, 'previous', entries, count)
52
+ }
53
+ end
54
+
55
+ private
56
+
57
+ attr_reader :endpoint, :per_page
58
+
59
+ def initialize(host:, port:, per_page: 20)
60
+ @endpoint = Endpoint.new(host: host, port: port)
61
+ @per_page = per_page
62
+ @event_store = {}
63
+ end
64
+
65
+ def links(stream_name, batch_size, direction, entries, count)
66
+ if entries.empty? || batch_size < 0
67
+ []
68
+ else
69
+ [{
70
+ 'uri' => "http://#{endpoint.url}/streams/#{stream_name}/#{batch_size}/#{direction}/#{count}",
71
+ 'relation' => direction
72
+ }]
73
+ end
74
+ end
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module EventStoreClient
4
+ class Subscription
5
+ attr_accessor :subscribers
6
+ attr_reader :stream, :name
7
+
8
+ private
9
+ def initialize(type:, name:)
10
+ @name = name
11
+ @subscribers = []
12
+ @stream = "$et-#{type}"
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ module EventStoreClient
4
+ class Subscriptions
5
+ def create(subscriber, event_types)
6
+ event_types.each do |type|
7
+ subscription = subscriptions[type.to_s] || Subscription.new(type: type, name: service)
8
+ subscription.subscribers |= [subscriber]
9
+ create_subscription(subscription) unless @subscriptions.key?(type.to_s)
10
+ @subscriptions[type.to_s] = subscription
11
+ end
12
+ end
13
+
14
+ def each
15
+ subscriptions.values.each do |subscription|
16
+ yield(subscription)
17
+ end
18
+ end
19
+
20
+ def get_updates(subscription)
21
+ connection.consume_feed(subscription.stream, subscription.name)
22
+ end
23
+
24
+ private
25
+
26
+ def create_subscription(subscription)
27
+ connection.subscribe(subscription.stream, name: subscription.name)
28
+ end
29
+
30
+ attr_reader :connection, :subscriptions, :service
31
+
32
+ def initialize(connection:, service: 'default')
33
+ @connection = connection
34
+ @service = service
35
+ @subscriptions = {}
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'dry-types'
4
+
5
+ module EventStoreClient
6
+ module Types
7
+ include Dry.Types()
8
+ end
9
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module EventStoreClient
4
+ VERSION = '0.1.0'
5
+ end
metadata ADDED
@@ -0,0 +1,122 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: event_store_client
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Sebastian Wilgosz
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2019-12-02 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: dry-struct
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: 1.1.1
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: 1.1.1
27
+ - !ruby/object:Gem::Dependency
28
+ name: faraday
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: 0.17.0
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: 0.17.0
41
+ - !ruby/object:Gem::Dependency
42
+ name: rss
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: 0.2.8
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: 0.2.8
55
+ - !ruby/object:Gem::Dependency
56
+ name: rspec
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ description: Easy to use client for event-sources applications written in ruby
70
+ email:
71
+ - sebastian@driggl.com
72
+ executables: []
73
+ extensions: []
74
+ extra_rdoc_files: []
75
+ files:
76
+ - MIT-LICENSE
77
+ - README.md
78
+ - Rakefile
79
+ - lib/event_store_client.rb
80
+ - lib/event_store_client/broker.rb
81
+ - lib/event_store_client/client.rb
82
+ - lib/event_store_client/configuration.rb
83
+ - lib/event_store_client/connection.rb
84
+ - lib/event_store_client/endpoint.rb
85
+ - lib/event_store_client/event.rb
86
+ - lib/event_store_client/mapper.rb
87
+ - lib/event_store_client/mapper/default.rb
88
+ - lib/event_store_client/serializer/json.rb
89
+ - lib/event_store_client/store_adapter.rb
90
+ - lib/event_store_client/store_adapter/api/client.rb
91
+ - lib/event_store_client/store_adapter/api/connection.rb
92
+ - lib/event_store_client/store_adapter/api/request_method.rb
93
+ - lib/event_store_client/store_adapter/in_memory.rb
94
+ - lib/event_store_client/subscription.rb
95
+ - lib/event_store_client/subscriptions.rb
96
+ - lib/event_store_client/types.rb
97
+ - lib/event_store_client/version.rb
98
+ homepage: https://github.com/yousty/event_store_client
99
+ licenses:
100
+ - MIT
101
+ metadata:
102
+ allowed_push_host: https://rubygems.org
103
+ post_install_message:
104
+ rdoc_options: []
105
+ require_paths:
106
+ - lib
107
+ required_ruby_version: !ruby/object:Gem::Requirement
108
+ requirements:
109
+ - - ">="
110
+ - !ruby/object:Gem::Version
111
+ version: '0'
112
+ required_rubygems_version: !ruby/object:Gem::Requirement
113
+ requirements:
114
+ - - ">="
115
+ - !ruby/object:Gem::Version
116
+ version: '0'
117
+ requirements: []
118
+ rubygems_version: 3.0.4
119
+ signing_key:
120
+ specification_version: 4
121
+ summary: Ruby integration for https://eventstore.org
122
+ test_files: []