-
-
Save jenglert/3539880a906da1cd1357 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
require 'cgi' | |
require 'net/http' | |
require 'net/https' | |
require 'uri' | |
require 'base64' | |
require 'rubygems' | |
require 'json' | |
require 'bigdecimal' | |
# Sends transactional emails. Supports immediate delivery of email. | |
module TransactionalEmailDeliveryService | |
class Client | |
def initialize(url, opts={}) | |
@url = HttpClient::Preconditions.assert_class('url', url, String) | |
@authorization = HttpClient::Preconditions.assert_class_or_nil('authorization', opts.delete(:authorization), HttpClient::Authorization) | |
HttpClient::Preconditions.assert_empty_opts(opts) | |
HttpClient::Preconditions.check_state(url.match(/http.+/i), "URL[%s] must start with http" % url) | |
end | |
def request(path=nil) | |
HttpClient::Preconditions.assert_class_or_nil('path', path, String) | |
request = HttpClient::Request.new(URI.parse(@url + path.to_s)) | |
if @authorization | |
request.with_auth(@authorization) | |
else | |
request | |
end | |
end | |
def email_addresses | |
@email_addresses ||= TransactionalEmailDeliveryService::Clients::EmailAddresses.new(self) | |
end | |
def messages | |
@messages ||= TransactionalEmailDeliveryService::Clients::Messages.new(self) | |
end | |
end | |
module Models | |
class EmailAddress | |
attr_reader :name, :address | |
def initialize(incoming={}) | |
opts = HttpClient::Helper.symbolize_keys(incoming) | |
@name = HttpClient::Helper.to_klass('name', opts.delete(:name), String, :required => true, :multiple => false) | |
@address = HttpClient::Helper.to_klass('address', opts.delete(:address), String, :required => true, :multiple => false) | |
end | |
end | |
class Message | |
attr_reader :not_used | |
def initialize(incoming={}) | |
opts = HttpClient::Helper.symbolize_keys(incoming) | |
@not_used = HttpClient::Helper.to_klass('not_used', opts.delete(:not_used), String, :required => true, :multiple => false) | |
end | |
end | |
end | |
module Clients | |
class Messages | |
def initialize(client) | |
@client = HttpClient::Preconditions.assert_class('client', client, TransactionalEmailDeliveryService::Client) | |
end | |
# Sends an email immediately. | |
def post(hash) | |
HttpClient::Preconditions.assert_class('hash', hash, Hash) | |
@client.request("/messages").with_json(hash.to_json).post | |
nil | |
end | |
end | |
end | |
module HttpClient | |
class Request | |
def initialize(uri) | |
@uri = Preconditions.assert_class('uri', uri, URI) | |
@params = nil | |
@body = nil | |
@auth = nil | |
@headers = {} | |
@header_keys_lower_case = [] | |
end | |
def with_header(name, value) | |
Preconditions.check_not_blank('name', name, "Header name is required") | |
Preconditions.check_not_blank('value', value, "Header value is required") | |
Preconditions.check_state(!@headers.has_key?(name), | |
"Duplicate header named[%s]" % name) | |
@headers[name] = value | |
@header_keys_lower_case << name.downcase | |
self | |
end | |
def with_auth(auth) | |
Preconditions.assert_class('auth', auth, HttpClient::Authorization) | |
Preconditions.check_state(@auth.nil?, "auth previously set") | |
if auth.scheme.name == AuthScheme::BASIC.name | |
@auth = auth | |
else | |
raise "Auth Scheme[#{auth.scheme.name}] not supported" | |
end | |
self | |
end | |
def with_query(params) | |
Preconditions.assert_class('params', params, Hash) | |
Preconditions.check_state(@params.nil?, "Already have query parameters") | |
@params = params | |
self | |
end | |
# Wrapper to set Content-Type header to application/json and set | |
# the provided json document as the body | |
def with_json(json) | |
@headers['Content-Type'] ||= 'application/json' | |
with_body(json) | |
end | |
def with_body(body) | |
Preconditions.check_not_blank('body', body) | |
@body = body | |
self | |
end | |
def get(&block) | |
do_request(Net::HTTP::Get, &block) | |
end | |
def delete(&block) | |
do_request(Net::HTTP::Delete, &block) | |
end | |
def options(&block) | |
do_request(Net::HTTP::Options, &block) | |
end | |
def post(&block) | |
do_request(Net::HTTP::Post, &block) | |
end | |
def put(&block) | |
do_request(Net::HTTP::Put, &block) | |
end | |
def do_request(klass) | |
Preconditions.assert_class('klass', klass, Class) | |
uri = @uri.to_s | |
if q = to_query(@params) | |
uri += "?%s" % q | |
end | |
request = klass.send(:new, uri) | |
curl = ['curl'] | |
if klass != Net::HTTP::Get | |
curl << "-X%s" % klass.name.split("::").last.upcase | |
end | |
if @body | |
# DEBUG path = "/tmp/rest_client.tmp" | |
# DEBUG File.open(path, "w") { |os| os << @body.to_s } | |
# DEBUG curl << "-d@%s" % path | |
request.body = @body | |
end | |
if @auth | |
curl << "-u \"%s:%s\"" % [@auth.username, @auth.password] | |
Preconditions.check_state(!@header_keys_lower_case.include?("authorization"), | |
"Cannot specify both an Authorization header and an explicit username") | |
user_pass = "%s:%s" % [@auth.username, @auth.password] | |
encoded = Base64.encode64(user_pass).to_s.split("\n").map(&:strip).join | |
request.add_field("Authorization", "Basic %s" % encoded) | |
end | |
@headers.each do |key, value| | |
curl << "-H \"%s: %s\"" % [key, value] | |
request.add_field(key, value) | |
end | |
curl << "'%s'" % uri | |
# DEBUG puts curl.join(" ") | |
raw_response = http_request(request) | |
response = raw_response.to_s == "" ? nil : JSON.parse(raw_response) | |
if block_given? | |
yield response | |
else | |
response | |
end | |
end | |
private | |
def to_query(params={}) | |
parts = (params || {}).map do |k,v| | |
"%s=%s" % [k, CGI.escape(v.to_s)] | |
end | |
parts.empty? ? nil : parts.join("&") | |
end | |
def http_request(request) | |
http = Net::HTTP.new(@uri.host, @uri.port) | |
if @uri.scheme == "https" | |
http.use_ssl = true | |
http.verify_mode = OpenSSL::SSL::VERIFY_NONE | |
end | |
response = http.request(request) | |
case response | |
when Net::HTTPSuccess | |
response.body | |
else | |
body = response.body rescue nil | |
raise HttpClient::ServerError.new(response.code.to_i, response.message, :body => body) | |
end | |
end | |
end | |
class ServerError < StandardError | |
attr_reader :code, :details, :body | |
def initialize(code, details, incoming={}) | |
opts = HttpClient::Helper.symbolize_keys(incoming) | |
@code = HttpClient::Preconditions.assert_class('code', code, Integer) | |
@details = HttpClient::Preconditions.assert_class('details', details, String) | |
@body = HttpClient::Preconditions.assert_class_or_nil('body', opts.delete(:body), String) | |
HttpClient::Preconditions.assert_empty_opts(opts) | |
end | |
def message | |
m = "%s %s" % [@code, @details] | |
if @body | |
m << ": %s" % @body | |
end | |
m | |
end | |
def body_json | |
JSON.parse(@body) | |
end | |
end | |
module Preconditions | |
def Preconditions.check_argument(expression, error_message=nil) | |
if !expression | |
raise error_message || "check_argument failed" | |
end | |
nil | |
end | |
def Preconditions.check_state(expression, error_message=nil) | |
if !expression | |
raise error_message || "check_state failed" | |
end | |
nil | |
end | |
def Preconditions.check_not_nil(field_name, reference, error_message=nil) | |
if reference.nil? | |
raise error_message || "argument for %s cannot be nil" % field_name | |
end | |
reference | |
end | |
def Preconditions.check_not_blank(field_name, reference, error_message=nil) | |
if reference.to_s.strip == "" | |
raise error_message || "argument for %s cannot be blank" % field_name | |
end | |
reference | |
end | |
# Throws an error if opts is not empty. Useful when parsing | |
# arguments to a function | |
def Preconditions.assert_empty_opts(opts) | |
if !opts.empty? | |
raise "Invalid opts: #{opts.keys.inspect}\n#{opts.inspect}" | |
end | |
end | |
# Asserts that value is not nill and is_?(klass). Returns | |
# value. Common use is | |
# | |
# amount = Preconditions.assert_class('amount', amount, BigDecimal) | |
def Preconditions.assert_class(field_name, value, klass) | |
Preconditions.check_not_nil('field_name', field_name) | |
Preconditions.check_not_nil('klass', klass) | |
Preconditions.check_not_nil('value', value, "Value for %s cannot be nil. Expected an instance of class %s" % [field_name, klass.name]) | |
Preconditions.check_state(value.is_a?(klass), | |
"Value for #{field_name} is of type[#{value.class}] - class[#{klass}] is required. value[#{value.inspect.to_s}]") | |
value | |
end | |
def Preconditions.assert_class_or_nil(field_name, value, klass) | |
if !value.nil? | |
Preconditions.assert_class(field_name, value, klass) | |
end | |
end | |
def Preconditions.assert_collection_of_class(field_name, values, klass) | |
values.each { |v| Preconditions.assert_class(field_name, v, klass) } | |
end | |
end | |
class AuthScheme | |
attr_reader :name | |
def initialize(name) | |
@name = HttpClient::Preconditions.check_not_blank('name', name) | |
end | |
BASIC = AuthScheme.new("basic") unless defined?(BASIC) | |
end | |
class Authorization | |
attr_reader :scheme, :username, :password | |
def initialize(scheme, username, opts={}) | |
@scheme = HttpClient::Preconditions.assert_class('schema', scheme, AuthScheme) | |
@username = HttpClient::Preconditions.check_not_blank('username', username, "username is required") | |
@password = HttpClient::Preconditions.assert_class_or_nil('password', opts.delete(:password), String) | |
HttpClient::Preconditions.assert_empty_opts(opts) | |
end | |
def Authorization.basic(username, password=nil) | |
Authorization.new(AuthScheme::BASIC, username, :password => password) | |
end | |
end | |
module Helper | |
def Helper.symbolize_keys(hash) | |
Preconditions.assert_class('hash', hash, Hash) | |
new_hash = {} | |
hash.each do |k, v| | |
new_hash[k.to_sym] = v | |
end | |
new_hash | |
end | |
def Helper.to_klass(field_name, value, klass, opts={}) | |
HttpClient::Preconditions.assert_class('field_name', field_name, String) | |
HttpClient::Preconditions.assert_class('klass', klass, Class) | |
required = opts.has_key?(:required) ? opts.delete(:required) : false | |
multiple = opts.has_key?(:multiple) ? opts.delete(:multiple) : false | |
HttpClient::Preconditions.assert_empty_opts(opts) | |
if multiple | |
HttpClient::Preconditions.assert_collection_of_class(field_name, value, klass) | |
if required | |
HttpClient::Preconditions.check_state(!value.empty?, "%s is required" % field_name) | |
end | |
value | |
elsif required | |
HttpClient::Preconditions.assert_class(field_name, value, klass) | |
else | |
HttpClient::Preconditions.assert_class_or_nil(field_name, value, klass) | |
end | |
end | |
def Helper.to_model_instance(field_name, klass, value, opts={}) | |
Helper.parse_args(field_name, value, opts) { |v| klass.send(:new, v) } | |
end | |
def Helper.to_big_decimal(field_name, value, opts={}) | |
Helper.parse_args(field_name, value, opts) { |v| BigDecimal.new(v.to_s) } | |
end | |
def Helper.to_uuid(field_name, value, opts={}) | |
Helper.parse_args(field_name, value, opts) do |v| | |
Preconditions.check_state(v.match(/^\w\w\w\w\w\w\w\w\-\w\w\w\w\-\w\w\w\w\-\w\w\w\w\-\w\w\w\w\w\w\w\w\w\w\w\w$/), | |
"Invalid guid[%s]" % v) | |
v | |
end | |
end | |
def Helper.to_date_time_iso8601(field_name, value, opts={}) | |
Helper.parse_args(field_name, value, opts) { |v| DateTime.parse(v) } | |
end | |
TRUE_STRINGS = ['t', 'true', 'y', 'yes', 'on', '1', 'trueclass'] unless defined?(TRUE_STRINGS) | |
FALSE_STRINGS = ['f', 'false', 'n', 'no', 'off', '0', 'falseclass'] unless defined?(FALSE_STRINGS) | |
def Helper.to_boolean(field_name, value, opts={}) | |
Helper.parse_args(field_name, value, opts) do |v| | |
string = value.to_s.strip.downcase | |
if TRUE_STRINGS.include?(string) | |
true | |
elsif FALSE_STRINGS.include?(string) | |
false | |
else | |
nil | |
end | |
end | |
end | |
def Helper.parse_args(field_name, value, opts={}, &block) | |
required = opts.has_key?(:required) ? opts.delete(:required) : false | |
multiple = opts.has_key?(:multiple) ? opts.delete(:multiple) : false | |
HttpClient::Preconditions.assert_empty_opts(opts) | |
if multiple | |
values = value || [] | |
if required | |
HttpClient::Preconditions.check_state(!values.empty?, "%s is required" % field_name) | |
end | |
if block_given? | |
values.map { |v| block.call(v) } | |
else | |
values | |
end | |
else | |
if required && value.nil? | |
raise "%s is required" % field_name | |
end | |
if value && block_given? | |
block.call(value) | |
else | |
value | |
end | |
end | |
end | |
end | |
end | |
end |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment