pass-ruby 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/lib/pass.rb ADDED
@@ -0,0 +1,242 @@
1
+ # Pass Ruby API
2
+
3
+ require 'cgi'
4
+ require 'set'
5
+ require 'openssl'
6
+ require 'rest_client'
7
+ require 'multi_json'
8
+
9
+ # Version
10
+ require 'pass/version'
11
+
12
+ # API operations
13
+ require 'pass/api_operations/create'
14
+
15
+ # API
16
+ require 'pass/util'
17
+ require 'pass/json'
18
+ require 'pass/pass_object'
19
+ require 'pass/api_resource'
20
+ require 'pass/session'
21
+
22
+ # Errors
23
+ require 'pass/errors/pass_error'
24
+ require 'pass/errors/api_error'
25
+ require 'pass/errors/api_connection_error'
26
+ require 'pass/errors/invalid_request_error'
27
+ require 'pass/errors/authentication_error'
28
+
29
+ module Pass
30
+ @@ssl_bundle_path = File.join(File.dirname(__FILE__), 'data/ca-certificates.crt')
31
+ @@api_token = nil
32
+ @@api_base = 'https://api.passauth.net'
33
+ @@verify_ssl_certs = true
34
+ @@api_version = nil
35
+
36
+ def self.api_url(url='')
37
+ @@api_base + url
38
+ end
39
+
40
+ def self.api_token=(api_token)
41
+ @@api_token = api_token
42
+ end
43
+
44
+ def self.api_token
45
+ @@api_token
46
+ end
47
+
48
+ def self.api_base=(api_base)
49
+ @@api_base = api_base
50
+ end
51
+
52
+ def self.api_base
53
+ @@api_base
54
+ end
55
+
56
+ def self.verify_ssl_certs=(verify)
57
+ @@verify_ssl_certs = verify
58
+ end
59
+
60
+ def self.verify_ssl_certs
61
+ @@verify_ssl_certs
62
+ end
63
+
64
+ def self.api_version=(version)
65
+ @@api_version = version
66
+ end
67
+
68
+ def self.api_version
69
+ @@api_version
70
+ end
71
+
72
+ def self.request(method, url, api_token, params={}, headers={})
73
+ api_token ||= @@api_token
74
+ raise AuthenticationError.new('No API token provided. (HINT: set your API token using "Pass.api_token = <API-TOKEN>".') unless api_token
75
+
76
+ if !verify_ssl_certs
77
+ unless @no_verify
78
+ $stderr.puts "WARNING: Running without SSL cert verification. Execute 'Pass.verify_ssl_certs = true' to enable verification."
79
+ @no_verify = true
80
+ end
81
+ ssl_opts = { :verify_ssl => false }
82
+ elsif !Util.file_readable(@@ssl_bundle_path)
83
+ unless @no_bundle
84
+ $stderr.puts "WARNING: Running without SSL cert verification because #{@@ssl_bundle_path} isn't readable"
85
+ @no_bundle = true
86
+ end
87
+ ssl_opts = { :verify_ssl => false }
88
+ else
89
+ ssl_opts = {
90
+ :verify_ssl => OpenSSL::SSL::VERIFY_PEER,
91
+ :ssl_ca_file => @@ssl_bundle_path
92
+ }
93
+ end
94
+ uname = (@@uname ||= RUBY_PLATFORM =~ /linux|darwin/i ? `uname -a 2>/dev/null`.strip : nil)
95
+ lang_version = "#{RUBY_VERSION} p#{RUBY_PATCHLEVEL} (#{RUBY_RELEASE_DATE})"
96
+ ua = {
97
+ :bindings_version => Pass::VERSION,
98
+ :lang => 'ruby',
99
+ :lang_version => lang_version,
100
+ :platform => RUBY_PLATFORM,
101
+ :publisher => 'pass',
102
+ :uname => uname
103
+ }
104
+
105
+ params = Util.objects_to_ids(params)
106
+ url = self.api_url(url)
107
+ case method.to_s.downcase.to_sym
108
+ when :get, :head, :delete
109
+ # Make params into GET parameters
110
+ if params && params.count > 0
111
+ query_string = Util.flatten_params(params).collect{|key, value| "#{key}=#{Util.url_encode(value)}"}.join('&')
112
+ url += "#{URI.parse(url).query ? '&' : '?'}#{query_string}"
113
+ end
114
+ payload = nil
115
+ else
116
+ payload = Util.flatten_params(params).collect{|(key, value)| "#{key}=#{Util.url_encode(value)}"}.join('&')
117
+ end
118
+
119
+ begin
120
+ headers = { :x_pass_client_user_agent => Pass::JSON.dump(ua) }.merge(headers)
121
+ rescue => e
122
+ headers = {
123
+ :x_pass_client_raw_user_agent => ua.inspect,
124
+ :error => "#{e} (#{e.class})"
125
+ }.merge(headers)
126
+ end
127
+
128
+ headers = {
129
+ :user_agent => "Pass/v1 RubyBindings/#{Pass::VERSION}",
130
+ :authorization => "Token #{api_token}",
131
+ :content_type => 'application/x-www-form-urlencoded',
132
+ :accept => 'application/vnd.pass.v1'
133
+ }.merge(headers)
134
+
135
+ if self.api_version
136
+ headers[:pass_version] = self.api_version
137
+ end
138
+
139
+ opts = {
140
+ :method => method,
141
+ :url => url,
142
+ :headers => headers,
143
+ :open_timeout => 30,
144
+ :payload => payload,
145
+ :timeout => 80
146
+ }.merge(ssl_opts)
147
+
148
+ begin
149
+ response = execute_request(opts)
150
+ rescue SocketError => e
151
+ self.handle_restclient_error(e)
152
+ rescue NoMethodError => e
153
+ # Work around RestClient bug
154
+ if e.message =~ /\WRequestFailed\W/
155
+ e = APIConnectionError.new('Unexpected HTTP response code')
156
+ self.handle_restclient_error(e)
157
+ else
158
+ raise
159
+ end
160
+ rescue RestClient::ExceptionWithResponse => e
161
+ if rcode = e.http_code and rbody = e.http_body
162
+ self.handle_api_error(rcode, rbody)
163
+ else
164
+ self.handle_restclient_error(e)
165
+ end
166
+ rescue RestClient::Exception, Errno::ECONNREFUSED => e
167
+ self.handle_restclient_error(e)
168
+ end
169
+
170
+ rbody = response.body
171
+ rcode = response.code
172
+ begin
173
+ # Would use :symbolize_names => true, but apparently there is
174
+ # some library out there that makes symbolize_names not work.
175
+ resp = Pass::JSON.load(rbody)
176
+ rescue MultiJson::DecodeError
177
+ raise APIError.new("Invalid response object from API: #{rbody.inspect} (HTTP response code was #{rcode})", rcode, rbody)
178
+ end
179
+
180
+ resp = Util.symbolize_names(resp)
181
+ [resp, api_token]
182
+ end
183
+
184
+ private
185
+
186
+ def self.execute_request(opts)
187
+ RestClient::Request.execute(opts)
188
+ end
189
+
190
+ def self.handle_api_error(rcode, rbody)
191
+ begin
192
+ error_obj = Pass::JSON.load(rbody)
193
+ error_obj = Util.symbolize_names(error_obj)
194
+ error = error_obj[:error] or raise PassError.new # escape from parsing
195
+ rescue MultiJson::DecodeError, PassError
196
+ raise APIError.new("Invalid response object from API: #{rbody.inspect} (HTTP response code was #{rcode})", rcode, rbody)
197
+ end
198
+
199
+ case rcode
200
+ when 400, 404 then
201
+ raise invalid_request_error(error, rcode, rbody, error_obj)
202
+ when 401
203
+ raise authentication_error(error, rcode, rbody, error_obj)
204
+ when 402
205
+ raise invalid_request_error(error, rcode, rbody, error_obj)
206
+ else
207
+ raise api_error(error, rcode, rbody, error_obj)
208
+ end
209
+ end
210
+
211
+ def self.invalid_request_error(error, rcode, rbody, error_obj)
212
+ InvalidRequestError.new(error[:message], error[:param], rcode, rbody, error_obj)
213
+ end
214
+
215
+ def self.authentication_error(error, rcode, rbody, error_obj)
216
+ AuthenticationError.new(error[:message], rcode, rbody, error_obj)
217
+ end
218
+
219
+ def self.card_error(error, rcode, rbody, error_obj)
220
+ CardError.new(error[:message], error[:param], error[:code], rcode, rbody, error_obj)
221
+ end
222
+
223
+ def self.api_error(error, rcode, rbody, error_obj)
224
+ APIError.new(error[:message], rcode, rbody, error_obj)
225
+ end
226
+
227
+ def self.handle_restclient_error(e)
228
+ case e
229
+ when RestClient::ServerBrokeConnection, RestClient::RequestTimeout
230
+ message = "Could not connect to Pass (#{@@api_base}). Please check your internet connection and try again. If this problem persists, you should check Pass's service status at https://twitter.com/passstatus, or let us know at support@passauth.net."
231
+ when RestClient::SSLCertificateNotVerified
232
+ message = "Could not verify Pass's SSL certificate. Please make sure that your network is not intercepting certificates. (Try going to https://api.passauth.net/v1 in your browser.) If this problem persists, let us know at support@passauth.net."
233
+ when SocketError
234
+ message = "Unexpected error communicating when trying to connect to Pass. HINT: You may be seeing this message because your DNS is not working. To check, try running 'host passauth.net' from the command line."
235
+ else
236
+ message = "Unexpected error communicating with Pass. If this problem persists, let us know at support@passauth.net."
237
+ end
238
+ message += "\n\n(Network error: #{e.message})"
239
+ raise APIConnectionError.new(message)
240
+ end
241
+
242
+ end
@@ -0,0 +1,16 @@
1
+ module Pass
2
+ module APIOperations
3
+ module Create
4
+ module ClassMethods
5
+ def create(params={}, api_token=nil)
6
+ response, api_token = Pass.request(:post, self.url, api_token, params)
7
+ Util.convert_to_pass_object(response, api_token)
8
+ end
9
+ end
10
+
11
+ def self.included(base)
12
+ base.extend(ClassMethods)
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,16 @@
1
+ module Stripe
2
+ module APIOperations
3
+ module List
4
+ module ClassMethods
5
+ def all(filters={}, api_key=nil)
6
+ response, api_key = Stripe.request(:get, url, api_key, filters)
7
+ Util.convert_to_stripe_object(response, api_key)
8
+ end
9
+ end
10
+
11
+ def self.included(base)
12
+ base.extend(ClassMethods)
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,33 @@
1
+ module Pass
2
+ class APIResource < PassObject
3
+ def self.class_name
4
+ self.name.split('::')[-1]
5
+ end
6
+
7
+ def self.url()
8
+ if self == APIResource
9
+ raise NotImplementedError.new('APIResource is an abstract class. You should perform actions on its subclasses (Charge, Customer, etc.)')
10
+ end
11
+ "/#{CGI.escape(class_name.downcase)}s"
12
+ end
13
+
14
+ def url
15
+ unless id = self['id']
16
+ raise InvalidRequestError.new("Could not determine which URL to request: #{self.class} instance has invalid ID: #{id.inspect}", 'id')
17
+ end
18
+ "#{self.class.url}/#{CGI.escape(id)}"
19
+ end
20
+
21
+ def refresh
22
+ response, api_token = Pass.request(:get, url, @api_token, @retrieve_options)
23
+ refresh_from(response, api_token)
24
+ self
25
+ end
26
+
27
+ def self.retrieve(id, api_token=nil)
28
+ instance = self.new(id, api_token)
29
+ instance.refresh
30
+ instance
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,4 @@
1
+ module Pass
2
+ class APIConnectionError < PassError
3
+ end
4
+ end
@@ -0,0 +1,4 @@
1
+ module Pass
2
+ class APIError < PassError
3
+ end
4
+ end
@@ -0,0 +1,4 @@
1
+ module Pass
2
+ class AuthenticationError < PassError
3
+ end
4
+ end
@@ -0,0 +1,10 @@
1
+ module Pass
2
+ class InvalidRequestError < PassError
3
+ attr_accessor :param
4
+
5
+ def initialize(message, param, http_status=nil, http_body=nil, json_body=nil)
6
+ super(message, http_status, http_body, json_body)
7
+ @param = param
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,20 @@
1
+ module Pass
2
+ class PassError < StandardError
3
+ attr_reader :message
4
+ attr_reader :http_status
5
+ attr_reader :http_body
6
+ attr_reader :json_body
7
+
8
+ def initialize(message=nil, http_status=nil, http_body=nil, json_body=nil)
9
+ @message = message
10
+ @http_status = http_status
11
+ @http_body = http_body
12
+ @json_body = json_body
13
+ end
14
+
15
+ def to_s
16
+ status_string = @http_status.nil? ? "" : "(Status #{@http_status}) "
17
+ "#{status_string}#{@message}"
18
+ end
19
+ end
20
+ end
data/lib/pass/json.rb ADDED
@@ -0,0 +1,21 @@
1
+ module Pass
2
+ module JSON
3
+ if MultiJson.respond_to?(:dump)
4
+ def self.dump(*args)
5
+ MultiJson.dump(*args)
6
+ end
7
+
8
+ def self.load(*args)
9
+ MultiJson.load(*args)
10
+ end
11
+ else
12
+ def self.dump(*args)
13
+ MultiJson.encode(*args)
14
+ end
15
+
16
+ def self.load(*args)
17
+ MultiJson.decode(*args)
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,159 @@
1
+ module Pass
2
+ class PassObject
3
+ include Enumerable
4
+
5
+ attr_accessor :api_token
6
+ @@permanent_attributes = Set.new([:api_token, :id])
7
+
8
+ # The default :id method is deprecated and isn't useful to us
9
+ if method_defined?(:id)
10
+ undef :id
11
+ end
12
+
13
+ def initialize(id=nil, api_token=nil)
14
+ # parameter overloading!
15
+ if id.kind_of?(Hash)
16
+ @retrieve_options = id.dup
17
+ @retrieve_options.delete(:id)
18
+ id = id[:id]
19
+ else
20
+ @retrieve_options = {}
21
+ end
22
+
23
+ @api_token = api_token
24
+ @values = {}
25
+ # This really belongs in APIResource, but not putting it there allows us
26
+ # to have a unified inspect method
27
+ @unsaved_values = Set.new
28
+ @transient_values = Set.new
29
+ self.id = id if id
30
+ end
31
+
32
+ def self.construct_from(values, api_token=nil)
33
+ obj = self.new(values[:id], api_token)
34
+ obj.refresh_from(values, api_token)
35
+ obj
36
+ end
37
+
38
+ def to_s(*args)
39
+ Pass::JSON.dump(@values, :pretty => true)
40
+ end
41
+
42
+ def inspect()
43
+ id_string = (self.respond_to?(:id) && !self.id.nil?) ? " id=#{self.id}" : ""
44
+ "#<#{self.class}:0x#{self.object_id.to_s(16)}#{id_string}> JSON: " + Pass::JSON.dump(@values, :pretty => true)
45
+ end
46
+
47
+ def refresh_from(values, api_token, partial=false)
48
+ @api_token = api_token
49
+
50
+ removed = partial ? Set.new : Set.new(@values.keys - values.keys)
51
+ added = Set.new(values.keys - @values.keys)
52
+ # Wipe old state before setting new. This is useful for e.g. updating a
53
+ # customer, where there is no persistent card parameter. Mark those values
54
+ # which don't persist as transient
55
+
56
+ instance_eval do
57
+ remove_accessors(removed)
58
+ add_accessors(added)
59
+ end
60
+ removed.each do |k|
61
+ @values.delete(k)
62
+ @transient_values.add(k)
63
+ @unsaved_values.delete(k)
64
+ end
65
+ values.each do |k, v|
66
+ @values[k] = Util.convert_to_pass_object(v, api_token)
67
+ @transient_values.delete(k)
68
+ @unsaved_values.delete(k)
69
+ end
70
+ end
71
+
72
+ def [](k)
73
+ k = k.to_sym if k.kind_of?(String)
74
+ @values[k]
75
+ end
76
+
77
+ def []=(k, v)
78
+ send(:"#{k}=", v)
79
+ end
80
+
81
+ def keys
82
+ @values.keys
83
+ end
84
+
85
+ def values
86
+ @values.values
87
+ end
88
+
89
+ def to_json(*a)
90
+ Pass::JSON.dump(@values)
91
+ end
92
+
93
+ def as_json(*a)
94
+ @values.as_json(*a)
95
+ end
96
+
97
+ def to_hash
98
+ @values
99
+ end
100
+
101
+ def each(&blk)
102
+ @values.each(&blk)
103
+ end
104
+
105
+ protected
106
+
107
+ def metaclass
108
+ class << self; self; end
109
+ end
110
+
111
+ def remove_accessors(keys)
112
+ metaclass.instance_eval do
113
+ keys.each do |k|
114
+ next if @@permanent_attributes.include?(k)
115
+ k_eq = :"#{k}="
116
+ remove_method(k) if method_defined?(k)
117
+ remove_method(k_eq) if method_defined?(k_eq)
118
+ end
119
+ end
120
+ end
121
+
122
+ def add_accessors(keys)
123
+ metaclass.instance_eval do
124
+ keys.each do |k|
125
+ next if @@permanent_attributes.include?(k)
126
+ k_eq = :"#{k}="
127
+ define_method(k) { @values[k] }
128
+ define_method(k_eq) do |v|
129
+ @values[k] = v
130
+ @unsaved_values.add(k)
131
+ end
132
+ end
133
+ end
134
+ end
135
+
136
+ def method_missing(name, *args)
137
+ # TODO: only allow setting in updateable classes.
138
+ if name.to_s.end_with?('=')
139
+ attr = name.to_s[0...-1].to_sym
140
+ @values[attr] = args[0]
141
+ @unsaved_values.add(attr)
142
+ add_accessors([attr])
143
+ return
144
+ else
145
+ return @values[name] if @values.has_key?(name)
146
+ end
147
+
148
+ begin
149
+ super
150
+ rescue NoMethodError => e
151
+ if @transient_values.include?(name)
152
+ raise NoMethodError.new(e.message + ". HINT: The '#{name}' attribute was set in the past, however. It was then wiped when refreshing the object with the result returned by Pass's API, probably as a result of a save(). The attributes currently available on this object are: #{@values.keys.join(', ')}")
153
+ else
154
+ raise
155
+ end
156
+ end
157
+ end
158
+ end
159
+ end