require 'cgi'
require 'hpricot'
require 'net/http'
module ThreeScale # :nodoc:
class Error < StandardError; end
# Base class for exceptions caused by user.
class UserError < Error; end
# Exception raised when contract between user and provider is not active.
# Contract can be inactive when it is pending (requires confirmation from
# provider), suspended or canceled.
class ContractNotActive < UserError; end
# Exception raised when usage limits configured for contract are already
# exceeded.
class LimitsExceeded < UserError; end
# Exception raised when +user_key+ is not valid. This can mean that contract
# between provider and user does not exists, or the passed +user_key+ does
# not correspond to the key associated with this contract.
class UserKeyInvalid < UserError; end
# Base class for exceptions caused by provider.
class ProviderError < Error; end
# Exception raised when some metric name in provider +usage+ hash does not
# correspond to metric configured for the service.
class MetricInvalid < ProviderError; end
# Exception raised when provider authentication key is not valid. The provider
# needs to make sure that the key used is the same as the one that was
# generated for him/her when he/she published a service on 3scale.
class ProviderKeyInvalid < ProviderError; end
# Exception raised when transaction corresponding to given +transaction_id+
# does not exists. Methods +confirm+ and +cancel+ need valid transaction id
# that is obtained by preceding call to +start+.
class TransactionNotFound < ProviderError; end
# Base class for exceptions caused by 3scale backend system.
class SystemError < Error; end
# Other error.
class UnknownError < SystemError; end
# This class provides interface to 3scale monitoring system.
#
# Objects of this class are stateless and can be shared through multiple
# transactions and by multiple clients.
class Interface
# Hostname of 3scale server.
attr_accessor :host
# Key that uniquely identifies the provider. This key is known only to the
# provider and to 3scale.
attr_accessor :provider_authentication_key
# Create a 3scale interface object.
#
# == Arguments
# +host+:: Hostname of 3scale backend server.
# +provider_authentication_key+:: Unique key that identifies this provider.
def initialize(host = nil, provider_authentication_key = nil)
@host = host
@provider_authentication_key = provider_authentication_key
end
# Starts a transaction. This can be used also to report estimated resource
# usage of the request.
#
# == Arguments
# +user_key+:: Key that uniquely identifies an user of the service.
# +usage+::
# A hash of metric names/values pairs that contains predicted resource
# usage of this request.
#
# For example, if this request is going to take 10MB of storage space,
# then this parameter could contain {'storage' => 10}. The values may be
# only approximate or they can be missing altogether. In these cases, the
# real values must be reported using method +confirm+.
#
# == Return values
# A hash containing there keys:
# :id::
# Transaction id. This is required for confirmation/cancellation of the
# transaction later.
# :provider_verification_key::
# This key should be sent back to the user so he/she can use it to verify
# the authenticity of the provider.
# :contract_name::
# This is name of the contract the user is singed for. This information
# can be used to serve different responses according to contract types,
# if that is desirable.
#
# == Exceptions
#
# ThreeScale::UserKeyInvalid:: +user_key+ is not valid
# ThreeScale::ProviderKeyInvalid:: +provider_authentication_key+ is not valid
# ThreeScale::MetricInvalid:: +usage+ contains invalid metrics
# ThreeScale::ContractNotActive:: contract is not active
# ThreeScale::LimitsExceeded:: usage limits are exceeded
# ThreeScale::UnknownError:: some other unexpected error
#
def start(user_key, usage = {})
uri = URI.parse("#{host}/transactions.xml")
params = {
'user_key' => prepare_key(user_key),
'provider_key' => provider_authentication_key
}
params.merge!(encode_params(usage, 'usage'))
response = Net::HTTP.post_form(uri, params)
if response.is_a?(Net::HTTPSuccess)
element = Hpricot::XML(response.body).at('transaction')
[:id, :provider_verification_key, :contract_name].inject({}) do |memo, key|
memo[key] = element.at(key).inner_text if element.at(key)
memo
end
else
handle_error(response.body)
end
end
# Confirms a transaction.
#
# == Arguments
#
# +transaction_id+::
# A transaction id obtained from previous call to +start+.
# +usage+::
# A hash of metric names/values pairs containing actual resource usage
# of this request. This parameter is required only if no usage information
# was passed to method +start+ for this transaction, or if it was only
# approximate.
#
# == Return values
#
# If there were no exceptions raised, returns true.
#
# == Exceptions
#
# ThreeScale::TransactionNotFound:: transactions does not exits
# ThreeScale::ProviderKeyInvalid:: +provider_authentication_key+ is not valid
# ThreeScale::MetricInvalid:: +usage+ contains invalid metrics
# ThreeScale::UnknownError:: some other unexpected error
#
def confirm(transaction_id, usage = {})
uri = URI.parse("#{host}/transactions/#{CGI.escape(transaction_id.to_s)}/confirm.xml")
params = {
'provider_key' => provider_authentication_key
}
params.merge!(encode_params(usage, 'usage'))
response = Net::HTTP.post_form(uri, params)
response.is_a?(Net::HTTPSuccess) ? true : handle_error(response.body)
end
# Cancels a transaction.
#
# Use this if request processing failed. Any estimated resource usage
# reported by preceding call to +start+ will be deleted. You don't have to
# call this if call to +start+ itself failed.
#
# == Arguments
#
# +transaction_id+::
# A transaction id obtained from previous call to +start+.
#
# == Return values
#
# If there were no exceptions raised, returns true.
#
# == Exceptions
#
# ThreeScale::TransactionNotFound:: transactions does not exits
# ThreeScale::ProviderKeyInvalid:: +provider_authentication_key+ is not valid
# ThreeScale::UnknownError:: some other unexpected error
#
def cancel(transaction_id)
uri = URI.parse("#{host}/transactions/#{CGI.escape(transaction_id.to_s)}.xml" +
"?provider_key=#{CGI.escape(provider_authentication_key)}")
response = Net::HTTP.start(uri.host, uri.port) do |http|
http.delete("#{uri.path}?#{uri.query}")
end
response.is_a?(Net::HTTPSuccess) ? true : handle_error(response.body)
end
KEY_PREFIX = '3scale-' # :nodoc:
# This can be used to quickly distinguish between keys used with 3scale
# system and any other keys the provider might use. Returns true if the key
# is for 3scale system.
def system_key?(key)
# Key should start with prefix
key.index(KEY_PREFIX) == 0
end
private
# Encode hash into form suitable for sending it as params of HTTP request.
def encode_params(params, prefix)
params.inject({}) do |memo, (key, value)|
memo["#{prefix}[#{CGI.escape(key)}]"] = CGI.escape(value.to_s)
memo
end
end
def prepare_key(key)
system_key?(key) ? key[KEY_PREFIX.length..-1] : key
end
CODES_TO_EXCEPTIONS = {
'user.exceeded_limits' => LimitsExceeded,
'user.invalid_key' => UserKeyInvalid,
'user.inactive_contract' => ContractNotActive,
'provider.invalid_key' => ProviderKeyInvalid,
'provider.invalid_metric' => MetricInvalid,
'provider.invalid_transaction_id' => TransactionNotFound} # :nodoc:
def handle_error(response)
element = Hpricot::XML(response).at('error')
raise UnknownError unless element
raise CODES_TO_EXCEPTIONS[element[:id]] || UnknownError, element.inner_text
end
end
end