oanda_api 0.8.3 → 0.9.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.
- 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
|