Skip to content

Instantly share code, notes, and snippets.

@sj26
Created December 14, 2011 13:39
Show Gist options
  • Save sj26/1476603 to your computer and use it in GitHub Desktop.
Save sj26/1476603 to your computer and use it in GitHub Desktop.
Yet Another Chargify HTTParty-based API gem for Ruby
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
@sj26
Copy link
Author

sj26 commented Mar 31, 2012

In each rails environment configuration file (config/environments/development.rb for instance) I have some config:

MyRails::Application.configure do
  # Chargify for billing
  config.chargify = {
    # v1
    :subdomain => "chargify-subdomain",
    :api_key => "abc123",
    :shared_key => "abc123",

    # v2
    :api_id => "abc123",
    :api_password => "abc123",
    :api_secret => "abc123",
  }
end

Then it works just like any other HTTParty-based API:

customer = Chargify.create_customer reference: "mycustomer", first_name: "John", last_name: "Smith", email: "[email protected]"
Chargify.create_subscription product_handle: "myproduct", customer_id: customer.id

You could also create several instances of Chargify::Client instead of the default Chargify.client. I wrote a test harness for this as well, but haven't published that yet. Will probably release as a gem eventually.

@warmwaffles
Copy link

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