event_store_client 0.2.3 → 0.2.4

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: 650872ba0eecbebfc09a26b58d3ff593bfd3d00db36a835e153233b665673f71
4
- data.tar.gz: e202dc48ce4a69d345e6fe4cfc6fe734d362e2d7e9b7c6d1d865511e84d9a255
3
+ metadata.gz: cc9b9f123804c920a08c07937465ae04afcd971d784912099f06822725742901
4
+ data.tar.gz: 0e5e5e7d863180a90fa20a6c6d1d2b8e76b479cb50489910af2327a33e0d2c7f
5
5
  SHA512:
6
- metadata.gz: e21ef0625b5e6b3faba04c19eda00efc736e408c787b6d60184eb666bc9ff88ff24a53f46a53efccafc0bd6c840c6beb42bd31229811115ba9ecbb9842c5c95c
7
- data.tar.gz: 68c5e7b7150857ca2091505ea6180f219ba38c2156751d8706f719acf47755da7f85ed22c420fce0583f8226f8c7603302f29038c0c6b39117bde0e78f230b1e
6
+ metadata.gz: 04e46ab7a710fc53c7be0d22bbadd59e6a075495174b88447cb82f054de5dcfa02607a1bf7cc03392952bdabb6dffc68ec264575264bd1fb3a9d5ac32447caea
7
+ data.tar.gz: 188cff27182fe3e6f83250c70a3a4a5cab9814b7ef1ce61a624af2bcd235d4cf950723e7551d8ec2612f64923b117592341ea4ada03fe44660c6f2f7713f05ce
@@ -1,27 +1,19 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module EventStoreClient
4
- def self.configure(&block)
5
- config = Configuration.instance
6
- config.configure(&block)
7
- end
8
4
  end
9
5
 
10
- require 'event_store_client/configuration'
11
6
  require 'event_store_client/types'
12
7
  require 'event_store_client/event'
13
8
  require 'event_store_client/deserialized_event'
14
-
15
9
  require 'event_store_client/serializer/json'
16
10
 
17
11
  require 'event_store_client/mapper'
18
12
 
19
- require 'event_store_client/endpoint'
13
+ require 'event_store_client/configuration'
20
14
 
21
15
  require 'event_store_client/store_adapter'
22
16
 
23
- require 'event_store_client/connection'
24
-
25
17
  require 'event_store_client/subscription'
26
18
  require 'event_store_client/subscriptions'
27
19
  require 'event_store_client/broker'
@@ -4,9 +4,10 @@ module EventStoreClient
4
4
  class Broker
5
5
  def call(subscriptions)
6
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 { |event| subscription.subscriber.call(event) }
7
+ res = connection.consume_feed(subscription.stream, subscription.name) || []
8
+ next if res[:events].none?
9
+ res[:events].each { |event| subscription.subscriber.call(event) }
10
+ connection.ack(res[:ack_uri])
10
11
  end
11
12
  end
12
13
 
@@ -8,13 +8,21 @@ module EventStoreClient
8
8
  WrongExpectedEventVersion = Class.new(StandardError)
9
9
 
10
10
  def publish(stream:, events:, expected_version: nil)
11
- connection.publish(stream: stream, events: events, expected_version: expected_version)
11
+ connection.append_to_stream(stream, events, expected_version: expected_version)
12
12
  rescue StoreAdapter::Api::Client::WrongExpectedEventVersion => e
13
13
  raise WrongExpectedEventVersion.new(e.message)
14
14
  end
15
15
 
16
- def read(stream, direction: 'forward', start: 0, all: false)
17
- connection.read(stream, direction: direction, start: start, all: all)
16
+ def read(stream, direction: 'forward', start: 0, all: false, resolve_links: true)
17
+ if all
18
+ connection.read_all_from_stream(
19
+ stream, start: start, direction: direction, resolve_links: resolve_links
20
+ )
21
+ else
22
+ connection.read(
23
+ stream, start: start, direction: direction, resolve_links: resolve_links
24
+ )
25
+ end
18
26
  end
19
27
 
20
28
  def subscribe(subscriber, to: [], polling: true)
@@ -81,12 +89,12 @@ module EventStoreClient
81
89
  attr_reader :subscriptions, :broker, :error_handler
82
90
 
83
91
  def config
84
- EventStoreClient::Configuration.instance
92
+ EventStoreClient.config
85
93
  end
86
94
 
87
95
  def initialize
88
96
  @threads = []
89
- @connection ||= Connection.new
97
+ @connection ||= EventStoreClient.adapter
90
98
  @error_handler ||= config.error_handler
91
99
  @service_name ||= 'default'
92
100
  @broker ||= Broker.new(connection: connection)
@@ -1,28 +1,47 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'dry-struct'
4
- require 'singleton'
3
+ require 'dry-configurable'
5
4
 
6
5
  module EventStoreClient
7
- class Configuration
8
- include Singleton
6
+ extend Dry::Configurable
9
7
 
10
- attr_accessor :per_page, :service_name, :mapper, :error_handler, :pid_path, :adapter
8
+ # Supported adapters: %i[api in_memory]
9
+ #
10
+ setting :adapter, :api
11
11
 
12
- def configure
13
- yield(self) if block_given?
14
- end
12
+ setting :error_handler
13
+ setting :eventstore_url, 'http://localhost:2113' do |value|
14
+ value.is_a?(URI) ? value : URI(value)
15
+ end
16
+
17
+ setting :eventstore_user, 'admin'
18
+ setting :eventstore_password, 'changeit'
19
+
20
+ setting :db_port, 2113
21
+
22
+ setting :per_page, 20
23
+ setting :pid_path, 'tmp/poll.pid'
15
24
 
16
- private
25
+ setting :service_name, 'default'
17
26
 
18
- def initialize
19
- @per_page = 20
20
- @pid_path = 'tmp/poll.pid'
21
- @mapper = Mapper::Default.new
22
- @service_name = 'default'
23
- @error_handler = nil
24
- @adapter = EventStoreClient::StoreAdapter::Api::Client.new(
25
- host: 'http://localhost', port: 2113, per_page: per_page
27
+ setting :mapper, Mapper::Default.new
28
+
29
+ def self.configure
30
+ yield(config) if block_given?
31
+ end
32
+
33
+ def self.adapter
34
+ case config.adapter
35
+ when :api
36
+ StoreAdapter::Api::Client.new(
37
+ config.eventstore_url,
38
+ per_page: config.per_page,
39
+ mapper: config.mapper,
40
+ connection_options: {}
41
+ )
42
+ else
43
+ StoreAdapter::InMemory.new(
44
+ mapper: config.mapper, per_page: config.per_page
26
45
  )
27
46
  end
28
47
  end
@@ -5,7 +5,8 @@ module EventStoreClient
5
5
  class Default
6
6
  def serialize(event)
7
7
  Event.new(
8
- type: event.class.to_s,
8
+ id: event.respond_to?(:id) ? event.id : nil,
9
+ type: (event.respond_to?(:type) ? event.type : nil) || event.class.to_s,
9
10
  data: serializer.serialize(event.data),
10
11
  metadata: serializer.serialize(event.metadata)
11
12
  )
@@ -7,6 +7,8 @@ module Serializer
7
7
  end
8
8
 
9
9
  def self.serialize(data)
10
+ return data if data.is_a?(String)
11
+
10
12
  JSON.generate(data)
11
13
  end
12
14
  end
@@ -7,14 +7,16 @@ module EventStoreClient
7
7
  WrongExpectedEventVersion = Class.new(StandardError)
8
8
 
9
9
  def append_to_stream(stream_name, events, expected_version: nil)
10
+ serialized_events = events.map { |event| mapper.serialize(event) }
10
11
  headers = {
11
12
  'ES-ExpectedVersion' => expected_version&.to_s
12
13
  }.reject { |_key, val| val.nil? || val.empty? }
13
14
 
14
- data = build_events_data(events)
15
+ data = build_events_data(serialized_events)
15
16
  response = make_request(:post, "/streams/#{stream_name}", body: data, headers: headers)
16
17
  validate_response(response, expected_version)
17
18
  response
19
+ serialized_events
18
20
  end
19
21
 
20
22
  def delete_stream(stream_name, hard_delete: false)
@@ -31,11 +33,39 @@ module EventStoreClient
31
33
  'Accept' => 'application/vnd.eventstore.atom+json'
32
34
  }
33
35
 
34
- make_request(
36
+ response = make_request(
35
37
  :get,
36
38
  "/streams/#{stream_name}/#{start}/#{direction}/#{count}",
37
39
  headers: headers
38
40
  )
41
+ return [] if response.body.nil? || response.body.empty?
42
+ JSON.parse(response.body)['entries'].map do |entry|
43
+ deserialize_event(entry)
44
+ end.reverse
45
+ end
46
+
47
+ def read_all_from_stream(stream, start: 0, resolve_links: true)
48
+ count = per_page
49
+ events = []
50
+ failed_requests_count = 0
51
+
52
+ while failed_requests_count < 3
53
+ begin
54
+ response =
55
+ read(stream, start: start, direction: 'forward', resolve_links: resolve_links)
56
+ failed_requests_count += 1 && next unless response.success? || response.status == 404
57
+ rescue Faraday::ConnectionFailed
58
+ failed_requests_count += 1
59
+ next
60
+ end
61
+ failed_requests_count = 0
62
+ break if response.body.nil? || response.body.empty?
63
+ entries = JSON.parse(response.body)['entries']
64
+ break if entries.empty?
65
+ events += entries.map { |entry| deserialize_event(entry) }.reverse
66
+ start += count
67
+ end
68
+ events
39
69
  end
40
70
 
41
71
  def join_streams(name, streams)
@@ -79,18 +109,31 @@ module EventStoreClient
79
109
  subscription_name,
80
110
  count: 1,
81
111
  long_poll: 0,
82
- resolve_links: true
112
+ resolve_links: true,
113
+ per_page: 20
83
114
  )
84
115
  headers = long_poll.positive? ? { 'ES-LongPoll' => long_poll.to_s } : {}
85
116
  headers['Content-Type'] = 'application/vnd.eventstore.competingatom+json'
86
117
  headers['Accept'] = 'application/vnd.eventstore.competingatom+json'
87
118
  headers['ES-ResolveLinktos'] = resolve_links.to_s
88
119
 
89
- make_request(
120
+ response = make_request(
90
121
  :get,
91
122
  "/subscriptions/#{stream_name}/#{subscription_name}/#{count}",
92
123
  headers: headers
93
124
  )
125
+
126
+ return [] if response.body || response.body.empty?
127
+
128
+ body = JSON.parse(response.body)
129
+
130
+ ack_info = body['links'].find { |link| link['relation'] == 'ackAll' }
131
+ return unless ack_info
132
+ ack_uri = ack_info['uri']
133
+ events = body['entries'].map do |entry|
134
+ deserialize_event(entry)
135
+ end
136
+ { ack_uri: ack_uri, events: events }
94
137
  end
95
138
 
96
139
  def link_to(stream_name, events, expected_version: nil)
@@ -106,7 +149,7 @@ module EventStoreClient
106
149
  headers: headers
107
150
  )
108
151
  validate_response(response, expected_version)
109
- response
152
+ true
110
153
  end
111
154
 
112
155
  def ack(url)
@@ -115,11 +158,12 @@ module EventStoreClient
115
158
 
116
159
  private
117
160
 
118
- attr_reader :endpoint, :per_page, :connection_options
161
+ attr_reader :uri, :per_page, :connection_options, :mapper
119
162
 
120
- def initialize(host:, port:, per_page: 20, connection_options: {})
121
- @endpoint = Endpoint.new(host: host, port: port)
163
+ def initialize(uri, per_page: 20, mapper:, connection_options: {})
164
+ @uri = uri
122
165
  @per_page = per_page
166
+ @mapper = mapper
123
167
  @connection_options = connection_options
124
168
  end
125
169
 
@@ -154,7 +198,7 @@ module EventStoreClient
154
198
  end
155
199
 
156
200
  def connection
157
- @connection ||= Api::Connection.new(endpoint, connection_options).call
201
+ @connection ||= Api::Connection.new(uri, connection_options).call
158
202
  end
159
203
 
160
204
  def validate_response(resp, expected_version)
@@ -164,6 +208,18 @@ module EventStoreClient
164
208
  "expected: #{expected_version}"
165
209
  )
166
210
  end
211
+
212
+ def deserialize_event(entry)
213
+ event = EventStoreClient::Event.new(
214
+ id: entry['eventId'],
215
+ title: entry['title'],
216
+ type: entry['eventType'],
217
+ data: entry['data'] || '{}',
218
+ metadata: entry['isMetaData'] ? entry['metaData'] : '{}'
219
+ )
220
+
221
+ mapper.deserialize(event)
222
+ end
167
223
  end
168
224
  end
169
225
  end
@@ -9,23 +9,27 @@ module EventStoreClient
9
9
  def call
10
10
  Faraday.new(
11
11
  {
12
- url: endpoint.url,
12
+ url: uri.to_s,
13
13
  headers: DEFAULT_HEADERS
14
14
  }.merge(options)
15
15
  ) do |conn|
16
- conn.basic_auth(ENV['EVENT_STORE_USER'], ENV['EVENT_STORE_PASSWORD'])
16
+ conn.basic_auth(config.eventstore_user, config.eventstore_password)
17
17
  conn.adapter Faraday.default_adapter
18
18
  end
19
19
  end
20
20
 
21
21
  private
22
22
 
23
- def initialize(endpoint, options = {})
24
- @endpoint = endpoint
23
+ def config
24
+ EventStoreClient.config
25
+ end
26
+
27
+ def initialize(uri, options = {})
28
+ @uri = uri
25
29
  @options = options
26
30
  end
27
31
 
28
- attr_reader :endpoint, :options
32
+ attr_reader :uri, :options
29
33
 
30
34
  DEFAULT_HEADERS = {
31
35
  'Content-Type' => 'application/vnd.eventstore.events+json'
@@ -33,7 +33,27 @@ module EventStoreClient
33
33
  read_stream_backward(stream_name, start: start)
34
34
  end
35
35
 
36
- Response.new(response.to_json, 200)
36
+ res = Response.new(response.to_json, 200)
37
+
38
+ return [] if res.body.nil? || res.body.empty?
39
+ JSON.parse(res.body)['entries'].map do |entry|
40
+ deserialize_event(entry)
41
+ end.reverse
42
+ end
43
+
44
+ def read_all_from_stream(stream_name, direction: 'forward', start: 0, resolve_links: true)
45
+ response =
46
+ if direction == 'forward'
47
+ read_stream_forward(stream_name, start: start)
48
+ else
49
+ read_stream_backward(stream_name, start: start)
50
+ end
51
+ res = Response.new(response.to_json, 200)
52
+
53
+ return [] if res.body.nil? || res.body.empty?
54
+ JSON.parse(res.body)['entries'].map do |entry|
55
+ deserialize_event(entry)
56
+ end.reverse
37
57
  end
38
58
 
39
59
  def subscribe_to_stream(stream_name, subscription_name, **)
@@ -54,6 +74,7 @@ module EventStoreClient
54
74
 
55
75
  def link_to(stream_name, events, **)
56
76
  append_to_stream(stream_name, events)
77
+ events
57
78
  end
58
79
 
59
80
  def ack(url)
@@ -64,11 +85,11 @@ module EventStoreClient
64
85
 
65
86
  private
66
87
 
67
- attr_reader :endpoint, :per_page
88
+ attr_reader :per_page, :mapper
68
89
 
69
- def initialize(host:, port:, per_page: 20)
70
- @endpoint = Endpoint.new(host: host, port: port)
90
+ def initialize(mapper:, per_page: 20)
71
91
  @per_page = per_page
92
+ @mapper = mapper
72
93
  @event_store = {}
73
94
  end
74
95
 
@@ -115,11 +136,25 @@ module EventStoreClient
115
136
  else
116
137
  [{
117
138
  'uri' =>
118
- "http://#{endpoint.url}/streams/#{stream_name}/#{batch_size}/#{direction}/#{count}",
139
+ "/streams/#{stream_name}/#{batch_size}/#{direction}/#{count}",
119
140
  'relation' => direction
120
141
  }]
121
142
  end
122
143
  end
144
+
145
+ private
146
+
147
+ def deserialize_event(entry)
148
+ event = EventStoreClient::Event.new(
149
+ id: entry['eventId'],
150
+ title: entry['title'],
151
+ type: entry['eventType'],
152
+ data: entry['data'] || '{}',
153
+ metadata: entry['isMetaData'] ? entry['metaData'] : '{}'
154
+ )
155
+
156
+ mapper.deserialize(event)
157
+ end
123
158
  end
124
159
  end
125
160
  end
@@ -25,8 +25,9 @@ module EventStoreClient
25
25
  private
26
26
 
27
27
  def create_subscription(subscription)
28
+ # store position somewhere.
28
29
  connection.join_streams(subscription.name, subscription.observed_streams)
29
- connection.subscribe(subscription.stream, name: subscription.name)
30
+ connection.subscribe_to_stream(subscription.stream, name: subscription.name)
30
31
  end
31
32
 
32
33
  attr_reader :connection, :subscriptions, :service
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module EventStoreClient
4
- VERSION = '0.2.3'
4
+ VERSION = '0.2.4'
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: event_store_client
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.3
4
+ version: 0.2.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Sebastian Wilgosz
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2020-10-21 00:00:00.000000000 Z
11
+ date: 2020-12-16 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: dry-schema
@@ -42,16 +42,16 @@ dependencies:
42
42
  name: faraday
43
43
  requirement: !ruby/object:Gem::Requirement
44
44
  requirements:
45
- - - ">="
45
+ - - "~>"
46
46
  - !ruby/object:Gem::Version
47
- version: 0.17.0
47
+ version: '1.0'
48
48
  type: :runtime
49
49
  prerelease: false
50
50
  version_requirements: !ruby/object:Gem::Requirement
51
51
  requirements:
52
- - - ">="
52
+ - - "~>"
53
53
  - !ruby/object:Gem::Version
54
- version: 0.17.0
54
+ version: '1.0'
55
55
  - !ruby/object:Gem::Dependency
56
56
  name: rss
57
57
  requirement: !ruby/object:Gem::Requirement
@@ -66,6 +66,20 @@ dependencies:
66
66
  - - ">="
67
67
  - !ruby/object:Gem::Version
68
68
  version: 0.2.8
69
+ - !ruby/object:Gem::Dependency
70
+ name: dry-configurable
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: '0.11'
76
+ type: :runtime
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: '0.11'
69
83
  - !ruby/object:Gem::Dependency
70
84
  name: rspec
71
85
  requirement: !ruby/object:Gem::Requirement
@@ -108,12 +122,10 @@ files:
108
122
  - lib/event_store_client/broker.rb
109
123
  - lib/event_store_client/client.rb
110
124
  - lib/event_store_client/configuration.rb
111
- - lib/event_store_client/connection.rb
112
125
  - lib/event_store_client/data_decryptor.rb
113
126
  - lib/event_store_client/data_encryptor.rb
114
127
  - lib/event_store_client/deserialized_event.rb
115
128
  - lib/event_store_client/encryption_metadata.rb
116
- - lib/event_store_client/endpoint.rb
117
129
  - lib/event_store_client/event.rb
118
130
  - lib/event_store_client/mapper.rb
119
131
  - lib/event_store_client/mapper/default.rb
@@ -148,7 +160,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
148
160
  - !ruby/object:Gem::Version
149
161
  version: '0'
150
162
  requirements: []
151
- rubygems_version: 3.0.8
163
+ rubygems_version: 3.1.4
152
164
  signing_key:
153
165
  specification_version: 4
154
166
  summary: Ruby integration for https://eventstore.org
@@ -1,112 +0,0 @@
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:, start:, all:, resolve_links: true)
14
- return read_all_from_stream(stream, start: start, resolve_links: resolve_links) if all
15
- read_from_stream(
16
- stream, direction: direction, start: start, resolve_links: resolve_links
17
- )
18
- end
19
-
20
- def delete_stream(stream); end
21
-
22
- def join_streams(name, streams)
23
- client.join_streams(name, streams)
24
- end
25
-
26
- def subscribe(stream, name:)
27
- client.subscribe_to_stream(stream, name)
28
- end
29
-
30
- def consume_feed(stream, subscription)
31
- response = client.consume_feed(stream, subscription)
32
- return [] unless response.body
33
- body = JSON.parse(response.body)
34
-
35
- ack = body['links'].find { |link| link['relation'] == 'ackAll' }
36
- return unless ack
37
- ack_uri = ack['uri']
38
- events = body['entries'].map do |entry|
39
- deserialize_event(entry)
40
- end
41
- client.ack(ack_uri)
42
- events
43
- end
44
-
45
- def link_to(stream, events, expected_version: nil)
46
- client.link_to(stream, events, expected_version: expected_version)
47
-
48
- true
49
- end
50
-
51
- private
52
-
53
- attr_reader :mapper, :per_page, :client
54
-
55
- def config
56
- EventStoreClient::Configuration.instance
57
- end
58
-
59
- def initialize
60
- @per_page = config.per_page
61
- @mapper = config.mapper
62
- @client = config.adapter
63
- end
64
-
65
- def read_from_stream(stream, direction:, start:, resolve_links:)
66
- response =
67
- client.read(
68
- stream, start: start, direction: direction, resolve_links: resolve_links
69
- )
70
- return [] if response.body.nil? || response.body.empty?
71
- JSON.parse(response.body)['entries'].map do |entry|
72
- deserialize_event(entry)
73
- end.reverse
74
- end
75
-
76
- def read_all_from_stream(stream, start:, resolve_links:)
77
- count = per_page
78
- events = []
79
- failed_requests_count = 0
80
-
81
- while failed_requests_count < 3
82
- begin
83
- response =
84
- client.read(stream, start: start, direction: 'forward', resolve_links: resolve_links)
85
- failed_requests_count += 1 && next unless response.success? || response.status == 404
86
- rescue Faraday::ConnectionFailed
87
- failed_requests_count += 1
88
- next
89
- end
90
- failed_requests_count = 0
91
- break if response.body.nil? || response.body.empty?
92
- entries = JSON.parse(response.body)['entries']
93
- break if entries.empty?
94
- events += entries.map { |entry| deserialize_event(entry) }.reverse
95
- start += count
96
- end
97
- events
98
- end
99
-
100
- def deserialize_event(entry)
101
- event = EventStoreClient::Event.new(
102
- id: entry['eventId'],
103
- title: entry['title'],
104
- type: entry['eventType'],
105
- data: entry['data'] || '{}',
106
- metadata: entry['isMetaData'] ? entry['metaData'] : '{}'
107
- )
108
-
109
- mapper.deserialize(event)
110
- end
111
- end
112
- end
@@ -1,14 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'dry-struct'
4
-
5
- module EventStoreClient
6
- class Endpoint < Dry::Struct
7
- attribute :host, Types::String
8
- attribute :port, Types::Coercible::Integer
9
-
10
- def url
11
- "#{host}:#{port}"
12
- end
13
- end
14
- end