kdonovan-trufina 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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>