baabedo 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.
@@ -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