Created
December 14, 2011 13:39
-
-
Save sj26/1476603 to your computer and use it in GitHub Desktop.
Yet Another Chargify HTTParty-based API gem for Ruby
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 'httparty' | |
# This may go away: | |
require 'active_support/all' | |
# (doesn't *require* Rails, but adds some niceties) | |
module Chargify | |
VERSION = "0.0.1".freeze | |
# Create a default `Client`, attempting to use Rails application config. | |
def self.client options=nil | |
@client ||= begin | |
options ||= Rails.application.config.chargify if defined? Rails | |
Client.new options | |
end | |
end | |
# Delegate direct calls to the default `Client`, `.client` | |
def self.method_missing method, *args, &block | |
client.send method, *args, &block | |
end | |
def self.respond_to? method, include_private=false | |
super or client.respond_to? method, include_private | |
end | |
# Raise an exception for responses with 4xx and 5xx status codes. | |
# Use PresentParser so parsing an error response doesn't raise. | |
module HTTPExceptions | |
def perform_request http_method, path, options | |
super.tap do |response| | |
response.error! if response.response.is_a?(Net::HTTPClientError) or response.response.is_a?(Net::HTTPServerError) | |
end | |
end | |
end | |
# Only parses body if present. | |
module PresentParser | |
protected | |
def xml | |
super unless body.blank? | |
end | |
def json | |
super unless body.blank? | |
end | |
def yaml | |
super unless body.blank? | |
end | |
end | |
# Turns XML, JSON and YAML bodies into `Hashie::Mash`s. | |
module MashedParser | |
protected | |
def xml | |
mashed super | |
end | |
def json | |
mashed super | |
end | |
def yaml | |
mashed super | |
end | |
private | |
def mashed thing | |
if thing.is_a? Hash | |
Hashie::Mash.new thing | |
elsif thing.is_a? Array | |
thing.map &method(:mashed) | |
else | |
thing | |
end | |
end | |
end | |
# Chargify | |
class Client | |
# v1 configuration | |
attr_accessor :subdomain, :api_key, :shared_key | |
# v2 configuration | |
attr_accessor :api_id, :api_password, :api_secret | |
def initialize options={} | |
options.each do |key, value| | |
send :"#{key}=", value | |
end | |
end | |
# APIs | |
# | |
# HTTParty uses class attribtues to set base_uri and some other friends | |
# so we make a client-specific subclass for each API-version which can | |
# have some defaults set. | |
# Subclassed for each `Client` in `#v1` | |
class V1 | |
include HTTParty | |
# extend HTTPExceptions | |
class_attribute :client | |
delegate :subdomain, :api_key, :shared_key, :to => :client | |
# Injected by `Client#v1` | |
# base_uri "https://#{subdomain}.chargify.com" | |
headers "Content-Type" => "application/json" | |
headers "User-Agent" => "Ruby sj26-chargify #{Chargify::VERSION}" | |
format :json | |
parser Class.new(HTTParty::Parser) { include PresentParser, MashedParser } | |
end | |
# Subclassed for each `Client` in `#v2` | |
class V2 < V1 | |
delegate :api_id, :api_password, :api_secret, :to => :client | |
base_uri "https://api.chargify.com/api/v2" | |
end | |
# A sub-class of `V1` with this `Client`'s configuration. | |
def v1 | |
@v1 ||= Class.new(V1).tap do |v1| | |
v1.client = self | |
v1.base_uri "https://#{subdomain}.chargify.com" | |
v1.basic_auth api_key, "x" | |
end | |
end | |
# A sub-class of `V2` with this `Client`'s configuration. | |
def v2 | |
@v2 ||= Class.new(V2).tap do |v2| | |
v2.client = self | |
v2.basic_auth api_id, api_password | |
end | |
end | |
# Customers | |
def customers | |
v1.get("/customers") | |
end | |
def customer id | |
v1.get("/customers/#{id}") | |
end | |
def customer_lookup customer_attributes | |
v1.get("/customers/lookup", :query => customer_attributes) | |
end | |
def customer_by_reference reference | |
customer_lookup :reference => reference | |
end | |
def create_customer customer_attributes | |
v1.post("/customers", :body => {:customer => customer_attributes}.to_json) | |
end | |
def update_customer id, customer_attributes | |
v1.put("/customers/#{id}", :body => {:customer => customer_attributes}.to_json) | |
end | |
# Products | |
def products | |
v1.get("/products") | |
end | |
def product id | |
v1.get("/products/#{id}") | |
end | |
def product_by_handle handle | |
v1.get("/products/handle/#{handle}") | |
end | |
def create_product product_attributes | |
v1.post("/products", :body => {:product => product_attributes}.to_json) | |
end | |
def update_product id, product_attributes | |
v1.put("/products/#{id}", :body => {:product => product_attributes}.to_json) | |
end | |
# Subscriptions | |
def subscriptions | |
v1.get("/subscriptions") | |
end | |
def subscription id | |
v1.get("/subscription/#{id}") | |
end | |
def create_subscription subscription_attributes | |
v1.post("/subscriptions", :body => {:subscription => subscription_attributes}.to_json) | |
end | |
def update_subscription id, subscription_attributes | |
v1.put("/subscriptions/#{id}", :body => {:subscription => subscription_attributes}.to_json) | |
end | |
def cancel_subscription id, subscription_attributes | |
v1.delete("/subscriptions/#{id}", :body => {:subscription => subscription_attributes}.to_json) | |
end | |
def reactivate_subscription | |
v1.put("/subscriptions/#{id}/reactivate") | |
end | |
def charge_subscription id, charge_attributes={} | |
v1.put("/subscriptions/#{id}/charge", :body => {:charge => charge_attributes}.to_json) | |
end | |
# Pass an individual product id or a `Hash` (like `{:product_handle => "something"}`) | |
def migrate_subscription id, product_id_or_attributes | |
product_id_or_attributes = {:product_id => product_id_or_attributes} unless product_id_or_attributes.is_a? Hash | |
v1.put("/subscriptions/#{id}/migrations", :body => product_id_attributes.to_json) | |
end | |
def adjust_subscription id, adjustment_attributes | |
v1.put("/subscriptions/#{id}/adjustments", :body => {:adjustment => adjustment_attributes}.to_json) | |
end | |
# Direct | |
# Post URL for a Chargify Direct signup | |
def direct_signup_uri | |
"#{v2.base_uri}/signups" | |
end | |
# Post URL for a Chargify Direct card update | |
def direct_card_update_uri subscription_id | |
"#{v2.base_uri}/subscriptions/#{subscription_id}/card_update" | |
end | |
# Helper to calculate signature for Chargify Direct secure parameters. | |
# Adds `#to_hidden_fields` to the resulting hash for convenience in | |
# views when used from Rails, maintaining output safety. | |
def direct_params data={} | |
{ | |
"secure[api_id]" => api_id, | |
"secure[nonce]" => nonce = rand(10 ** 30).to_s.rjust(30,'0'), | |
"secure[data]" => data.to_query, | |
"secure[signature]" => OpenSSL::HMAC.hexdigest(OpenSSL::Digest::Digest.new('sha1'), api_secret, "#{api_id}#{nonce}#{data}"), | |
}.tap { |options| options.extend(HashToHiddenFields) if defined? Rails } | |
end | |
# Calls | |
# Fetches and verifies a response from a returned Chargify Direct request | |
def call id | |
Hashie::Mash.new(v2.get("calls/#{id}")).tap do |call| | |
raise "unverified response" unless response.signature == OpenSSL::HMAC.hexdigest(OpenSSL::Digest::Digest.new('sha1'), api_secret, "#{api_id}#{response.timestamp}#{response.nonce}#{response.status_code}#{response.result_code}#{response.call_id}") | |
end | |
end | |
end | |
# +nodoc+ | |
module HashToHiddenFields | |
def to_hidden_fields | |
Rails.application.send(:helper).safe_join(map do |name, value| | |
Rails.application.send(:helper).hidden_field_tag name, value | |
end) | |
end | |
end | |
end |
Will probably release as a gem eventually.
I would love to see this
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
In each rails environment configuration file (
config/environments/development.rb
for instance) I have some config:Then it works just like any other HTTParty-based API:
You could also create several instances of
Chargify::Client
instead of the defaultChargify.client
. I wrote a test harness for this as well, but haven't published that yet. Will probably release as a gem eventually.