rainforest 1.0.1

Sign up to get free protection for your applications and to get access to all the features.
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