diff --git a/CHANGELOG.md b/CHANGELOG.md index e81dc55..b5f07b7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ * Your contribution here. * [#57](https://github.com/dblock/iex-ruby-client/pull/57): Update properties for chart api endpoint - [@brunjo](https://github.com/brunjo). +* [#58](https://github.com/dblock/iex-ruby-client/pull/58): Add search endpoint - [@pmn4](https://github.com/pmn4). ### 1.1.0 (2019/07/08) diff --git a/README.md b/README.md index cd072b6..647d436 100644 --- a/README.md +++ b/README.md @@ -26,6 +26,7 @@ A Ruby client for the [The IEX Cloud API](https://iexcloud.io/docs/api/). - [Get Sector Performance](#get-sector-performance) - [Get Largest Trades](#get-largest-trades) - [Get a Quote for Crypto Currencies](#get-a-quote-for-crypto-currencies) + - [Search](#search) - [Configuration](#configuration) - [Errors](#errors) - [SymbolNotFound](#symbolnotfound) @@ -369,6 +370,21 @@ crypto.high_dollar #'$3,590' See [#crypto](https://iexcloud.io/docs/api/#crypto) for detailed documentation or [crypto.rb](lib/iex/resources/crypto.rb) for returned fields. +### Search + +Searches for companies* + +``` Ruby +results = client.search('msft') + +results.first.company_name # 'Microsoft' +``` + +See [#search](https://iexcloud.io/docs/api/#search) for detailed documentation. + +* - IEX documentation suggests this endpoint will one day return more than just +Companies. + ## Configuration You can configure client options globally or directly with a `IEX::Api::Client` instance. diff --git a/lib/iex/api.rb b/lib/iex/api.rb index ae9f53d..8bf74a5 100644 --- a/lib/iex/api.rb +++ b/lib/iex/api.rb @@ -10,6 +10,7 @@ require_relative 'endpoints/ohlc' require_relative 'endpoints/price' require_relative 'endpoints/quote' +require_relative 'endpoints/search' require_relative 'endpoints/sectors' require_relative 'endpoints/crypto' diff --git a/lib/iex/api/client.rb b/lib/iex/api/client.rb index 01d9732..e11b548 100644 --- a/lib/iex/api/client.rb +++ b/lib/iex/api/client.rb @@ -14,6 +14,7 @@ class Client include Endpoints::Ohlc include Endpoints::Price include Endpoints::Quote + include Endpoints::Search include Endpoints::Sectors include Cloud::Connection diff --git a/lib/iex/endpoints/search.rb b/lib/iex/endpoints/search.rb new file mode 100644 index 0000000..ee8dd98 --- /dev/null +++ b/lib/iex/endpoints/search.rb @@ -0,0 +1,21 @@ +module IEX + module Endpoints + module Search + def search(fragment, options = {}) + raise ArgumentError, 'Fragment is required' if fragment.blank? + + get([ + 'search', + fragment + ].compact.join('/'), options).map do |data| + # Per the IEX documentation, any type of Resource could be returned + # this should become a case statement: + # https://iexcloud.io/docs/api/#search + IEX::Resources::Company.new(data) + end + rescue Faraday::ResourceNotFound => e + raise IEX::Errors::SearchNotFoundError.new(fragment, e.response[:body]) + end + end + end +end diff --git a/lib/iex/errors.rb b/lib/iex/errors.rb index 2a2db39..7c46967 100644 --- a/lib/iex/errors.rb +++ b/lib/iex/errors.rb @@ -1,3 +1,4 @@ +require_relative 'errors/search_not_found_error' require_relative 'errors/symbol_not_found_error' require_relative 'errors/client_error' require_relative 'errors/permission_denied_error' diff --git a/lib/iex/errors/client_error.rb b/lib/iex/errors/client_error.rb index 94cbea9..4b0ccda 100644 --- a/lib/iex/errors/client_error.rb +++ b/lib/iex/errors/client_error.rb @@ -1,6 +1,8 @@ +require_relative 'error' + module IEX module Errors - class ClientError < StandardError + class ClientError < Error attr_reader :response def initialize(response) diff --git a/lib/iex/errors/error.rb b/lib/iex/errors/error.rb new file mode 100644 index 0000000..d2f6a12 --- /dev/null +++ b/lib/iex/errors/error.rb @@ -0,0 +1,5 @@ +module IEX + module Errors + class Error < StandardError; end + end +end diff --git a/lib/iex/errors/search_not_found_error.rb b/lib/iex/errors/search_not_found_error.rb new file mode 100644 index 0000000..ef7c1c2 --- /dev/null +++ b/lib/iex/errors/search_not_found_error.rb @@ -0,0 +1,16 @@ +require_relative 'error' + +module IEX + module Errors + class SearchNotFoundError < Error + attr_reader :response + attr_reader :fragment + + def initialize(fragment, response) + @response = response + @fragment = fragment + super %(Fragment "#{fragment}" Not Found) + end + end + end +end diff --git a/lib/iex/errors/symbol_not_found_error.rb b/lib/iex/errors/symbol_not_found_error.rb index 60bc234..187e427 100644 --- a/lib/iex/errors/symbol_not_found_error.rb +++ b/lib/iex/errors/symbol_not_found_error.rb @@ -1,8 +1,10 @@ +require_relative 'error' + module IEX module Errors - class SymbolNotFoundError < StandardError - attr_reader :symbol + class SymbolNotFoundError < Error attr_reader :response + attr_reader :symbol def initialize(symbol, response) @response = response diff --git a/spec/fixtures/iex/client/search/missing.yml b/spec/fixtures/iex/client/search/missing.yml new file mode 100644 index 0000000..11de2f7 --- /dev/null +++ b/spec/fixtures/iex/client/search/missing.yml @@ -0,0 +1,49 @@ +--- +http_interactions: +- request: + method: get + uri: https://sandbox.iexapis.com/v1/search/?token=Tsk_341c973d296e4e18aa61dd63050ce235 + body: + encoding: US-ASCII + string: '' + headers: + Accept: + - application/json; charset=utf-8 + User-Agent: + - IEX Ruby Client/1.1.1 + Accept-Encoding: + - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 + response: + status: + code: 404 + message: Not Found + headers: + Server: + - nginx + Date: + - Wed, 27 Nov 2019 15:51:35 GMT + Content-Type: + - text/html; charset=utf-8 + Transfer-Encoding: + - chunked + Connection: + - keep-alive + Set-Cookie: + - ctoken=44b29922fd294bfcb199b760fb8df1f6; Max-Age=43200; Path=/; Expires=Thu, + 28 Nov 2019 03:51:35 GMT + Strict-Transport-Security: + - max-age=15768000 + Access-Control-Allow-Origin: + - "*" + Access-Control-Allow-Credentials: + - 'true' + Access-Control-Allow-Methods: + - GET, OPTIONS + Access-Control-Allow-Headers: + - Origin, X-Requested-With, Content-Type, Accept + body: + encoding: ASCII-8BIT + string: Not Found + http_version: + recorded_at: Wed, 27 Nov 2019 15:51:35 GMT +recorded_with: VCR 5.0.0 diff --git a/spec/fixtures/iex/client/search/msft.yml b/spec/fixtures/iex/client/search/msft.yml new file mode 100644 index 0000000..78f82e2 --- /dev/null +++ b/spec/fixtures/iex/client/search/msft.yml @@ -0,0 +1,56 @@ +--- +http_interactions: +- request: + method: get + uri: https://sandbox.iexapis.com/v1/search/msft?token=Tsk_341c973d296e4e18aa61dd63050ce235 + body: + encoding: US-ASCII + string: '' + headers: + Accept: + - application/json; charset=utf-8 + User-Agent: + - IEX Ruby Client/1.1.1 + Accept-Encoding: + - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 + response: + status: + code: 200 + message: OK + headers: + Server: + - nginx + Date: + - Wed, 27 Nov 2019 15:49:19 GMT + Content-Type: + - application/json; charset=utf-8 + Transfer-Encoding: + - chunked + Connection: + - keep-alive + Set-Cookie: + - ctoken=6f5b5acf6f91490c842435746ca2e35e; Max-Age=43200; Path=/; Expires=Thu, + 28 Nov 2019 03:49:19 GMT + Iexcloud-Messages-Used: + - '1' + Iexcloud-Premium-Messages-Used: + - '0' + X-Content-Type-Options: + - nosniff + Strict-Transport-Security: + - max-age=15768000 + Access-Control-Allow-Origin: + - "*" + Access-Control-Allow-Credentials: + - 'true' + Access-Control-Allow-Methods: + - GET, OPTIONS + Access-Control-Allow-Headers: + - Origin, X-Requested-With, Content-Type, Accept + body: + encoding: ASCII-8BIT + string: '[{"symbol":"MSFT","securityName":"ocrsaoMorootnitr Cfip","securityType":"sc","region":"US","exchange":"NSA"},{"symbol":"MSFT-MM","securityName":"MntraoptosrocCofiori + ","securityType":"cs","region":"MX","exchange":"EXM"}]' + http_version: + recorded_at: Wed, 27 Nov 2019 15:49:19 GMT +recorded_with: VCR 5.0.0 diff --git a/spec/fixtures/iex/client/search/unknown.yml b/spec/fixtures/iex/client/search/unknown.yml new file mode 100644 index 0000000..a4108cd --- /dev/null +++ b/spec/fixtures/iex/client/search/unknown.yml @@ -0,0 +1,55 @@ +--- +http_interactions: +- request: + method: get + uri: https://sandbox.iexapis.com/v1/search/unreconized-search-term?token=Tsk_341c973d296e4e18aa61dd63050ce235 + body: + encoding: US-ASCII + string: '' + headers: + Accept: + - application/json; charset=utf-8 + User-Agent: + - IEX Ruby Client/1.1.1 + Accept-Encoding: + - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 + response: + status: + code: 200 + message: OK + headers: + Server: + - nginx + Date: + - Wed, 27 Nov 2019 15:57:06 GMT + Content-Type: + - application/json; charset=utf-8 + Content-Length: + - '2' + Connection: + - keep-alive + Set-Cookie: + - ctoken=cf0d7cb12b534db19a3b4f94328647af; Max-Age=43200; Path=/; Expires=Thu, + 28 Nov 2019 03:57:06 GMT + Iexcloud-Messages-Used: + - '0' + Iexcloud-Premium-Messages-Used: + - '0' + X-Content-Type-Options: + - nosniff + Strict-Transport-Security: + - max-age=15768000 + Access-Control-Allow-Origin: + - "*" + Access-Control-Allow-Credentials: + - 'true' + Access-Control-Allow-Methods: + - GET, OPTIONS + Access-Control-Allow-Headers: + - Origin, X-Requested-With, Content-Type, Accept + body: + encoding: UTF-8 + string: "[]" + http_version: + recorded_at: Wed, 27 Nov 2019 15:57:06 GMT +recorded_with: VCR 5.0.0 diff --git a/spec/iex/endpoints/search_spec.rb b/spec/iex/endpoints/search_spec.rb new file mode 100644 index 0000000..163969a --- /dev/null +++ b/spec/iex/endpoints/search_spec.rb @@ -0,0 +1,34 @@ +require 'spec_helper' + +describe IEX::Endpoints::Search do + context '#search' do + # Search falls outside of the normal testing because it is not exactly + # RESTy. The search endpoint (eventually) can return any type of resource + + include_context 'client' + + before do + # search is not supported on the free tier, except in the sandbox. In + # order for VCR to record the response, we need the endpoint to work + client.endpoint = 'https://sandbox.iexapis.com/v1' + # using the documentation-provided token to use in the Sandbox + client.publishable_token = 'Tsk_341c973d296e4e18aa61dd63050ce235' + end + + it 'searches for resources', vcr: { cassette_name: 'client/search/msft' } do + results = client.search('msft') + results.each do |result| + expect(result).to be_a(IEX::Resources::Company) + end + end + + it 'handles missing search terms', vcr: { cassette_name: 'client/search/missing' } do + expect { client.search('') } + .to raise_error(ArgumentError, 'Fragment is required') + end + + it 'handles unknown search terms', vcr: { cassette_name: 'client/search/unknown' } do + expect(client.search('unreconized-search-term')).to be_empty + end + end +end