trufina 0.2.5

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.
@@ -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>