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.
- checksums.yaml +7 -0
- data/.gitignore +14 -0
- data/.rspec +2 -0
- data/Gemfile +4 -0
- data/Guardfile +77 -0
- data/LICENSE.txt +22 -0
- data/README.md +65 -0
- data/Rakefile +2 -0
- data/baabedo.gemspec +28 -0
- data/lib/baabedo/api_object.rb +263 -0
- data/lib/baabedo/api_operations/create.rb +16 -0
- data/lib/baabedo/api_operations/delete.rb +11 -0
- data/lib/baabedo/api_operations/list.rb +17 -0
- data/lib/baabedo/api_operations/request.rb +41 -0
- data/lib/baabedo/api_operations/update.rb +17 -0
- data/lib/baabedo/api_resource.rb +37 -0
- data/lib/baabedo/client.rb +29 -0
- data/lib/baabedo/errors/api_connection_error.rb +4 -0
- data/lib/baabedo/errors/api_error.rb +4 -0
- data/lib/baabedo/errors/authentication_error.rb +4 -0
- data/lib/baabedo/errors/baabedo_error.rb +20 -0
- data/lib/baabedo/errors/invalid_request_error.rb +10 -0
- data/lib/baabedo/list_object.rb +33 -0
- data/lib/baabedo/util.rb +159 -0
- data/lib/baabedo/version.rb +3 -0
- data/lib/baabedo.rb +242 -0
- data/lib/data/ca-certificates.crt +5165 -0
- data/spec/spec_helper.rb +98 -0
- data/spec/unit/baabedo/client_spec.rb +19 -0
- data/spec/unit/baabedo_spec.rb +6 -0
- metadata +160 -0
@@ -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,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
|
data/lib/baabedo/util.rb
ADDED
@@ -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
|
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
|