paid 1.0.1 → 1.0.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (53) hide show
  1. checksums.yaml +4 -4
  2. data/History.txt +6 -0
  3. data/Rakefile +1 -1
  4. data/VERSION +1 -1
  5. data/lib/paid.rb +32 -207
  6. data/lib/paid/account.rb +22 -10
  7. data/lib/paid/api_list.rb +56 -17
  8. data/lib/paid/api_method.rb +93 -0
  9. data/lib/paid/api_resource.rb +130 -8
  10. data/lib/paid/customer.rb +104 -40
  11. data/lib/paid/errors/api_connection_error.rb +1 -1
  12. data/lib/paid/errors/api_error.rb +25 -3
  13. data/lib/paid/errors/authentication_error.rb +1 -1
  14. data/lib/paid/errors/paid_error.rb +2 -9
  15. data/lib/paid/event.rb +28 -17
  16. data/lib/paid/event_data.rb +10 -0
  17. data/lib/paid/headers_builder.rb +75 -0
  18. data/lib/paid/invoice.rb +55 -16
  19. data/lib/paid/params_builder.rb +26 -0
  20. data/lib/paid/path_builder.rb +38 -0
  21. data/lib/paid/plan.rb +38 -13
  22. data/lib/paid/refund_list.rb +26 -0
  23. data/lib/paid/requester.rb +97 -0
  24. data/lib/paid/subscription.rb +47 -16
  25. data/lib/paid/transaction.rb +64 -16
  26. data/lib/paid/util.rb +14 -40
  27. data/paid.gemspec +1 -1
  28. data/test/paid/{api_class_test.rb → _api_resource_test.rb} +31 -17
  29. data/test/paid/account_test.rb +3 -3
  30. data/test/paid/api_list_test.rb +14 -8
  31. data/test/paid/api_method_test.rb +89 -0
  32. data/test/paid/customer_test.rb +20 -10
  33. data/test/paid/event_test.rb +3 -4
  34. data/test/paid/headers_builder_test.rb +39 -0
  35. data/test/paid/invoice_test.rb +3 -3
  36. data/test/paid/params_builder_test.rb +57 -0
  37. data/test/paid/path_builder_test.rb +67 -0
  38. data/test/paid/plan_test.rb +3 -3
  39. data/test/paid/requester_test.rb +86 -0
  40. data/test/paid/subscription_test.rb +3 -3
  41. data/test/paid/transaction_test.rb +4 -4
  42. data/test/paid/util_test.rb +36 -35
  43. data/test/test_data.rb +9 -2
  44. data/test/test_helper.rb +14 -14
  45. metadata +23 -19
  46. data/lib/paid/api_class.rb +0 -338
  47. data/lib/paid/api_singleton.rb +0 -5
  48. data/lib/paid/errors/invalid_request_error.rb +0 -10
  49. data/test/mock_resource.rb +0 -69
  50. data/test/paid/api_resource_test.rb +0 -28
  51. data/test/paid/api_singleton_test.rb +0 -12
  52. data/test/paid/authentication_test.rb +0 -50
  53. data/test/paid/status_codes_test.rb +0 -63
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 7bda48a84419fc70d7200704ab421c9b4980e49a
4
- data.tar.gz: 4ac182f0c95b23739c0ee2346b3b3b73874b999f
3
+ metadata.gz: 2e07a6ca29545de7c32a15851ab9742341f9c0a4
4
+ data.tar.gz: dd51cf68c35c3211960950660273101a5d306126
5
5
  SHA512:
6
- metadata.gz: ae4a45c77debf4fe5cccff176525159bcf74d72e0e2700028321c63d21b906df022ad33dfe306c7d1cf91bba5329de65b7062b023afb10e2797b579725c84e40
7
- data.tar.gz: 3fe73b9564435a8824ad55f99954c85e5e303c4ae8a915440ad37c27cc197e319381ec0ece68e916ce5b912d0556b4a421368f4bd0d52b3c842fa64ff909c378
6
+ metadata.gz: 9a9dd416fff9f14807f3773311ffadacee30624275e1a2ea50616ecf616593894f4223a1a0f32a26a5aa94f1a475d01eeb4db4ef0d813e854bc28c80aea9009d
7
+ data.tar.gz: f3d20bde96f8e59e963236dbc97df50bcfb75969b6a32fcf7fc908c3cdf67481cbb0bf2078264842b1a4487ca28bdc91b371b182546d7c18db6964dd65e706b7
data/History.txt CHANGED
@@ -1,3 +1,9 @@
1
+ === 1.0.2 2015-04-13
2
+
3
+ * 1 minor enhancement:
4
+ * Adding support for refunds endpoint.
5
+
6
+
1
7
  === 1.0 2015-03-04
2
8
 
3
9
  * 1 major enhancement:
data/Rakefile CHANGED
@@ -4,7 +4,7 @@ require './tasks/api_test.rb'
4
4
  task :default => [:test]
5
5
 
6
6
  Rake::TestTask.new do |t|
7
- t.pattern = './test/**/*_test.rb'
7
+ t.pattern = './test/**/[^_]*_test.rb'
8
8
  end
9
9
 
10
10
  task :test_api, [:api_key] do |t, args|
data/VERSION CHANGED
@@ -1 +1 @@
1
- 1.0.1
1
+ 1.0.2
data/lib/paid.rb CHANGED
@@ -10,226 +10,51 @@ require 'base64'
10
10
  # Version
11
11
  require 'paid/version'
12
12
 
13
- # Resources
14
- require 'paid/api_class'
15
- require 'paid/api_resource'
16
- require 'paid/api_singleton'
17
- require 'paid/api_list'
18
- require 'paid/util'
19
-
20
- # Requires for classes
21
- require 'paid/transaction'
22
- require 'paid/invoice'
23
- require 'paid/event'
24
- require 'paid/customer'
25
- require 'paid/plan'
26
- require 'paid/subscription'
27
- require 'paid/account'
28
-
29
13
  # Errors
30
14
  require 'paid/errors/paid_error'
31
15
  require 'paid/errors/api_error'
32
16
  require 'paid/errors/api_connection_error'
33
- require 'paid/errors/invalid_request_error'
34
17
  require 'paid/errors/authentication_error'
35
18
 
36
- module Paid
37
- @api_base = "https://api.paidapi.com"
38
- @api_key = nil
39
-
40
- class << self
41
- attr_accessor :api_key, :api_base, :api_test
42
- end
43
-
44
- def self.api_url(path='')
45
- "#{@api_base}#{path}"
46
- end
47
-
48
- def self.request(method, path, params={}, headers={})
49
- verify_api_key(api_key)
50
-
51
- url = api_url(path)
52
-
53
- request_opts = { :verify_ssl => false }
54
-
55
- if [:get, :head, :delete].include?(method.to_s.downcase.to_sym)
56
- unless params.empty?
57
- url += URI.parse(url).query ? '&' : '?' + Util.query_string(params)
58
- end
59
- params = nil
60
- end
61
-
62
- headers = default_headers.update(basic_auth_headers(api_key)).update(headers)
63
- request_opts.update(:headers => headers,
64
- :method => method,
65
- :open_timeout => 30,
66
- :payload => params,
67
- :url => url,
68
- :timeout => 60)
69
-
70
- begin
71
- response = execute_request(request_opts)
72
- rescue Exception => e
73
- handle_request_error(e, url)
74
- end
75
-
76
- parse(response)
77
- end
78
-
79
- # Mostly here for stubbing out during tests.
80
- def self.execute_request(opts)
81
- RestClient::Request.execute(opts)
82
- end
83
-
84
- def self.parse(response)
85
- begin
86
- json = JSON.parse(response.body)
87
- rescue JSON::ParserError
88
- raise APIError.generic(response.code, response.body)
89
- end
90
-
91
- # TODO(jonclahoun): Remove this when Paid's API returns the correct status code.
92
- json = Util.symbolize_keys(json)
93
- if json.has_key?(:error)
94
- raise PaidError.new(json[:error][:message], response.code, response.body, json)
95
- end
96
- json
97
- end
98
-
99
- def self.default_headers
100
- headers = {
101
- :user_agent => "Paid/::API_VERSION:: RubyBindings/#{Paid::VERSION}",
102
- :content_type => 'application/x-www-form-urlencoded'
103
- }
104
-
105
- begin
106
- headers.update(:x_paid_client_user_agent => JSON.generate(user_agent))
107
- rescue => e
108
- headers.update(:x_paid_client_raw_user_agent => user_agent.inspect,
109
- :error => "#{e} (#{e.class})")
110
- end
111
- headers
112
- end
113
-
114
- def self.basic_auth_headers(api_key=@api_key)
115
- api_key ||= @api_key
116
- unless api_key
117
- raise ArgumentError.new('No API key provided. Set your API key using "Paid.api_key = <API-KEY>".')
118
- end
119
-
120
- base_64_key = Base64.encode64("#{api_key}:")
121
- {
122
- "Authorization" => "Basic #{base_64_key}",
123
- }
124
- end
125
-
126
- def self.user_agent
127
- @uname ||= get_uname
128
- lang_version = "#{RUBY_VERSION} p#{RUBY_PATCHLEVEL} (#{RUBY_RELEASE_DATE})"
129
-
130
- {
131
- :bindings_version => Paid::VERSION,
132
- :lang => 'ruby',
133
- :lang_version => lang_version,
134
- :platform => RUBY_PLATFORM,
135
- :publisher => 'paid',
136
- :uname => @uname
137
- }
138
- end
139
-
140
- def self.get_uname
141
- `uname -a 2>/dev/null`.strip if RUBY_PLATFORM =~ /linux|darwin/i
142
- rescue Errno::ENOMEM => ex # couldn't create subprocess
143
- "uname lookup failed"
144
- end
145
-
146
- def self.verify_api_key(api_key)
147
- unless api_key
148
- raise AuthenticationError.new('No API key provided. ' +
149
- 'Set your API key using "Paid.api_key = <API-KEY>". ' +
150
- 'You can generate API keys from the Paid web interface. ' +
151
- 'See http://docs.paidapi.com/#authentication for details, or email hello@paidapi.com ' +
152
- 'if you have any questions.')
153
- end
154
-
155
- if api_key =~ /\s/
156
- raise AuthenticationError.new('Your API key is invalid, as it contains ' +
157
- 'whitespace. (HINT: You can double-check your API key from the ' +
158
- 'Paid web interface. See http://docs.paidapi.com/#authentication for details, or ' +
159
- 'email hello@paidapi.com if you have any questions.)')
160
- end
161
- end
162
-
163
- def self.handle_request_error(error, url)
164
- # First we see if this is an error with a response, and if it is
165
- # we check to see if there is an http code and body to work with.
166
- if error.is_a?(RestClient::ExceptionWithResponse)
167
- if error.http_code && error.http_body
168
- handle_api_error(error.http_code, error.http_body)
169
- end
170
- end
171
-
172
- # If we got here then the error hasn't been handled yet.
173
- # Handle it as a connection error.
174
- handle_connection_error(error, url)
175
-
176
- # Finally if we get here we don't know what type of error it is, so just raise it.
177
- raise error
178
- end
179
-
180
- def self.handle_connection_error(error, url)
181
- message = "An error occurred while connecting to Paid at #{url}."
19
+ # Wrapper around RestClient
20
+ require 'paid/requester'
182
21
 
183
- case error
184
- when RestClient::RequestTimeout
185
- message += connection_message
22
+ # Builders for creating API methods.
23
+ require 'paid/path_builder'
24
+ require 'paid/headers_builder'
25
+ require 'paid/params_builder'
26
+ require 'paid/api_method'
186
27
 
187
- when RestClient::ServerBrokeConnection
188
- message = "The connection to the server at (#{url}) broke before the " \
189
- "request completed. #{connection_message}"
190
-
191
- when RestClient::SSLCertificateNotVerified
192
- message = "Could not verify Paid's SSL certificate. " \
193
- "Please make sure that your network is not intercepting certificates. " \
194
- "(Try going to https://api.paidapi.com/v0 in your browser.) " \
195
- "If this problem persists, let us know at hello@paidapi.com."
196
-
197
- when SocketError
198
- message = "Unexpected error when trying to connect to Paid. " \
199
- "You may be seeing this message because your DNS is not working. " \
200
- "To check, try running 'host api.paidapi.com' from the command line."
28
+ # Generic resources
29
+ require 'paid/api_resource'
30
+ require 'paid/api_list'
31
+ require 'paid/util'
201
32
 
202
- else
203
- message = "Unexpected error communicating with Paid. " \
204
- "If this problem persists, let us know at hello@paidapi.com. #{connection_message}"
205
- end
33
+ # API specific resources
34
+ require 'paid/account'
35
+ require 'paid/customer'
36
+ require 'paid/event'
37
+ require 'paid/event_data'
38
+ require 'paid/invoice'
39
+ require 'paid/plan'
40
+ require 'paid/subscription'
41
+ require 'paid/transaction'
42
+ require 'paid/refund_list'
206
43
 
207
- raise APIConnectionError.new(message + "\n\n(Network error: #{error.message}")
208
- end
44
+ module Paid
45
+ @api_key = nil
209
46
 
210
- def self.connection_message
211
- "Please check your internet connection and try again. " \
212
- "If this problem persists, you should check Paid's service status at " \
213
- "https://twitter.com/paidstatus, or let us know at hello@paidapi.com."
214
- end
47
+ @api_base = "https://api.paidapi.com/v0"
48
+ @api_staging = "https://api-staging.paidapi.com/v0"
49
+ @auth_header = nil
50
+ @api_version = "v0"
51
+ @support_email = "support@paidapi.com"
52
+ @docs_url = "https://paidapi.com/docs"
215
53
 
216
- def self.handle_api_error(rcode, rbody)
217
- begin
218
- error_obj = JSON.parse(rbody)
219
- rescue JSON::ParserError
220
- raise APIError.generic(rcode, rbody)
221
- end
222
- error_obj = Util.symbolize_keys(error_obj)
223
- raise APIError.generic(rcode, rbody) unless error = error_obj[:error]
224
54
 
225
- case rcode
226
- when 400, 404
227
- raise InvalidRequestError.new(error[:message], error[:param], rcode, rbody, error_obj)
228
- when 401
229
- raise AuthenticationError.new(error[:message], rcode, rbody, error_obj)
230
- else
231
- raise APIError.new(error[:message], rcode, rbody, error_obj)
232
- end
55
+ class << self
56
+ attr_accessor :api_key, :api_base, :api_version
57
+ attr_reader :auth_header, :support_email, :docs_url, :api_staging
233
58
  end
234
59
 
235
60
  end
data/lib/paid/account.rb CHANGED
@@ -1,17 +1,29 @@
1
1
  module Paid
2
- class Account < APISingleton
3
- # attribute :object inherited from APISingleton
4
- attribute :id
5
- attribute :business_name
6
- attribute :business_url
7
- attribute :business_logo
2
+ class Account < APIResource
3
+ attr_reader :id
4
+ attr_reader :object
5
+ attr_accessor :business_name
6
+ attr_accessor :business_url
7
+ attr_accessor :business_logo
8
8
 
9
- api_class_method :retrieve, :get, ":path"
9
+ def self.retrieve(params={}, headers={})
10
+ method = APIMethod.new(:get, "/account", params, headers, self)
11
+ self.new(method.execute, method)
12
+ end
10
13
 
11
- def self.path
12
- "/v0/account"
14
+ def refresh(params={}, headers={})
15
+ method = APIMethod.new(:get, "/account", params, headers, self)
16
+ self.refresh_from(method.execute, method)
13
17
  end
14
18
 
15
- APIClass.register_subclass(self, "account")
19
+ # Everything below here is used behind the scenes.
20
+ APIResource.register_api_subclass(self, "account")
21
+ @api_attributes = {
22
+ :id => { :readonly => true },
23
+ :object => { :readonly => true },
24
+ :business_name => nil,
25
+ :business_url => nil,
26
+ :business_logo => nil
27
+ }
16
28
  end
17
29
  end
data/lib/paid/api_list.rb CHANGED
@@ -1,9 +1,46 @@
1
1
  module Paid
2
- class APIList < APIClass
2
+ class APIList < APIResource
3
3
  include Enumerable
4
4
 
5
- attribute :object
6
- attribute :data
5
+ attr_reader :data
6
+ attr_reader :klass
7
+
8
+ def initialize(klass, json={}, api_method=nil)
9
+ if klass.is_a?(Class)
10
+ @klass = klass
11
+ else
12
+ @klass = Util.constantize(klass)
13
+ end
14
+
15
+ refresh_from(json)
16
+ end
17
+
18
+ def refresh_from(json, api_method=nil)
19
+ unless json.is_a?(Hash)
20
+ json = {
21
+ :data => json
22
+ }
23
+ end
24
+ json = Util.symbolize_keys(json)
25
+
26
+ clear_api_attributes
27
+ @api_method = api_method
28
+ @data = []
29
+ @json = Util.sorta_deep_clone(json)
30
+
31
+ json.each do |k, v|
32
+ if k.to_sym == :data
33
+ if v.respond_to?(:map)
34
+ instance_variable_set("@#{k}", v.map{ |i| @klass.new(i) })
35
+ else
36
+ instance_variable_set("@#{k}", v || [])
37
+ end
38
+ elsif self.class.api_attribute_names.include?(k)
39
+ instance_variable_set("@#{k}", determine_api_attribute_value(k, v))
40
+ end
41
+ end
42
+ self
43
+ end
7
44
 
8
45
  def [](k)
9
46
  data[k]
@@ -25,23 +62,25 @@ module Paid
25
62
  data.each(&blk)
26
63
  end
27
64
 
28
- def self.constructor(klass)
29
- lambda do |json|
30
- instance = self.new
31
- instance.json = json
65
+ def inspect
66
+ "#<#{self.class}[#{@klass}]:0x#{self.object_id.to_s(16)}> Data: " + JSON.pretty_generate(inspect_data)
67
+ end
32
68
 
33
- json.each do |k, v|
34
- if attribute_names.include?(k.to_sym)
35
- if k.to_sym == :data
36
- instance.send("#{k}=", v.map{ |i| klass.construct(i) })
37
- else
38
- instance.send("#{k}=", v)
39
- end
40
- end
69
+ def inspect_data
70
+ ret = []
71
+ data.each do |d|
72
+ if d.respond_to?(:inspect_nested)
73
+ ret << d.inspect_nested
74
+ else
75
+ ret << d
41
76
  end
42
- instance.clear_changed_attributes
43
- instance
44
77
  end
78
+ ret
45
79
  end
80
+
81
+
82
+ @api_attributes = {
83
+ :data => { :readonly => true }
84
+ }
46
85
  end
47
86
  end
@@ -0,0 +1,93 @@
1
+ module Paid
2
+ class APIMethod
3
+
4
+ attr_accessor :path
5
+ attr_accessor :method
6
+ attr_accessor :params
7
+ attr_accessor :headers
8
+
9
+ attr_accessor :response_body
10
+ attr_accessor :response_code
11
+ attr_accessor :error
12
+
13
+ attr_accessor :api_key
14
+ attr_accessor :api_base
15
+
16
+ def initialize(method, path, params, headers, object, api_key=nil, api_base=nil)
17
+ @api_key = api_key || Paid.api_key
18
+ @api_base = api_base || Paid.api_base
19
+
20
+ @method = method.to_sym
21
+ @path = PathBuilder.build(path, object, params)
22
+ @params = ParamsBuilder.build(params)
23
+ @headers = HeadersBuilder.build(headers, @api_key, Paid.auth_header)
24
+ end
25
+
26
+ def execute
27
+ begin
28
+ response = Requester.request(method, url, params, headers)
29
+ @response_body = response.body
30
+ @response_code = response.code
31
+ rescue Exception => e
32
+ @response_body = e.http_body if e.respond_to?(:http_body)
33
+ @response_code = e.http_code if e.respond_to?(:http_code)
34
+ @error = compose_error(e)
35
+ raise @error
36
+ end
37
+
38
+ response_json
39
+ end
40
+
41
+ def url
42
+ "#{api_base}#{@path}"
43
+ end
44
+
45
+ def response_json
46
+ begin
47
+ json = Util.symbolize_keys(JSON.parse(@response_body))
48
+ rescue JSON::ParserError
49
+ raise APIError.new("Unable to parse the server response as JSON.", self)
50
+ end
51
+ end
52
+
53
+ def compose_error(error)
54
+ msg = "An error occured while making the API call."
55
+
56
+ case error
57
+ when RestClient::ExceptionWithResponse
58
+ return error_with_response(error)
59
+
60
+ when RestClient::RequestTimeout
61
+ msg = "The request timed out while making the API call."
62
+
63
+ when RestClient::ServerBrokeConnection
64
+ msg = "The connection to the server broke before the request completed."
65
+
66
+ when SocketError
67
+ msg = "An unexpected error occured while trying to connect to " \
68
+ "the API. You may be seeing this message because your DNS is " \
69
+ "not working. To check, try running 'host #{Paid.api_base}' "\
70
+ "from the command line."
71
+
72
+ else
73
+ msg = "An unexpected error occured. If this problem persists let us " \
74
+ "know at #{Paid.support_email}."
75
+ end
76
+
77
+ return APIConnectionError.new(msg, self)
78
+ end
79
+
80
+ # Handle a few common cases.
81
+ def error_with_response(error)
82
+ case @response_code
83
+ when 400, 404
84
+ return APIError.new("Invalid request. Please check the URL and parameters.", self)
85
+ when 401
86
+ return AuthenticationError.new("Authentication failed. Please check your API key and verify that it is correct.", self)
87
+ else
88
+ return APIError.new("An error occured while making the API call.", self)
89
+ end
90
+ end
91
+
92
+ end
93
+ end