paid 0.1.0 → 1.0.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.
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