kdonovan-trufina 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/lib/requests.rb ADDED
@@ -0,0 +1,132 @@
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
+ libxml = XML::Document.string( self.to_xml )
50
+ libxml.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
+ end
130
+
131
+ end
132
+ end
data/lib/responses.rb ADDED
@@ -0,0 +1,90 @@
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
+ noko = Nokogiri::XML(raw_xml)
12
+
13
+ if Trufina::Config.debug?
14
+ puts "Received XML:\n\n"
15
+ puts noko.to_xml
16
+ puts "\n\n"
17
+ end
18
+
19
+ # Try to find an appropriate local happymapper class
20
+ begin
21
+ klass = "Trufina::Responses::#{noko.root.name.gsub('Trufina', '')}".constantize
22
+ return klass.parse(noko.to_xml)
23
+ rescue
24
+ raise Exceptions::UnknownResponseType.new("Raw XML: \n\n#{noko}")
25
+ end
26
+ end
27
+ end
28
+
29
+ # These classes are used to parse responses from the Trufina API.
30
+ # There's a class in this module for each possible Trufina response
31
+ # (plus AccessNotification, which is basically an asynchronous response).
32
+ module Responses
33
+
34
+ class RequestFailure
35
+ include HappyMapper
36
+ tag 'TrufinaRequestFailure'
37
+
38
+ element :error, String, :tag => 'Error', :attributes => {:kind => String}
39
+ end
40
+
41
+ class AccessNotification
42
+ include HappyMapper
43
+ tag 'TrufinaAccessNotification'
44
+
45
+ element :prt, String, :tag => 'PRT'
46
+ element :tnid, String, :tag => 'TNID'
47
+ end
48
+
49
+ class AccessResponse
50
+ include HappyMapper
51
+ tag 'TrufinaAccessResponse'
52
+
53
+ element :prt, String, :tag => 'PRT'
54
+ element :data, Elements::AccessResponseGroup, :single => true
55
+ element :error, String, :tag => 'Error'
56
+ end
57
+
58
+ class InfoResponse
59
+ include HappyMapper
60
+ tag 'TrufinaInfoResponse'
61
+
62
+ element :prt, String, :tag => 'PRT'
63
+ element :tnid, String, :tag => 'TNID'
64
+ element :pur, String, :tag => 'PUR'
65
+ element :data, Elements::AccessResponseGroup, :single => true
66
+ element :error, String, :tag => 'Error'
67
+ end
68
+
69
+ class LoginInfoResponse
70
+ include HappyMapper
71
+ tag 'TrufinaLoginInfoResponse'
72
+
73
+ element :tlid, String, :tag => 'TLID'
74
+ element :prt, String, :tag => 'PRT'
75
+ element :pur, String, :tag => 'PUR'
76
+ element :data, Elements::AccessResponseGroup, :single => true
77
+ element :error, String, :tag => 'Error'
78
+ end
79
+
80
+ class LoginResponse
81
+ include HappyMapper
82
+ tag 'TrufinaLoginResponse'
83
+
84
+ element :prt, String, :tag => 'PRT'
85
+ element :plid, String, :tag => 'PLID'
86
+ element :error, String, :tag => 'Error'
87
+ end
88
+
89
+ end
90
+ end
data/lib/trufina.rb ADDED
@@ -0,0 +1,143 @@
1
+ require 'net/http'
2
+ require 'net/https'
3
+ require 'ostruct'
4
+ require 'open-uri'
5
+
6
+ require 'nokogiri'
7
+
8
+ # Provides a DSL to easily interact with the XML API offered by Trufina.com.
9
+ class Trufina
10
+
11
+ class << self
12
+
13
+ # Creates and sends a login request for the specified PRT
14
+ #
15
+ # Examples:
16
+ #
17
+ # Trufina.login_request(Time.now)
18
+ # Trufina.login_request(Time.now, :requested => [:phone], :seed => {:name => {:first => 'Foo', :surname => 'Bar'}})
19
+ #
20
+ # Options:
21
+ # * requested -- Hash of requested info to be returned once the user is done with Trufina
22
+ # * seed -- Hash of seed data used to prefill the user's forms at Trufina's website
23
+ def login_request(prt, opts = {})
24
+ opts[:requested] ||= {:name => [:first, :surname]}
25
+ opts[:seed]
26
+ xml = Requests::LoginRequest.new(:prt => prt, :data => opts[:requested], :seed => opts[:seed]).render
27
+ sendToTrufina(xml)
28
+ end
29
+
30
+ # Given a PRT, send the login request an return the redirect URL
31
+ #
32
+ # Sends login request to get a PLID from Trufina, then uses that to build
33
+ # a redirect URL specific to this user.
34
+ #
35
+ # Once user completes filling out their information and makes it available
36
+ # to us, Trufina will ping us with an access_notification to let us know
37
+ # it's there and we should ask for it.
38
+ #
39
+ # Options:
40
+ # * demo -- Boolean value. If true, and Trufina::Config.staging? is true, returns demo URL
41
+ # * requested -- Hash of requested info to be returned once the user is done with Trufina
42
+ # * seed -- Hash of seed data used to prefill the user's forms at Trufina's website
43
+ def login_url(prt, opts = {})
44
+ plid = login_request(prt, :requested => opts.delete(:requested), :seed => opts.delete(:seed)).plid
45
+ login_url_from_plid( plid, opts.delete(:demo) )
46
+ end
47
+
48
+ # This should be exposed to the internet to receive Trufina's postback after
49
+ # a user follows the login_url and completes a profile
50
+ #
51
+ # Receives the access notification, and automatically sends a request for
52
+ # the actual information.
53
+ def handle_access_notification(raw_xml)
54
+ info_request( parseFromTrufina(raw_xml).tnid )
55
+ end
56
+
57
+ # Given a TNID, send info_request
58
+ def info_request(tnid)
59
+ xml = Requests::InfoRequest.new(:tnid => tnid).render
60
+ sendToTrufina(xml)
61
+ end
62
+
63
+ # Given a TLID, send login_info_request
64
+ def login_info_request(tlid)
65
+ xml = Requests::LoginInfoRequest.new(:tlid => tlid).render
66
+ sendToTrufina(xml)
67
+ end
68
+
69
+ # Given either an auth hash containing a PUR and a PRT (e.g. from an InfoResponse
70
+ # or LoginInfoResponse) or a suitable Trufina::*Response object directly (i.e.
71
+ # we can just pass the results of a Trufina.login_info_request directly for auth),
72
+ # as well as a data hash containing any data fields we wish to
73
+ # request about the specified user, sends a request for data off to Trufina.
74
+ # Trufina will respond immediately with a status of "pending" for the newly
75
+ # requested information, will notify the user via email that we're requesting
76
+ # new info, and finally will notify us via an AccessNotification if/when the
77
+ # user grants us access to the additional data.
78
+ def access_request(auth = {}, data = {})
79
+ auth = {:pur => auth.pur, :prt => auth.prt} unless auth.is_a?(Hash)
80
+ xml = Requests::AccessRequest.new( auth.merge(:data => data) ).render
81
+ sendToTrufina(xml)
82
+ end
83
+
84
+
85
+ protected
86
+
87
+ def domain # :nodoc:
88
+ Config.staging? ? 'staging.trufina.com' : 'www.trufina.com'
89
+ end
90
+
91
+ def endpoint # :nodoc:
92
+ '/WebServices/API/'
93
+ end
94
+
95
+ def schema
96
+ @@schema ||= XML::Schema.from_string(open("http://www.trufina.com/api/truapi.xsd").read)
97
+ end
98
+
99
+ # Send the specified XML to Trufina's servers
100
+ def sendToTrufina(xml)
101
+ puts "Sending XML to #{domain}#{endpoint}:\n\n#{xml}\n\n" if Trufina::Config.debug?
102
+
103
+ # Connection Info
104
+ api = Net::HTTP.new( domain, 443 )
105
+ api.use_ssl = true
106
+ api.verify_mode = OpenSSL::SSL::VERIFY_NONE # Prevent annoying warnings
107
+
108
+ # Request info
109
+ method_call = Net::HTTP::Post.new( endpoint, {'Content-Type' => 'text/xml'} )
110
+ method_call.body = xml
111
+
112
+ if Config.staging?
113
+ method_call.basic_auth(Config.staging_access[:username], Config.staging_access[:password])
114
+ end
115
+
116
+ # OK, execute the actual call
117
+ response = api.request(method_call)
118
+ raise Exceptions::NetworkError.new(response.msg) unless response.is_a?(Net::HTTPSuccess)
119
+ parseFromTrufina(response.body)
120
+ end
121
+
122
+ # Try to make something useful from Trufina's XML responses
123
+ def parseFromTrufina(raw_xml)
124
+ response = Trufina::Response.parse(raw_xml)
125
+
126
+ # Raise exception if we've received an error
127
+ if response.is_a?(Trufina::Responses::RequestFailure) # Big error -- the entire returned XML is to tell us
128
+ raise Exceptions::TrufinaResponseException.new("#{response.error.kind}: #{response.error}")
129
+ elsif response.respond_to?(:error) && response.error # Smaller error, noted inline
130
+ raise Exceptions::TrufinaResponseException.new("Error in #{response.class.name}: #{response.error}")
131
+ end
132
+
133
+ return response
134
+ end
135
+
136
+ # Given a PLID (from a login_request), return a url to send the user to
137
+ def login_url_from_plid(plid, is_demo = nil)
138
+ path = (Config.staging? && is_demo) ? "/DemoPartnerLogin/DemoLogin/#{plid}" : "/PartnerLogin/Login/#{plid}"
139
+ "http://#{domain}#{path}"
140
+ end
141
+
142
+ end
143
+ 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,4 @@
1
+ # desc "Explaining what the task does"
2
+ # task :trufina do
3
+ # # Task goes here
4
+ # end
@@ -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
+ <TrufinaInfoRequest xmlns="http://www.trufina.com/truapi/1/0">
3
+ <PID><%= Trufina::Config.credentials[:PID] %></PID>
4
+ <TNID><%= @tnid %></TNID>
5
+ <PAK><%= Trufina::Config.credentials[:PAK] %></PAK>
6
+ </TrufinaInfoRequest>
@@ -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>
@@ -0,0 +1,32 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <TrufinaLoginRequest 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
+ <CancelURL><%= Trufina::Config.endpoints[:cancel] %></CancelURL>
7
+ <SuccessURL><%= Trufina::Config.endpoints[:success] %></SuccessURL>
8
+ <FailureURL><%= Trufina::Config.endpoints[:failure] %></FailureURL>
9
+ <AccessRequest>
10
+ <Name>
11
+ <First />
12
+ </Name>
13
+ <Age/>
14
+ <ResidenceAddress>
15
+ <State/>
16
+ </ResidenceAddress>
17
+ </AccessRequest>
18
+ <SeedInfo>
19
+ <Name>
20
+ <First>John</First>
21
+ <Surname>Doe</Surname>
22
+ </Name>
23
+ <DateOfBirth>1964-09-01</DateOfBirth>
24
+ <ResidenceAddress>
25
+ <StreetAddress>1 Main Street</StreetAddress>
26
+ <StreetAddress>Suite E</StreetAddress>
27
+ <City>Burlingame</City>
28
+ <State>CA</State>
29
+ <PostalCode>94010</PostalCode>
30
+ </ResidenceAddress>
31
+ </SeedInfo>
32
+ </TrufinaLoginRequest>