trufina 0.2.5

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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>