oanda_api 0.8.3 → 0.9.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/{.rspec_non_jruby → .rspec} +0 -0
- data/CHANGELOG.md +11 -0
- data/Gemfile +5 -0
- data/README.md +29 -9
- data/lib/oanda_api.rb +3 -0
- data/lib/oanda_api/client/client.rb +3 -3
- data/lib/oanda_api/client/namespace_proxy.rb +14 -9
- data/lib/oanda_api/client/token_client.rb +2 -3
- data/lib/oanda_api/client/username_client.rb +1 -1
- data/lib/oanda_api/configuration.rb +3 -3
- data/lib/oanda_api/errors.rb +4 -1
- data/lib/oanda_api/resource/heartbeat.rb +13 -0
- data/lib/oanda_api/resource_base.rb +17 -3
- data/lib/oanda_api/resource_collection.rb +7 -8
- data/lib/oanda_api/streaming/client.rb +153 -0
- data/lib/oanda_api/streaming/request.rb +135 -0
- data/lib/oanda_api/utils/utils.rb +2 -2
- data/lib/oanda_api/version.rb +1 -1
- data/spec/oanda_api/resource_base_spec.rb +47 -0
- data/spec/oanda_api/resource_collection_spec.rb +2 -2
- data/spec/oanda_api/streaming/client_spec.rb +132 -0
- data/spec/oanda_api/streaming/request_spec.rb +172 -0
- data/spec/spec_helper.rb +4 -0
- metadata +121 -138
@@ -0,0 +1,135 @@
|
|
1
|
+
module OandaAPI
|
2
|
+
module Streaming
|
3
|
+
# An HTTP 1.1 streaming request. Used to create a persistent connection
|
4
|
+
# with the server and continuously download a stream of resource
|
5
|
+
# representations. Resources are emitted as {OandaAPI::ResourceBase}
|
6
|
+
# instances.
|
7
|
+
#
|
8
|
+
# @!attribute [rw] client
|
9
|
+
# @return [OandaAPI::Streaming::Client] a streaming client instance.
|
10
|
+
#
|
11
|
+
# @!attribute [rw] emit_heartbeats
|
12
|
+
# @return [boolean]
|
13
|
+
#
|
14
|
+
# @!attribute [r] uri
|
15
|
+
# @return [URI::HTTPS] a URI instance.
|
16
|
+
#
|
17
|
+
# @!attribute [r] request
|
18
|
+
# @return [URI::HTTPS] a URI instance.
|
19
|
+
class Request
|
20
|
+
attr_accessor :client, :emit_heartbeats
|
21
|
+
attr_reader :uri, :request
|
22
|
+
|
23
|
+
# Creates an OandaAPI::Streaming::Request instance.
|
24
|
+
# @param [Streaming::Client] client a streaming client instance that can be used to
|
25
|
+
# send signals to an instance of this Streaming::Request.
|
26
|
+
# @param [String] uri an absolute URI to the service endpoint.
|
27
|
+
# @param [Hash] query a list of query parameters, unencoded. The list
|
28
|
+
# is converted into a query string. See {OandaAPI::Client#query_string_normalizer}.
|
29
|
+
# @param [Hash] headers a list of header values that will be sent with the request.
|
30
|
+
def initialize(client: nil, uri:, query: {}, headers: {})
|
31
|
+
self.client = client.nil? ? self : client
|
32
|
+
@uri = URI uri
|
33
|
+
@uri.query = OandaAPI::Client.default_options[:query_string_normalizer].call(query)
|
34
|
+
@http = Net::HTTP.new @uri.host, 443
|
35
|
+
@http.use_ssl = true
|
36
|
+
@http.verify_mode = OpenSSL::SSL::VERIFY_PEER
|
37
|
+
@request = Net::HTTP::Get.new @uri
|
38
|
+
headers.each_pair { |pair| @request.add_field(*pair) }
|
39
|
+
end
|
40
|
+
|
41
|
+
# Sets the client attribute
|
42
|
+
# @param [OandaAPI::Streaming::Client] value
|
43
|
+
# @return [void]
|
44
|
+
# @raise [ArgumentError] if value is not an OandaAPI::Streaming::Client instance.
|
45
|
+
def client=(value)
|
46
|
+
fail ArgumentError, "Expecting an OandaAPI::Streaming::Client" unless (value.is_a?(OandaAPI::Streaming::Client) || value.is_a?(OandaAPI::Streaming::Request))
|
47
|
+
@client = value
|
48
|
+
end
|
49
|
+
|
50
|
+
# @return [boolean] true if heatbeats are emitted.
|
51
|
+
def emit_heartbeats?
|
52
|
+
!!@emit_heartbeats
|
53
|
+
end
|
54
|
+
|
55
|
+
# Signals the streaming request to disconnect and terminates streaming.
|
56
|
+
# @return [void]
|
57
|
+
def stop!
|
58
|
+
@stop_requested = true
|
59
|
+
end
|
60
|
+
|
61
|
+
# Returns `true` if the request has been signalled to terminate. See {#stop!}.
|
62
|
+
# @return [boolean]
|
63
|
+
def stop_requested?
|
64
|
+
!!@stop_requested
|
65
|
+
end
|
66
|
+
|
67
|
+
# @return [true] if the instance is connected and streaming a response.
|
68
|
+
def running?
|
69
|
+
!!@running
|
70
|
+
end
|
71
|
+
|
72
|
+
# Emits a stream of {OandaAPI::ResourceBase} instances, depending
|
73
|
+
# on the endpoint that the request is servicing, either
|
74
|
+
# {OandaAPI::Resource::Price} or {OandaAPI::Resource::Transaction}
|
75
|
+
# instances are emitted. When #emit_heartbeats? is `true`, then
|
76
|
+
# resources could also be {OandaAPI::Resource::Heartbeat}.
|
77
|
+
#
|
78
|
+
# Note this method runs as an infinite loop and will block indefinitely
|
79
|
+
# until either the connection is halted or a {#stop!} signal is recieved.
|
80
|
+
#
|
81
|
+
# @yield [OandaAPI::ResourceBase, OandaAPI::Streaming::Client] Each resource found in the response
|
82
|
+
# stream is yielded as they are received. The client instance controlling the
|
83
|
+
# streaming request is also yielded. It can be used to issue a signaller.#stop! to terminate the resquest.
|
84
|
+
# @raise [OandaAPI::StreamingDisconnect] if the endpoint was disconnected by server.
|
85
|
+
# @raise [OandaAPI::RequestError] if an unexpected resource is returned.
|
86
|
+
# @return [void]
|
87
|
+
def stream(&block)
|
88
|
+
@stop_requested = false
|
89
|
+
@running = true
|
90
|
+
# @http.set_debug_output $stderr
|
91
|
+
@http.request(@request) do |response|
|
92
|
+
response.read_body do |chunk|
|
93
|
+
handle_response(chunk).each do |resource|
|
94
|
+
block.call(resource, @client)
|
95
|
+
return if stop_requested?
|
96
|
+
end
|
97
|
+
return if stop_requested?
|
98
|
+
sleep 0.01
|
99
|
+
end
|
100
|
+
end
|
101
|
+
ensure
|
102
|
+
@running = false
|
103
|
+
@http.finish if @http.started?
|
104
|
+
end
|
105
|
+
|
106
|
+
private
|
107
|
+
|
108
|
+
# @private
|
109
|
+
# Converts a raw json response into {OandaAPI::ResourceBase} instances.
|
110
|
+
# @return [Array<OandaAPI::ResourceBase>] depending on the endpoint
|
111
|
+
# that the request is servicing, which is either an array of
|
112
|
+
# {OandaAPI::Resource::Price} or {OandaAPI::Resource::Transaction} instances.
|
113
|
+
# When #emit_heartbeats? is `true`, then the instance could be an {OandaAPI::Resource::Heartbeat}.
|
114
|
+
# @raise [OandaAPI::StreamingDisconnect] if the endpoint was disconnected by server.
|
115
|
+
# @raise [OandaAPI::RequestError] if an unexpected resource is returned.
|
116
|
+
def handle_response(response)
|
117
|
+
response.split("\r\n").map do |json|
|
118
|
+
parsed_response = JSON.parse json
|
119
|
+
case
|
120
|
+
when parsed_response["heartbeat"]
|
121
|
+
OandaAPI::Resource::Heartbeat.new parsed_response["heartbeat"] if emit_heartbeats?
|
122
|
+
when parsed_response["tick"]
|
123
|
+
OandaAPI::Resource::Price.new parsed_response["tick"]
|
124
|
+
when parsed_response["transaction"]
|
125
|
+
OandaAPI::Resource::Transaction.new parsed_response["transaction"]
|
126
|
+
when parsed_response["disconnect"]
|
127
|
+
raise OandaAPI::StreamingDisconnect, parsed_response["disconnect"]["message"]
|
128
|
+
else
|
129
|
+
raise OandaAPI::RequestError, "unknown resource: #{json}"
|
130
|
+
end
|
131
|
+
end.compact
|
132
|
+
end
|
133
|
+
end
|
134
|
+
end
|
135
|
+
end
|
@@ -45,7 +45,7 @@ module OandaAPI
|
|
45
45
|
# Yields all keys of a hash, and safely applies whatever transform
|
46
46
|
# the block provides. Supports nested hashes.
|
47
47
|
#
|
48
|
-
# @param [Object] value can be a
|
48
|
+
# @param [Object] value can be a `Hash`, an `Array` or scalar object type.
|
49
49
|
#
|
50
50
|
# @param [Block] block transforms the yielded key.
|
51
51
|
#
|
@@ -68,7 +68,7 @@ module OandaAPI
|
|
68
68
|
# transform the block provides to the values.
|
69
69
|
# Supports nested hashes and arrays.
|
70
70
|
#
|
71
|
-
# @param [Object] value can be a
|
71
|
+
# @param [Object] value can be a `Hash`, an `Array` or scalar object type.
|
72
72
|
#
|
73
73
|
# @param [Object] key
|
74
74
|
#
|
data/lib/oanda_api/version.rb
CHANGED
@@ -0,0 +1,47 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
describe "OandaAPI::ResourceBase" do
|
3
|
+
class MyClass < OandaAPI::ResourceBase
|
4
|
+
attr_accessor :webbed_feet
|
5
|
+
end
|
6
|
+
|
7
|
+
class MyCustomizedClass < OandaAPI::ResourceBase
|
8
|
+
attr_accessor :webbed_feet
|
9
|
+
|
10
|
+
def custom_attributes
|
11
|
+
super.merge(webbed_feet: "customized #{webbed_feet}")
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
describe "#initialize" do
|
16
|
+
it "initializes writer methods with matching attributes" do
|
17
|
+
obj = MyClass.new webbed_feet: "webbed feet"
|
18
|
+
expect(obj.webbed_feet).to eq "webbed feet"
|
19
|
+
end
|
20
|
+
|
21
|
+
it "initializes snake_case writer methods with matching camelCase attributes" do
|
22
|
+
obj = MyClass.new webbedFeet: "webbed feet"
|
23
|
+
expect(obj.webbed_feet).to eq "webbed feet"
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
describe "#location" do
|
28
|
+
it "sets location" do
|
29
|
+
obj = MyClass.new location: "location"
|
30
|
+
expect(obj.location).to eq "location"
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
describe "#to_json" do
|
35
|
+
it "serializes all of an instance's attributes" do
|
36
|
+
obj = MyClass.new webbedFeet: "webbed feet", location: "location", extraAttribute: "extra"
|
37
|
+
h = JSON.parse obj.to_json
|
38
|
+
expect(h).to include("webbed_feet" => "webbed feet", "extra_attribute" => "extra", "location" => "location")
|
39
|
+
end
|
40
|
+
|
41
|
+
it "serializes all of an instance's customized attributes" do
|
42
|
+
obj = MyCustomizedClass.new webbedFeet: "webbed feet"
|
43
|
+
h = JSON.parse obj.to_json
|
44
|
+
expect(h).to include("webbed_feet" => "customized webbed feet")
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
@@ -26,7 +26,7 @@ describe "OandaAPI::ResourceCollection" do
|
|
26
26
|
describe "when the collection element is empty" do
|
27
27
|
let(:resource_collection) { OandaAPI::ResourceCollection.new({ candles: [] }, resource_descriptor) }
|
28
28
|
it "returns a ResourceCollection" do
|
29
|
-
|
29
|
+
expect(resource_collection).to be_an(OandaAPI::ResourceCollection)
|
30
30
|
end
|
31
31
|
it "is an empty collection" do
|
32
32
|
expect(resource_collection.empty?).to be true
|
@@ -106,4 +106,4 @@ describe "OandaAPI::ResourceCollection" do
|
|
106
106
|
expect(resource_collection.respond_to?(:location)).to be true
|
107
107
|
end
|
108
108
|
end
|
109
|
-
end
|
109
|
+
end
|
@@ -0,0 +1,132 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'uri'
|
3
|
+
require 'webmock/rspec'
|
4
|
+
|
5
|
+
describe "OandaAPI::Streaming::Client" do
|
6
|
+
let(:client) { OandaAPI::Streaming::Client.new(:practice, "token") }
|
7
|
+
|
8
|
+
describe "#initialize" do
|
9
|
+
it "creates a Streaming::Client instance" do
|
10
|
+
expect(client).to be_a(OandaAPI::Streaming::Client)
|
11
|
+
end
|
12
|
+
|
13
|
+
it "sets the authorization token" do
|
14
|
+
expect(client.auth_token).to eq("token")
|
15
|
+
end
|
16
|
+
|
17
|
+
it "sets the domain" do
|
18
|
+
expect(client.domain).to eq(:practice)
|
19
|
+
end
|
20
|
+
|
21
|
+
it "sets authorization headers" do
|
22
|
+
expect(client.headers).to eq(client.auth)
|
23
|
+
end
|
24
|
+
|
25
|
+
it "defaults emit_heartbeats to false" do
|
26
|
+
expect(client.emit_heartbeats?).to be false
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
describe "#api_uri" do
|
31
|
+
let(:client) { Struct.new(:domain) { include OandaAPI::Client }.new }
|
32
|
+
it "is domain specific" do
|
33
|
+
uris = {}
|
34
|
+
OandaAPI::DOMAINS.each do |domain|
|
35
|
+
client.domain = domain
|
36
|
+
uris[client.api_uri("/path")] = domain
|
37
|
+
end
|
38
|
+
expect(uris.size).to eq(3)
|
39
|
+
end
|
40
|
+
|
41
|
+
it "is an absolute URI" do
|
42
|
+
OandaAPI::DOMAINS.each do |domain|
|
43
|
+
client.domain = domain
|
44
|
+
uri = URI.parse client.api_uri("/path")
|
45
|
+
expect(uri.absolute?).to be true
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
describe "#auth" do
|
51
|
+
it "returns a hash with an Authorization key" do
|
52
|
+
expect(client.auth["Authorization"]).to eq("Bearer token")
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
describe "#domain=" do
|
57
|
+
OandaAPI::DOMAINS.each do |domain|
|
58
|
+
it "allows domain :#{domain} " do
|
59
|
+
client.domain = domain
|
60
|
+
expect(client.domain).to be(domain)
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
it "doesn't allow invalid domains" do
|
65
|
+
expect { client.domain = :bogus }.to raise_error(ArgumentError)
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
describe "#emit_heartbeats=" do
|
70
|
+
it "sets the emits heartbeats attribute" do
|
71
|
+
[true, false].each do |value|
|
72
|
+
client.emit_heartbeats = value
|
73
|
+
expect(client.emit_heartbeats?).to be value
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
describe "headers" do
|
79
|
+
it "has a reader and writer" do
|
80
|
+
client.headers = { key: "value" }
|
81
|
+
expect(client.headers).to eq(key: "value")
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
describe "#running?" do
|
86
|
+
it "returns true if a streaming request is running" do
|
87
|
+
events_json = <<-END
|
88
|
+
{"heartbeat":{"time":"2014-05-26T13:58:40Z"}}\r\n
|
89
|
+
{"transaction":{"id":10001}}\r\n
|
90
|
+
{"transaction":{"id":10002}}
|
91
|
+
END
|
92
|
+
stub_request(:get, "https://stream-fxpractice.oanda.com/v1/events").to_return(body: events_json, status: 200)
|
93
|
+
|
94
|
+
client = OandaAPI::Streaming::Client.new(:practice, "token")
|
95
|
+
expect(client.running?).to be false
|
96
|
+
client.events.stream do |_event, signaller|
|
97
|
+
expect(client.running?).to be true
|
98
|
+
signaller.stop!
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
describe "#stop!" do
|
104
|
+
events_json = <<-END
|
105
|
+
{"transaction":{"id": 1}}\r\n
|
106
|
+
{"transaction":{"id": 2}}
|
107
|
+
END
|
108
|
+
|
109
|
+
context "without using #stop!" do
|
110
|
+
it "emits all objects in the stream" do
|
111
|
+
stub_request(:get, "https://stream-fxpractice.oanda.com/v1/events").to_return(body: events_json, status: 200)
|
112
|
+
client = OandaAPI::Streaming::Client.new(:practice, "token")
|
113
|
+
event_ids = []
|
114
|
+
client.events.stream { |event| event_ids << event.id }
|
115
|
+
expect(event_ids).to contain_exactly(1, 2)
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
context "when using #stop!" do
|
120
|
+
it "terminates emitting objects in the stream" do
|
121
|
+
stub_request(:get, "https://stream-fxpractice.oanda.com/v1/events").to_return(body: events_json, status: 200)
|
122
|
+
client = OandaAPI::Streaming::Client.new(:practice, "token")
|
123
|
+
event_ids = []
|
124
|
+
client.events.stream do |event, signaller|
|
125
|
+
event_ids << event.id
|
126
|
+
signaller.stop!
|
127
|
+
end
|
128
|
+
expect(event_ids).to contain_exactly(1)
|
129
|
+
end
|
130
|
+
end
|
131
|
+
end
|
132
|
+
end
|
@@ -0,0 +1,172 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'uri'
|
3
|
+
require 'webmock/rspec'
|
4
|
+
|
5
|
+
describe "OandaAPI::Streaming::Request" do
|
6
|
+
let(:streaming_request) {
|
7
|
+
OandaAPI::Streaming::Request.new(uri: "https://a.url.com",
|
8
|
+
query: { account: 1234, instruments: %w[AUD_CAD AUD_CHF] },
|
9
|
+
headers: { "some-header" => "header value" })
|
10
|
+
}
|
11
|
+
|
12
|
+
describe "#initialize" do
|
13
|
+
it "creates a Streaming::Request instance" do
|
14
|
+
expect(streaming_request).to be_an(OandaAPI::Streaming::Request)
|
15
|
+
end
|
16
|
+
|
17
|
+
it "initializes the uri attribute" do
|
18
|
+
expect(streaming_request.uri.to_s).to eq "https://a.url.com?account=1234&instruments=AUD_CAD%2CAUD_CHF"
|
19
|
+
end
|
20
|
+
|
21
|
+
it "initializes the request headers attribute" do
|
22
|
+
expect(streaming_request.request["some-header"]).to eq "header value"
|
23
|
+
end
|
24
|
+
|
25
|
+
it "initializes the request's client attribute" do
|
26
|
+
client = OandaAPI::Streaming::Client.new :practice, "token"
|
27
|
+
streaming_request = OandaAPI::Streaming::Request.new(client: client,
|
28
|
+
uri: "https://a.url.com",
|
29
|
+
query: { account: 1234, instruments: %w[AUD_CAD AUD_CHF] },
|
30
|
+
headers: { "some-header" => "header value" })
|
31
|
+
expect(streaming_request.client).to eq client
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
describe "#client=" do
|
36
|
+
it "sets the request's client attribute" do
|
37
|
+
client = OandaAPI::Streaming::Client.new(:practice, "token")
|
38
|
+
streaming_request.client = client
|
39
|
+
expect(streaming_request.client).to eq client
|
40
|
+
end
|
41
|
+
|
42
|
+
it "fails if the value is not an Oanda::Streaming::Client" do
|
43
|
+
expect { streaming_request.client = "" }.to raise_error(ArgumentError)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
describe "#stop!" do
|
48
|
+
it "sets the stop_requested signal" do
|
49
|
+
expect(streaming_request.stop_requested?).to be false
|
50
|
+
streaming_request.stop!
|
51
|
+
expect(streaming_request.stop_requested?).to be true
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
describe "#stream" do
|
56
|
+
it "yields all resources returned in the response stream" do
|
57
|
+
events_json = <<-END
|
58
|
+
{"transaction":{"id": 1}}\r\n
|
59
|
+
{"transaction":{"id": 2}}
|
60
|
+
END
|
61
|
+
ids = []
|
62
|
+
stub_request(:any, /\.com/).to_return(body: events_json, status: 200)
|
63
|
+
streaming_request.stream { |resource| ids << resource.id }
|
64
|
+
expect(ids).to contain_exactly(1, 2)
|
65
|
+
end
|
66
|
+
|
67
|
+
context "when emit_heartbeats? is false" do
|
68
|
+
it "ignores 'heartbeats' in the response stream" do
|
69
|
+
events_json = <<-END
|
70
|
+
{"heartbeat":{"id" : 0}}\r\n
|
71
|
+
{"transaction":{"id": 1}}\r\n
|
72
|
+
{"heartbeat":{"id" : 0}}\r\n
|
73
|
+
{"transaction":{"id": 2}}\r\n
|
74
|
+
{"heartbeat":{"id" : 0}}
|
75
|
+
END
|
76
|
+
ids = []
|
77
|
+
stub_request(:any, /\.com/).to_return(body: events_json, status: 200)
|
78
|
+
streaming_request.emit_heartbeats = false
|
79
|
+
streaming_request.stream { |resource| ids << resource.id }
|
80
|
+
expect(ids).to contain_exactly(1, 2)
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
context "when emit_heartbeats? is true" do
|
85
|
+
it "includes 'heartbeats' in the response stream" do
|
86
|
+
events_json = <<-END
|
87
|
+
{"heartbeat":{"id" : 10}}\r\n
|
88
|
+
{"transaction":{"id": 1}}\r\n
|
89
|
+
{"heartbeat":{"id" : 20}}\r\n
|
90
|
+
{"transaction":{"id": 2}}\r\n
|
91
|
+
{"heartbeat":{"id" : 30}}
|
92
|
+
END
|
93
|
+
transactions = []
|
94
|
+
heartbeats = 0
|
95
|
+
stub_request(:any, /\.com/).to_return(body: events_json, status: 200)
|
96
|
+
streaming_request.emit_heartbeats = true
|
97
|
+
streaming_request.stream do |resource|
|
98
|
+
if resource.is_a? OandaAPI::Resource::Heartbeat
|
99
|
+
heartbeats += 1
|
100
|
+
else
|
101
|
+
transactions << resource.id
|
102
|
+
end
|
103
|
+
end
|
104
|
+
expect(transactions).to contain_exactly(1, 2)
|
105
|
+
expect(heartbeats).to be 3
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
context "when a 'disconnect' is received the response stream" do
|
110
|
+
it "raises an OandaAPI::StreamingDisconnect" do
|
111
|
+
events_json = <<-END
|
112
|
+
{"transaction":{"id": 1}}\r\n
|
113
|
+
{"disconnect":{"code":60,"message":"Access Token connection limit exceeded"}}\r\n
|
114
|
+
{"transaction":{"id": 2}}
|
115
|
+
END
|
116
|
+
|
117
|
+
stub_request(:any, /\.com/).to_return(body: events_json, status: 200)
|
118
|
+
expect {
|
119
|
+
streaming_request.stream { |resource| resource }
|
120
|
+
}.to raise_error(OandaAPI::StreamingDisconnect, /connection limit exceeded/)
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
context "when an unknown resource type is received the response stream" do
|
125
|
+
it "raises an OandaAPI::RequestError" do
|
126
|
+
events_json = <<-END
|
127
|
+
{"transaction":{"id": 1}}\r\n
|
128
|
+
{"sponge-bob":{"is": "awesome"}}\r\n
|
129
|
+
{"transaction":{"id": 2}}
|
130
|
+
END
|
131
|
+
|
132
|
+
stub_request(:any, /\.com/).to_return(body: events_json, status: 200)
|
133
|
+
expect {
|
134
|
+
streaming_request.stream { |resource| resource }
|
135
|
+
}.to raise_error(OandaAPI::RequestError, /unknown resource/)
|
136
|
+
end
|
137
|
+
end
|
138
|
+
|
139
|
+
it "yields an object that responds to stop! and terminates streaming when called" do
|
140
|
+
events_json = <<-END
|
141
|
+
{"transaction":{"id": 1}}\r\n
|
142
|
+
{"transaction":{"id": 2}}\r\n
|
143
|
+
{"transaction":{"id": 3}}
|
144
|
+
END
|
145
|
+
stub_request(:any, /\.com/).to_return(body: events_json, status: 200)
|
146
|
+
ids = []
|
147
|
+
streaming_request.stream do |resource, signaller|
|
148
|
+
ids << resource.id
|
149
|
+
signaller.stop! if ids.size > 1
|
150
|
+
end
|
151
|
+
expect(ids).to contain_exactly(1, 2)
|
152
|
+
end
|
153
|
+
|
154
|
+
context "when the stream contains only heartbeats" do
|
155
|
+
it "terminates streaming when a stop signal is received" do
|
156
|
+
events_json = <<-END
|
157
|
+
{"heartbeat":{"id": 1}}\r\n
|
158
|
+
{"heartbeat":{"id": 2}}\r\n
|
159
|
+
{"heartbeat":{"id": 3}}
|
160
|
+
END
|
161
|
+
stub_request(:any, /\.com/).to_return(body: events_json, status: 200)
|
162
|
+
heartbeats = 0
|
163
|
+
streaming_request.emit_heartbeats = true
|
164
|
+
streaming_request.stream do |_resource, signaller|
|
165
|
+
heartbeats += 1
|
166
|
+
signaller.stop!
|
167
|
+
end
|
168
|
+
expect(heartbeats).to eq 1
|
169
|
+
end
|
170
|
+
end
|
171
|
+
end
|
172
|
+
end
|