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
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: acd4fd8d0c3ea2366a13704be64eed1765bc7541
4
+ data.tar.gz: 5d7e118d0735f1da69ad6d04d315e455953e44f0
5
+ SHA512:
6
+ metadata.gz: 77daeece70472dca00a5157a3db3cd1b2e5d9eb4ec14526bbe2d9b9680767afaa30df5386f108e4d6dcf66112cab48106981d987d2e3037aa1902fe8d73e624f
7
+ data.tar.gz: 9ea6a5974b22f56cb695064cf8c9c1eca1222aecfd35aeca94288c27de4fb413aa3bfc49311e8e1bc4769a436c857a47dbd5bb405adb4549f1d4fa84e4034bcf
data/Gemfile ADDED
@@ -0,0 +1,8 @@
1
+ source "https://rubygems.org"
2
+ gemspec
3
+
4
+ if Gem::Version.new(RUBY_VERSION.dup) < Gem::Version.new('1.9.3')
5
+ gem 'i18n', '< 0.7'
6
+ gem 'rest-client', '~> 1.6.8'
7
+ gem 'activesupport', '~> 3.2'
8
+ end
data/Gemfile.lock ADDED
@@ -0,0 +1,54 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ paid (0.0.1)
5
+ json (~> 1.8.1)
6
+ mime-types (>= 1.25, < 3.0)
7
+ rest-client (~> 1.4)
8
+
9
+ GEM
10
+ remote: https://rubygems.org/
11
+ specs:
12
+ activesupport (4.2.0)
13
+ i18n (~> 0.7)
14
+ json (~> 1.7, >= 1.7.7)
15
+ minitest (~> 5.1)
16
+ thread_safe (~> 0.3, >= 0.3.4)
17
+ tzinfo (~> 1.1)
18
+ bourne (1.5.0)
19
+ mocha (>= 0.13.2, < 0.15)
20
+ i18n (0.7.0)
21
+ json (1.8.2)
22
+ metaclass (0.0.4)
23
+ mime-types (2.4.3)
24
+ minitest (5.5.1)
25
+ mocha (0.13.3)
26
+ metaclass (~> 0.0.1)
27
+ netrc (0.10.2)
28
+ power_assert (0.2.2)
29
+ rake (10.4.2)
30
+ rest-client (1.7.2)
31
+ mime-types (>= 1.16, < 3.0)
32
+ netrc (~> 0.7)
33
+ shoulda (3.4.0)
34
+ shoulda-context (~> 1.0, >= 1.0.1)
35
+ shoulda-matchers (~> 1.0, >= 1.4.1)
36
+ shoulda-context (1.2.1)
37
+ shoulda-matchers (1.5.6)
38
+ activesupport (>= 3.0.0)
39
+ bourne (~> 1.3)
40
+ test-unit (3.0.9)
41
+ power_assert
42
+ thread_safe (0.3.4)
43
+ tzinfo (1.2.2)
44
+ thread_safe (~> 0.1)
45
+
46
+ PLATFORMS
47
+ ruby
48
+
49
+ DEPENDENCIES
50
+ mocha (~> 0.13.2)
51
+ paid!
52
+ rake
53
+ shoulda (~> 3.4.0)
54
+ test-unit
data/MIT-LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright 2015 Ryan Jackson
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.rdoc ADDED
@@ -0,0 +1,35 @@
1
+ = Paid Ruby bindings
2
+
3
+ == Installation
4
+
5
+ gem 'paid'
6
+
7
+ == Requirements
8
+
9
+ * Ruby 1.8.7 or above. (Ruby 1.8.6 may work if you load
10
+ ActiveSupport.) For Ruby versions before 1.9.2, you'll need to add this to your Gemfile:
11
+
12
+ if Gem::Version.new(RUBY_VERSION) < Gem::Version.new('1.9.2')
13
+ gem 'rest-client', '~> 1.6.8'
14
+ end
15
+
16
+
17
+ * rest-client, json
18
+
19
+ == Mirrors
20
+
21
+ The paid gem is mirrored on Rubygems, so you should be able to
22
+ install it via <tt>gem install paid</tt> if desired.
23
+
24
+ Note that if you are installing via bundler, you should be sure to use the https
25
+ rubygems source in your Gemfile, as any gems fetched over http could potentially be
26
+ compromised in transit and alter the code of gems fetched securely over https:
27
+
28
+ source 'https://rubygems.org'
29
+
30
+ gem 'rails'
31
+ gem 'paid'
32
+
33
+ == Development
34
+
35
+ Test cases can be run with: `bundle exec rake test`
data/Rakefile ADDED
@@ -0,0 +1,34 @@
1
+ begin
2
+ require 'bundler/setup'
3
+ rescue LoadError
4
+ puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
5
+ end
6
+
7
+ require 'rdoc/task'
8
+
9
+ RDoc::Task.new(:rdoc) do |rdoc|
10
+ rdoc.rdoc_dir = 'rdoc'
11
+ rdoc.title = 'Paid'
12
+ rdoc.options << '--line-numbers'
13
+ rdoc.rdoc_files.include('README.rdoc')
14
+ rdoc.rdoc_files.include('lib/**/*.rb')
15
+ end
16
+
17
+
18
+
19
+
20
+
21
+
22
+ Bundler::GemHelper.install_tasks
23
+
24
+ require 'rake/testtask'
25
+
26
+ Rake::TestTask.new(:test) do |t|
27
+ t.libs << 'lib'
28
+ t.libs << 'test'
29
+ t.pattern = 'test/**/*_test.rb'
30
+ t.verbose = false
31
+ end
32
+
33
+
34
+ task default: :test
File without changes
@@ -0,0 +1,4 @@
1
+ module Paid
2
+ class Account < SingletonAPIResource
3
+ end
4
+ end
@@ -0,0 +1,17 @@
1
+ module Paid
2
+ module APIOperations
3
+ module Create
4
+ module ClassMethods
5
+ def create(params={}, opts={})
6
+ api_key, headers = Util.parse_opts(opts)
7
+ response, api_key = Paid.request(:post, self.url, api_key, params, headers)
8
+ Util.convert_to_paid_object(response, api_key)
9
+ end
10
+ end
11
+
12
+ def self.included(base)
13
+ base.extend(ClassMethods)
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,11 @@
1
+ module Paid
2
+ module APIOperations
3
+ module Delete
4
+ def delete(params = {}, opts={})
5
+ api_key, headers = Util.parse_opts(opts)
6
+ response, api_key = Paid.request(:delete, url, api_key || @api_key, params, headers)
7
+ refresh_from(response, api_key)
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,17 @@
1
+ module Paid
2
+ module APIOperations
3
+ module List
4
+ module ClassMethods
5
+ def all(filters={}, opts={})
6
+ api_key, headers = Util.parse_opts(opts)
7
+ response, api_key = Paid.request(:get, url, api_key, filters, headers)
8
+ Util.convert_to_paid_object(response, api_key)
9
+ end
10
+ end
11
+
12
+ def self.included(base)
13
+ base.extend(ClassMethods)
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,57 @@
1
+ module Paid
2
+ module APIOperations
3
+ module Update
4
+ def save(opts={})
5
+ values = serialize_params(self).merge(opts)
6
+
7
+ if @values[:metadata]
8
+ values[:metadata] = serialize_metadata
9
+ end
10
+
11
+ if values.length > 0
12
+ values.delete(:id)
13
+
14
+ response, api_key = Paid.request(:post, url, @api_key, values)
15
+ refresh_from(response, api_key)
16
+ end
17
+ self
18
+ end
19
+
20
+ def serialize_metadata
21
+ if @unsaved_values.include?(:metadata)
22
+ # the metadata object has been reassigned
23
+ # i.e. as object.metadata = {key => val}
24
+ metadata_update = @values[:metadata] # new hash
25
+ new_keys = metadata_update.keys.map(&:to_sym)
26
+ # remove keys at the server, but not known locally
27
+ keys_to_unset = @previous_metadata.keys - new_keys
28
+ keys_to_unset.each {|key| metadata_update[key] = ''}
29
+
30
+ metadata_update
31
+ else
32
+ # metadata is a PaidObject, and can be serialized normally
33
+ serialize_params(@values[:metadata])
34
+ end
35
+ end
36
+
37
+ def serialize_params(obj)
38
+ case obj
39
+ when nil
40
+ ''
41
+ when PaidObject
42
+ unsaved_keys = obj.instance_variable_get(:@unsaved_values)
43
+ obj_values = obj.instance_variable_get(:@values)
44
+ update_hash = {}
45
+
46
+ unsaved_keys.each do |k|
47
+ update_hash[k] = serialize_params(obj_values[k])
48
+ end
49
+
50
+ update_hash
51
+ else
52
+ obj
53
+ end
54
+ end
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,32 @@
1
+ module Paid
2
+ class APIResource < PaidObject
3
+ def self.class_name
4
+ self.name.split('::')[-1]
5
+ end
6
+
7
+ def self.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
+ "/v0/#{CGI.escape(class_name.downcase)}s"
12
+ end
13
+
14
+ def url
15
+ unless id = self['id']
16
+ raise InvalidRequestError.new("Could not determine which URL to request: #{self.class} instance has invalid ID: #{id.inspect}", 'id')
17
+ end
18
+ "#{self.class.url}/#{CGI.escape(id)}"
19
+ end
20
+
21
+ def refresh
22
+ response, api_key = Paid.request(:get, url, @api_key, @retrieve_options)
23
+ refresh_from(response, api_key)
24
+ end
25
+
26
+ def self.retrieve(id, api_key=nil)
27
+ instance = self.new(id, api_key)
28
+ instance.refresh
29
+ instance
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,55 @@
1
+ require 'uri'
2
+ require 'digest/sha1'
3
+
4
+ module Paid
5
+ module CertificateBlacklist
6
+
7
+ BLACKLIST = {
8
+ "api.paidapi.com" => [
9
+ '',
10
+ ],
11
+ "revoked.paidapi.com" => [
12
+ '',
13
+ ]
14
+ }
15
+
16
+ # Preflight the SSL certificate presented by the backend. This isn't 100%
17
+ # bulletproof, in that we're not actually validating the transport used to
18
+ # communicate with Paid, merely that the first attempt to does not use a
19
+ # revoked certificate.
20
+
21
+ # Unfortunately the interface to OpenSSL doesn't make it easy to check the
22
+ # certificate before sending potentially sensitive data on the wire. This
23
+ # approach raises the bar for an attacker significantly.
24
+
25
+ def self.check_ssl_cert(uri, ca_file)
26
+ uri = URI.parse(uri)
27
+
28
+ sock = TCPSocket.new(uri.host, uri.port)
29
+ ctx = OpenSSL::SSL::SSLContext.new
30
+ ctx.set_params(:verify_mode => OpenSSL::SSL::VERIFY_PEER,
31
+ :ca_file => ca_file)
32
+
33
+ socket = OpenSSL::SSL::SSLSocket.new(sock, ctx)
34
+ socket.connect
35
+
36
+ certificate = socket.peer_cert.to_der
37
+ fingerprint = Digest::SHA1.hexdigest(certificate)
38
+
39
+ if blacklisted_certs = BLACKLIST[uri.host]
40
+ if blacklisted_certs.include?(fingerprint)
41
+ raise APIConnectionError.new(
42
+ "Invalid server certificate. You tried to connect to a server that" +
43
+ "has a revoked SSL certificate, which means we cannot securely send" +
44
+ "data to that server. Please email support@paidapi.com if you need" +
45
+ "help connecting to the correct API server."
46
+ )
47
+ end
48
+ end
49
+
50
+ socket.close
51
+
52
+ return true
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,16 @@
1
+ module Paid
2
+ class Customer < APIResource
3
+ include Paid::APIOperations::Create
4
+ include Paid::APIOperations::Delete
5
+ include Paid::APIOperations::Update
6
+ include Paid::APIOperations::List
7
+
8
+ def invoices
9
+ Invoice.all({ :customer => id }, @api_key)
10
+ end
11
+
12
+ def transactions
13
+ Transaction.all({ :customer => id }, @api_key)
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,4 @@
1
+ module Paid
2
+ class APIConnectionError < PaidError
3
+ end
4
+ end
@@ -0,0 +1,4 @@
1
+ module Paid
2
+ class APIError < PaidError
3
+ end
4
+ end
@@ -0,0 +1,4 @@
1
+ module Paid
2
+ class AuthenticationError < PaidError
3
+ end
4
+ end
@@ -0,0 +1,10 @@
1
+ module Paid
2
+ class InvalidRequestError < PaidError
3
+ attr_accessor :param
4
+
5
+ def initialize(message, param, http_status=nil, http_body=nil, json_body=nil)
6
+ super(message, http_status, http_body, json_body)
7
+ @param = param
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,20 @@
1
+ module Paid
2
+ class PaidError < StandardError
3
+ attr_reader :message
4
+ attr_reader :http_status
5
+ attr_reader :http_body
6
+ attr_reader :json_body
7
+
8
+ def initialize(message=nil, http_status=nil, http_body=nil, json_body=nil)
9
+ @message = message
10
+ @http_status = http_status
11
+ @http_body = http_body
12
+ @json_body = json_body
13
+ end
14
+
15
+ def to_s
16
+ status_string = @http_status.nil? ? "" : "(Status #{@http_status}) "
17
+ "#{status_string}#{@message}"
18
+ end
19
+ end
20
+ end
data/lib/paid/event.rb ADDED
@@ -0,0 +1,5 @@
1
+ module Paid
2
+ class Event < APIResource
3
+ include Paid::APIOperations::List
4
+ end
5
+ end
@@ -0,0 +1,7 @@
1
+ module Paid
2
+ class Invoice < APIResource
3
+ include Paid::APIOperations::List
4
+ include Paid::APIOperations::Update
5
+ include Paid::APIOperations::Create
6
+ end
7
+ end
@@ -0,0 +1,37 @@
1
+ module Paid
2
+ class ListObject < PaidObject
3
+
4
+ def [](k)
5
+ case k
6
+ when String, Symbol
7
+ super
8
+ else
9
+ raise ArgumentError.new("You tried to access the #{k.inspect} index, but ListObject types only support String keys. (HINT: List calls return an object with a 'data' (which is the data array). You likely want to call #data[#{k.inspect}])")
10
+ end
11
+ end
12
+
13
+ def each(&blk)
14
+ self.data.each(&blk)
15
+ end
16
+
17
+ def retrieve(id, api_key=nil)
18
+ api_key ||= @api_key
19
+ response, api_key = Paid.request(:get,"#{url}/#{CGI.escape(id)}", api_key)
20
+ Util.convert_to_paid_object(response, api_key)
21
+ end
22
+
23
+ def create(params={}, opts={})
24
+ api_key, headers = Util.parse_opts(opts)
25
+ api_key ||= @api_key
26
+ response, api_key = Paid.request(:post, url, api_key, params, headers)
27
+ Util.convert_to_paid_object(response, api_key)
28
+ end
29
+
30
+ def all(params={}, opts={})
31
+ api_key, headers = Util.parse_opts(opts)
32
+ api_key ||= @api_key
33
+ response, api_key = Paid.request(:get, url, api_key, params, headers)
34
+ Util.convert_to_paid_object(response, api_key)
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,187 @@
1
+ module Paid
2
+ class PaidObject
3
+ include Enumerable
4
+
5
+ attr_accessor :api_key
6
+ @@permanent_attributes = Set.new([:api_key, :id])
7
+
8
+ # The default :id method is deprecated and isn't useful to us
9
+ if method_defined?(:id)
10
+ undef :id
11
+ end
12
+
13
+ def initialize(id=nil, api_key=nil)
14
+ # parameter overloading!
15
+ if id.kind_of?(Hash)
16
+ @retrieve_options = id.dup
17
+ @retrieve_options.delete(:id)
18
+ id = id[:id]
19
+ else
20
+ @retrieve_options = {}
21
+ end
22
+
23
+ @api_key = api_key
24
+ @values = {}
25
+ # This really belongs in APIResource, but not putting it there allows us
26
+ # to have a unified inspect method
27
+ @unsaved_values = Set.new
28
+ @transient_values = Set.new
29
+ @values[:id] = id if id
30
+ end
31
+
32
+ def self.construct_from(values, api_key=nil)
33
+ self.new(values[:id], api_key).refresh_from(values, api_key)
34
+ end
35
+
36
+ def to_s(*args)
37
+ JSON.pretty_generate(@values)
38
+ end
39
+
40
+ def inspect
41
+ id_string = (self.respond_to?(:id) && !self.id.nil?) ? " id=#{self.id}" : ""
42
+ "#<#{self.class}:0x#{self.object_id.to_s(16)}#{id_string}> JSON: " + JSON.pretty_generate(@values)
43
+ end
44
+
45
+ def refresh_from(values, api_key, partial=false)
46
+ @api_key = api_key
47
+
48
+ @previous_metadata = values[:metadata]
49
+ removed = partial ? Set.new : Set.new(@values.keys - values.keys)
50
+ added = Set.new(values.keys - @values.keys)
51
+
52
+ instance_eval do
53
+ remove_accessors(removed)
54
+ add_accessors(added)
55
+ end
56
+ removed.each do |k|
57
+ @values.delete(k)
58
+ @transient_values.add(k)
59
+ @unsaved_values.delete(k)
60
+ end
61
+ values.each do |k, v|
62
+ @values[k] = Util.convert_to_paid_object(v, api_key)
63
+ @transient_values.delete(k)
64
+ @unsaved_values.delete(k)
65
+ end
66
+
67
+ return self
68
+ end
69
+
70
+ def [](k)
71
+ @values[k.to_sym]
72
+ end
73
+
74
+ def []=(k, v)
75
+ send(:"#{k}=", v)
76
+ end
77
+
78
+ def keys
79
+ @values.keys
80
+ end
81
+
82
+ def values
83
+ @values.values
84
+ end
85
+
86
+ def to_json(*a)
87
+ JSON.generate(@values)
88
+ end
89
+
90
+ def as_json(*a)
91
+ @values.as_json(*a)
92
+ end
93
+
94
+ def to_hash
95
+ @values.inject({}) do |acc, (key, value)|
96
+ acc[key] = value.respond_to?(:to_hash) ? value.to_hash : value
97
+ acc
98
+ end
99
+ end
100
+
101
+ def each(&blk)
102
+ @values.each(&blk)
103
+ end
104
+
105
+ def _dump(level)
106
+ Marshal.dump([@values, @api_key])
107
+ end
108
+
109
+ def self._load(args)
110
+ values, api_key = Marshal.load(args)
111
+ construct_from(values, api_key)
112
+ end
113
+
114
+ if RUBY_VERSION < '1.9.2'
115
+ def respond_to?(symbol)
116
+ @values.has_key?(symbol) || super
117
+ end
118
+ end
119
+
120
+ protected
121
+
122
+ def metaclass
123
+ class << self; self; end
124
+ end
125
+
126
+ def remove_accessors(keys)
127
+ metaclass.instance_eval do
128
+ keys.each do |k|
129
+ next if @@permanent_attributes.include?(k)
130
+ k_eq = :"#{k}="
131
+ remove_method(k) if method_defined?(k)
132
+ remove_method(k_eq) if method_defined?(k_eq)
133
+ end
134
+ end
135
+ end
136
+
137
+ def add_accessors(keys)
138
+ metaclass.instance_eval do
139
+ keys.each do |k|
140
+ next if @@permanent_attributes.include?(k)
141
+ k_eq = :"#{k}="
142
+ define_method(k) { @values[k] }
143
+ define_method(k_eq) do |v|
144
+ if v == ""
145
+ raise ArgumentError.new(
146
+ "You cannot set #{k} to an empty string." +
147
+ "We interpret empty strings as nil in requests." +
148
+ "You may set #{self}.#{k} = nil to delete the property.")
149
+ end
150
+ @values[k] = v
151
+ @unsaved_values.add(k)
152
+ end
153
+ end
154
+ end
155
+ end
156
+
157
+ def method_missing(name, *args)
158
+ # TODO: only allow setting in updateable classes.
159
+ if name.to_s.end_with?('=')
160
+ attr = name.to_s[0...-1].to_sym
161
+ add_accessors([attr])
162
+ begin
163
+ mth = method(name)
164
+ rescue NameError
165
+ raise NoMethodError.new("Cannot set #{attr} on this object. HINT: you can't set: #{@@permanent_attributes.to_a.join(', ')}")
166
+ end
167
+ return mth.call(args[0])
168
+ else
169
+ return @values[name] if @values.has_key?(name)
170
+ end
171
+
172
+ begin
173
+ super
174
+ rescue NoMethodError => e
175
+ if @transient_values.include?(name)
176
+ raise NoMethodError.new(e.message + ". HINT: The '#{name}' attribute was set in the past, however. It was then wiped when refreshing the object with the result returned by Paid's API, probably as a result of a save(). The attributes currently available on this object are: #{@values.keys.join(', ')}")
177
+ else
178
+ raise
179
+ end
180
+ end
181
+ end
182
+
183
+ def respond_to_missing?(symbol, include_private = false)
184
+ @values && @values.has_key?(symbol) || super
185
+ end
186
+ end
187
+ end