oanda_api 0.9.0 → 0.9.2

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 2c7acc3d30cf5e19c55528d74251a43ad5e6387c
4
- data.tar.gz: 6bb298acd99fb97dd08d82d86455e910694e56cc
3
+ metadata.gz: 9f7f97bda3c0ece6ba2dfbabf7b59cab04991de0
4
+ data.tar.gz: efcdee86eeca8013b772aac230a5a33476051b12
5
5
  SHA512:
6
- metadata.gz: f44a232f8c05ee18ec10b8a0e98cba28d865e8533ba2b0efdf7eca8e87936a5e9fadbc2ee49c56a5916f81b7d7063d8dbf9b9371c87d920a7d7f36992bac8f8b
7
- data.tar.gz: 63c786402973185453b85fee525936234ac690a49b5119922dfbebb6fc4613512f36cdbe5cfc0bc5d883e2fafec20cf29bd8ea3f19d20fffe6b11e0305d25b7b
6
+ metadata.gz: 27f14d9b88d7a01908c566ed116bea03c60079a7807ae5449224baabb511ab1ffea8924c6da67aaa47e94253eea5f400cbbac48582897a2f34e2c43392c6e797
7
+ data.tar.gz: 4dea06ea3ca3763503b44370b14b9adcd2b618a6fc6f6b28cd2ac6d85ed779cb9b8314f198b896eb80434028f81bc912d1dab8f57d74df53f86420f3e467c922
@@ -1,11 +1,29 @@
1
1
  # Change Log
2
2
 
3
- ## 0.90
3
+ ## 0.9.0
4
4
 
5
5
  * Add support for live [Streaming](http://developer.oanda.com/rest-live/streaming/).
6
6
  * Add #to_json serialization to resource classes.
7
- * fixed Yardoc formatting.
7
+ * Fixed Yardoc formatting.
8
8
 
9
9
  ###What's New?
10
10
  OandaAPI now supports Oanda's streaming API for consuming realtime ticks and account transactions. See the example in the README and have a look at the specs for `OandaAPI::Streaming::Client`.
11
-
11
+
12
+
13
+
14
+ ## 0.9.1
15
+
16
+ * Fixed JSON serialization for nested ResourceBase instances.
17
+ * Fixed some edge cases that could generate ParseError when parsing streaming JSON.
18
+ * Added support for streaming JSON parsers [yajl-ruby](https://github.com/brianmario/yajl-ruby) and [gson](https://github.com/avsej/gson.rb).
19
+ * Serialized JSON is now deserialized using symbolized names to avoid garbage collection overhead that occurs when strings are used.
20
+
21
+ ###What's New?
22
+ As with version 0.9.0, `OandaAPI::Streaming::Client` will use the JSON gem parser if it is the only JSON parser installed. However, the JSON gem does not support parsing streams of objects very robustly (i.e. it expects complete documents, or the stream to delimit multiple objects consistently). If you are using the streaming API, and install either the [yajl-ruby](https://github.com/brianmario/yajl-ruby) gem (for MRI and Rubinius ruby) or the [gson](https://github.com/avsej/gson.rb) gem (for jruby), then `OandaAPI::Streaming::Client` will use either of those streaming JSON gems for a gain in reliability.
23
+
24
+
25
+
26
+ ## 0.9.2
27
+
28
+ * Specify version of HTTParty as 0.13.3 until HTTParty issue #398 is resolved.
29
+ * Now support any whitespace as delimiting multiple JSON objects in streaming API with `OandaAPI::Streaming::Adapters::Generic`.
data/Gemfile CHANGED
@@ -4,15 +4,19 @@ gemspec
4
4
 
5
5
  group :development do
6
6
  gem "pry"
7
- gem 'pry-stack_explorer'
8
- gem 'pry-byebug'
9
-
7
+ gem "pry-stack_explorer"
8
+ gem "pry-byebug"
10
9
  gem "guard"
11
10
  gem "guard-rspec"
12
11
  gem "fuubar"
13
12
  gem "redcarpet"
14
13
  gem "github-markup"
15
- gem "rubocop", require: false
14
+ gem "rubocop", :require => false
16
15
  end
17
16
 
18
- gem 'simplecov', :require => false, :group => :test
17
+ group :test do
18
+ gem "simplecov", :require => false
19
+ gem "codeclimate-test-reporter", :group => :test, :require => nil
20
+ # gem "yajl-ruby" # install this if you're running under mri or rubinius
21
+ # gem "gson" # install this if you're running under jruby
22
+ end
data/README.md CHANGED
@@ -1,4 +1,6 @@
1
1
  # OandaAPI
2
+ [![Code Climate](https://codeclimate.com/github/nukeproof/oanda_api/badges/gpa.svg)](https://codeclimate.com/github/nukeproof/oanda_api) [![Test Coverage](https://codeclimate.com/github/nukeproof/oanda_api/badges/coverage.svg)](https://codeclimate.com/github/nukeproof/oanda_api)
3
+
2
4
  Access Oanda FX accounts, get market data, trade, build trading strategies using Ruby.
3
5
  ## Synopsis
4
6
  OandaAPI is a simple Ruby wrapper for the [Oanda REST API](http://developer.oanda.com/rest-live/introduction/).
@@ -128,7 +130,7 @@ client = OandaAPI::Streaming::Client.new(:practice, ENV.fetch("OANDA_PRACTICE_TO
128
130
  prices = client.prices(account_id: 1234, instruments: %w[AUD_CAD AUD_CHF])
129
131
  prices.stream do |price|
130
132
  # Note: The code in this block should handle the price
131
- # as efficently as possible, otherwise the connection could timeout.
133
+ # as efficiently as possible, otherwise the connection could timeout.
132
134
  # For example, you could publish the tick on a queue to be handled
133
135
  # by some other thread or process.
134
136
  price # => OandaAPI::Resource::Price
@@ -3,6 +3,7 @@ require 'httparty'
3
3
  require 'persistent_httparty'
4
4
  require 'http/exceptions'
5
5
  require 'time'
6
+ require 'erb' # Hack to fix issue with httparty 0.13.4 [issue 398](https://github.com/jnunemaker/httparty/issues/398)
6
7
 
7
8
  require_relative 'oanda_api/configuration'
8
9
  require_relative 'oanda_api/client/client'
@@ -11,6 +12,7 @@ require_relative 'oanda_api/client/resource_descriptor'
11
12
  require_relative 'oanda_api/client/token_client'
12
13
  require_relative 'oanda_api/client/username_client'
13
14
  require_relative 'oanda_api/streaming/client'
15
+ require_relative 'oanda_api/streaming/json_parser'
14
16
  require_relative 'oanda_api/streaming/request'
15
17
  require_relative 'oanda_api/errors'
16
18
  require_relative 'oanda_api/resource_base'
@@ -1,5 +1,6 @@
1
1
  require 'httparty'
2
2
  require 'http/exceptions'
3
+ require_relative 'json_parser'
3
4
 
4
5
  module OandaAPI
5
6
  # List of valid subdomains clients can access.
@@ -17,6 +18,9 @@ module OandaAPI
17
18
  keep_alive: 30,
18
19
  pool_size: 2
19
20
 
21
+ # Use a custom JSON parser
22
+ parser OandaAPI::Client::JsonParser
23
+
20
24
  # Resource URI templates
21
25
  BASE_URI = {
22
26
  live: "https://api-fxtrade.oanda.com/[API_VERSION]",
@@ -0,0 +1,16 @@
1
+ module OandaAPI
2
+ module Client
3
+ #
4
+ # Overrides the default JSON parser to symbolize names.
5
+ class JsonParser < HTTParty::Parser
6
+ SupportedFormats = {"application/json" => :json}
7
+
8
+ protected
9
+
10
+ # perform json parsing on body
11
+ def json
12
+ JSON.parse body, symbolize_names: true
13
+ end
14
+ end
15
+ end
16
+ end
@@ -3,6 +3,51 @@ module OandaAPI
3
3
  # Candle value object.
4
4
  # See the Oanda Developer Guide for information about {http://developer.oanda.com/rest-live/rates/#retrieveInstrumentHistory Candles}.
5
5
  class Candle < ResourceBase
6
+
7
+ # Granularity Constants
8
+ # See http://developer.oanda.com/rest-live/rates/#aboutCandlestickRepresentation
9
+ module Granularity
10
+ # Top of minute alignments
11
+ S5 = "S5"
12
+ S10 = "S10"
13
+ S15 = "S15"
14
+ S30 = "S30"
15
+ M1 = "M1"
16
+
17
+ # Top of hour alignments
18
+ M2 = "M2"
19
+ M3 = "M3"
20
+ M4 = "M4"
21
+ M5 = "M5"
22
+ M10 = "M10"
23
+ M15 = "M15"
24
+ M30 = "M30"
25
+ H1 = "H1"
26
+
27
+ # Start of day alignments (default 17:00, Timezone/New York)
28
+ H2 = "H2"
29
+ H3 = "H3"
30
+ H4 = "H4"
31
+ H6 = "H6"
32
+ H8 = "H8"
33
+ H12 = "H12"
34
+ D = "D1"
35
+
36
+ # Start of week alignment (default Friday)
37
+ W = "W"
38
+
39
+ # Start of month alignment (first day of month)
40
+ M = "M"
41
+
42
+ VALID_GRANULARITIES = [S5,S10,S15,S30,M1,M2,M3,M4,M5,M10,M15,M30,H1,H2,H3,H4,H6,H8,H12,D,W,M]
43
+ end
44
+
45
+ # Candle Formats
46
+ module Format
47
+ BIDASK = "bidask"
48
+ MIDPOINT = "midpoint"
49
+ end
50
+
6
51
  attr_accessor :close_ask,
7
52
  :close_bid,
8
53
  :close_mid,
@@ -22,8 +22,8 @@ module OandaAPI
22
22
 
23
23
  # Serializes an instance as JSON
24
24
  # @return [String] a stringified JSON representation of an instance
25
- def to_json
26
- JSON.generate @_attributes.merge(custom_attributes)
25
+ def to_json(*args)
26
+ JSON.generate @_attributes.merge(custom_attributes), *args
27
27
  end
28
28
 
29
29
  private
@@ -0,0 +1,19 @@
1
+ module OandaAPI
2
+ module Streaming
3
+ #
4
+ # Raised if an invalid adapter is used with {OandaAPI::Streaming::JsonParser}
5
+ class AdapterError < ArgumentError
6
+ attr_reader :cause
7
+
8
+ def self.build(original_exception)
9
+ message = "Did not recognize your adapter specification (#{original_exception.message})."
10
+ new(message).tap do |exception|
11
+ exception.instance_eval do
12
+ @cause = original_exception
13
+ set_backtrace original_exception.backtrace
14
+ end
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,29 @@
1
+ module OandaAPI
2
+ module Streaming
3
+ #
4
+ # Everything related to `Streaming::Adapters`
5
+ module Adapters
6
+ #
7
+ # Uses the JSON library. This parser does not handle multiple json objects in a json stream
8
+ # unless the objects are separated with whitespace.
9
+ module Generic
10
+ extend self
11
+
12
+ # A delimiter for separating multiple json objects in a stream.
13
+ DELIMITER = "<oanda_api::delimiter>"
14
+ MULTI_OBJECT_DELIMITER = "}#{DELIMITER}{"
15
+
16
+ # Deserializes a stream of JSON objects.
17
+ # @param [String] string serialized json.
18
+ # @return [Array<Hash>] an array of hashes.
19
+ def parse(string)
20
+ string.strip!
21
+ return [] if string.empty?
22
+ string.gsub(/}\s*{/, MULTI_OBJECT_DELIMITER).split(DELIMITER).map do |json|
23
+ JSON.parse json, symbolize_names: true
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,31 @@
1
+ require 'gson'
2
+
3
+ module OandaAPI
4
+ module Streaming
5
+ module Adapters
6
+ #
7
+ # Can be used if the ruby engine (`RUBY_ENGINE`) is jruby. Uses the {https://github.com/avsej/gson.rb gson} gem.
8
+ # Handles streams of multiple JSON objects that may or may not be delimited with whitespace.
9
+ module Gson
10
+ extend self
11
+
12
+ # Deserializes a stream of JSON objects.
13
+ # @param [String] string serialized json.
14
+ # @return [Array<Hash>] an array of hashes.
15
+ def parse(string)
16
+ string.strip!
17
+ return [] if string.empty?
18
+ [parser.decode(string)].flatten
19
+ end
20
+
21
+ private
22
+
23
+ # @private
24
+ # Memoized parser instance.
25
+ def parser
26
+ @parser ||= ::Gson::Decoder.new(symbolize_keys: true)
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,31 @@
1
+ require 'yajl'
2
+
3
+ module OandaAPI
4
+ module Streaming
5
+ module Adapters
6
+ #
7
+ # Can be used if the ruby engine (`RUBY_ENGINE`) is NOT jruby. Uses the {https://github.com/brianmario/yajl-ruby yajl-ruby} gem.
8
+ # Handles streams of multiple JSON objects that may or may not be delimited with whitespace.
9
+ module Yajl
10
+ extend self
11
+
12
+ # Deserializes a stream of JSON objects.
13
+ # @param [String] string serialized json.
14
+ # @return [Array<Hash>] an array of hashes.
15
+ def parse(string)
16
+ results = []
17
+ parser.parse(string) { |hash| results << hash }
18
+ results
19
+ end
20
+
21
+ private
22
+
23
+ # @private
24
+ # Memoized parser instance.
25
+ def parser
26
+ @parser ||= ::Yajl::Parser.new(symbolize_keys: true)
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
@@ -27,7 +27,7 @@ module OandaAPI
27
27
  #
28
28
  # # -- Stopping the stream --
29
29
  # # You may add a second argument to the block to yield the client itself.
30
- # # You can use the client's `#stop!` method to terminate streaming.
30
+ # # You can use it to issue a client.stop! to terminate streaming.
31
31
  # @prices = []
32
32
  # prices = client.prices(account_id: 1234, instruments: %w[AUD_CAD AUD_CHF])
33
33
  # prices.stream do |price, client|
@@ -0,0 +1,125 @@
1
+ require_relative 'adapter_error'
2
+
3
+ module OandaAPI
4
+ module Streaming
5
+ #
6
+ # Used to deserialize a stream of JSON objects. Will load and use a streaming JSON parser
7
+ # if one is installed, otherwise defaults to use the JSON gem.
8
+ module JsonParser
9
+ extend self
10
+
11
+ # Map parser adapters to the gem library they require.
12
+ REQUIREMENT_MAP = {
13
+ gson: "Gson",
14
+ yajl: "Yajl"
15
+ }
16
+
17
+ # Loads (if not already loaded) and returns the current adapter class.
18
+ # @return [.parse] a class implementing a `.parse` method
19
+ def adapter
20
+ return @adapter if defined?(@adapter) && @adapter
21
+
22
+ # Load default adapter
23
+ self.use nil
24
+ @adapter
25
+ end
26
+
27
+ # Returns a symbol identifying either the currently loaded adapter or
28
+ # one that can be loaded. Preference is given to an adapter optimized
29
+ # for parsing streaming json if it is installed.
30
+ #
31
+ # @return [Symbol] a symbol identifying an adapter either currently loaded or that
32
+ # one that can be loaded.
33
+ def default_adapter
34
+ jruby? ? try_adapter(:gson) : try_adapter(:yajl)
35
+ end
36
+
37
+ # Loads the requested adapter.
38
+ # @param [nil, String, Symbol, Module, Class] new_adapter identifies an adapter to load.
39
+ # @return[.parse] a Module or Class that implements a `.parse` method for deserializing a stream of JSON objects.
40
+ def use(new_adapter)
41
+ @adapter = load_adapter(new_adapter)
42
+ end
43
+ alias adapter= use
44
+
45
+ # Loads the requested adapter.
46
+ # @param [String, Symbol, nil, false, Class, Module] new_adapter identifies an adapter to load.
47
+ # @return [.parse] a Module or Class that implements a `.parse` method for deserializing a stream of JSON objects.
48
+ # @raise [AdapterError] if the adapter cannot be loaded.
49
+ def load_adapter(new_adapter)
50
+ case new_adapter
51
+ when String, Symbol
52
+ load_adapter_from_string_name new_adapter.to_s
53
+ when NilClass, FalseClass
54
+ load_adapter default_adapter
55
+ when Class, Module
56
+ new_adapter
57
+ else
58
+ fail ::LoadError, new_adapter
59
+ end
60
+ rescue ::LoadError => exception
61
+ raise AdapterError.build(exception)
62
+ end
63
+
64
+ private
65
+
66
+ # @return [true] if jRuby is the ruby execution engine
67
+ def jruby?
68
+ defined?(RUBY_ENGINE) && (RUBY_ENGINE =~ /jruby/i)
69
+ end
70
+
71
+ # Loads the requested adapter.
72
+ # @param [String] name the adapter to load.
73
+ # @return [Class, Module] the loaded adapter
74
+ def load_adapter_from_string_name(name)
75
+ require_relative "adapters/#{name.downcase}"
76
+ klass_name = name.to_s.split('_').map(&:capitalize) * ''
77
+ OandaAPI::Streaming::Adapters.const_get(klass_name)
78
+ end
79
+
80
+ # Checks if the requested adapter is loadable.
81
+ # Returns a symbol identifiying either the requested adapter or
82
+ # a generic loadable adapter.
83
+ # @param [Symbol] sym identifies an adapter.
84
+ # @return [Symbol] a symbol identifying a loadable adapter.
85
+ def try_adapter(sym)
86
+ begin
87
+ return sym if Kernel.const_get sym.to_s.capitalize
88
+ rescue ::NameError
89
+ end
90
+
91
+ begin
92
+ require REQUIREMENT_MAP.fetch sym
93
+ return sym
94
+ rescue ::LoadError
95
+ warning
96
+ return :generic
97
+ end
98
+ end
99
+
100
+ # Writes a warning to stdout about the generic json parser.
101
+ # @return[void]
102
+ def warning
103
+ Kernel.warn <<-END
104
+ +------------------------------------------------------------------+
105
+ + Warning: You're currently using a JSON parser that doesn't +
106
+ + handle streams of JSON objects very well. For faster +
107
+ + and more reliable parsing, it's recommended to install one of +
108
+ + following gems dependent on the Ruby engine you're using. Once +
109
+ + installed, OandaAPI::Streaming will detect the upgraded parser +
110
+ + and use it. +
111
+ + +
112
+ + RUBY_ENGINE Recommended JSON Parsing Gem +
113
+ + =========== ================================================== +
114
+ + ruby or rbx yajl-ruby (http://github.com/brianmario/yajl-ruby) +
115
+ + install with: gem install yajl-ruby +
116
+ + +
117
+ + +
118
+ + jruby gson (https://github.com/avsej/gson.rb) +
119
+ + install with: gem install gson +
120
+ +------------------------------------------------------------------+
121
+ END
122
+ end
123
+ end
124
+ end
125
+ end
@@ -9,7 +9,7 @@ module OandaAPI
9
9
  # @return [OandaAPI::Streaming::Client] a streaming client instance.
10
10
  #
11
11
  # @!attribute [rw] emit_heartbeats
12
- # @return [boolean]
12
+ # @return [boolean] true if heartbeats are emitted.
13
13
  #
14
14
  # @!attribute [r] uri
15
15
  # @return [URI::HTTPS] a URI instance.
@@ -20,14 +20,14 @@ module OandaAPI
20
20
  attr_accessor :client, :emit_heartbeats
21
21
  attr_reader :uri, :request
22
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.
23
+ # Creates a `Streaming::Request` instance.
24
+ # @param [Streaming::Client] client a streaming client instance which can be used to
25
+ # send signals to an instance of this `Streaming::Request` class.
26
26
  # @param [String] uri an absolute URI to the service endpoint.
27
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}.
28
+ # is converted into a query string. See `OandaAPI::Client#query_string_normalizer`.
29
29
  # @param [Hash] headers a list of header values that will be sent with the request.
30
- def initialize(client: nil, uri:, query: {}, headers: {})
30
+ def initialize(client: nil, uri: nil, query: {}, headers: {})
31
31
  self.client = client.nil? ? self : client
32
32
  @uri = URI uri
33
33
  @uri.query = OandaAPI::Client.default_options[:query_string_normalizer].call(query)
@@ -41,9 +41,9 @@ module OandaAPI
41
41
  # Sets the client attribute
42
42
  # @param [OandaAPI::Streaming::Client] value
43
43
  # @return [void]
44
- # @raise [ArgumentError] if value is not an OandaAPI::Streaming::Client instance.
44
+ # @raise [ArgumentError] if value is not an {OandaAPI::Streaming::Client} instance.
45
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))
46
+ fail ArgumentError, "Expecting an OandaAPI::Streaming::Client" unless value.is_a?(OandaAPI::Streaming::Client) || value.is_a?(OandaAPI::Streaming::Request)
47
47
  @client = value
48
48
  end
49
49
 
@@ -64,7 +64,7 @@ module OandaAPI
64
64
  !!@stop_requested
65
65
  end
66
66
 
67
- # @return [true] if the instance is connected and streaming a response.
67
+ # @return [boolean] true if the instance is connected and is streaming a response.
68
68
  def running?
69
69
  !!@running
70
70
  end
@@ -72,15 +72,15 @@ module OandaAPI
72
72
  # Emits a stream of {OandaAPI::ResourceBase} instances, depending
73
73
  # on the endpoint that the request is servicing, either
74
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}.
75
+ # instances are emitted. When {#emit_heartbeats?} is `true`, then
76
+ # {OandaAPI::Resource::Heartbeat} could also be emitted.
77
77
  #
78
78
  # Note this method runs as an infinite loop and will block indefinitely
79
79
  # until either the connection is halted or a {#stop!} signal is recieved.
80
80
  #
81
81
  # @yield [OandaAPI::ResourceBase, OandaAPI::Streaming::Client] Each resource found in the response
82
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.
83
+ # streaming request is also yielded. It can be used to issue a `client.stop!` to terminate the resquest.
84
84
  # @raise [OandaAPI::StreamingDisconnect] if the endpoint was disconnected by server.
85
85
  # @raise [OandaAPI::RequestError] if an unexpected resource is returned.
86
86
  # @return [void]
@@ -113,23 +113,28 @@ module OandaAPI
113
113
  # When #emit_heartbeats? is `true`, then the instance could be an {OandaAPI::Resource::Heartbeat}.
114
114
  # @raise [OandaAPI::StreamingDisconnect] if the endpoint was disconnected by server.
115
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
116
+ def handle_response(string)
117
+ parse(string).map do |parsed_response|
119
118
  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"]
119
+ when parsed_response[:heartbeat]
120
+ OandaAPI::Resource::Heartbeat.new parsed_response[:heartbeat] if emit_heartbeats?
121
+ when parsed_response[:tick]
122
+ OandaAPI::Resource::Price.new parsed_response[:tick]
123
+ when parsed_response[:transaction]
124
+ OandaAPI::Resource::Transaction.new parsed_response[:transaction]
125
+ when parsed_response[:disconnect]
126
+ fail OandaAPI::StreamingDisconnect, parsed_response[:disconnect][:message]
128
127
  else
129
- raise OandaAPI::RequestError, "unknown resource: #{json}"
128
+ fail OandaAPI::RequestError, "unknown resource: #{parsed_response}"
130
129
  end
131
130
  end.compact
132
131
  end
132
+
133
+ # @private
134
+ # Uses the best json parser available for optimal performance and stream parsing ability.
135
+ def parse(string)
136
+ OandaAPI::Streaming::JsonParser.adapter.parse string
137
+ end
133
138
  end
134
139
  end
135
140
  end
@@ -1,3 +1,3 @@
1
1
  module OandaAPI
2
- VERSION = "0.9.0"
2
+ VERSION = "0.9.2"
3
3
  end
@@ -21,12 +21,12 @@ Gem::Specification.new do |s|
21
21
  s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
22
22
  s.require_paths = ["lib"]
23
23
 
24
- s.add_dependency "httparty", "~> 0.13"
24
+ s.add_dependency "httparty", "0.13.3"
25
25
  s.add_dependency "persistent_httparty", "~> 0.1"
26
26
  s.add_dependency "http-exceptions", "~> 0.0"
27
27
 
28
- s.add_development_dependency "rspec", "~> 3.1"
28
+ s.add_development_dependency "rspec", "~> 3.2"
29
29
  s.add_development_dependency "vcr", "~> 2.9"
30
- s.add_development_dependency "webmock", "~> 1.20"
30
+ s.add_development_dependency "webmock", "~> 1.21"
31
31
  s.add_development_dependency "yard", "~> 0.8"
32
32
  end
@@ -0,0 +1,13 @@
1
+ require 'spec_helper'
2
+
3
+ describe "OandaAPI::Client::JsonParser" do
4
+
5
+ it "deserializes json using symbolized keys" do
6
+ [
7
+ ["{\"a\":[{\"b\":{\"3\":3}}]}", { :a => [:b => { :"3" => 3 }] }],
8
+ [ "\r\n \r\n {\"a\":\"a\"} \r\n ", { :a => "a"}]
9
+ ].each do |serialized, deserialized|
10
+ expect(OandaAPI::Client::JsonParser.call serialized, :json).to eq(deserialized)
11
+ end
12
+ end
13
+ end
@@ -43,5 +43,11 @@ describe "OandaAPI::ResourceBase" do
43
43
  h = JSON.parse obj.to_json
44
44
  expect(h).to include("webbed_feet" => "customized webbed feet")
45
45
  end
46
+
47
+ it "serializes when nested" do
48
+ obj = MyCustomizedClass.new webbedFeet: "webbed feet"
49
+ a = JSON.parse [obj].to_json
50
+ expect(a.first).to include("webbed_feet" => "customized webbed feet")
51
+ end
46
52
  end
47
53
  end
@@ -0,0 +1,7 @@
1
+ require 'spec_helper'
2
+ require 'shared/adapter'
3
+ require 'oanda_api/streaming/adapters/generic'
4
+
5
+ describe "OandaAPI::Streaming::Adapters::Generic" do
6
+ it_behaves_like 'an adapter', described_class
7
+ end
@@ -0,0 +1,7 @@
1
+ require 'spec_helper'
2
+ require 'shared/adapter'
3
+ require 'oanda_api/streaming/adapters/gson' if gem_installed? :Gson
4
+
5
+ describe "OandaAPI::Streaming::Adapters::Gson", :if => gem_installed?(:Gson) do
6
+ it_behaves_like 'an adapter', described_class
7
+ end
@@ -0,0 +1,7 @@
1
+ require 'spec_helper'
2
+ require 'shared/adapter'
3
+ require 'oanda_api/streaming/adapters/yajl' if gem_installed? :Yajl
4
+
5
+ describe "OandaAPI::Streaming::Adapters::Yajl", :if => gem_installed?(:Yajl) do
6
+ it_behaves_like 'an adapter', described_class
7
+ end
@@ -0,0 +1,65 @@
1
+ require 'spec_helper'
2
+
3
+ describe "OandaAPI::Streaming::JsonParser" do
4
+
5
+ context "when no streaming json parsers are available" do
6
+ around do |example|
7
+ simulate_no_adapters{ example.call }
8
+ end
9
+
10
+ it "defaults to :generic adapter" do
11
+ silence_warnings do
12
+ expect(OandaAPI::Streaming::JsonParser.default_adapter).to eq(:generic)
13
+ end
14
+ end
15
+
16
+ it "prints a warning about using a non-optimal JSON parser" do
17
+ expect(Kernel).to receive(:warn).with(/warning/i)
18
+ OandaAPI::Streaming::JsonParser.default_adapter
19
+ end
20
+ end
21
+
22
+ it "defaults to the best available JSON parser for jruby", :if => gem_installed?(:Gson) do
23
+ # Clear memoized variable that may have been set by previous tests
24
+ OandaAPI::Streaming::JsonParser.send(:remove_instance_variable, :@adapter) if OandaAPI::Streaming::JsonParser.instance_variable_defined?(:@adapter)
25
+ expect(OandaAPI::Streaming::JsonParser.adapter.to_s).to eq("OandaAPI::Streaming::Adapters::Gson")
26
+ end
27
+
28
+ it "defaults to the best available JSON parser for non-jruby", :if => gem_installed?(:Yajl) do
29
+ # Clear memoized variable that may have been set by previous tests
30
+ OandaAPI::Streaming::JsonParser.send(:remove_instance_variable, :@adapter) if OandaAPI::Streaming::JsonParser.instance_variable_defined?(:@adapter)
31
+ expect(OandaAPI::Streaming::JsonParser.adapter.to_s).to eq("OandaAPI::Streaming::Adapters::Yajl")
32
+ end
33
+
34
+ describe "explicitly overriding the adapter" do
35
+ after(:each) do
36
+ OandaAPI::Streaming::JsonParser.adapter = nil
37
+ end
38
+
39
+ it 'is settable via a symbol' do
40
+ OandaAPI::Streaming::JsonParser.use :generic
41
+ expect(OandaAPI::Streaming::JsonParser.adapter).to eq(OandaAPI::Streaming::Adapters::Generic)
42
+ end
43
+
44
+ it 'is settable via a case-insensitive string' do
45
+ OandaAPI::Streaming::JsonParser.use 'generic'
46
+ expect(OandaAPI::Streaming::JsonParser.adapter).to eq(OandaAPI::Streaming::Adapters::Generic)
47
+ end
48
+
49
+ it 'is settable via a class' do
50
+ adapter = Class.new
51
+ OandaAPI::Streaming::JsonParser.use adapter
52
+ expect(OandaAPI::Streaming::JsonParser.adapter).to eq(adapter)
53
+ end
54
+
55
+ it 'is settable via a module' do
56
+ adapter = Module.new
57
+ OandaAPI::Streaming::JsonParser.use adapter
58
+ expect(OandaAPI::Streaming::JsonParser.adapter).to eq(adapter)
59
+ end
60
+ end
61
+
62
+ it 'throws AdapterError on bad input' do
63
+ expect{ OandaAPI::Streaming::JsonParser.use 'bad adapter' }.to raise_error(OandaAPI::Streaming::AdapterError, /bad adapter/)
64
+ end
65
+ end
@@ -3,6 +3,7 @@ require 'uri'
3
3
  require 'webmock/rspec'
4
4
 
5
5
  describe "OandaAPI::Streaming::Request" do
6
+
6
7
  let(:streaming_request) {
7
8
  OandaAPI::Streaming::Request.new(uri: "https://a.url.com",
8
9
  query: { account: 1234, instruments: %w[AUD_CAD AUD_CHF] },
@@ -60,7 +61,10 @@ describe "OandaAPI::Streaming::Request" do
60
61
  END
61
62
  ids = []
62
63
  stub_request(:any, /\.com/).to_return(body: events_json, status: 200)
63
- streaming_request.stream { |resource| ids << resource.id }
64
+
65
+ streaming_request.stream do |resource|
66
+ ids << resource.id
67
+ end
64
68
  expect(ids).to contain_exactly(1, 2)
65
69
  end
66
70
 
@@ -168,5 +172,29 @@ describe "OandaAPI::Streaming::Request" do
168
172
  expect(heartbeats).to eq 1
169
173
  end
170
174
  end
175
+
176
+ context "when the stream contains multiple undelimited objects" do
177
+ events_json = <<-END
178
+ {"tick":{"bid": 1}}{"tick":{"bid": 2}}\r\n{"tick":{"bid": 3}}
179
+ END
180
+
181
+ it "yields all of the objects" do
182
+ case
183
+ when gem_installed?(:Yajl)
184
+ OandaAPI::Streaming::JsonParser.use(:yajl)
185
+ when gem_installed?(:Gson)
186
+ OandaAPI::Streaming::JsonParser.use(:gson)
187
+ else
188
+ OandaAPI::Streaming::JsonParser.use(:generic)
189
+ end
190
+
191
+ stub_request(:any, /\.com/).to_return(body: events_json, status: 200)
192
+ ticks = []
193
+ streaming_request.stream do |resource|
194
+ ticks << resource.bid
195
+ end
196
+ expect(ticks).to contain_exactly(1, 2, 3)
197
+ end
198
+ end
171
199
  end
172
200
  end
@@ -0,0 +1,19 @@
1
+ shared_examples_for 'an adapter' do |adapter|
2
+
3
+ before{ OandaAPI::Streaming::JsonParser.use adapter }
4
+
5
+ describe ".parse" do
6
+ it "deserializes json using symbolized keys" do
7
+ [
8
+ ["{\"a\":[{\"b\":{\"3\":3}}]}", [{ :a => [:b => { :"3" => 3 }] }]],
9
+ ["{\"a\":\"a\"} \r\n ", [{ :a => "a"}]],
10
+ ["", []],
11
+ [" ", []],
12
+ ["\r\n", []]
13
+
14
+ ].each do |serialized, deserialized|
15
+ expect(OandaAPI::Streaming::JsonParser.adapter.parse(serialized)).to eq(deserialized)
16
+ end
17
+ end
18
+ end
19
+ end
@@ -1,6 +1,9 @@
1
1
  lib = File.expand_path("../../lib", __FILE__)
2
2
  $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
3
3
 
4
+ require 'codeclimate-test-reporter'
5
+ CodeClimate::TestReporter.start
6
+
4
7
  require 'simplecov'
5
8
  SimpleCov.start
6
9
 
@@ -12,3 +15,64 @@ RSpec.configure do |c|
12
15
  OandaAPI.configure { |config| config.use_request_throttling = false }
13
16
  end
14
17
  end
18
+
19
+ def jruby?
20
+ defined?(RUBY_ENGINE) && (RUBY_ENGINE =~ /jruby/i)
21
+ end
22
+
23
+ def gem_installed?(const)
24
+ begin
25
+ require const.to_s.downcase unless Object.const_defined? const
26
+ true
27
+ rescue ::LoadError => e
28
+ false
29
+ end
30
+ end
31
+
32
+ # Makes the adapter requirements break (i.e. simulates that a gem required
33
+ # for an adapter is not installed).
34
+ def break_requirements
35
+ requirements = OandaAPI::Streaming::JsonParser::REQUIREMENT_MAP
36
+ OandaAPI::Streaming::JsonParser::REQUIREMENT_MAP.each do | adapter, library|
37
+ OandaAPI::Streaming::JsonParser::REQUIREMENT_MAP[adapter] = "foo/#{library}"
38
+ end
39
+
40
+ yield
41
+ ensure
42
+ requirements.keys do |adapter|
43
+ OandaAPI::Streaming::JsonParser::REQUIREMENT_MAP[adapter] = requirements[adapter]
44
+ end
45
+ end
46
+
47
+ # Silences warnings issued by Kerel.warn
48
+ def silence_warnings
49
+ old_verbose, $VERBOSE = $VERBOSE, nil
50
+ yield
51
+ ensure
52
+ $VERBOSE = old_verbose
53
+ end
54
+
55
+ def simulate_no_adapters
56
+ break_requirements do
57
+ undefine_constants :Yajl, :Gson do
58
+ yield
59
+ end
60
+ end
61
+ end
62
+
63
+ def undefine_constants(*consts)
64
+ values = {}
65
+ consts.each do |const|
66
+ if Object.const_defined?(const)
67
+ values[const] = Object.const_get(const)
68
+ Object.send :remove_const, const
69
+ end
70
+ end
71
+
72
+ yield
73
+
74
+ ensure
75
+ values.each do |const, value|
76
+ Object.const_set const, value
77
+ end
78
+ end
@@ -4,4 +4,5 @@ VCR.configure do |c|
4
4
  c.hook_into :webmock
5
5
  c.configure_rspec_metadata!
6
6
  c.default_cassette_options = { record: :new_episodes }
7
+ c.ignore_hosts 'codeclimate.com'
7
8
  end
metadata CHANGED
@@ -1,29 +1,29 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: oanda_api
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.9.0
4
+ version: 0.9.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Dean Missikowski
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2015-03-11 00:00:00.000000000 Z
11
+ date: 2015-05-15 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: httparty
15
15
  requirement: !ruby/object:Gem::Requirement
16
16
  requirements:
17
- - - "~>"
17
+ - - '='
18
18
  - !ruby/object:Gem::Version
19
- version: '0.13'
19
+ version: 0.13.3
20
20
  type: :runtime
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
- - - "~>"
24
+ - - '='
25
25
  - !ruby/object:Gem::Version
26
- version: '0.13'
26
+ version: 0.13.3
27
27
  - !ruby/object:Gem::Dependency
28
28
  name: persistent_httparty
29
29
  requirement: !ruby/object:Gem::Requirement
@@ -58,14 +58,14 @@ dependencies:
58
58
  requirements:
59
59
  - - "~>"
60
60
  - !ruby/object:Gem::Version
61
- version: '3.1'
61
+ version: '3.2'
62
62
  type: :development
63
63
  prerelease: false
64
64
  version_requirements: !ruby/object:Gem::Requirement
65
65
  requirements:
66
66
  - - "~>"
67
67
  - !ruby/object:Gem::Version
68
- version: '3.1'
68
+ version: '3.2'
69
69
  - !ruby/object:Gem::Dependency
70
70
  name: vcr
71
71
  requirement: !ruby/object:Gem::Requirement
@@ -86,14 +86,14 @@ dependencies:
86
86
  requirements:
87
87
  - - "~>"
88
88
  - !ruby/object:Gem::Version
89
- version: '1.20'
89
+ version: '1.21'
90
90
  type: :development
91
91
  prerelease: false
92
92
  version_requirements: !ruby/object:Gem::Requirement
93
93
  requirements:
94
94
  - - "~>"
95
95
  - !ruby/object:Gem::Version
96
- version: '1.20'
96
+ version: '1.21'
97
97
  - !ruby/object:Gem::Dependency
98
98
  name: yard
99
99
  requirement: !ruby/object:Gem::Requirement
@@ -127,6 +127,7 @@ files:
127
127
  - Rakefile
128
128
  - lib/oanda_api.rb
129
129
  - lib/oanda_api/client/client.rb
130
+ - lib/oanda_api/client/json_parser.rb
130
131
  - lib/oanda_api/client/namespace_proxy.rb
131
132
  - lib/oanda_api/client/resource_descriptor.rb
132
133
  - lib/oanda_api/client/token_client.rb
@@ -144,7 +145,12 @@ files:
144
145
  - lib/oanda_api/resource/transaction.rb
145
146
  - lib/oanda_api/resource_base.rb
146
147
  - lib/oanda_api/resource_collection.rb
148
+ - lib/oanda_api/streaming/adapter_error.rb
149
+ - lib/oanda_api/streaming/adapters/generic.rb
150
+ - lib/oanda_api/streaming/adapters/gson.rb
151
+ - lib/oanda_api/streaming/adapters/yajl.rb
147
152
  - lib/oanda_api/streaming/client.rb
153
+ - lib/oanda_api/streaming/json_parser.rb
148
154
  - lib/oanda_api/streaming/request.rb
149
155
  - lib/oanda_api/utils/utils.rb
150
156
  - lib/oanda_api/version.rb
@@ -176,6 +182,7 @@ files:
176
182
  - spec/fixtures/vcr_cassettes/sandbox_client_account.yml
177
183
  - spec/fixtures/vcr_cassettes/sandbox_instrument_EUR_USD.yml
178
184
  - spec/oanda_api/client/client_spec.rb
185
+ - spec/oanda_api/client/json_parser_spec.rb
179
186
  - spec/oanda_api/client/namespace_proxy_spec.rb
180
187
  - spec/oanda_api/client/resource_descriptor_spec.rb
181
188
  - spec/oanda_api/client/token_client_spec.rb
@@ -189,9 +196,14 @@ files:
189
196
  - spec/oanda_api/examples/transactions_spec.rb
190
197
  - spec/oanda_api/resource_base_spec.rb
191
198
  - spec/oanda_api/resource_collection_spec.rb
199
+ - spec/oanda_api/streaming/adapters/generic_spec.rb
200
+ - spec/oanda_api/streaming/adapters/gson_spec.rb
201
+ - spec/oanda_api/streaming/adapters/yajl_spec.rb
192
202
  - spec/oanda_api/streaming/client_spec.rb
203
+ - spec/oanda_api/streaming/json_parser_spec.rb
193
204
  - spec/oanda_api/streaming/request_spec.rb
194
205
  - spec/oanda_api/utils/utils_spec.rb
206
+ - spec/shared/adapter.rb
195
207
  - spec/spec_helper.rb
196
208
  - spec/support/client_helper.rb
197
209
  - spec/support/vcr.rb
@@ -247,6 +259,7 @@ test_files:
247
259
  - spec/fixtures/vcr_cassettes/sandbox_client_account.yml
248
260
  - spec/fixtures/vcr_cassettes/sandbox_instrument_EUR_USD.yml
249
261
  - spec/oanda_api/client/client_spec.rb
262
+ - spec/oanda_api/client/json_parser_spec.rb
250
263
  - spec/oanda_api/client/namespace_proxy_spec.rb
251
264
  - spec/oanda_api/client/resource_descriptor_spec.rb
252
265
  - spec/oanda_api/client/token_client_spec.rb
@@ -260,9 +273,14 @@ test_files:
260
273
  - spec/oanda_api/examples/transactions_spec.rb
261
274
  - spec/oanda_api/resource_base_spec.rb
262
275
  - spec/oanda_api/resource_collection_spec.rb
276
+ - spec/oanda_api/streaming/adapters/generic_spec.rb
277
+ - spec/oanda_api/streaming/adapters/gson_spec.rb
278
+ - spec/oanda_api/streaming/adapters/yajl_spec.rb
263
279
  - spec/oanda_api/streaming/client_spec.rb
280
+ - spec/oanda_api/streaming/json_parser_spec.rb
264
281
  - spec/oanda_api/streaming/request_spec.rb
265
282
  - spec/oanda_api/utils/utils_spec.rb
283
+ - spec/shared/adapter.rb
266
284
  - spec/spec_helper.rb
267
285
  - spec/support/client_helper.rb
268
286
  - spec/support/vcr.rb