rainforest 1.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 (47) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +4 -0
  3. data/.travis.yml +10 -0
  4. data/CONTRIBUTORS +1 -0
  5. data/Gemfile +2 -0
  6. data/History.txt +4 -0
  7. data/LICENSE +21 -0
  8. data/README.md +50 -0
  9. data/Rakefile +15 -0
  10. data/VERSION +1 -0
  11. data/bin/rainforest-console +7 -0
  12. data/gemfiles/default-with-activesupport.gemfile +3 -0
  13. data/gemfiles/json.gemfile +4 -0
  14. data/gemfiles/yajl.gemfile +4 -0
  15. data/lib/.DS_Store +0 -0
  16. data/lib/data/ca-certificates.crt +3918 -0
  17. data/lib/rainforest.rb +271 -0
  18. data/lib/rainforest/api_operations/create.rb +16 -0
  19. data/lib/rainforest/api_operations/delete.rb +11 -0
  20. data/lib/rainforest/api_operations/list.rb +18 -0
  21. data/lib/rainforest/api_operations/update.rb +61 -0
  22. data/lib/rainforest/api_resource.rb +33 -0
  23. data/lib/rainforest/errors/api_connection_error.rb +4 -0
  24. data/lib/rainforest/errors/api_error.rb +4 -0
  25. data/lib/rainforest/errors/authentication_error.rb +4 -0
  26. data/lib/rainforest/errors/invalid_request_error.rb +10 -0
  27. data/lib/rainforest/errors/rainforest_error.rb +20 -0
  28. data/lib/rainforest/json.rb +21 -0
  29. data/lib/rainforest/list_object.rb +35 -0
  30. data/lib/rainforest/rainforest_object.rb +168 -0
  31. data/lib/rainforest/run.rb +8 -0
  32. data/lib/rainforest/singleton_api_resource.rb +20 -0
  33. data/lib/rainforest/test.rb +14 -0
  34. data/lib/rainforest/util.rb +101 -0
  35. data/lib/rainforest/version.rb +3 -0
  36. data/rainforest.gemspec +26 -0
  37. data/test/stripe/account_test.rb +14 -0
  38. data/test/stripe/api_resource_test.rb +345 -0
  39. data/test/stripe/charge_test.rb +67 -0
  40. data/test/stripe/coupon_test.rb +11 -0
  41. data/test/stripe/customer_test.rb +70 -0
  42. data/test/stripe/invoice_test.rb +20 -0
  43. data/test/stripe/list_object_test.rb +16 -0
  44. data/test/stripe/metadata_test.rb +114 -0
  45. data/test/stripe/util_test.rb +29 -0
  46. data/test/test_helper.rb +356 -0
  47. metadata +191 -0
data/lib/rainforest.rb ADDED
@@ -0,0 +1,271 @@
1
+ # Rainforest Ruby bindings
2
+ # API spec at https://rainforest.com/docs/api
3
+ require 'cgi'
4
+ require 'set'
5
+ require 'openssl'
6
+ require 'rest_client'
7
+ require 'multi_json'
8
+
9
+ # Version
10
+ require 'rainforest/version'
11
+
12
+ # API operations
13
+ require 'rainforest/api_operations/create'
14
+ require 'rainforest/api_operations/update'
15
+ require 'rainforest/api_operations/delete'
16
+ require 'rainforest/api_operations/list'
17
+
18
+ # Resources
19
+ require 'rainforest/util'
20
+ require 'rainforest/json'
21
+ require 'rainforest/rainforest_object'
22
+ require 'rainforest/api_resource'
23
+ require 'rainforest/singleton_api_resource'
24
+ require 'rainforest/list_object'
25
+ require 'rainforest/test'
26
+ require 'rainforest/run'
27
+
28
+ # Errors
29
+ require 'rainforest/errors/rainforest_error'
30
+ require 'rainforest/errors/api_error'
31
+ require 'rainforest/errors/api_connection_error'
32
+ require 'rainforest/errors/invalid_request_error'
33
+ require 'rainforest/errors/authentication_error'
34
+
35
+ module Rainforest
36
+ @api_base = 'https://app.rainforestqa.com/api'
37
+ @api_version = 1
38
+
39
+ # TODO(jon): Verify that this will work with rainforest
40
+ @ssl_bundle_path = File.dirname(__FILE__) + '/data/ca-certificates.crt'
41
+ @verify_ssl_certs = true
42
+
43
+ class << self
44
+ attr_accessor :api_key, :api_base, :verify_ssl_certs, :api_version
45
+ end
46
+
47
+ def self.api_url(url='')
48
+ @api_base + "/" + @api_version.to_s + url
49
+ end
50
+
51
+ def self.request(method, url, api_key, params={}, headers={})
52
+ unless api_key ||= @api_key
53
+ raise AuthenticationError.new('No API key provided. ' +
54
+ 'Set your API key using "Rainforest.api_key = <API-KEY>". ' +
55
+ 'You can generate API keys from the Rainforest web interface. ' +
56
+ 'See https://rainforest.com/api for details, or email support@rainforest.com ' +
57
+ 'if you have any questions.')
58
+ end
59
+
60
+ if api_key =~ /\s/
61
+ raise AuthenticationError.new('Your API key is invalid, as it contains ' +
62
+ 'whitespace. (HINT: You can double-check your API key from the ' +
63
+ 'Rainforest web interface. See https://rainforest.com/api for details, or ' +
64
+ 'email support@rainforest.com if you have any questions.)')
65
+ end
66
+
67
+ request_opts = { :verify_ssl => false }
68
+
69
+ if ssl_preflight_passed?
70
+ request_opts.update(:verify_ssl => OpenSSL::SSL::VERIFY_PEER,
71
+ :ssl_ca_file => @ssl_bundle_path)
72
+ end
73
+
74
+ params = Util.objects_to_ids(params)
75
+ url = api_url(url)
76
+
77
+ case method.to_s.downcase.to_sym
78
+ when :get, :head, :delete
79
+ # Make params into GET parameters
80
+ url += "#{URI.parse(url).query ? '&' : '?'}#{uri_encode(params)}" if params && params.any?
81
+ payload = nil
82
+ else
83
+ payload = uri_encode(params)
84
+ end
85
+
86
+ request_opts.update(:headers => request_headers(api_key).update(headers),
87
+ :method => method, :open_timeout => 30,
88
+ :payload => payload, :url => url, :timeout => 80)
89
+
90
+ begin
91
+ response = execute_request(request_opts)
92
+ rescue SocketError => e
93
+ handle_restclient_error(e)
94
+ rescue NoMethodError => e
95
+ # Work around RestClient bug
96
+ if e.message =~ /\WRequestFailed\W/
97
+ e = APIConnectionError.new('Unexpected HTTP response code')
98
+ handle_restclient_error(e)
99
+ else
100
+ raise
101
+ end
102
+ rescue RestClient::ExceptionWithResponse => e
103
+ if rcode = e.http_code and rbody = e.http_body
104
+ handle_api_error(rcode, rbody)
105
+ else
106
+ handle_restclient_error(e)
107
+ end
108
+ rescue RestClient::Exception, Errno::ECONNREFUSED => e
109
+ handle_restclient_error(e)
110
+ end
111
+
112
+ [parse(response), api_key]
113
+ end
114
+
115
+ private
116
+
117
+ def self.ssl_preflight_passed?
118
+ if !verify_ssl_certs && !@no_verify
119
+ $stderr.puts "WARNING: Running without SSL cert verification. " +
120
+ "Execute 'Rainforest.verify_ssl_certs = true' to enable verification."
121
+
122
+ @no_verify = true
123
+
124
+ elsif !Util.file_readable(@ssl_bundle_path) && !@no_bundle
125
+ $stderr.puts "WARNING: Running without SSL cert verification " +
126
+ "because #{@ssl_bundle_path} isn't readable"
127
+
128
+ @no_bundle = true
129
+ end
130
+
131
+ !(@no_verify || @no_bundle)
132
+ end
133
+
134
+ def self.user_agent
135
+ @uname ||= get_uname
136
+ lang_version = "#{RUBY_VERSION} p#{RUBY_PATCHLEVEL} (#{RUBY_RELEASE_DATE})"
137
+
138
+ {
139
+ :bindings_version => Rainforest::VERSION,
140
+ :lang => 'ruby',
141
+ :lang_version => lang_version,
142
+ :platform => RUBY_PLATFORM,
143
+ :publisher => 'rainforest',
144
+ :uname => @uname
145
+ }
146
+
147
+ end
148
+
149
+ def self.get_uname
150
+ `uname -a 2>/dev/null`.strip if RUBY_PLATFORM =~ /linux|darwin/i
151
+ rescue Errno::ENOMEM => ex # couldn't create subprocess
152
+ "uname lookup failed"
153
+ end
154
+
155
+ def self.uri_encode(params)
156
+ Util.flatten_params(params).
157
+ map { |k,v| "#{k}=#{Util.url_encode(v)}" }.join('&')
158
+ end
159
+
160
+ def self.request_headers(api_key)
161
+ headers = {
162
+ :user_agent => "Rainforest/#{api_version} RubyBindings/#{Rainforest::VERSION}",
163
+
164
+ # TODO(jon): Ask Rainforest guys about using http basic auth
165
+ # :authorization => "Bearer #{api_key}",
166
+ "Accept" => "application/json",
167
+ "CLIENT_TOKEN" => api_key,
168
+ :content_type => 'application/x-www-form-urlencoded'
169
+ }
170
+
171
+ headers[:rainforest_version] = api_version if api_version
172
+
173
+ begin
174
+ headers.update(:x_rainforest_client_user_agent => Rainforest::JSON.dump(user_agent))
175
+ rescue => e
176
+ headers.update(:x_rainforest_client_raw_user_agent => user_agent.inspect,
177
+ :error => "#{e} (#{e.class})")
178
+ end
179
+ end
180
+
181
+ def self.execute_request(opts)
182
+ RestClient::Request.execute(opts)
183
+ end
184
+
185
+ def self.parse(response)
186
+ puts response.body
187
+ begin
188
+ # Would use :symbolize_names => true, but apparently there is
189
+ # some library out there that makes symbolize_names not work.
190
+ response = Rainforest::JSON.load(response.body)
191
+ rescue MultiJson::DecodeError
192
+ raise general_api_error(response.code, response.body)
193
+ end
194
+
195
+ Util.symbolize_names(response)
196
+ end
197
+
198
+ def self.general_api_error(rcode, rbody)
199
+ APIError.new("Invalid response object from API: #{rbody.inspect} " +
200
+ "(HTTP response code was #{rcode})", rcode, rbody)
201
+ end
202
+
203
+ def self.handle_api_error(rcode, rbody)
204
+ begin
205
+ error_obj = Rainforest::JSON.load(rbody)
206
+ error_obj = Util.symbolize_names(error_obj)
207
+ error = error_obj[:error] or raise RainforestError.new # escape from parsing
208
+
209
+ rescue MultiJson::DecodeError, RainforestError
210
+ raise general_api_error(rcode, rbody)
211
+ end
212
+
213
+ case rcode
214
+ when 400, 404
215
+ raise invalid_request_error error, rcode, rbody, error_obj
216
+ when 401
217
+ raise authentication_error error, rcode, rbody, error_obj
218
+ when 402
219
+ raise card_error error, rcode, rbody, error_obj
220
+ else
221
+ raise api_error error, rcode, rbody, error_obj
222
+ end
223
+
224
+ end
225
+
226
+ def self.invalid_request_error(error, rcode, rbody, error_obj)
227
+ InvalidRequestError.new(error[:message], error[:param], rcode,
228
+ rbody, error_obj)
229
+ end
230
+
231
+ def self.authentication_error(error, rcode, rbody, error_obj)
232
+ AuthenticationError.new(error[:message], rcode, rbody, error_obj)
233
+ end
234
+
235
+ def self.card_error(error, rcode, rbody, error_obj)
236
+ CardError.new(error[:message], error[:param], error[:code],
237
+ rcode, rbody, error_obj)
238
+ end
239
+
240
+ def self.api_error(error, rcode, rbody, error_obj)
241
+ APIError.new(error[:message], rcode, rbody, error_obj)
242
+ end
243
+
244
+ def self.handle_restclient_error(e)
245
+ case e
246
+ when RestClient::ServerBrokeConnection, RestClient::RequestTimeout
247
+ message = "Could not connect to Rainforest (#{@api_base}). " +
248
+ "Please check your internet connection and try again. " +
249
+ "If this problem persists, you should check Rainforest's service status at " +
250
+ "https://twitter.com/rainforeststatus, or let us know at support@rainforest.com."
251
+
252
+ when RestClient::SSLCertificateNotVerified
253
+ message = "Could not verify Rainforest's SSL certificate. " +
254
+ "Please make sure that your network is not intercepting certificates. " +
255
+ "(Try going to https://api.rainforest.com/v1 in your browser.) " +
256
+ "If this problem persists, let us know at support@rainforest.com."
257
+
258
+ when SocketError
259
+ message = "Unexpected error communicating when trying to connect to Rainforest. " +
260
+ "You may be seeing this message because your DNS is not working. " +
261
+ "To check, try running 'host rainforest.com' from the command line."
262
+
263
+ else
264
+ message = "Unexpected error communicating with Rainforest. " +
265
+ "If this problem persists, let us know at support@rainforest.com."
266
+
267
+ end
268
+
269
+ raise APIConnectionError.new(message + "\n\n(Network error: #{e.message})")
270
+ end
271
+ end
@@ -0,0 +1,16 @@
1
+ module Rainforest
2
+ module APIOperations
3
+ module Create
4
+ module ClassMethods
5
+ def create(params={}, api_key=nil)
6
+ response, api_key = Rainforest.request(:post, self.url, api_key, params)
7
+ Util.convert_to_rainforest_object(response, api_key)
8
+ end
9
+ end
10
+
11
+ def self.included(base)
12
+ base.extend(ClassMethods)
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,11 @@
1
+ module Rainforest
2
+ module APIOperations
3
+ module Delete
4
+ def delete
5
+ response, api_key = Rainforest.request(:delete, url, @api_key)
6
+ refresh_from(response, api_key)
7
+ self
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,18 @@
1
+ module Rainforest
2
+ module APIOperations
3
+ module List
4
+ module ClassMethods
5
+ def all(filters={}, api_key=nil)
6
+ response, api_key = Rainforest.request(:get, url, api_key, filters)
7
+
8
+ # TODO(jon): Suggest an object attribute be returned instead of this.
9
+ Util.convert_to_rainforest_object(response, api_key, class_name.downcase)
10
+ end
11
+ end
12
+
13
+ def self.included(base)
14
+ base.extend(ClassMethods)
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,61 @@
1
+ module Rainforest
2
+ module APIOperations
3
+ module Update
4
+ def save
5
+ values = serialize_params(self)
6
+
7
+ if @values[:metadata]
8
+ values[:metadata] = serialize_metadata
9
+ end
10
+
11
+ if values.length > 0
12
+ values.delete(:id)
13
+
14
+ if self.id
15
+ response, api_key = Rainforest.request(:put, url + "/#{self.id}", @api_key, values)
16
+ else
17
+ response, api_key = Rainforest.request(:post, url, @api_key, values)
18
+ end
19
+ refresh_from(response, api_key)
20
+ end
21
+ self
22
+ end
23
+
24
+ def serialize_metadata
25
+ if @unsaved_values.include?(:metadata)
26
+ # the metadata object has been reassigned
27
+ # i.e. as object.metadata = {key => val}
28
+ metadata_update = @values[:metadata] # new hash
29
+ new_keys = metadata_update.keys.map(&:to_sym)
30
+ # remove keys at the server, but not known locally
31
+ keys_to_unset = @previous_metadata.keys - new_keys
32
+ keys_to_unset.each {|key| metadata_update[key] = ''}
33
+
34
+ metadata_update
35
+ else
36
+ # metadata is a RainforestObject, and can be serialized normally
37
+ serialize_params(@values[:metadata])
38
+ end
39
+ end
40
+
41
+ def serialize_params(obj)
42
+ case obj
43
+ when nil
44
+ ''
45
+ when RainforestObject
46
+ unsaved_keys = obj.instance_variable_get(:@unsaved_values)
47
+ obj_values = obj.instance_variable_get(:@values)
48
+ update_hash = {}
49
+
50
+ unsaved_keys.each do |k|
51
+ update_hash[k] = serialize_params(obj_values[k])
52
+ end
53
+
54
+ update_hash
55
+ else
56
+ obj
57
+ end
58
+ end
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,33 @@
1
+ module Rainforest
2
+ class APIResource < RainforestObject
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 (Charge, Customer, etc.)')
10
+ end
11
+ "/#{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.to_s)}"
19
+ end
20
+
21
+ def refresh
22
+ response, api_key = Rainforest.request(:get, url, @api_key, @retrieve_options)
23
+ refresh_from(response, api_key)
24
+ self
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
+ end
33
+ end
@@ -0,0 +1,4 @@
1
+ module Rainforest
2
+ class APIConnectionError < RainforestError
3
+ end
4
+ end
@@ -0,0 +1,4 @@
1
+ module Rainforest
2
+ class APIError < RainforestError
3
+ end
4
+ end
@@ -0,0 +1,4 @@
1
+ module Rainforest
2
+ class AuthenticationError < RainforestError
3
+ end
4
+ end
@@ -0,0 +1,10 @@
1
+ module Rainforest
2
+ class InvalidRequestError < RainforestError
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