paid 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (44) hide show
  1. checksums.yaml +7 -0
  2. data/Gemfile +8 -0
  3. data/Gemfile.lock +54 -0
  4. data/MIT-LICENSE +20 -0
  5. data/README.rdoc +35 -0
  6. data/Rakefile +34 -0
  7. data/lib/data/ca-certificates.crt +0 -0
  8. data/lib/paid/account.rb +4 -0
  9. data/lib/paid/api_operations/create.rb +17 -0
  10. data/lib/paid/api_operations/delete.rb +11 -0
  11. data/lib/paid/api_operations/list.rb +17 -0
  12. data/lib/paid/api_operations/update.rb +57 -0
  13. data/lib/paid/api_resource.rb +32 -0
  14. data/lib/paid/certificate_blacklist.rb +55 -0
  15. data/lib/paid/customer.rb +16 -0
  16. data/lib/paid/errors/api_connection_error.rb +4 -0
  17. data/lib/paid/errors/api_error.rb +4 -0
  18. data/lib/paid/errors/authentication_error.rb +4 -0
  19. data/lib/paid/errors/invalid_request_error.rb +10 -0
  20. data/lib/paid/errors/paid_error.rb +20 -0
  21. data/lib/paid/event.rb +5 -0
  22. data/lib/paid/invoice.rb +7 -0
  23. data/lib/paid/list_object.rb +37 -0
  24. data/lib/paid/paid_object.rb +187 -0
  25. data/lib/paid/singleton_api_resource.rb +20 -0
  26. data/lib/paid/transaction.rb +7 -0
  27. data/lib/paid/util.rb +127 -0
  28. data/lib/paid/version.rb +3 -0
  29. data/lib/paid.rb +280 -0
  30. data/lib/tasks/paid_tasks.rake +4 -0
  31. data/paid.gemspec +30 -0
  32. data/test/paid/account_test.rb +12 -0
  33. data/test/paid/api_resource_test.rb +361 -0
  34. data/test/paid/certificate_blacklist_test.rb +18 -0
  35. data/test/paid/customer_test.rb +35 -0
  36. data/test/paid/invoice_test.rb +26 -0
  37. data/test/paid/list_object_test.rb +16 -0
  38. data/test/paid/metadata_test.rb +104 -0
  39. data/test/paid/paid_object_test.rb +27 -0
  40. data/test/paid/transaction_test.rb +49 -0
  41. data/test/paid/util_test.rb +59 -0
  42. data/test/test_data.rb +106 -0
  43. data/test/test_helper.rb +41 -0
  44. metadata +203 -0
@@ -0,0 +1,20 @@
1
+ module Paid
2
+ class SingletonAPIResource < APIResource
3
+ def self.url
4
+ if self == SingletonAPIResource
5
+ raise NotImplementedError.new('SingletonAPIResource is an abstract class. You should perform actions on its subclasses (Account, etc.)')
6
+ end
7
+ "/v0/#{CGI.escape(class_name.downcase)}"
8
+ end
9
+
10
+ def url
11
+ self.class.url
12
+ end
13
+
14
+ def self.retrieve(api_key=nil)
15
+ instance = self.new(nil, api_key)
16
+ instance.refresh
17
+ instance
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,7 @@
1
+ module Paid
2
+ class Transaction < APIResource
3
+ include Paid::APIOperations::List
4
+ include Paid::APIOperations::Create
5
+ include Paid::APIOperations::Update
6
+ end
7
+ end
data/lib/paid/util.rb ADDED
@@ -0,0 +1,127 @@
1
+ module Paid
2
+ module Util
3
+ def self.objects_to_ids(h)
4
+ case h
5
+ when APIResource
6
+ h.id
7
+ when Hash
8
+ res = {}
9
+ h.each { |k, v| res[k] = objects_to_ids(v) unless v.nil? }
10
+ res
11
+ when Array
12
+ h.map { |v| objects_to_ids(v) }
13
+ else
14
+ h
15
+ end
16
+ end
17
+
18
+ def self.object_classes
19
+ @object_classes ||= {
20
+ # data structures
21
+ 'list' => ListObject,
22
+
23
+ # business objects
24
+ 'transaction' => Transaction,
25
+ 'customer' => Customer,
26
+ 'event' => Event,
27
+ 'invoice' => Invoice
28
+ }
29
+ end
30
+
31
+ def self.convert_to_paid_object(resp, api_key)
32
+ case resp
33
+ when Array
34
+ resp.map { |i| convert_to_paid_object(i, api_key) }
35
+ when Hash
36
+ # Try converting to a known object class. If none available, fall back to generic PaidObject
37
+ object_classes.fetch(resp[:object], PaidObject).construct_from(resp, api_key)
38
+ else
39
+ resp
40
+ end
41
+ end
42
+
43
+ def self.file_readable(file)
44
+ # This is nominally equivalent to File.readable?, but that can
45
+ # report incorrect results on some more oddball filesystems
46
+ # (such as AFS)
47
+ begin
48
+ File.open(file) { |f| }
49
+ rescue
50
+ false
51
+ else
52
+ true
53
+ end
54
+ end
55
+
56
+ def self.symbolize_names(object)
57
+ case object
58
+ when Hash
59
+ new_hash = {}
60
+ object.each do |key, value|
61
+ key = (key.to_sym rescue key) || key
62
+ new_hash[key] = symbolize_names(value)
63
+ end
64
+ new_hash
65
+ when Array
66
+ object.map { |value| symbolize_names(value) }
67
+ else
68
+ object
69
+ end
70
+ end
71
+
72
+ def self.url_encode(key)
73
+ URI.escape(key.to_s, Regexp.new("[^#{URI::PATTERN::UNRESERVED}]"))
74
+ end
75
+
76
+ def self.flatten_params(params, parent_key=nil)
77
+ result = []
78
+ params.each do |key, value|
79
+ calculated_key = parent_key ? "#{parent_key}[#{url_encode(key)}]" : url_encode(key)
80
+ if value.is_a?(Hash)
81
+ result += flatten_params(value, calculated_key)
82
+ elsif value.is_a?(Array)
83
+ result += flatten_params_array(value, calculated_key)
84
+ else
85
+ result << [calculated_key, value]
86
+ end
87
+ end
88
+ result
89
+ end
90
+
91
+ def self.flatten_params_array(value, calculated_key)
92
+ result = []
93
+ value.each do |elem|
94
+ if elem.is_a?(Hash)
95
+ result += flatten_params(elem, calculated_key)
96
+ elsif elem.is_a?(Array)
97
+ result += flatten_params_array(elem, calculated_key)
98
+ else
99
+ result << ["#{calculated_key}[]", elem]
100
+ end
101
+ end
102
+ result
103
+ end
104
+
105
+ # The secondary opts argument can either be a string or hash
106
+ # Turn this value into an api_key and a set of headers
107
+ def self.parse_opts(opts)
108
+ case opts
109
+ when NilClass
110
+ return nil, {}
111
+ when String
112
+ return opts, {}
113
+ when Hash
114
+ headers = {}
115
+ if opts[:idempotency_key]
116
+ headers[:idempotency_key] = opts[:idempotency_key]
117
+ end
118
+ if opts[:paid_account]
119
+ headers[:paid_account] = opts[:paid_account]
120
+ end
121
+ return opts[:api_key], headers
122
+ else
123
+ raise TypeError.new("parse_opts expects a string or a hash")
124
+ end
125
+ end
126
+ end
127
+ end
@@ -0,0 +1,3 @@
1
+ module Paid
2
+ VERSION = "0.0.1"
3
+ end
data/lib/paid.rb ADDED
@@ -0,0 +1,280 @@
1
+ # Paid Ruby bindings
2
+ # API spec at https://docs.paidapi.com
3
+ require 'cgi'
4
+ require 'set'
5
+ require 'openssl'
6
+ require 'rest_client'
7
+ require 'json'
8
+
9
+ # Version
10
+ require 'paid/version'
11
+
12
+ # API operations
13
+ require 'paid/api_operations/create'
14
+ require 'paid/api_operations/update'
15
+ require 'paid/api_operations/delete'
16
+ require 'paid/api_operations/list'
17
+
18
+ # Resources
19
+ require 'paid/util'
20
+ require 'paid/paid_object'
21
+ require 'paid/api_resource'
22
+ require 'paid/singleton_api_resource'
23
+ require 'paid/list_object'
24
+ require 'paid/account'
25
+ require 'paid/customer'
26
+ require 'paid/certificate_blacklist'
27
+ require 'paid/invoice'
28
+ require 'paid/transaction'
29
+ require 'paid/event'
30
+
31
+ # Errors
32
+ require 'paid/errors/paid_error'
33
+ require 'paid/errors/api_error'
34
+ require 'paid/errors/api_connection_error'
35
+ require 'paid/errors/invalid_request_error'
36
+ require 'paid/errors/authentication_error'
37
+
38
+ module Paid
39
+ DEFAULT_CA_BUNDLE_PATH = File.dirname(__FILE__) + '/data/ca-certificates.crt'
40
+ @api_base = 'https://api.paidapi.com'
41
+
42
+ @ssl_bundle_path = DEFAULT_CA_BUNDLE_PATH
43
+ @verify_ssl_certs = false
44
+ @CERTIFICATE_VERIFIED = false
45
+
46
+
47
+ class << self
48
+ attr_accessor :api_key, :api_base, :verify_ssl_certs, :api_version
49
+ end
50
+
51
+ def self.api_url(url='', api_base_url=nil)
52
+ (api_base_url || @api_base) + url
53
+ end
54
+
55
+ def self.request(method, url, api_key, params={}, headers={}, api_base_url=nil)
56
+ api_base_url = api_base_url || @api_base
57
+
58
+
59
+ unless api_key ||= @api_key
60
+ raise AuthenticationError.new('No API key provided. ' +
61
+ 'Set your API key using "Paid.api_key = <API-KEY>". ' +
62
+ 'You can generate API keys from the Paid web interface. ' +
63
+ 'See https://paidapi.com/api for details, or email hello@paidapi.com ' +
64
+ 'if you have any questions.')
65
+ end
66
+
67
+ if api_key =~ /\s/
68
+ raise AuthenticationError.new('Your API key is invalid, as it contains ' +
69
+ 'whitespace. (HINT: You can double-check your API key from the ' +
70
+ 'Paid web interface. See https://paidapi.com/api for details, or ' +
71
+ 'email hello@paidapi.com if you have any questions.)')
72
+ end
73
+
74
+
75
+ request_opts = { :verify_ssl => false }
76
+
77
+ if ssl_preflight_passed?
78
+ request_opts.update(:verify_ssl => OpenSSL::SSL::VERIFY_PEER,
79
+ :ssl_ca_file => @ssl_bundle_path)
80
+ end
81
+
82
+ if @verify_ssl_certs and !@CERTIFICATE_VERIFIED
83
+ @CERTIFICATE_VERIFIED = CertificateBlacklist.check_ssl_cert(api_base_url, @ssl_bundle_path)
84
+ end
85
+
86
+ params = Util.objects_to_ids(params)
87
+ url = api_url(url, api_base_url)
88
+
89
+ case method.to_s.downcase.to_sym
90
+ when :get, :head, :delete
91
+ # Make params into GET parameters
92
+ url += "#{URI.parse(url).query ? '&' : '?'}#{uri_encode(params)}" if params && params.any?
93
+ payload = nil
94
+ else
95
+ if headers[:content_type] && headers[:content_type] == "multipart/form-data"
96
+ payload = params
97
+ else
98
+ payload = uri_encode(params)
99
+ end
100
+ end
101
+
102
+ request_opts.update(:headers => request_headers(api_key).update(headers),
103
+ :method => method, :open_timeout => 30,
104
+ :payload => payload, :url => url, :timeout => 80)
105
+ begin
106
+ response = execute_request(request_opts)
107
+ rescue SocketError => e
108
+ handle_restclient_error(e, api_base_url)
109
+ rescue NoMethodError => e
110
+ # Work around RestClient bug
111
+ if e.message =~ /\WRequestFailed\W/
112
+ e = APIConnectionError.new('Unexpected HTTP response code')
113
+ handle_restclient_error(e, api_base_url)
114
+ else
115
+ raise
116
+ end
117
+ rescue RestClient::ExceptionWithResponse => e
118
+ if rcode = e.http_code and rbody = e.http_body
119
+ handle_api_error(rcode, rbody)
120
+ else
121
+ handle_restclient_error(e, api_base_url)
122
+ end
123
+ rescue RestClient::Exception, Errno::ECONNREFUSED => e
124
+ handle_restclient_error(e, api_base_url)
125
+ rescue Exception => e
126
+ end
127
+
128
+
129
+ [parse(response), api_key]
130
+ end
131
+
132
+ private
133
+
134
+ def self.ssl_preflight_passed?
135
+ if !verify_ssl_certs && !@no_verify
136
+ "Execute 'Paid.verify_ssl_certs = true' to enable verification."
137
+
138
+ @no_verify = true
139
+
140
+ elsif !Util.file_readable(@ssl_bundle_path) && !@no_bundle
141
+ "because #{@ssl_bundle_path} isn't readable"
142
+
143
+ @no_bundle = true
144
+ end
145
+
146
+ !(@no_verify || @no_bundle)
147
+ end
148
+
149
+ def self.user_agent
150
+ @uname ||= get_uname
151
+ lang_version = "#{RUBY_VERSION} p#{RUBY_PATCHLEVEL} (#{RUBY_RELEASE_DATE})"
152
+
153
+ {
154
+ :bindings_version => Paid::VERSION,
155
+ :lang => 'ruby',
156
+ :lang_version => lang_version,
157
+ :platform => RUBY_PLATFORM,
158
+ :publisher => 'paid',
159
+ :uname => @uname
160
+ }
161
+
162
+ end
163
+
164
+ def self.get_uname
165
+ `uname -a 2>/dev/null`.strip if RUBY_PLATFORM =~ /linux|darwin/i
166
+ rescue Errno::ENOMEM => ex # couldn't create subprocess
167
+ "uname lookup failed"
168
+ end
169
+
170
+ def self.uri_encode(params)
171
+ Util.flatten_params(params).
172
+ map { |k,v| "#{k}=#{Util.url_encode(v)}" }.join('&')
173
+ end
174
+
175
+ def self.request_headers(api_key)
176
+ headers = {
177
+ :user_agent => "Paid/v0 RubyBindings/#{Paid::VERSION}",
178
+ :authorization => "Bearer #{api_key}",
179
+ :content_type => 'application/x-www-form-urlencoded'
180
+ }
181
+
182
+ headers[:paid_version] = api_version if api_version
183
+
184
+ begin
185
+ headers.update(:x_paid_client_user_agent => JSON.generate(user_agent))
186
+ rescue => e
187
+ headers.update(:x_paid_client_raw_user_agent => user_agent.inspect,
188
+ :error => "#{e} (#{e.class})")
189
+ end
190
+ end
191
+
192
+ def self.execute_request(opts)
193
+ RestClient::Request.execute(opts)
194
+ end
195
+
196
+ def self.parse(response)
197
+ begin
198
+ # Would use :symbolize_names => true, but apparently there is
199
+ # some library out there that makes symbolize_names not work.
200
+ response = JSON.parse(response.body)
201
+ rescue JSON::ParserError
202
+ raise general_api_error(response.code, response.body)
203
+ end
204
+
205
+ Util.symbolize_names(response)
206
+ end
207
+
208
+ def self.general_api_error(rcode, rbody)
209
+ APIError.new("Invalid response object from API: #{rbody.inspect} " +
210
+ "(HTTP response code was #{rcode})", rcode, rbody)
211
+ end
212
+
213
+ def self.handle_api_error(rcode, rbody)
214
+ begin
215
+ error_obj = JSON.parse(rbody)
216
+ error_obj = Util.symbolize_names(error_obj)
217
+ error = error_obj[:error] or raise PaidError.new # escape from parsing
218
+
219
+ rescue JSON::ParserError, PaidError
220
+ raise general_api_error(rcode, rbody)
221
+ end
222
+
223
+ case rcode
224
+ when 400, 404
225
+ raise invalid_request_error error, rcode, rbody, error_obj
226
+ when 401
227
+ raise authentication_error error, rcode, rbody, error_obj
228
+ else
229
+ raise api_error error, rcode, rbody, error_obj
230
+ end
231
+
232
+ end
233
+
234
+ def self.invalid_request_error(error, rcode, rbody, error_obj)
235
+ InvalidRequestError.new(error[:message], error[:param], rcode,
236
+ rbody, error_obj)
237
+ end
238
+
239
+ def self.authentication_error(error, rcode, rbody, error_obj)
240
+ AuthenticationError.new(error[:message], rcode, rbody, error_obj)
241
+ end
242
+
243
+ def self.api_error(error, rcode, rbody, error_obj)
244
+ APIError.new(error[:message], rcode, rbody, error_obj)
245
+ end
246
+
247
+ def self.handle_restclient_error(e, api_base_url=nil)
248
+ api_base_url = @api_base unless api_base_url
249
+ connection_message = "Please check your internet connection and try again. " \
250
+ "If this problem persists, you should check Paid's service status at " \
251
+ "https://twitter.com/paidstatus, or let us know at hello@paidapi.com."
252
+
253
+ case e
254
+ when RestClient::RequestTimeout
255
+ message = "Could not connect to Paid (#{api_base_url}). #{connection_message}"
256
+
257
+ when RestClient::ServerBrokeConnection
258
+ message = "The connection to the server (#{api_base_url}) broke before the " \
259
+ "request completed. #{connection_message}"
260
+
261
+ when RestClient::SSLCertificateNotVerified
262
+ message = "Could not verify Paid's SSL certificate. " \
263
+ "Please make sure that your network is not intercepting certificates. " \
264
+ "(Try going to https://api.paidapi.com/v0 in your browser.) " \
265
+ "If this problem persists, let us know at hello@paidapi.com."
266
+
267
+ when SocketError
268
+ message = "Unexpected error communicating when trying to connect to Paid. " \
269
+ "You may be seeing this message because your DNS is not working. " \
270
+ "To check, try running 'host paidapi.com' from the command line."
271
+
272
+ else
273
+ message = "Unexpected error communicating with Paid. " \
274
+ "If this problem persists, let us know at hello@paidapi.com."
275
+
276
+ end
277
+
278
+ raise APIConnectionError.new(message + "\n\n(Network error: #{e.message})")
279
+ end
280
+ end
@@ -0,0 +1,4 @@
1
+ # desc "Explaining what the task does"
2
+ # task :paid do
3
+ # # Task goes here
4
+ # end
data/paid.gemspec ADDED
@@ -0,0 +1,30 @@
1
+ $:.unshift(File.join(File.dirname(__FILE__), 'lib'))
2
+
3
+ # Maintain your gem's version:
4
+ require "paid/version"
5
+
6
+ # Describe your gem and declare its dependencies:
7
+ Gem::Specification.new do |s|
8
+ s.name = 'paid'
9
+ s.version = Paid::VERSION
10
+ s.authors = ['Ryan Jackson']
11
+ s.email = ['ryan@paidapi.com']
12
+ s.homepage = 'https://docs.paidapi.com'
13
+ s.summary = 'Ruby bindings for Paid API'
14
+ s.description = 'Paid is the programmatic way to manage payments. See https://paidapi.com for details.'
15
+ s.license = 'MIT'
16
+
17
+ s.add_dependency('rest-client', '~> 1.4')
18
+ s.add_dependency('mime-types', '>= 1.25', '< 3.0')
19
+ s.add_dependency('json', '~> 1.8.1')
20
+
21
+ s.add_development_dependency('mocha', '~> 0.13.2')
22
+ s.add_development_dependency('shoulda', '~> 3.4.0')
23
+ s.add_development_dependency('test-unit')
24
+ s.add_development_dependency('rake')
25
+
26
+ s.files = `git ls-files`.split("\n")
27
+ s.test_files = `git ls-files -- test/*`.split("\n")
28
+ s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
29
+ s.require_paths = ['lib']
30
+ end
@@ -0,0 +1,12 @@
1
+ require File.expand_path('../../test_helper', __FILE__)
2
+
3
+ module Paid
4
+ class AccountTest < Test::Unit::TestCase
5
+ should "account should be retrievable" do
6
+ resp = {:email => "test+bindings@paidapi.com"}
7
+ @mock.expects(:get).once.returns(test_response(resp))
8
+ a = Paid::Account.retrieve
9
+ assert_equal "test+bindings@paidapi.com", a.email
10
+ end
11
+ end
12
+ end