Browse Source

Updated README and added most recent shopify_api.rb from Shopify.

Dennis Theisen 15 years ago
parent
commit
3c8b7a2cb2
5 changed files with 303 additions and 5 deletions
  1. 1 1
      LICENSE
  2. 12 3
      README.rdoc
  3. 1 1
      Rakefile
  4. 1 0
      VERSION
  5. 288 0
      lib/shopify_api.rb

+ 1 - 1
LICENSE

@@ -1,4 +1,4 @@
-Copyright (c) 2009 "Dennis Theisen"
+Copyright (c) 2009 "JadedPixel inc."
 
 Permission is hereby granted, free of charge, to any person obtaining
 a copy of this software and associated documentation files (the

+ 12 - 3
README.rdoc

@@ -1,7 +1,16 @@
-= shopify_api
+= Shopify API
+
+The Shopify API is implemented as XML over HTTP using all four verbs (GET/POST/PUT/DELETE). Each resource, like Order, Product, or Collection, has its own URL and is manipulated in isolation. In other words, we’ve tried to make the API follow the REST principles as much as possible.
+
+All API usage happens through Shopify applications, created by either shop owners for their own shops, or by Shopify Partners for use by shop owners.
+
+Shop owners can create applications for themselves through their own admin (under the Preferences > Applications tab).
+
+Shopify Partners create applications through their admin.
+
+For more information and detailed documentation visit http://api.shopify.com
 
-Description goes here.
 
 == Copyright
 
-Copyright (c) 2009 "Dennis Theisen". See LICENSE for details.
+Copyright (c) 2009 "JadedPixel inc.". See LICENSE for details.

+ 1 - 1
Rakefile

@@ -8,7 +8,7 @@ begin
     gem.summary = %Q{TODO}
     gem.email = "dennis@jadedpixel.com"
     gem.homepage = "http://github.com/Soleone/shopify_api"
-    gem.authors = [""Dennis Theisen""]
+    gem.authors = ["Dennis Theisen"]
     gem.rubyforge_project = "shopify_api"
     # gem is a Gem::Specification... see http://www.rubygems.org/read/chapter/20 for additional settings
   end

+ 1 - 0
VERSION

@@ -0,0 +1 @@
+0.0.0

+ 288 - 0
lib/shopify_api.rb

@@ -0,0 +1,288 @@
+require 'ostruct'
+require 'digest/md5'
+
+module ShopifyAPI
+  # 
+  #  The Shopify API authenticates each call via HTTP Authentication, using
+  #    * the application's API key as the username, and
+  #    * a hex digest of the application's shared secret and an 
+  #      authentication token as the password.
+  #  
+  #  Generation & acquisition of the beforementioned looks like this (assuming the ):
+  # 
+  #    0. Developer (that's you) registers Application (and provides a
+  #       callback url) and receives an API key and a shared secret
+  # 
+  #    1. User visits Application and are told they need to authenticate the
+  #       application first for read/write permission to their data (needs to
+  #       happen only once). User is asked for their shop url.
+  # 
+  #    2. Application redirects to Shopify : GET <user's shop url>/admin/api/auth?app=<API key>
+  #       (See Session#create_permission_url)
+  # 
+  #    3. User logs-in to Shopify, approves application permission request
+  # 
+  #    4. Shopify redirects to the Application's callback url (provided during
+  #       registration), including the shop's name, and an authentication token in the parameters:
+  #         GET client.com/customers?shop=snake-oil.myshopify.com&t=a94a110d86d2452eb3e2af4cfb8a3828
+  # 
+  #    5. Authentication password computed using the shared secret and the
+  #       authentication token (see Session#computed_password)
+  # 
+  #    6. Profit!
+  #       (API calls can now authenticate through HTTP using the API key, and
+  #       computed password)
+  # 
+  #  LoginController and ShopifyLoginProtection use the Session class to set ActiveResource::Base.site
+  #  so that all API calls are authorized transparently and end up just looking like this:
+  # 
+  #    # get 3 products
+  #    @products = ShopifyAPI::Product.find(:all, :params => {:limit => 3})
+  #    
+  #    # get latest 3 orders
+  #    @orders = ShopifyAPI::Order.find(:all, :params => {:limit => 3, :order => "created_at DESC" })
+  # 
+  #  As an example of what your LoginController should look like, take a look
+  #  at the following:
+  # 
+  #    class LoginController < ApplicationController
+  #      def index
+  #        # Ask user for their #{shop}.myshopify.com address
+  #      end
+  #    
+  #      def authenticate
+  #        redirect_to ShopifyAPI::Session.new(params[:shop]).create_permission_url
+  #      end
+  #    
+  #      # Shopify redirects the logged-in user back to this action along with
+  #      # the authorization token t.
+  #      # 
+  #      # This token is later combined with the developer's shared secret to form
+  #      # the password used to call API methods.
+  #      def finalize
+  #        shopify_session = ShopifyAPI::Session.new(params[:shop], params[:t])
+  #        if shopify_session.valid?
+  #          session[:shopify] = shopify_session
+  #          flash[:notice] = "Logged in to shopify store."
+  #    
+  #          return_address = session[:return_to] || '/home'
+  #          session[:return_to] = nil
+  #          redirect_to return_address
+  #        else
+  #          flash[:error] = "Could not log in to Shopify store."
+  #          redirect_to :action => 'index'
+  #        end
+  #      end
+  #    
+  #      def logout
+  #        session[:shopify] = nil
+  #        flash[:notice] = "Successfully logged out."
+  #    
+  #        redirect_to :action => 'index'
+  #      end
+  #    end
+  # 
+  class Session
+    cattr_accessor :api_key
+    cattr_accessor :secret
+    cattr_accessor :protocol 
+    self.protocol = 'https'
+
+    attr_accessor :url, :token, :name
+    
+    def self.setup(params)
+      params.each { |k,value| send("#{k}=", value) }
+    end
+
+    def initialize(url, token = nil)
+      url.gsub!(/https?:\/\//, '')                            # remove http:// or https://
+      url = "#{url}.myshopify.com" unless url.include?('.')   # extend url to myshopify.com if no host is given
+      
+      self.url, self.token = url, token
+    end
+    
+    def shop
+      Shop.current
+    end
+    
+    def create_permission_url
+      "http://#{url}/admin/api/auth?api_key=#{api_key}"
+    end
+
+    # Used by ActiveResource::Base to make all non-authentication API calls
+    # 
+    # (ActiveResource::Base.site set in ShopifyLoginProtection#shopify_session)
+    def site
+      "#{protocol}://#{api_key}:#{computed_password}@#{url}/admin"
+    end
+
+    def valid?
+      [url, token].all?
+    end
+
+    private
+
+    # The secret is computed by taking the shared_secret which we got when 
+    # registring this third party application and concating the request_to it, 
+    # and then calculating a MD5 hexdigest. 
+    def computed_password
+      Digest::MD5.hexdigest(secret + token.to_s)
+    end
+  end
+
+  # Shop object. Use Shop.current to receive 
+  # the shop. Since you can only ever reference your own
+  # shop this model does not have a .find method.
+  #
+  class Shop
+    def self.current
+      ActiveResource::Base.find(:one, :from => "/admin/shop.xml")
+    end
+  end               
+
+  # Custom collection
+  #
+  class CustomCollection < ActiveResource::Base
+    def products
+      Product.find(:all, :params => {:collection_id => self.id})
+    end
+    
+    def add_product(product)
+      Collect.create(:collection_id => self.id, :product_id => product.id)
+    end
+    
+    def remove_product(product)
+      collect = Collect.find(:first, :params => {:collection_id => self.id, :product_id => product.id})
+      collect.destroy if collect
+    end
+  end                                                                 
+  
+  class SmartCollection < ActiveResource::Base
+    def products
+      Product.find(:all, :params => {:collection_id => self.id})
+    end
+  end                                                                 
+
+  # For adding/removing products from custom collections
+  class Collect < ActiveResource::Base
+  end
+
+  class ShippingAddress < ActiveResource::Base
+  end
+
+  class BillingAddress < ActiveResource::Base
+  end         
+
+  class LineItem < ActiveResource::Base 
+  end       
+
+  class ShippingLine < ActiveResource::Base
+  end  
+
+  class Order < ActiveResource::Base  
+
+    def close; load_attributes_from_response(post(:close)); end
+
+    def open; load_attributes_from_response(post(:open)); end
+
+    def transactions
+      Transaction.find(:all, :params => { :order_id => id })
+    end
+    
+    def capture(amount = "")
+      Transaction.create(:amount => amount, :kind => "capture", :order_id => id)
+    end
+  end
+  
+  class Product < ActiveResource::Base
+
+    # Share all items of this store with the 
+    # shopify marketplace
+    def self.share; post :share;  end    
+    def self.unshare; delete :share; end
+
+    # compute the price range
+    def price_range
+      prices = variants.collect(&:price)
+      format =  "%0.2f"
+      if prices.min != prices.max
+        "#{format % prices.min} - #{format % prices.max}"
+      else
+        format % prices.min
+      end
+    end
+    
+    def collections
+      CustomCollection.find(:all, :params => {:product_id => self.id})
+    end
+    
+    def smart_collections
+      SmartCollection.find(:all, :params => {:product_id => self.id})
+    end
+    
+    def add_to_collection(collection)
+      collection.add_product(self)
+    end
+    
+    def remove_from_collection(collection)
+      collection.remove_product(self)
+    end
+  end
+  
+  class Variant < ActiveResource::Base
+    self.prefix = "/admin/products/:product_id/"
+  end
+  
+  class Image < ActiveResource::Base
+    self.prefix = "/admin/products/:product_id/"
+    
+    # generate a method for each possible image variant
+    [:pico, :icon, :thumb, :small, :medium, :large, :original].each do |m|
+      reg_exp_match = "/\\1_#{m}.\\2"
+      define_method(m) { src.gsub(/\/(.*)\.(\w{2,4})/, reg_exp_match) }
+    end
+    
+    def attach_image(data, filename = nil)
+      attributes[:attachment] = Base64.encode64(data)
+      attributes[:filename] = filename unless filename.nil?
+    end
+  end
+
+  class Transaction < ActiveResource::Base
+    self.prefix = "/admin/orders/:order_id/"
+  end
+  
+  class Fulfillment < ActiveResource::Base
+    self.prefix = "/admin/orders/:order_id/"
+  end
+
+  class Country < ActiveResource::Base
+  end
+
+  class Page < ActiveResource::Base
+  end
+  
+  class Blog < ActiveResource::Base
+    def articles
+      Article.find(:all, :params => { :blog_id => id })
+    end
+  end
+  
+  class Article < ActiveResource::Base
+    self.prefix = "/admin/blogs/:blog_id/"
+  end
+
+  class Comment < ActiveResource::Base 
+    def remove; load_attributes_from_response(post(:remove)); end
+    def ham; load_attributes_from_response(post(:ham)); end
+    def spam; load_attributes_from_response(post(:spam)); end
+    def approve; load_attributes_from_response(post(:approve)); end        
+  end
+  
+  class Province < ActiveResource::Base
+    self.prefix = "/admin/countries/:country_id/"
+  end
+  
+  class Redirect < ActiveResource::Base
+  end
+end