trufina 0.2.5
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +1 -0
- data/CHANGELOG +16 -0
- data/MIT-LICENSE +20 -0
- data/README.rdoc +179 -0
- data/Rakefile +54 -0
- data/TODO +5 -0
- data/VERSION +1 -0
- data/init.rb +1 -0
- data/lib/config.rb +64 -0
- data/lib/elements.rb +203 -0
- data/lib/exceptions.rb +15 -0
- data/lib/requests.rb +139 -0
- data/lib/responses.rb +86 -0
- data/lib/trufina.rb +153 -0
- data/rails/init.rb +12 -0
- data/tasks/trufina_tasks.rake +4 -0
- data/test/fixtures/requests/access_request.xml +16 -0
- data/test/fixtures/requests/info_request.xml +6 -0
- data/test/fixtures/requests/login_info_request.xml +6 -0
- data/test/fixtures/requests/login_request.xml +32 -0
- data/test/fixtures/requests/login_request_simple.xml +11 -0
- data/test/fixtures/responses/access_notification.xml +5 -0
- data/test/fixtures/responses/access_response.xml +13 -0
- data/test/fixtures/responses/info_response.xml +20 -0
- data/test/fixtures/responses/login_info_response.xml +15 -0
- data/test/fixtures/responses/login_response.xml +5 -0
- data/test/fixtures/schema.xsd +1064 -0
- data/test/test_helper.rb +15 -0
- data/test/trufina_test.rb +8 -0
- data/trufina.gemspec +73 -0
- data/trufina.yml.template +31 -0
- metadata +95 -0
data/lib/exceptions.rb
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
# This file defines all Trufina Exceptions
|
2
|
+
|
3
|
+
class Trufina
|
4
|
+
module Exceptions
|
5
|
+
class TrufinaException < StandardError; end
|
6
|
+
class ConfigFileError < TrufinaException; end
|
7
|
+
class MissingToken < TrufinaException; end
|
8
|
+
class MissingRequiredElements < TrufinaException; end
|
9
|
+
class MissingRequiredAttributes < TrufinaException; end
|
10
|
+
class InvalidElement < TrufinaException; end
|
11
|
+
class NetworkError < TrufinaException; end
|
12
|
+
class UnknownResponseType < TrufinaException; end
|
13
|
+
class TrufinaResponseException < TrufinaException; end
|
14
|
+
end
|
15
|
+
end
|
data/lib/requests.rb
ADDED
@@ -0,0 +1,139 @@
|
|
1
|
+
# Contains all Trufina::Requests::* classes.
|
2
|
+
|
3
|
+
class Trufina
|
4
|
+
|
5
|
+
# These classes are used to generate requests to the Trufina API.
|
6
|
+
# There's a class in this module for each possible Trufina API call.
|
7
|
+
module Requests
|
8
|
+
|
9
|
+
API_NAMESPACE_URL = "http://www.trufina.com/truapi/1/0"
|
10
|
+
class BaseRequest
|
11
|
+
include AllowCreationFromHash
|
12
|
+
|
13
|
+
def initialize(hash = {})
|
14
|
+
super # init method in AllowCreationFromHash
|
15
|
+
autofill_from_config
|
16
|
+
end
|
17
|
+
|
18
|
+
def render
|
19
|
+
validate_contents
|
20
|
+
# validate_against_schema # -- Functioning code doesn't validate, waiting for feedback from Trufina before implementing
|
21
|
+
to_xml
|
22
|
+
end
|
23
|
+
|
24
|
+
protected
|
25
|
+
|
26
|
+
# Automatically assign any required auth or URL information from the global config
|
27
|
+
def autofill_from_config
|
28
|
+
self.pid ||= Config.credentials[:PID] if self.respond_to?(:pid=)
|
29
|
+
self.pak ||= Config.credentials[:PAK] if self.respond_to?(:pak=)
|
30
|
+
|
31
|
+
# Note: URLs are optional fields, but prefilling from config anyway
|
32
|
+
self.cancel_url ||= Config.endpoints[:cancel] if self.respond_to?(:cancel_url=)
|
33
|
+
self.success_url ||= Config.endpoints[:success] if self.respond_to?(:success_url=)
|
34
|
+
self.failure_url ||= Config.endpoints[:failure] if self.respond_to?(:failure_url=)
|
35
|
+
end
|
36
|
+
|
37
|
+
# Ensure all required data is set BEFORE sending the request off to the remote API
|
38
|
+
def validate_contents
|
39
|
+
missing_elements = self.class.elements.map(&:name).select {|e| !self.respond_to?(e) || self.send(e).nil?}
|
40
|
+
raise Exceptions::MissingRequiredElements.new(missing_elements.join(', ')) unless missing_elements.empty?
|
41
|
+
|
42
|
+
missing_attributes = self.class.attributes.map(&:name).select {|a| !self.respond_to?(a) || self.send(a).nil?}
|
43
|
+
raise Exceptions::MissingRequiredAttributes.new(missing_attributes.join(', ')) unless missing_attributes.empty?
|
44
|
+
end
|
45
|
+
|
46
|
+
# We have access to Trufina's XML schema, so we might as well validate against it before we hit their servers
|
47
|
+
# http://codeidol.com/other/rubyckbk/XML-and-HTML/Validating-an-XML-Document/
|
48
|
+
def validate_against_schema
|
49
|
+
lxml = XML::Document.string( self.to_xml )
|
50
|
+
lxml.validate(Trufina.schema)
|
51
|
+
end
|
52
|
+
|
53
|
+
end
|
54
|
+
|
55
|
+
# When we receive a TrufinaAccessNotification from Trufina, we can then use
|
56
|
+
# the included TNID to receive shared user data (note the TNID is valid for
|
57
|
+
# 14 days) with an InfoRequest. We receive this notification when the user
|
58
|
+
# changes their info or share permissions, or after we send a TrufinaAccessRequest.
|
59
|
+
#
|
60
|
+
# Similar to LoginInfoRequest, but no user interaction.
|
61
|
+
class InfoRequest < BaseRequest
|
62
|
+
include HappyMapper
|
63
|
+
tag 'TrufinaInfoRequest'
|
64
|
+
namespace_url API_NAMESPACE_URL
|
65
|
+
|
66
|
+
element :pid, String, :tag => 'PID'
|
67
|
+
element :tnid, String, :tag => 'TNID'
|
68
|
+
element :pak, String, :tag => 'PAK'
|
69
|
+
end
|
70
|
+
|
71
|
+
# We redirect user to Trufina, they complete registration, Trufina redirects
|
72
|
+
# them back to our success url with an attached TLID. We then have 15 minutes
|
73
|
+
# to use this TLID to retreive the shared data with a LoginInfoRequest.
|
74
|
+
#
|
75
|
+
# Similar to InfoRequest, but requires user interaction.
|
76
|
+
class LoginInfoRequest < BaseRequest
|
77
|
+
include HappyMapper
|
78
|
+
tag 'TrufinaLoginInfoRequest'
|
79
|
+
namespace_url API_NAMESPACE_URL
|
80
|
+
|
81
|
+
element :pid, String, :tag => 'PID'
|
82
|
+
element :tlid, String, :tag => 'TLID'
|
83
|
+
element :pak, String, :tag => 'PAK'
|
84
|
+
end
|
85
|
+
|
86
|
+
# Once we've completed the login flow and retreived our information,
|
87
|
+
# if we want additional information later we ask for it with the
|
88
|
+
# AccessRequest.
|
89
|
+
#
|
90
|
+
# The AccessResponse will contain a status of "pending" for the
|
91
|
+
# additional credentials, and Trufina will notify the user via email
|
92
|
+
# that a partner is requesting a new credential. Once the user grants
|
93
|
+
# permission for that credential, the Partner will be notified via a
|
94
|
+
# AccessNotification.
|
95
|
+
class AccessRequest < BaseRequest
|
96
|
+
include HappyMapper
|
97
|
+
tag 'TrufinaAccessRequest'
|
98
|
+
namespace_url API_NAMESPACE_URL
|
99
|
+
|
100
|
+
element :pid, String, :tag => 'PID'
|
101
|
+
element :prt, String, :tag => 'PRT'
|
102
|
+
element :pak, String, :tag => 'PAK'
|
103
|
+
element :pur, String, :tag => 'PUR'
|
104
|
+
|
105
|
+
element :data, Elements::AccessRequest, :single => true
|
106
|
+
end
|
107
|
+
|
108
|
+
|
109
|
+
# When we wan to send a user to Trufina to register and/or provide their
|
110
|
+
# information and allow us access, we send this to Trufina, who sends us
|
111
|
+
# back a PLID we can use to generate the redirect URL to which we should
|
112
|
+
# send the user.
|
113
|
+
class LoginRequest < BaseRequest
|
114
|
+
# TODO -- DOCS UNCLEAR! MAY need a xlmns="" for each of these elements..?
|
115
|
+
include HappyMapper
|
116
|
+
tag 'TrufinaLoginRequest'
|
117
|
+
namespace_url API_NAMESPACE_URL
|
118
|
+
|
119
|
+
element :pid, String, :tag => 'PID'
|
120
|
+
element :prt, String, :tag => 'PRT'
|
121
|
+
element :pak, String, :tag => 'PAK'
|
122
|
+
|
123
|
+
element :cancel_url, String, :tag => 'CancelURL'
|
124
|
+
element :success_url, String, :tag => 'SuccessURL'
|
125
|
+
element :failure_url, String, :tag => 'FailureURL'
|
126
|
+
|
127
|
+
element :data, Elements::AccessRequest, :single => true
|
128
|
+
element :seed, Elements::SeedInfoGroup, :single => true
|
129
|
+
|
130
|
+
def initialize *args
|
131
|
+
super(args)
|
132
|
+
|
133
|
+
# Trufina is brilliant, and they fail if this isn't in the request (even though they don't actually read the value)
|
134
|
+
seed.residence_address.timeframe = 'current' if seed && seed.residence_address
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|
138
|
+
end
|
139
|
+
end
|
data/lib/responses.rb
ADDED
@@ -0,0 +1,86 @@
|
|
1
|
+
# Contains all Trufina::Responses::* classes.
|
2
|
+
|
3
|
+
class Trufina
|
4
|
+
class Response
|
5
|
+
# Given returned Trufina XML, instantiate the proper HappyMapper wrapper.
|
6
|
+
#
|
7
|
+
# (Note that this does not perform any error checking beyond unknown
|
8
|
+
# root node name -- the higher level error checking is handled in the
|
9
|
+
# Trufina.parseFromTrufina method)
|
10
|
+
def self.parse(raw_xml)
|
11
|
+
xml = LibXML::XML::Parser.string(raw_xml).parse
|
12
|
+
|
13
|
+
puts "Received XML:\n\n#{xml}\n\n" if Trufina::Config.debug?
|
14
|
+
|
15
|
+
# Try to find an appropriate local happymapper class
|
16
|
+
begin
|
17
|
+
klass = "Trufina::Responses::#{xml.root.name.gsub('Trufina', '')}".constantize
|
18
|
+
return klass.parse(xml)
|
19
|
+
rescue
|
20
|
+
raise Exceptions::UnknownResponseType.new("Raw XML: \n\n#{xml}")
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
# These classes are used to parse responses from the Trufina API.
|
26
|
+
# There's a class in this module for each possible Trufina response
|
27
|
+
# (plus AccessNotification, which is basically an asynchronous response).
|
28
|
+
module Responses
|
29
|
+
|
30
|
+
class RequestFailure
|
31
|
+
include HappyMapper
|
32
|
+
tag 'TrufinaRequestFailure'
|
33
|
+
|
34
|
+
element :error, String, :tag => 'Error', :attributes => {:kind => String}
|
35
|
+
end
|
36
|
+
|
37
|
+
class AccessNotification
|
38
|
+
include HappyMapper
|
39
|
+
tag 'TrufinaAccessNotification'
|
40
|
+
|
41
|
+
element :prt, String, :tag => 'PRT'
|
42
|
+
element :tnid, String, :tag => 'TNID'
|
43
|
+
end
|
44
|
+
|
45
|
+
class AccessResponse
|
46
|
+
include HappyMapper
|
47
|
+
tag 'TrufinaAccessResponse'
|
48
|
+
|
49
|
+
element :prt, String, :tag => 'PRT'
|
50
|
+
element :data, Elements::AccessResponseGroup, :single => true
|
51
|
+
element :error, String, :tag => 'Error'
|
52
|
+
end
|
53
|
+
|
54
|
+
class InfoResponse
|
55
|
+
include HappyMapper
|
56
|
+
tag 'TrufinaInfoResponse'
|
57
|
+
|
58
|
+
element :prt, String, :tag => 'PRT'
|
59
|
+
element :tnid, String, :tag => 'TNID'
|
60
|
+
element :pur, String, :tag => 'PUR'
|
61
|
+
element :data, Elements::AccessResponseGroup, :single => true
|
62
|
+
element :error, String, :tag => 'Error'
|
63
|
+
end
|
64
|
+
|
65
|
+
class LoginInfoResponse
|
66
|
+
include HappyMapper
|
67
|
+
tag 'TrufinaLoginInfoResponse'
|
68
|
+
|
69
|
+
element :tlid, String, :tag => 'TLID'
|
70
|
+
element :prt, String, :tag => 'PRT'
|
71
|
+
element :pur, String, :tag => 'PUR'
|
72
|
+
element :data, Elements::AccessResponseGroup, :single => true
|
73
|
+
element :error, String, :tag => 'Error'
|
74
|
+
end
|
75
|
+
|
76
|
+
class LoginResponse
|
77
|
+
include HappyMapper
|
78
|
+
tag 'TrufinaLoginResponse'
|
79
|
+
|
80
|
+
element :prt, String, :tag => 'PRT'
|
81
|
+
element :plid, String, :tag => 'PLID'
|
82
|
+
element :error, String, :tag => 'Error'
|
83
|
+
end
|
84
|
+
|
85
|
+
end
|
86
|
+
end
|
data/lib/trufina.rb
ADDED
@@ -0,0 +1,153 @@
|
|
1
|
+
require 'net/http'
|
2
|
+
require 'net/https'
|
3
|
+
require 'ostruct'
|
4
|
+
require 'open-uri'
|
5
|
+
|
6
|
+
# Provides a DSL to easily interact with the XML API offered by Trufina.com.
|
7
|
+
class Trufina
|
8
|
+
|
9
|
+
class << self
|
10
|
+
|
11
|
+
# Creates and sends a login request for the specified PRT
|
12
|
+
#
|
13
|
+
# Examples:
|
14
|
+
#
|
15
|
+
# Trufina.login_request(Time.now)
|
16
|
+
# Trufina.login_request(Time.now, :requested => [:phone], :seed => {:name => {:first => 'Foo', :last => 'Bar'}})
|
17
|
+
#
|
18
|
+
# Options:
|
19
|
+
# * requested -- Hash of requested info to be returned once the user is done with Trufina
|
20
|
+
# * seed -- Hash of seed data used to prefill the user's forms at Trufina's website
|
21
|
+
def login_request(prt, opts = {})
|
22
|
+
opts[:requested] ||= {:name => [:first, :last]}
|
23
|
+
xml = Requests::LoginRequest.new(:prt => prt, :data => opts[:requested], :seed => remove_empties_from_hash(opts[:seed] || [])).render
|
24
|
+
sendToTrufina(xml)
|
25
|
+
end
|
26
|
+
|
27
|
+
# Given a PRT, send the login request an return the redirect URL
|
28
|
+
#
|
29
|
+
# Sends login request to get a PLID from Trufina, then uses that to build
|
30
|
+
# a redirect URL specific to this user.
|
31
|
+
#
|
32
|
+
# Once user completes filling out their information and makes it available
|
33
|
+
# to us, Trufina will ping us with an access_notification to let us know
|
34
|
+
# it's there and we should ask for it.
|
35
|
+
#
|
36
|
+
# Options:
|
37
|
+
# * demo -- Boolean value. If true, and Trufina::Config.staging? is true, returns demo URL
|
38
|
+
# * requested -- Hash of requested info to be returned once the user is done with Trufina
|
39
|
+
# * seed -- Hash of seed data used to prefill the user's forms at Trufina's website
|
40
|
+
def login_url(prt, opts = {})
|
41
|
+
plid = login_request(prt, :requested => opts.delete(:requested), :seed => opts.delete(:seed)).plid
|
42
|
+
login_url_from_plid( plid, opts.delete(:demo) )
|
43
|
+
end
|
44
|
+
|
45
|
+
# This should be exposed to the internet to receive Trufina's postback after
|
46
|
+
# a user follows the login_url and completes a profile
|
47
|
+
#
|
48
|
+
# Receives the access notification, and automatically sends a request for
|
49
|
+
# the actual information.
|
50
|
+
def handle_access_notification(raw_xml)
|
51
|
+
info_request( parseFromTrufina(raw_xml).tnid )
|
52
|
+
end
|
53
|
+
|
54
|
+
# Given a TNID, send info_request
|
55
|
+
def info_request(tnid)
|
56
|
+
xml = Requests::InfoRequest.new(:tnid => tnid).render
|
57
|
+
sendToTrufina(xml)
|
58
|
+
end
|
59
|
+
|
60
|
+
# Given a TLID, send login_info_request
|
61
|
+
def login_info_request(tlid)
|
62
|
+
xml = Requests::LoginInfoRequest.new(:tlid => tlid).render
|
63
|
+
sendToTrufina(xml)
|
64
|
+
end
|
65
|
+
|
66
|
+
# Given either an auth hash containing a PUR and a PRT (e.g. from an InfoResponse
|
67
|
+
# or LoginInfoResponse) or a suitable Trufina::*Response object directly (i.e.
|
68
|
+
# we can just pass the results of a Trufina.login_info_request directly for auth),
|
69
|
+
# as well as a data hash containing any data fields we wish to
|
70
|
+
# request about the specified user, sends a request for data off to Trufina.
|
71
|
+
# Trufina will respond immediately with a status of "pending" for the newly
|
72
|
+
# requested information, will notify the user via email that we're requesting
|
73
|
+
# new info, and finally will notify us via an AccessNotification if/when the
|
74
|
+
# user grants us access to the additional data.
|
75
|
+
def access_request(auth = {}, data = {})
|
76
|
+
auth = {:pur => auth.pur, :prt => auth.prt} unless auth.is_a?(Hash)
|
77
|
+
xml = Requests::AccessRequest.new( auth.merge(:data => data) ).render
|
78
|
+
sendToTrufina(xml)
|
79
|
+
end
|
80
|
+
|
81
|
+
# Retreive Trufina's XSD Schema
|
82
|
+
def schema
|
83
|
+
@@schema ||= XML::Schema.from_string(open("http://www.trufina.com/api/truapi.xsd").read)
|
84
|
+
end
|
85
|
+
|
86
|
+
|
87
|
+
protected
|
88
|
+
|
89
|
+
def domain # :nodoc:
|
90
|
+
Config.staging? ? 'staging.trufina.com' : 'www.trufina.com'
|
91
|
+
end
|
92
|
+
|
93
|
+
def endpoint # :nodoc:
|
94
|
+
'/WebServices/API/'
|
95
|
+
end
|
96
|
+
|
97
|
+
# Send the specified XML to Trufina's servers
|
98
|
+
def sendToTrufina(xml)
|
99
|
+
puts "Sending XML to #{domain}#{endpoint}:\n\n#{xml}\n\n" if Trufina::Config.debug?
|
100
|
+
|
101
|
+
# Connection Info
|
102
|
+
api = Net::HTTP.new( domain, 443 )
|
103
|
+
api.use_ssl = true
|
104
|
+
api.verify_mode = OpenSSL::SSL::VERIFY_NONE # Prevent annoying warnings
|
105
|
+
|
106
|
+
# Request info
|
107
|
+
method_call = Net::HTTP::Post.new( endpoint, {'Content-Type' => 'text/xml'} )
|
108
|
+
method_call.body = xml
|
109
|
+
|
110
|
+
if Config.staging?
|
111
|
+
method_call.basic_auth(Config.staging_access[:username], Config.staging_access[:password])
|
112
|
+
end
|
113
|
+
|
114
|
+
# OK, execute the actual call
|
115
|
+
response = api.request(method_call)
|
116
|
+
raise Exceptions::NetworkError.new(response.msg) unless response.is_a?(Net::HTTPSuccess)
|
117
|
+
parseFromTrufina(response.body)
|
118
|
+
end
|
119
|
+
|
120
|
+
# Try to make something useful from Trufina's XML responses
|
121
|
+
def parseFromTrufina(raw_xml)
|
122
|
+
response = Trufina::Response.parse(raw_xml)
|
123
|
+
|
124
|
+
# Raise exception if we've received an error
|
125
|
+
if response.is_a?(Trufina::Responses::RequestFailure) # Big error -- the entire returned XML is to tell us
|
126
|
+
raise Exceptions::TrufinaResponseException.new("#{response.error.kind}: #{response.error}")
|
127
|
+
elsif response.respond_to?(:error) && response.error # Smaller error, noted inline
|
128
|
+
raise Exceptions::TrufinaResponseException.new("Error in #{response.class.name}: #{response.error}")
|
129
|
+
end
|
130
|
+
|
131
|
+
return response
|
132
|
+
end
|
133
|
+
|
134
|
+
# Given a PLID (from a login_request), return a url to send the user to
|
135
|
+
def login_url_from_plid(plid, is_demo = nil)
|
136
|
+
path = (Config.staging? && is_demo) ? "/DemoPartnerLogin/DemoLogin/#{plid}" : "/PartnerLogin/Login/#{plid}"
|
137
|
+
auth = Config.staging? ? "#{Config.staging_access[:username]}:#{Config.staging_access[:password]}@" : ''
|
138
|
+
"http://#{auth}#{domain}#{path}"
|
139
|
+
end
|
140
|
+
|
141
|
+
# Removes any hash keys with empty values - seed data can't have any blanks, or Trufina gets mad
|
142
|
+
def remove_empties_from_hash(old_hash)
|
143
|
+
new_hash = {}
|
144
|
+
old_hash.each do |key, value|
|
145
|
+
next if value.nil? || value == '' || value == [] || value == {}
|
146
|
+
new_hash[key] = value.is_a?(Hash) ? remove_empties_from_hash(value) : value
|
147
|
+
end
|
148
|
+
|
149
|
+
return new_hash
|
150
|
+
end
|
151
|
+
|
152
|
+
end
|
153
|
+
end
|
data/rails/init.rb
ADDED
@@ -0,0 +1,12 @@
|
|
1
|
+
base = File.join(File.dirname(__FILE__), '..')
|
2
|
+
|
3
|
+
# Require jimmyz's happymapper to enable to_xml for happymap classes
|
4
|
+
require 'happymapper'
|
5
|
+
|
6
|
+
# Require the rest of the plugin files
|
7
|
+
require File.join(base, 'lib', 'exceptions.rb')
|
8
|
+
require File.join(base, 'lib', 'config.rb')
|
9
|
+
require File.join(base, 'lib', 'elements.rb')
|
10
|
+
require File.join(base, 'lib', 'requests.rb')
|
11
|
+
require File.join(base, 'lib', 'responses.rb')
|
12
|
+
require File.join(base, 'lib', 'trufina.rb')
|
@@ -0,0 +1,16 @@
|
|
1
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
2
|
+
<TrufinaAccessRequest xmlns="http://www.trufina.com/truapi/1/0">
|
3
|
+
<PID><%= Trufina::Config.credentials[:PID] %></PID>
|
4
|
+
<PRT><%= @prt %></PRT>
|
5
|
+
<PAK><%= Trufina::Config.credentials[:PAK] %></PAK>
|
6
|
+
<PUR><%= @pur %></PUR>
|
7
|
+
<AccessRequest>
|
8
|
+
<Name>
|
9
|
+
<First />
|
10
|
+
</Name>
|
11
|
+
<Age />
|
12
|
+
<ResidenceAddress>
|
13
|
+
<State />
|
14
|
+
</ResidenceAddress>
|
15
|
+
</AccessRequest>
|
16
|
+
</TrufinaAccessRequest>
|
@@ -0,0 +1,6 @@
|
|
1
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
2
|
+
<TrufinaLoginInfoRequest xmlns="http://www.trufina.com/truapi/1/0">
|
3
|
+
<PID><%= Trufina::Config.credentials[:PID] %></PID>
|
4
|
+
<TLID><%= @tlid %></TLID>
|
5
|
+
<PAK><%= Trufina::Config.credentials[:PAK] %></PAK>
|
6
|
+
</TrufinaLoginInfoRequest>
|