yodlicious 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 3a1c24f5be0b2cd579d65a6e4729d7023716a82e
4
+ data.tar.gz: d540dbae32b123cdff4a6a77a222a2fedf8a8c7e
5
+ SHA512:
6
+ metadata.gz: 0b003b8a7e0a7b6cfa4611ea324665a8108da6f6bd4b1ba35394b5ba768b229701a438fc40791b07b555af357b163f37ff4e858eaa6844c272c9ef638e74374a
7
+ data.tar.gz: 0c7b8cb612bb4c4a50dced5363fba4f8c7e94ea5f74d320584ab96746ecf0df2c6d34cecb0747ec96880b5ea2156a05501a3f55e5bf9bd385d67ba9ddad9490e
data/.gitignore ADDED
@@ -0,0 +1,14 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
10
+ *.bundle
11
+ *.so
12
+ *.o
13
+ *.a
14
+ mkmf.log
data/.rspec ADDED
@@ -0,0 +1,4 @@
1
+ --color
2
+ --format doc
3
+ --tag ~skip
4
+ --require spec_helper
data/Gemfile ADDED
@@ -0,0 +1,10 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in yodlicious.gemspec
4
+ gemspec
5
+
6
+ gem 'rspec-its'
7
+ gem 'faraday', '0.9.1'
8
+ gem 'faraday_middleware', '~>0.9.1'
9
+ gem 'socksify', '~>1.6.0'
10
+
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2015 Drew Nichols
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,128 @@
1
+ # Yodlicious 0.0.1_alpha [ ![Codeship Status for liftforward/yodlicious](https://codeship.com/projects/71603f00-9393-0132-dcd0-1a9a253548c0/status?branch=master)](https://codeship.com/projects/62288)
2
+
3
+ Yodlicisous is a ruby gem wrapping the Yodlee REST(ish) API. We had to build this for our integration with Yodlee which was somewhat more painfull than it should have been so we figured we share to be a good neighbor.
4
+
5
+ ## Installation
6
+
7
+ Add this line to your application's Gemfile:
8
+
9
+ ```ruby
10
+ gem 'yodlicious'
11
+ ```
12
+
13
+ And then execute:
14
+
15
+ $ bundle install
16
+
17
+ Or install it yourself as:
18
+
19
+ $ gem install yodlicious
20
+
21
+ ## Usage
22
+
23
+ ### Configuration
24
+
25
+ We needed to use the Yodlee API both within a rails app and outside with multiple Yodlee connections concurrently. As such we provided both the option for a global default configuration and a instance specific configuration. For instance specific:
26
+
27
+ ```ruby
28
+ require "yodlicious"
29
+
30
+ config = {
31
+ base_url: "https://consolidatedsdk.yodlee.com/yodsoap/srest/my-cobranded-path/v1.0",
32
+ cobranded_username: "my-cobranded-user",
33
+ cobranded_password: "my-cobranded-password"
34
+ }
35
+
36
+ yodlee_api = Yodlicious::YodleeApi.new(config)
37
+
38
+ ```
39
+ When in a Rails app it can be more convenient to use a global default configuration. To use global defaults:
40
+ ```ruby
41
+ #/<myproject>/config/initializers/yodlicious.rb
42
+ require 'yodlicious'
43
+
44
+ #setting default configurations for Yodlicious
45
+ Yodlicious::Config.base_url = ENV['YODLEE_BASE_URL']
46
+ Yodlicious::Config.cobranded_username = ENV['YODLEE_COBRANDED_USERNAME']
47
+ Yodlicious::Config.cobranded_password = ENV['YODLEE_COBRANDED_PASSWORD']
48
+
49
+ #setting yodlicious logger to use the Rails logger
50
+ Yodlicious::Config.logger = Rails.logger
51
+ ```
52
+ and wherever you want to use the api simply create a new one and it will pickup the global defaults.
53
+ ```ruby
54
+ yodlee_api = Yodlicious::YodleeApi.new
55
+ ```
56
+ If for any reason you need to, you can pass a hash into the constructor and it will use any provided hash values over the defaults. Note this is done on each value not the entire hash.
57
+
58
+ You can also update an existing instances of the YodleeApi's configuration with the configure method. For example:
59
+ ```ruby
60
+
61
+ yodlee_api = Yodlicious::YodleeApi.new { base_url: 'http://yodlee.com/blablabla' }
62
+
63
+ yodlee_api.configure { base_url: 'https://secure.yodlee.com/blablabla }
64
+
65
+ puts yodlee_api.base_url
66
+ ```
67
+ will output
68
+ ```
69
+ https://secure.yodlee.com/blablabla
70
+ ```
71
+
72
+ ### Configuring the proxy
73
+
74
+ If you're Yodlee account is like ours Yodlee will whitelist certain IPs for access and you'll need to proxy all of your API requests through that IP. You can set the proxy with the proxy_url key. Currently the proxy supports, http, https, and socks proxies. Simply set the proxy_url property in the config hash passed to YodleeApi and it should begin using the proxy. For example:
75
+
76
+ ```
77
+ config = {
78
+ base_url: "https://consolidatedsdk.yodlee.com/yodsoap/srest/my-cobranded-path/v1.0",
79
+ cobranded_username: "my-cobranded-user",
80
+ cobranded_password: "my-cobranded-password",
81
+ proxy_url: "https://my-proxy-server-on-the-whitelist:my=proxy-port/"
82
+ }
83
+
84
+ yodlee_api = Yodlicious::YodleeApi.new(config)
85
+ ```
86
+
87
+ ## Working with the API
88
+
89
+ The Yodlee Api responses are somewhat veried (especially the errors) and as such we build Yodlicious as a pretty thin layer around their request/response model. We didn't attempt to map all their JSON responses into models or anything fancy like that. Instead we simply created a method for each API endpoint which takes the required parameters and return a response object. That said, Response object does provide some conveniences to make up for the inconsisten deliver of errors from Yodlee's APIs.
90
+
91
+ ### Starting your cobranded session
92
+
93
+ Once you've configured an instance of the YodleeAPI the first thing you must do is start a Yodlee cobranded session. This is also a good rails console test to see if everything is configured correctly:
94
+
95
+ ```ruby
96
+ pry(main)> yodlee_api = Yodlicious::YodleeApi.new
97
+ pry(main)> response = yodlee_api.cobranded_login
98
+ pry(main)> response.success?
99
+ => true
100
+ ```
101
+ As you probably suspect the call to cobranded_login wraps the ```/authenticate/coblogin``` endpoint call. If this is a success the YodleeApi instance will cache the cobranded session id yodlee returned and use it on all subsequent api calls. You can also access this value if desired with YodleeApi#cobranded_session_token.
102
+ ```
103
+ pry(main)> yodlee_api.cobranded_session_token
104
+ => "12162013_1:a0b1ac3e32a2e656f8f5bd21de23ae1721ffd9dab8bee9f29811f5959bbf102f16c98354eba252bb030dc96e267bd2489a40562f18e09ee8ba9038d19280cc43"
105
+ ```
106
+ At this point something has probably gone wrong for you and you want to see the response json from ```/authenticate/coblogin```. To do this simply use ```response#body```.
107
+ ```
108
+ pry(main)> response.body
109
+ => {"Error"=>[{"errorDetail"=>"Invalid Cobrand Credentials"}]}
110
+ ```
111
+
112
+ ## Contributing
113
+
114
+ 1. Fork it ( https://github.com/liftforward/yodlicious/fork )
115
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
116
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
117
+ 4. Push to the branch (`git push origin my-new-feature`)
118
+ 5. Create a new Pull Request
119
+
120
+ ### Running the integration suite
121
+
122
+ To run the Yodlicious integration tests you'll need an approved yodlee account. This is more than the one offered here [https://devnow.yodlee.com/user/register]. (Some of the integration suite will work against the devnow APIs but not all. On my todo list is to separate them out to make testing easier.) The integration suite expects these values to be set in the following environment variables:
123
+ ```
124
+ YODLEE_BASE_URL="https://consolidatedsdk.yodlee.com/yodsoap/srest/my-cobranded-path/v1.0"
125
+ YODLEE_COBRANDED_USERNAME="my-cobranded-user"
126
+ YODLEE_COBRANDED_PASSWORD="my-cobranded-password"
127
+ YODLICIOUS_PROXY_URL="https://my-proxy-server-on-the-whitelist:my=proxy-port/"
128
+ ```
data/Rakefile ADDED
@@ -0,0 +1,2 @@
1
+ require "bundler/gem_tasks"
2
+
@@ -0,0 +1,13 @@
1
+
2
+
3
+ module Yodlicious
4
+ class Config
5
+ class << self
6
+ attr_accessor :base_url, :cobranded_username, :cobranded_password, :proxy_url, :logger
7
+ end
8
+
9
+ self.logger = Logger.new(STDOUT)
10
+ self.logger.level = Logger::WARN
11
+
12
+ end
13
+ end
@@ -0,0 +1,28 @@
1
+ module Yodlicious
2
+ class ParameterTranslator
3
+ def site_login_form_to_add_site_account_params site_login_form
4
+
5
+ params = { "credentialFields.enclosedType" => "com.yodlee.common.FieldInfoSingle" }
6
+
7
+ i = 0
8
+ site_login_form['componentList'].each { |field|
9
+ # puts "field=#{field}"
10
+ params["credentialFields[#{i}].displayName"] = field['displayName']
11
+ params["credentialFields[#{i}].fieldType.typeName"] = field['fieldType']['typeName']
12
+ params["credentialFields[#{i}].helpText"] = field['helpText']
13
+ params["credentialFields[#{i}].maxlength"] = field['maxlength']
14
+ params["credentialFields[#{i}].name"] = field['name']
15
+ params["credentialFields[#{i}].size"] = field['size']
16
+ params["credentialFields[#{i}].value"] = field['value']
17
+ params["credentialFields[#{i}].valueIdentifier"] = field['valueIdentifier']
18
+ params["credentialFields[#{i}].valueMask"] = field['valueMask']
19
+ params["credentialFields[#{i}].isEditable"] = field['isEditable']
20
+ params["credentialFields[#{i}].value"] = field['value']
21
+
22
+ i += 1
23
+ }
24
+
25
+ params
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,30 @@
1
+ module Yodlicious
2
+ class Response
3
+
4
+ def initialize body
5
+ @body = body
6
+ end
7
+
8
+ def success?
9
+ !fail?
10
+ end
11
+
12
+ def fail?
13
+ body.kind_of?(Hash) && (body['errorOccurred'] == 'true' || body.has_key?('Error'))
14
+ end
15
+
16
+ def body
17
+ @body
18
+ end
19
+
20
+ def error
21
+ if body.kind_of?(Hash)
22
+ if body.has_key?('Error')
23
+ body['Error'][0]['errorDetail']
24
+ elsif body['errorOccurred'] == 'true'
25
+ body['exceptionType']
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,3 @@
1
+ module Yodlicious
2
+ VERSION = "0.0.1"
3
+ end
@@ -0,0 +1,276 @@
1
+ require 'json'
2
+
3
+
4
+ module Yodlicious
5
+ class YodleeApi
6
+ attr_reader :base_url, :cobranded_username, :cobranded_password, :proxy_url, :logger
7
+
8
+ def initialize config = {}
9
+ configure config
10
+ end
11
+
12
+ def configure config = {}
13
+ validate config
14
+ @base_url = config[:base_url] || Yodlicious::Config.base_url
15
+ @cobranded_username = config[:cobranded_username] || Yodlicious::Config.cobranded_username
16
+ @cobranded_password = config[:cobranded_password] || Yodlicious::Config.cobranded_password
17
+ @proxy_url = config[:proxy_url] || Yodlicious::Config.proxy_url
18
+ @logger = config[:logger] || Yodlicious::Config.logger
19
+
20
+ info_log "YodleeApi configured with base_url=#{base_url} cobranded_username=#{cobranded_username} proxy_url=#{proxy_url} logger=#{logger}"
21
+ end
22
+
23
+ def validate config
24
+ [:proxy_url, :base_url, :cobranded_username, :cobranded_password, :logger].each { |key|
25
+ if config.has_key?(key) && config[key].nil?
26
+ raise "Invalid config provided to YodleeApi. Values may not be nil or blank."
27
+ end
28
+ }
29
+ end
30
+
31
+ def proxy_opts
32
+ proxy_opts = {}
33
+
34
+ unless proxy_url == nil
35
+ proxy_opts[:uri] = URI.parse(proxy_url)
36
+ proxy_opts[:socks] = use_socks?
37
+ end
38
+
39
+ proxy_opts
40
+ end
41
+
42
+ def use_socks?
43
+ return proxy_url != nil && proxy_url.start_with?('socks')
44
+ end
45
+
46
+ def cobranded_login
47
+ params = {
48
+ cobrandLogin: cobranded_username,
49
+ cobrandPassword: cobranded_password
50
+ }
51
+
52
+ response = execute_api '/authenticate/coblogin', params
53
+
54
+ if response.success?
55
+ @cobranded_auth = response.body
56
+ else
57
+ @cobranded_auth = nil
58
+ end
59
+
60
+ response
61
+ end
62
+
63
+ def user_login login, pass
64
+ params = {
65
+ login: login,
66
+ password: pass
67
+ }
68
+
69
+ response = cobranded_session_execute_api '/authenticate/login', params
70
+
71
+ if response.success?
72
+ @user_auth = response.body
73
+ else
74
+ @user_auth = nil
75
+ end
76
+
77
+ response
78
+ end
79
+
80
+ def logout_user
81
+ user_session_execute_api '/jsonsdk/Login/logout'
82
+ end
83
+
84
+ def register_user username, password, emailAddress, options = {}
85
+ params = {
86
+ 'userCredentials.loginName' => username,
87
+ 'userCredentials.password' => password,
88
+ 'userCredentials.objectInstanceType' => 'com.yodlee.ext.login.PasswordCredentials',
89
+ 'userProfile.emailAddress' => emailAddress
90
+ #todo add in user preferences
91
+ }.merge(options)
92
+
93
+ response = cobranded_session_execute_api "/jsonsdk/UserRegistration/register3", params
94
+
95
+ if response.success?
96
+ @user_auth = response.body
97
+ else
98
+ @user_auth = nil
99
+ end
100
+
101
+ response
102
+ end
103
+
104
+ def login_or_register_user username, password, email
105
+ info_log "attempting to login #{username}"
106
+ response = user_login(username, password)
107
+
108
+ #TODO look into what other errors could occur here
109
+ if response.fail? && response.error == "Invalid User Credentials"
110
+ info_log "invalid credentials for #{username} attempting to register"
111
+ response = register_user username, password, email
112
+ end
113
+
114
+ if response.success?
115
+ @user_auth = response.body
116
+ else
117
+ @user_auth = nil
118
+ end
119
+
120
+ response
121
+ end
122
+
123
+ def unregister_user
124
+ user_session_execute_api '/jsonsdk/UserRegistration/unregister'
125
+ end
126
+
127
+ def site_search search_string
128
+ user_session_execute_api "/jsonsdk/SiteTraversal/searchSite", { siteSearchString: search_string }
129
+ end
130
+
131
+ def search_content_services search_string
132
+ user_session_execute_api "/jsonsdk/Search/searchContentServices", { keywords: search_string }
133
+ end
134
+
135
+ def add_site_account site_id, site_login_form
136
+ params = {
137
+ siteId: site_id
138
+ }.merge(translator.site_login_form_to_add_site_account_params(site_login_form))
139
+
140
+ user_session_execute_api '/jsonsdk/SiteAccountManagement/addSiteAccount1', params
141
+ end
142
+
143
+
144
+ def add_site_account_and_wait site_id, site_login_form, refresh_interval = 0.5, refresh_trys = 5
145
+ response = add_site_account(site_id, site_login_form)
146
+
147
+ #TODO validate response with assert
148
+ if response.success?
149
+
150
+ if response.body['siteRefreshInfo']['siteRefreshStatus']['siteRefreshStatus'] == 'REFRESH_TRIGGERED' &&
151
+ response.body['siteRefreshInfo']['siteRefreshMode']['refreshMode'] == 'NORMAL'
152
+
153
+ site_account_id = response.body['siteAccountId']
154
+ trys = 1
155
+ begin
156
+ info_log "try #{trys} to get refresh_info for #{site_id}"
157
+ trys += 1
158
+ sleep(refresh_interval)
159
+ refresh_info_response = get_site_refresh_info site_account_id
160
+ response.body['siteRefreshInfo'] = refresh_info_response.body unless refresh_info_response.fail?
161
+ end until (refresh_info_response.success? && refresh_info_response.body['siteRefreshStatus']['siteRefreshStatus'] != 'REFRESH_TRIGGERED') || trys > refresh_trys
162
+ end
163
+
164
+ response
165
+ end
166
+ end
167
+
168
+ def get_site_refresh_info site_account_id
169
+ user_session_execute_api '/jsonsdk/Refresh/getSiteRefreshInfo', { memSiteAccId: site_account_id }
170
+ end
171
+
172
+ def get_item_summaries
173
+ user_session_execute_api '/jsonsdk/DataService/getItemSummaries', { 'bridgetAppId' => '10003200' }
174
+ end
175
+
176
+ def get_item_summaries_for_site site_account_id
177
+ user_session_execute_api '/jsonsdk/DataService/getItemSummariesForSite', { memSiteAccId: site_account_id }
178
+ end
179
+
180
+ def get_all_site_accounts
181
+ user_session_execute_api '/jsonsdk/SiteAccountManagement/getAllSiteAccounts'
182
+ end
183
+
184
+ def get_site_info site_id
185
+ params = {
186
+ 'siteFilter.siteId' => site_id,
187
+ 'siteFilter.reqSpecifier' => 16
188
+ }
189
+ cobranded_session_execute_api '/jsonsdk/SiteTraversal/getSiteInfo', params
190
+ end
191
+
192
+ def execute_user_search_request options = {}
193
+ params = {
194
+ 'transactionSearchRequest.containerType' => 'All',
195
+ 'transactionSearchRequest.lowerFetchLimit' => 1,
196
+ 'transactionSearchRequest.higherFetchLimit' => 500,
197
+ 'transactionSearchRequest.resultRange.startNumber' => 1,
198
+ 'transactionSearchRequest.resultRange.endNumber' => 10,
199
+ 'transactionSearchRequest.searchClients.clientId' => 1,
200
+ 'transactionSearchRequest.searchClients.clientName' => 'DataSearchService',
201
+ 'transactionSearchRequest.ignoreUserInput' => true,
202
+ #todo make it so that we can pass a simpler hash of arguments
203
+ # 'transactionSearchRequest.userInput' => nil,
204
+ # 'transactionSearchRequest.searchFilter.currencyCode' => nil,
205
+ # 'transactionSearchRequest.searchFilter.postDateRange.fromDate' => nil,
206
+ # 'transactionSearchRequest.searchFilter.postDateRange.toDate' => nil,
207
+ # 'transactionSearchRequest.searchFilter.itemAccountId.identifier' => nil,
208
+ 'transactionSearchRequest.searchFilter.transactionSplitType' => 'ALL_TRANSACTION'
209
+ }.merge(options)
210
+
211
+ user_session_execute_api "/jsonsdk/TransactionSearchService/executeUserSearchRequest", params
212
+ end
213
+
214
+ def cobranded_session_execute_api uri, params = {}
215
+ params = {
216
+ cobSessionToken: cobranded_session_token,
217
+ }.merge(params)
218
+
219
+ execute_api uri, params
220
+ end
221
+
222
+ def user_session_execute_api uri, params = {}
223
+ params = {
224
+ userSessionToken: user_session_token
225
+ }.merge(params)
226
+
227
+ cobranded_session_execute_api uri, params
228
+ end
229
+
230
+ def execute_api uri, params = {}
231
+ debug_log "calling #{uri} with #{params}"
232
+ ssl_opts = { verify: false }
233
+ connection = Faraday.new(url: base_url, ssl: ssl_opts, request: { proxy: proxy_opts })
234
+
235
+ response = connection.post("#{base_url}#{uri}", params)
236
+ debug_log "response=#{response.status} body=#{response.body} headers=#{response.headers}"
237
+
238
+ case response.status
239
+ when 200
240
+ Response.new(JSON.parse(response.body))
241
+ else
242
+ end
243
+ end
244
+
245
+ def translator
246
+ @translator ||= ParameterTranslator.new
247
+ end
248
+
249
+ def cobranded_auth
250
+ @cobranded_auth
251
+ end
252
+
253
+ def user_auth
254
+ @user_auth
255
+ end
256
+
257
+ def cobranded_session_token
258
+ return nil if cobranded_auth.nil?
259
+ cobranded_auth.fetch('cobrandConversationCredentials',{}).fetch('sessionToken','dude')
260
+ end
261
+
262
+ def user_session_token
263
+ return nil if user_auth.nil?
264
+ user_auth.fetch('userContext',{}).fetch('conversationCredentials',{}).fetch('sessionToken',nil)
265
+ end
266
+
267
+ def debug_log msg
268
+ logger.info msg
269
+ end
270
+
271
+ def info_log msg
272
+ logger.info msg
273
+ end
274
+
275
+ end
276
+ end
data/lib/yodlicious.rb ADDED
@@ -0,0 +1,27 @@
1
+ require 'rubygems'
2
+ require 'logger'
3
+ require 'faraday'
4
+ require 'socksify'
5
+ require 'socksify/http'
6
+
7
+ require File.dirname(__FILE__) + "/yodlicious/version"
8
+ require File.dirname(__FILE__) + "/yodlicious/config"
9
+ require File.dirname(__FILE__) + "/yodlicious/parameter_translator"
10
+ require File.dirname(__FILE__) + "/yodlicious/response"
11
+ require File.dirname(__FILE__) + "/yodlicious/yodlicious"
12
+
13
+ class Faraday::Adapter::NetHttp
14
+ def net_http_connection(env)
15
+ if proxy = env[:request][:proxy]
16
+ if proxy[:socks]
17
+ # TCPSocket.socks_username = proxy[:user] if proxy[:user]
18
+ # TCPSocket.socks_password = proxy[:password] if proxy[:password]
19
+ Net::HTTP::SOCKSProxy(proxy[:uri].host, proxy[:uri].port)
20
+ else
21
+ Net::HTTP::Proxy(proxy[:uri].host, proxy[:uri].port, proxy[:uri].user, proxy[:uri].password)
22
+ end
23
+ else
24
+ Net::HTTP
25
+ end.new(env[:url].host, env[:url].port)
26
+ end
27
+ end