event_store_client 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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: []