baabedo 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,20 @@
1
+ module Baabedo
2
+ class BaabedoError < 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
@@ -0,0 +1,10 @@
1
+ module Baabedo
2
+ class InvalidRequestError < BaabedoError
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,33 @@
1
+ module Baabedo
2
+ class ListObject < APIObject
3
+ include Baabedo::APIOperations::Request
4
+
5
+ def [](k)
6
+ case k
7
+ when String, Symbol
8
+ super
9
+ else
10
+ 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}])")
11
+ end
12
+ end
13
+
14
+ def each(&blk)
15
+ self.data.each(&blk)
16
+ end
17
+
18
+ def retrieve(id, opts={})
19
+ response, opts = request(:get,"#{url}/#{CGI.escape(id)}", {}, opts)
20
+ Util.convert_to_api_object(response, opts)
21
+ end
22
+
23
+ def create(params={}, opts={})
24
+ response, opts = request(:post, url, params, opts)
25
+ Util.convert_to_api_object(response, opts)
26
+ end
27
+
28
+ def all(params={}, opts={})
29
+ response, opts = request(:get, url, params, opts)
30
+ Util.convert_to_api_object(response, opts)
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,159 @@
1
+ module Baabedo
2
+ module Util
3
+ def self.objects_to_ids(h)
4
+ case h
5
+ when APIResource
6
+ h.id
7
+ when Hash
8
+ res = {}
9
+ h.each { |k, v| res[k] = objects_to_ids(v) unless v.nil? }
10
+ res
11
+ when Array
12
+ h.map { |v| objects_to_ids(v) }
13
+ else
14
+ h
15
+ end
16
+ end
17
+
18
+ # def self.object_classes
19
+ # @object_classes ||= {
20
+ # # data structures
21
+ # 'list' => ListObject,
22
+ #
23
+ # # business objects
24
+ # 'account' => Account,
25
+ # 'application_fee' => ApplicationFee,
26
+ # 'balance' => Balance,
27
+ # 'balance_transaction' => BalanceTransaction,
28
+ # 'card' => Card,
29
+ # 'charge' => Charge,
30
+ # 'coupon' => Coupon,
31
+ # 'customer' => Customer,
32
+ # 'event' => Event,
33
+ # 'fee_refund' => ApplicationFeeRefund,
34
+ # 'invoiceitem' => InvoiceItem,
35
+ # 'invoice' => Invoice,
36
+ # 'plan' => Plan,
37
+ # 'recipient' => Recipient,
38
+ # 'refund' => Refund,
39
+ # 'subscription' => Subscription,
40
+ # 'file_upload' => FileUpload,
41
+ # 'transfer' => Transfer,
42
+ # 'transfer_reversal' => Reversal,
43
+ # 'bitcoin_receiver' => BitcoinReceiver,
44
+ # 'bitcoin_transaction' => BitcoinTransaction
45
+ # }
46
+ # end
47
+ def self.camelize(str)
48
+ str.split('_').collect(&:capitalize).join # "product_order" -> "ProductOrder"
49
+ end
50
+
51
+ def self.class_for_type(type)
52
+ if type =~ /^list\./
53
+ ListObject
54
+ elsif Kernel.const_defined?(camelize(type))
55
+ Kernel.const_get(camelize(type))
56
+ else
57
+ APIObject
58
+ end
59
+ end
60
+
61
+ def self.convert_to_api_object(resp, opts)
62
+ case resp
63
+ when Array
64
+ resp.map { |i| convert_to_api_object(i, opts) }
65
+ when Hash
66
+ # Try converting to a known object class. If none available, fall back to generic APIObject
67
+ class_for_type(resp[:type]).construct_from(resp, opts)
68
+ else
69
+ resp
70
+ end
71
+ end
72
+
73
+ def self.file_readable(file)
74
+ # This is nominally equivalent to File.readable?, but that can
75
+ # report incorrect results on some more oddball filesystems
76
+ # (such as AFS)
77
+ begin
78
+ File.open(file) { |f| }
79
+ rescue
80
+ false
81
+ else
82
+ true
83
+ end
84
+ end
85
+
86
+ def self.symbolize_names(object)
87
+ case object
88
+ when Hash
89
+ new_hash = {}
90
+ object.each do |key, value|
91
+ key = (key.to_sym rescue key) || key
92
+ new_hash[key] = symbolize_names(value)
93
+ end
94
+ new_hash
95
+ when Array
96
+ object.map { |value| symbolize_names(value) }
97
+ else
98
+ object
99
+ end
100
+ end
101
+
102
+ def self.url_encode(key)
103
+ URI.escape(key.to_s, Regexp.new("[^#{URI::PATTERN::UNRESERVED}]"))
104
+ end
105
+
106
+ def self.flatten_params(params, parent_key=nil)
107
+ result = []
108
+ params.each do |key, value|
109
+ calculated_key = parent_key ? "#{parent_key}[#{url_encode(key)}]" : url_encode(key)
110
+ if value.is_a?(Hash)
111
+ result += flatten_params(value, calculated_key)
112
+ elsif value.is_a?(Array)
113
+ result += flatten_params_array(value, calculated_key)
114
+ else
115
+ result << [calculated_key, value]
116
+ end
117
+ end
118
+ result
119
+ end
120
+
121
+ def self.flatten_params_array(value, calculated_key)
122
+ result = []
123
+ value.each do |elem|
124
+ if elem.is_a?(Hash)
125
+ result += flatten_params(elem, calculated_key)
126
+ elsif elem.is_a?(Array)
127
+ result += flatten_params_array(elem, calculated_key)
128
+ else
129
+ result << ["#{calculated_key}[]", elem]
130
+ end
131
+ end
132
+ result
133
+ end
134
+
135
+ # The secondary opts argument can either be a string or hash
136
+ # Turn this value into an access_token and a set of headers
137
+ def self.normalize_opts(opts)
138
+ case opts
139
+ when String
140
+ {:access_token => opts}
141
+ when Hash
142
+ check_access_token!(opts.fetch(:access_token)) if opts.has_key?(:access_token)
143
+ opts.clone
144
+ else
145
+ raise TypeError.new('normalize_opts expects a string or a hash')
146
+ end
147
+ end
148
+
149
+ def self.check_string_argument!(key)
150
+ raise TypeError.new("argument must be a string") unless key.is_a?(String)
151
+ key
152
+ end
153
+
154
+ def self.check_access_token!(key)
155
+ raise TypeError.new("access_token must be a string") unless key.is_a?(String)
156
+ key
157
+ end
158
+ end
159
+ end
@@ -0,0 +1,3 @@
1
+ module Baabedo
2
+ VERSION = "0.0.1"
3
+ end
data/lib/baabedo.rb ADDED
@@ -0,0 +1,242 @@
1
+ require 'cgi'
2
+ require 'openssl'
3
+ require 'rbconfig'
4
+ require 'set'
5
+ require 'socket'
6
+
7
+ require 'rest-client'
8
+ require 'json'
9
+
10
+ require "baabedo/version"
11
+
12
+ # Operations
13
+ require 'baabedo/api_operations/create'
14
+ require 'baabedo/api_operations/update'
15
+ require 'baabedo/api_operations/delete'
16
+ require 'baabedo/api_operations/list'
17
+ require 'baabedo/api_operations/request'
18
+ # Resources
19
+ require 'baabedo/util'
20
+ require 'baabedo/api_object'
21
+ require 'baabedo/api_resource'
22
+ require 'baabedo/list_object'
23
+
24
+ require 'baabedo/company'
25
+
26
+ # Errors
27
+ require 'baabedo/errors/baabedo_error'
28
+ require 'baabedo/errors/api_error'
29
+ require 'baabedo/errors/api_connection_error'
30
+ require 'baabedo/errors/invalid_request_error'
31
+ require 'baabedo/errors/authentication_error'
32
+
33
+ require 'baabedo/client'
34
+
35
+ module Baabedo
36
+ DEFAULT_CA_BUNDLE_PATH = File.dirname(__FILE__) + '/data/ca-certificates.crt'
37
+ @api_base = 'https://api.baabedo.com'
38
+ @api_version = 'v1beta'
39
+ @mutex = Mutex.new
40
+
41
+ @ssl_bundle_path = DEFAULT_CA_BUNDLE_PATH
42
+ @verify_ssl_certs = true
43
+
44
+ class << self
45
+ attr_accessor :access_token, :api_base, :verify_ssl_certs, :api_version
46
+ end
47
+
48
+ def self.api_url(url='', api_base_url=nil)
49
+ (api_base_url || @api_base) + url
50
+ end
51
+
52
+ def self.with_mutex
53
+ @mutex.synchronize { yield }
54
+ end
55
+
56
+ def self.request(method, url, access_token, params={}, headers={}, api_base_url=nil)
57
+ api_base_url = api_base_url || @api_base
58
+
59
+ unless access_token ||= @access_token
60
+ raise AuthenticationError.new('No API key provided. ' \
61
+ 'Set your API key using "Baabedo.access_token = <API-KEY>". ' \
62
+ 'You can currently only request an API key via out in-app support.')
63
+ end
64
+
65
+ if access_token =~ /\s/
66
+ raise AuthenticationError.new('Your API key is invalid, as it contains ' \
67
+ 'whitespace.)')
68
+ # 'whitespace. (HINT: You can double-check your API key from the ' \
69
+ # 'Stripe web interface. See https://stripe.com/api for details, or ' \
70
+ # 'email support@stripe.com if you have any questions.)')
71
+ end
72
+
73
+ request_opts = {}
74
+ if verify_ssl_certs
75
+ request_opts = {:verify_ssl => OpenSSL::SSL::VERIFY_PEER,
76
+ :ssl_ca_file => @ssl_bundle_path}
77
+ else
78
+ request_opts = {:verify_ssl => false}
79
+ unless @verify_ssl_warned
80
+ @verify_ssl_warned = true
81
+ $stderr.puts("WARNING: Running without SSL cert verification. " \
82
+ "You should never do this in production. " \
83
+ "Execute 'Baabedo.verify_ssl_certs = true' to enable verification.")
84
+ end
85
+ end
86
+
87
+ params = Util.objects_to_ids(params)
88
+ url = api_url(url, api_base_url)
89
+
90
+ case method.to_s.downcase.to_sym
91
+ when :get, :head, :delete
92
+ # Make params into GET parameters
93
+ url += "#{URI.parse(url).query ? '&' : '?'}#{uri_encode(params)}" if params && params.any?
94
+ payload = nil
95
+ else
96
+ if headers[:content_type] && headers[:content_type] == "multipart/form-data"
97
+ payload = params
98
+ else
99
+ payload = uri_encode(params)
100
+ end
101
+ end
102
+
103
+ request_opts.update(:headers => request_headers(access_token).update(headers),
104
+ :method => method, :open_timeout => 30,
105
+ :payload => payload, :url => url, :timeout => 80)
106
+
107
+ begin
108
+ response = execute_request(request_opts)
109
+ rescue SocketError => e
110
+ handle_restclient_error(e, api_base_url)
111
+ rescue NoMethodError => e
112
+ # Work around RestClient bug
113
+ if e.message =~ /\WRequestFailed\W/
114
+ e = APIConnectionError.new('Unexpected HTTP response code')
115
+ handle_restclient_error(e, api_base_url)
116
+ else
117
+ raise
118
+ end
119
+ rescue RestClient::ExceptionWithResponse => e
120
+ if rcode = e.http_code and rbody = e.http_body
121
+ handle_api_error(rcode, rbody)
122
+ else
123
+ handle_restclient_error(e, api_base_url)
124
+ end
125
+ rescue RestClient::Exception, Errno::ECONNREFUSED => e
126
+ handle_restclient_error(e, api_base_url)
127
+ end
128
+
129
+ [parse(response), access_token]
130
+ end
131
+
132
+ private
133
+
134
+ def self.uri_encode(params)
135
+ Util.flatten_params(params).
136
+ map { |k,v| "#{k}=#{Util.url_encode(v)}" }.join('&')
137
+ end
138
+
139
+ def self.request_headers(access_token)
140
+ headers = {
141
+ :user_agent => "Baabedo/v1 RubyBindings/#{Baabedo::VERSION}",
142
+ :authorization => "Bearer #{access_token}",
143
+ :content_type => 'application/x-www-form-urlencoded'
144
+ }
145
+ end
146
+
147
+ def self.execute_request(opts)
148
+ RestClient::Request.execute(opts)
149
+ end
150
+
151
+ def self.parse(response)
152
+ begin
153
+ # Would use :symbolize_names => true, but apparently there is
154
+ # some library out there that makes symbolize_names not work.
155
+ response = JSON.parse(response.body)
156
+ rescue JSON::ParserError
157
+ raise general_api_error(response.code, response.body)
158
+ end
159
+
160
+ Util.symbolize_names(response)
161
+ end
162
+
163
+ def self.general_api_error(rcode, rbody)
164
+ APIError.new("Invalid response object from API: #{rbody.inspect} " +
165
+ "(HTTP response code was #{rcode})", rcode, rbody)
166
+ end
167
+
168
+ def self.handle_api_error(rcode, rbody)
169
+ begin
170
+ error_obj = JSON.parse(rbody)
171
+ error_obj = Util.symbolize_names(error_obj)
172
+ error = error_obj[:error] or raise BaabedoError.new # escape from parsing
173
+
174
+ rescue JSON::ParserError, BaabedoError
175
+ raise general_api_error(rcode, rbody)
176
+ end
177
+
178
+ case rcode
179
+ when 400, 404
180
+ raise invalid_request_error error, rcode, rbody, error_obj
181
+ when 401
182
+ raise authentication_error error, rcode, rbody, error_obj
183
+ when 402
184
+ raise card_error error, rcode, rbody, error_obj
185
+ else
186
+ raise api_error error, rcode, rbody, error_obj
187
+ end
188
+
189
+ end
190
+
191
+ def self.invalid_request_error(error, rcode, rbody, error_obj)
192
+ InvalidRequestError.new(error[:message], error[:param], rcode,
193
+ rbody, error_obj)
194
+ end
195
+
196
+ def self.authentication_error(error, rcode, rbody, error_obj)
197
+ AuthenticationError.new(error[:message], rcode, rbody, error_obj)
198
+ end
199
+
200
+ def self.card_error(error, rcode, rbody, error_obj)
201
+ CardError.new(error[:message], error[:param], error[:code],
202
+ rcode, rbody, error_obj)
203
+ end
204
+
205
+ def self.api_error(error, rcode, rbody, error_obj)
206
+ APIError.new(error[:message], rcode, rbody, error_obj)
207
+ end
208
+
209
+ def self.handle_restclient_error(e, api_base_url=nil)
210
+ api_base_url = @api_base unless api_base_url
211
+ connection_message = "Please check your internet connection and try again. " \
212
+ "If this problem persists, let us know at api@baabedo.com."
213
+ # "If this problem persists, you should check Baabedo's service status at " \
214
+ # "https://twitter.com/baabedostatus, or let us know at api@baabedo.com."
215
+
216
+ case e
217
+ when RestClient::RequestTimeout
218
+ message = "Could not connect to Baabedo (#{api_base_url}). #{connection_message}"
219
+
220
+ when RestClient::ServerBrokeConnection
221
+ message = "The connection to the server (#{api_base_url}) broke before the " \
222
+ "request completed. #{connection_message}"
223
+
224
+ when RestClient::SSLCertificateNotVerified
225
+ message = "Could not verify Baabedo's SSL certificate. " \
226
+ "Please make sure that your network is not intercepting certificates. " \
227
+ "If this problem persists, let us know at api@baabedo.com."
228
+
229
+ when SocketError
230
+ message = "Unexpected error communicating when trying to connect to Baabedo. " \
231
+ "You may be seeing this message because your DNS is not working. " \
232
+ "To check, try running 'host baabedo.com' from the command line."
233
+
234
+ else
235
+ message = "Unexpected error communicating with Baabedo. " \
236
+ "If this problem persists, let us know at api@baabedo.com."
237
+
238
+ end
239
+
240
+ raise APIConnectionError.new(message + "\n\n(Network error: #{e.message})")
241
+ end
242
+ end