paid 0.1.0 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (64) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +5 -1
  3. data/.travis.yml +16 -0
  4. data/History.txt +4 -0
  5. data/README.md +58 -0
  6. data/Rakefile +9 -29
  7. data/VERSION +1 -0
  8. data/bin/paid-console +7 -0
  9. data/gemfiles/default-with-activesupport.gemfile +10 -0
  10. data/gemfiles/json.gemfile +12 -0
  11. data/gemfiles/yajl.gemfile +12 -0
  12. data/lib/paid.rb +129 -177
  13. data/lib/paid/account.rb +14 -1
  14. data/lib/paid/api_class.rb +336 -0
  15. data/lib/paid/api_list.rb +47 -0
  16. data/lib/paid/api_resource.rb +8 -25
  17. data/lib/paid/api_singleton.rb +5 -0
  18. data/lib/paid/customer.rb +36 -21
  19. data/lib/paid/errors/api_error.rb +6 -0
  20. data/lib/paid/event.rb +22 -1
  21. data/lib/paid/invoice.rb +16 -21
  22. data/lib/paid/plan.rb +18 -2
  23. data/lib/paid/subscription.rb +17 -11
  24. data/lib/paid/transaction.rb +19 -12
  25. data/lib/paid/util.rb +53 -106
  26. data/lib/paid/version.rb +1 -1
  27. data/paid.gemspec +10 -11
  28. data/tasks/api_test.rb +187 -0
  29. data/test/mock_resource.rb +69 -0
  30. data/test/paid/account_test.rb +41 -4
  31. data/test/paid/api_class_test.rb +412 -0
  32. data/test/paid/api_list_test.rb +17 -0
  33. data/test/paid/api_resource_test.rb +13 -343
  34. data/test/paid/api_singleton_test.rb +12 -0
  35. data/test/paid/authentication_test.rb +50 -0
  36. data/test/paid/customer_test.rb +189 -29
  37. data/test/paid/event_test.rb +74 -0
  38. data/test/paid/invoice_test.rb +101 -20
  39. data/test/paid/plan_test.rb +84 -8
  40. data/test/paid/status_codes_test.rb +63 -0
  41. data/test/paid/subscription_test.rb +100 -20
  42. data/test/paid/transaction_test.rb +110 -37
  43. data/test/paid/util_test.rb +15 -24
  44. data/test/test_data.rb +144 -93
  45. data/test/test_helper.rb +6 -4
  46. metadata +32 -26
  47. data/Gemfile.lock +0 -54
  48. data/README.rdoc +0 -35
  49. data/lib/data/ca-certificates.crt +0 -0
  50. data/lib/paid/alias.rb +0 -16
  51. data/lib/paid/api_operations/create.rb +0 -17
  52. data/lib/paid/api_operations/delete.rb +0 -11
  53. data/lib/paid/api_operations/list.rb +0 -17
  54. data/lib/paid/api_operations/update.rb +0 -57
  55. data/lib/paid/certificate_blacklist.rb +0 -55
  56. data/lib/paid/list_object.rb +0 -37
  57. data/lib/paid/paid_object.rb +0 -187
  58. data/lib/paid/singleton_api_resource.rb +0 -20
  59. data/lib/tasks/paid_tasks.rake +0 -4
  60. data/test/paid/alias_test.rb +0 -22
  61. data/test/paid/certificate_blacklist_test.rb +0 -18
  62. data/test/paid/list_object_test.rb +0 -16
  63. data/test/paid/paid_object_test.rb +0 -27
  64. data/test/paid/properties_test.rb +0 -103
data/lib/paid/account.rb CHANGED
@@ -1,4 +1,17 @@
1
1
  module Paid
2
- class Account < SingletonAPIResource
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
8
+
9
+ api_class_method :retrieve, :get, ":path"
10
+
11
+ def self.path
12
+ "/v0/account"
13
+ end
14
+
15
+ APIClass.register_subclass(self, "account")
3
16
  end
4
17
  end
@@ -0,0 +1,336 @@
1
+ module Paid
2
+ class APIClass
3
+ attr_accessor :json
4
+
5
+ def self.path
6
+ raise NotImplementedError.new("APIClass is an abstract class. Please refer to its subclasses: #{subclasses}")
7
+ end
8
+
9
+ def path
10
+ raise NotImplementedError.new("APIClass is an abstract class. Please refer to its subclasses: #{APIClass.subclasses}")
11
+ end
12
+
13
+ def self.api_class_method(name, method, path=nil, opts={})
14
+ singleton = class << self; self end
15
+ singleton.send(:define_method, name, api_lambda(name, method, path, opts))
16
+ end
17
+
18
+ def self.api_instance_method(name, method, path=nil, opts={})
19
+ self.send(:define_method, name, api_lambda(name, method, path, opts))
20
+ end
21
+
22
+ def self.attribute(name, klass=nil)
23
+ @attribute_names ||= Set.new
24
+ @attribute_names << name.to_sym
25
+
26
+ self.send(:define_method, "#{name}", attribute_get_lambda(name))
27
+ self.send(:define_method, "#{name}=", attribute_set_lambda(name, klass))
28
+ end
29
+
30
+ def attributes
31
+ attributes = {}
32
+ self.class.attribute_names.each do |attr|
33
+ attributes[attr.to_sym] = self.send(attr)
34
+ end
35
+ attributes
36
+ end
37
+
38
+ def set_attributes
39
+ attributes.select{|k, v| !v.nil? }
40
+ end
41
+
42
+ def self.attribute_names
43
+ @attribute_names ||= Set.new
44
+ unless self == APIClass
45
+ @attribute_names + self.superclass.attribute_names
46
+ else
47
+ @attribute_names
48
+ end
49
+ end
50
+
51
+ def mark_attribute_changed(attr_name)
52
+ @changed_attribute_names ||= Set.new
53
+ @changed_attribute_names << attr_name.to_sym
54
+ end
55
+
56
+ def changed_attribute_names
57
+ @changed_attribute_names ||= Set.new
58
+ attributes.each do |key, val|
59
+ next if @changed_attribute_names.include?(key)
60
+ if val.is_a?(Array) || val.is_a?(Hash)
61
+ @changed_attribute_names << key if json[key] != val
62
+ end
63
+ end
64
+ @changed_attribute_names
65
+ end
66
+
67
+ def changed_attributes
68
+ ret = {}
69
+ changed_attribute_names.each do |attr|
70
+ ret[attr] = send(attr)
71
+ end
72
+ ret
73
+ end
74
+
75
+ def clear_changed_attributes
76
+ @changed_attribute_names = Set.new
77
+ end
78
+
79
+
80
+ def self.changed_lambda
81
+ # This runs in the context of an instance since it is used in
82
+ # an api_instance_method
83
+ lambda do |instance|
84
+ instance.changed_attributes
85
+ end
86
+ end
87
+
88
+ def initialize(id=nil)
89
+ refresh_from(id)
90
+ end
91
+
92
+ def self.construct(json={})
93
+ self.new.refresh_from(json)
94
+ end
95
+
96
+ def refresh_from(json={})
97
+ unless json.is_a?(Hash)
98
+ json = { :id => json }
99
+ end
100
+ self.json = Util.sorta_deep_clone(json)
101
+
102
+ json.each do |k, v|
103
+ if self.class.attribute_names.include?(k.to_sym)
104
+ self.send("#{k}=", v)
105
+ end
106
+ end
107
+ clear_changed_attributes
108
+ self
109
+ end
110
+
111
+ # Alias, but dont declare it as one because we need to use overloaded methods.
112
+ def construct(json={})
113
+ refresh_from(json)
114
+ end
115
+
116
+ def self.subclasses
117
+ return @subclasses ||= Set.new
118
+ end
119
+
120
+ def self.subclass_fetch(name)
121
+ @subclasses_hash ||= {}
122
+ if @subclasses_hash.has_key?(name)
123
+ @subclasses_hash[name]
124
+ end
125
+ end
126
+
127
+ def self.register_subclass(subclass, name=nil)
128
+ @subclasses ||= Set.new
129
+ @subclasses << subclass
130
+
131
+ unless name.nil?
132
+ @subclasses_hash ||= {}
133
+ @subclasses_hash[name] = subclass
134
+ end
135
+ end
136
+
137
+ def inspect
138
+ id_string = (self.respond_to?(:id) && !self.id.nil?) ? " id=#{self.id}" : ""
139
+ "#<#{self.class}:0x#{self.object_id.to_s(16)}#{id_string}> JSON: " + JSON.pretty_generate(set_attributes)
140
+ end
141
+
142
+ def to_json(*a)
143
+ JSON.generate(set_attributes)
144
+ end
145
+
146
+
147
+ private
148
+
149
+ def instance_variables_include?(name)
150
+ if RUBY_VERSION <= '1.9'
151
+ instance_variables.include?("@#{name}")
152
+ else
153
+ instance_variables.include?("@#{name}".to_sym)
154
+ end
155
+ end
156
+
157
+ def self.attribute_get_lambda(name)
158
+ lambda do
159
+ unless instance_variables_include?(name)
160
+ nil
161
+ else
162
+ instance_variable_get("@#{name}")
163
+ end
164
+ end
165
+ end
166
+
167
+ def self.attribute_set_lambda(name, klass=nil)
168
+ lambda do |val|
169
+ if klass
170
+ val = klass.construct(val)
171
+ end
172
+ instance_variable_set("@#{name}", val)
173
+ mark_attribute_changed(name)
174
+ end
175
+ end
176
+
177
+ def self.api_lambda(out_name, out_method, out_path=nil, out_opts={})
178
+ # Path, Opts, and Klass are all optional, so we have to determine
179
+ # which were provided using the criteria:
180
+ temp = [out_path, out_opts]
181
+ out_path = temp.select{ |t| t.is_a?(String) }.first || nil
182
+ out_opts = temp.select{ |t| t.is_a?(Hash) }.first || {}
183
+
184
+ out_arg_names = out_opts[:arguments] || []
185
+ out_constructor = out_opts[:constructor] || :self
186
+ out_default_params = out_opts[:default_params] || {}
187
+
188
+ lambda do |*args|
189
+ # Make sure we have clean data
190
+ constructor = out_constructor
191
+ method = out_method
192
+ path = nil
193
+ path = out_path.dup if out_path
194
+ arg_names = nil
195
+ arg_names = out_arg_names.dup if out_arg_names
196
+ default_params = out_default_params # dont need to dup this since it isn't modified directly
197
+
198
+ validate_args(arg_names, *args)
199
+ arguments = compose_arguments(method, arg_names, *args)
200
+ composed_path = compose_api_path(path, arguments)
201
+ unused_args = determine_unused_args(path, arg_names, arguments)
202
+ arguments[:params] = compose_params(arguments[:params], unused_args, default_params)
203
+
204
+ resp = Paid.request(method, composed_path, arguments[:params], arguments[:opts])
205
+
206
+
207
+ if constructor.is_a?(Class)
208
+ constructor.construct(resp)
209
+ elsif constructor.is_a?(Proc)
210
+ constructor.call(resp)
211
+ elsif constructor.is_a?(Symbol)
212
+ if constructor == :self
213
+ self.construct(resp)
214
+ else
215
+ raise ArgumentError.new("Invalid constructor. See method definition.")
216
+ end
217
+ end
218
+ end
219
+ end
220
+
221
+ def self.validate_args(arg_names, *args)
222
+ # Make sure we have valid arguments
223
+ if args.length > arg_names.length
224
+ if args.length > arg_names.length + 2 # more than params and opts were included
225
+ raise ArgumentError.new("Too many arguments")
226
+ else
227
+ # Params and opts are allowed, but they must be hashes
228
+ args[arg_names.length..-1].each do |arg|
229
+ unless arg.is_a?(Hash) || arg.nil?
230
+ raise ArgumentError.new("Invalid Param or Opts argument")
231
+ end
232
+ end
233
+ end
234
+ end
235
+
236
+ if args.length < arg_names.length
237
+ missing = arg_names[args.length..-1]
238
+ raise ArgumentError.new("Missing arguments #{missing}")
239
+ end
240
+ end
241
+ def validate_args(arg_names, *args)
242
+ self.class.validate_args(arg_names, *args)
243
+ end
244
+
245
+ # Priority: params > unused_args > default_params
246
+ def self.compose_params(params={}, unused_args={}, default_params={}, this=self)
247
+ ret = {}
248
+
249
+ # Handle the default params
250
+ if default_params.is_a?(Proc)
251
+ default_params = default_params.call(this)
252
+ elsif default_params.is_a?(Symbol)
253
+ default_params = this.send(default_params)
254
+ end
255
+
256
+ ret.update(default_params || {})
257
+ ret.update(unused_args || {})
258
+ ret.update(params || {})
259
+ ret
260
+ end
261
+ def compose_params(params={}, unused_args={}, default_params={})
262
+ self.class.compose_params(params, unused_args, default_params, self)
263
+ end
264
+
265
+ def self.compose_arguments(method, arg_names, *args)
266
+ arguments = {}
267
+ names = arg_names.dup + [:params, :opts]
268
+
269
+ names.each_with_index do |k, i|
270
+ arguments[k] = args[i] if args.length > i
271
+ end
272
+ arguments[:params] ||= {}
273
+ arguments[:opts] ||= {}
274
+
275
+ arguments
276
+ end
277
+ def compose_arguments(method, arg_names, *args)
278
+ self.class.compose_arguments(method, arg_names, *args)
279
+ end
280
+
281
+ def self.compose_api_path(path, arguments, this=self)
282
+ # Setup the path using the following attribute order:
283
+ # 1. Args passed in
284
+ # 2. Args on this
285
+ # 3. Args on this.class
286
+ ret = (path || this.path).dup
287
+ if ret.include?(":")
288
+ missing = Set.new
289
+ matches = ret.scan(/:([^\/]*)/).flatten.map(&:to_sym)
290
+ matches.each do |match|
291
+ value = arguments[match]
292
+ begin
293
+ value ||= this.send(match)
294
+ rescue NoMethodError
295
+ end
296
+ begin
297
+ value ||= this.class.send(match) unless this.class == Class
298
+ rescue NoMethodError
299
+ end
300
+
301
+ if value.nil?
302
+ missing << match
303
+ end
304
+
305
+ ret.sub!(match.inspect, "#{value}")
306
+ end
307
+
308
+ unless missing.empty?
309
+ raise InvalidRequestError.new("Could not determine the full URL to request. Missing the following values: #{missing.to_a.join(', ')}.")
310
+ end
311
+ end
312
+ ret
313
+ end
314
+ def compose_api_path(path, arguments)
315
+ self.class.compose_api_path(path, arguments, self)
316
+ end
317
+
318
+ def self.determine_unused_args(path, arg_names, arguments, this=self)
319
+ unused = Set.new(arg_names)
320
+ path ||= this.path
321
+ if path.include?(":")
322
+ matches = path.scan(/:([^\/]*)/).flatten.map(&:to_sym)
323
+ matches.each{ |m| unused.delete(m) }
324
+ end
325
+ ret = {}
326
+ unused.each do |arg_name|
327
+ ret[arg_name] = arguments[arg_name]
328
+ end
329
+ ret
330
+ end
331
+ def determine_unused_args(path, arg_names, arguments)
332
+ self.class.determine_unused_args(path, arg_names, arguments, self)
333
+ end
334
+
335
+ end
336
+ end
@@ -0,0 +1,47 @@
1
+ module Paid
2
+ class APIList < APIClass
3
+ include Enumerable
4
+
5
+ attribute :object
6
+ attribute :data
7
+
8
+ def [](k)
9
+ data[k]
10
+ end
11
+
12
+ def []=(k, v)
13
+ data[k]=v
14
+ end
15
+
16
+ def last
17
+ data.last
18
+ end
19
+
20
+ def length
21
+ data.length
22
+ end
23
+
24
+ def each(&blk)
25
+ data.each(&blk)
26
+ end
27
+
28
+ def self.constructor(klass)
29
+ lambda do |json|
30
+ instance = self.new
31
+ instance.json = json
32
+
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
41
+ end
42
+ instance.clear_changed_attributes
43
+ instance
44
+ end
45
+ end
46
+ end
47
+ end
@@ -1,33 +1,16 @@
1
1
  module Paid
2
- class APIResource < PaidObject
3
- def self.class_name
4
- self.name.split('::')[-1]
5
- end
2
+ class APIResource < APIClass
3
+ attribute :id
4
+ attribute :object
6
5
 
7
- def self.api_url
8
- if self == APIResource
9
- raise NotImplementedError.new('APIResource is an abstract class. You should perform actions on its subclasses (Transaction, Customer, etc.)')
10
- end
11
- require 'active_support/inflector'
12
- "/v0/#{CGI.escape(class_name.downcase).pluralize}"
13
- end
6
+ api_instance_method :refresh, :get, :constructor => :self
14
7
 
15
- def api_url
16
- unless id = self['id']
17
- raise InvalidRequestError.new("Could not determine which URL to request: #{self.class} instance has invalid ID: #{id.inspect}", 'id')
8
+ def path(base=self.class.path)
9
+ unless id
10
+ raise InvalidRequestError.new("Could not determine which URL to request: #{self.class} instance has an invalid ID: #{id.inspect}", 'id')
18
11
  end
19
- "#{self.class.api_url}/#{CGI.escape(id.to_s)}"
12
+ "#{base}/#{id}"
20
13
  end
21
14
 
22
- def refresh
23
- response, api_key = Paid.request(:get, api_url, @api_key, @retrieve_options)
24
- refresh_from(response, api_key)
25
- end
26
-
27
- def self.retrieve(id, api_key=nil)
28
- instance = self.new(id, api_key)
29
- instance.refresh
30
- instance
31
- end
32
15
  end
33
16
  end
@@ -0,0 +1,5 @@
1
+ module Paid
2
+ class APISingleton < APIClass
3
+ attribute :object
4
+ end
5
+ end