paid 1.0.1 → 1.0.2

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