paid 0.0.1

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