pass-ruby 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/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