Ver Fonte

Split shopify_api.rb into separate files

Travis Haynes há 13 anos atrás
pai
commit
fa8f791ce4

+ 4 - 520
lib/shopify_api.rb

@@ -10,536 +10,20 @@ module ShopifyAPI
   
   METAFIELD_ENABLED_CLASSES = %w( Order Product CustomCollection SmartCollection Page Blog Article Variant)
   EVENT_ENABLED_CLASSES = %w( Order Product CustomCollection SmartCollection Page Blog Article )
-  
+
   module Countable
     def count(options = {})
       Integer(get(:count, options))
     end
   end
   
-  module Metafields
-    def metafields
-      Metafield.find(:all, :params => {:resource => self.class.collection_name, :resource_id => id})
-    end
-    
-    def add_metafield(metafield)
-      raise ArgumentError, "You can only add metafields to resource that has been saved" if new?
-      
-      metafield.prefix_options = {
-        :resource => self.class.collection_name,
-        :resource_id => id
-      }
-      metafield.save
-      metafield
-    end
-  end
-
-  module Events
-    def events
-      Event.find(:all, :params => {:resource => self.class.collection_name, :resource_id => id})
-    end
-  end
-    
-  # 
-  #  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:
-  # 
-  #    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?api_key=<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 Shopify::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, params = nil)
-      self.url, self.token = url, token
-
-      if params
-        unless self.class.validate_signature(params) && params[:timestamp].to_i > 24.hours.ago.utc.to_i
-          raise "Invalid Signature: Possible malicious login" 
-        end
-      end
-
-      self.class.prepare_url(self.url)
-    end
-    
-    def shop
-      Shop.current
-    end
-    
-    def create_permission_url
-      return nil if url.blank? || api_key.blank?
-      "http://#{url}/admin/api/auth?api_key=#{api_key}"
-    end
-
-    # Used by ActiveResource::Base to make all non-authentication API calls
-    # 
-    # (ShopifyAPI::Base.site set in ShopifyLoginProtection#shopify_session)
-    def site
-      "#{protocol}://#{api_key}:#{computed_password}@#{url}/admin"
-    end
-
-    def valid?
-      url.present? && token.present?
-    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
-    
-    def self.prepare_url(url)
-      return nil if url.blank?
-      url.gsub!(/https?:\/\//, '')                            # remove http:// or https://
-      url.concat(".myshopify.com") unless url.include?('.')   # extend url to myshopify.com if no host is given
-    end
-    
-    def self.validate_signature(params)
-      return false unless signature = params[:signature]
-
-      sorted_params = params.except(:signature, :action, :controller).collect{|k,v|"#{k}=#{v}"}.sort.join
-      Digest::MD5.hexdigest(secret + sorted_params) == signature
-    end
-  end
-  
   class Base < ActiveResource::Base
     extend Countable
   end
-
-  # Shop object. Use Shop.current to receive 
-  # the shop.
-  class Shop < Base
-    def self.current
-      find(:one, :from => "/admin/shop.#{format.extension}")
-    end
-    
-    def metafields
-      Metafield.find(:all)
-    end
-    
-    def add_metafield(metafield)
-      raise ArgumentError, "You can only add metafields to resource that has been saved" if new?      
-      metafield.save
-      metafield
-    end
-    
-    def events
-      Event.find(:all)
-    end
-  end               
-
-  # Custom collection
-  #
-  class CustomCollection < 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 < Base
-    def products
-      Product.find(:all, :params => {:collection_id => self.id})
-    end
-  end  
-  
-  # For adding/removing products from custom collections
-  class Collect < Base
-  end
-
-  class ShippingAddress < Base
-  end
-
-  class BillingAddress < Base
-  end         
-
-  class LineItem < Base 
-  end       
-
-  class ShippingLine < Base
-  end  
-
-  class NoteAttribute < Base
-  end
-
-  class Order < Base
-    def close;  load_attributes_from_response(post(:close, {}, only_id)); end
-    def open;   load_attributes_from_response(post(:open, {}, only_id)); end
-
-    def cancel(options = {})
-      load_attributes_from_response(post(:cancel, options, only_id))
-    end
-
-    def transactions
-      Transaction.find(:all, :params => { :order_id => id })
-    end
-
-    def capture(amount = "")
-      Transaction.create(:amount => amount, :kind => "capture", :order_id => id)
-    end
-
-    def only_id
-      encode(:only => :id, :include => [], :methods => [], :fields => [])
-    end
-  end
-
-  class Product < 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 < Base
-    self.prefix = "/admin/products/:product_id/"
-    
-    def self.prefix(options={})
-      options[:product_id].nil? ? "/admin/" : "/admin/products/#{options[:product_id]}/"
-    end
-  end
-  
-  class Image < Base
-    self.prefix = "/admin/products/:product_id/"
-    
-    # generate a method for each possible image variant
-    [:pico, :icon, :thumb, :small, :compact, :medium, :large, :grande, :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 < Base
-    self.prefix = "/admin/orders/:order_id/"
-  end
-  
-  class Fulfillment < Base
-    self.prefix = "/admin/orders/:order_id/"
-  end
-
-  class Country < Base
-  end
-
-  class Page < Base
-  end
-  
-  class Blog < Base
-    def articles
-      Article.find(:all, :params => { :blog_id => id })
-    end
-  end
-  
-  class Article < Base
-    self.prefix = "/admin/blogs/:blog_id/"
-    
-    def comments
-      Comment.find(:all, :params => { :article_id => id })
-    end
-  end
-  
-  class Metafield < Base
-    self.prefix = "/admin/:resource/:resource_id/"
-    
-    # Hack to allow both Shop and other Metafields in through the same AR class
-    def self.prefix(options={})
-      options[:resource].nil? ? "/admin/" : "/admin/#{options[:resource]}/#{options[:resource_id]}/"
-    end
-            
-    def value
-      return if attributes["value"].nil?
-      attributes["value_type"] == "integer" ? attributes["value"].to_i : attributes["value"]
-    end
-
-  end
-
-  class Comment < Base
-    def remove;   load_attributes_from_response(post(:remove, {}, only_id)); end
-    def ham;      load_attributes_from_response(post(:ham, {}, only_id)); end
-    def spam;     load_attributes_from_response(post(:spam, {}, only_id)); end
-    def approve;  load_attributes_from_response(post(:approve, {}, only_id)); end
-    def restore;  load_attributes_from_response(post(:restore, {}, only_id)); end
-    def not_spam; load_attributes_from_response(post(:not_spam, {}, only_id)); end
-
-    def only_id
-      encode(:only => :id)
-    end
-  end
-
-  class Province < Base
-    self.prefix = "/admin/countries/:country_id/"
-  end
-  
-  class Redirect < Base
-  end
-  
-  class Webhook < Base
-  end
-  
-  class Event < Base
-    self.prefix = "/admin/:resource/:resource_id/"
-    
-    # Hack to allow both Shop and other Events in through the same AR class
-    def self.prefix(options={})
-      options[:resource].nil? ? "/admin/" : "/admin/#{options[:resource]}/#{options[:resource_id]}/"
-    end
-  end
-  
-  class Customer < Base
-  end
-
-  class CustomerGroup < Base
-  end
-  
-  class Theme < Base
-  end
-  
-  # Assets represent the files that comprise your theme.
-  # There are different buckets which hold different kinds
-  # of assets, each corresponding to one of the folders
-  # within a theme's zip file: "layout", "templates",
-  # "snippets", "assets", and "config". The full key of an
-  # asset always starts with the bucket name, and the path
-  # separator is a forward slash, like layout/theme.liquid
-  # or assets/bg-body.gif.
-  #
-  # Initialize with a key:
-  #   asset = ShopifyAPI::Asset.new(:key => 'assets/special.css', :theme_id => 12345)
-  # 
-  # Find by key:
-  #   asset = ShopifyAPI::Asset.find('assets/image.png', :params => {:theme_id => 12345})
-  # 
-  # Get the text or binary value:
-  #   asset.value # decodes from attachment attribute if necessary
-  # 
-  # You can provide new data for assets in a few different ways:
-  # 
-  #   * assign text data for the value directly:
-  #       asset.value = "div.special {color:red;}"
-  #     
-  #   * provide binary data for the value:
-  #       asset.attach(File.read('image.png'))
-  #     
-  #   * set a URL from which Shopify will fetch the value:
-  #       asset.src = "http://mysite.com/image.png"
-  #     
-  #   * set a source key of another of your assets from which
-  #     the value will be copied:
-  #       asset.source_key = "assets/another_image.png"
-  class Asset < Base
-    self.primary_key = 'key'
-    self.prefix = "/admin/themes/:theme_id/"
-    
-    def self.prefix(options={})
-      options[:theme_id].nil? ? "/admin/" : "/admin/themes/#{options[:theme_id]}/"
-    end
-    
-    def self.element_path(id, prefix_options = {}, query_options = nil) #:nodoc:
-      prefix_options, query_options = split_options(prefix_options) if query_options.nil?
-      "#{prefix(prefix_options)}#{collection_name}.#{format.extension}#{query_string(query_options)}"
-    end
-    
-    # find an asset by key:
-    #   ShopifyAPI::Asset.find('layout/theme.liquid', :params => {:theme_id => 99})
-    def self.find(*args)
-      if args[0].is_a?(Symbol)
-        super
-      else
-        params = {:asset => {:key => args[0]}}
-        params = params.merge(args[1][:params]) if args[1] && args[1][:params]
-        path_prefix = params[:theme_id] ? "/admin/themes/#{params[:theme_id]}" : "/admin"
-        find(:one, :from => "#{path_prefix}/assets.#{format.extension}", :params => params)
-      end
-    end
-    
-    # For text assets, Shopify returns the data in the 'value' attribute.
-    # For binary assets, the data is base-64-encoded and returned in the
-    # 'attachment' attribute. This accessor returns the data in both cases.
-    def value
-      attributes['value'] ||
-      (attributes['attachment'] ? Base64.decode64(attributes['attachment']) : nil)
-    end
-    
-    def attach(data)
-      self.attachment = Base64.encode64(data)
-    end
-    
-    def destroy
-      connection.delete(element_path(prefix_options.merge(:asset => {:key => key})), self.class.headers)
-    end
-    
-    def new?
-      false
-    end
-    
-    def method_missing(method_symbol, *arguments) #:nodoc:
-      if %w{value= attachment= src= source_key=}.include?(method_symbol)
-        wipe_value_attributes
-      end
-      super
-    end
-    
-    private
-    
-    def wipe_value_attributes
-      %w{value attachment src source_key}.each do |attr|
-        attributes.delete(attr)
-      end
-    end
-  end
-  
-  class RecurringApplicationCharge < Base
-    undef_method :test
-
-    def self.current
-      find(:all).find{|charge| charge.status == 'active'}
-    end
-    
-    def cancel
-      load_attributes_from_response(self.destroy)
-    end
-    
-    def activate
-      load_attributes_from_response(post(:activate))
-    end
-  end
-
-  class ApplicationCharge < Base
-    undef_method :test
-
-    def activate
-      load_attributes_from_response(post(:activate))
-    end
-  end
-
-  class ProductSearchEngine < Base
-  end
   
-  class ScriptTag < Base
+  ignore_files = ['cli.rb']
+  Dir[File.join(File.dirname(__FILE__), 'shopify_api', '**/*.rb')].each do |file|
+    require file unless ignore_files.include?(File.basename(file))
   end
   
   # Include Metafields module in all enabled classes

+ 9 - 0
lib/shopify_api/application_charge.rb

@@ -0,0 +1,9 @@
+module ShopifyAPI
+  class ApplicationCharge < Base
+    undef_method :test
+
+    def activate
+      load_attributes_from_response(post(:activate))
+    end
+  end
+end

+ 9 - 0
lib/shopify_api/article.rb

@@ -0,0 +1,9 @@
+module ShopifyAPI
+  class Article < Base
+    self.prefix = "/admin/blogs/:blog_id/"
+    
+    def comments
+      Comment.find(:all, :params => { :article_id => id })
+    end
+  end
+end

+ 95 - 0
lib/shopify_api/asset.rb

@@ -0,0 +1,95 @@
+module ShopifyAPI
+  # Assets represent the files that comprise your theme.
+  # There are different buckets which hold different kinds
+  # of assets, each corresponding to one of the folders
+  # within a theme's zip file: "layout", "templates",
+  # "snippets", "assets", and "config". The full key of an
+  # asset always starts with the bucket name, and the path
+  # separator is a forward slash, like layout/theme.liquid
+  # or assets/bg-body.gif.
+  #
+  # Initialize with a key:
+  #   asset = ShopifyAPI::Asset.new(:key => 'assets/special.css', :theme_id => 12345)
+  # 
+  # Find by key:
+  #   asset = ShopifyAPI::Asset.find('assets/image.png', :params => {:theme_id => 12345})
+  # 
+  # Get the text or binary value:
+  #   asset.value # decodes from attachment attribute if necessary
+  # 
+  # You can provide new data for assets in a few different ways:
+  # 
+  #   * assign text data for the value directly:
+  #       asset.value = "div.special {color:red;}"
+  #     
+  #   * provide binary data for the value:
+  #       asset.attach(File.read('image.png'))
+  #     
+  #   * set a URL from which Shopify will fetch the value:
+  #       asset.src = "http://mysite.com/image.png"
+  #     
+  #   * set a source key of another of your assets from which
+  #     the value will be copied:
+  #       asset.source_key = "assets/another_image.png"
+  class Asset < Base
+    self.primary_key = 'key'
+    self.prefix = "/admin/themes/:theme_id/"
+    
+    def self.prefix(options={})
+      options[:theme_id].nil? ? "/admin/" : "/admin/themes/#{options[:theme_id]}/"
+    end
+    
+    def self.element_path(id, prefix_options = {}, query_options = nil) #:nodoc:
+      prefix_options, query_options = split_options(prefix_options) if query_options.nil?
+      "#{prefix(prefix_options)}#{collection_name}.#{format.extension}#{query_string(query_options)}"
+    end
+    
+    # find an asset by key:
+    #   ShopifyAPI::Asset.find('layout/theme.liquid', :params => {:theme_id => 99})
+    def self.find(*args)
+      if args[0].is_a?(Symbol)
+        super
+      else
+        params = {:asset => {:key => args[0]}}
+        params = params.merge(args[1][:params]) if args[1] && args[1][:params]
+        path_prefix = params[:theme_id] ? "/admin/themes/#{params[:theme_id]}" : "/admin"
+        find(:one, :from => "#{path_prefix}/assets.#{format.extension}", :params => params)
+      end
+    end
+    
+    # For text assets, Shopify returns the data in the 'value' attribute.
+    # For binary assets, the data is base-64-encoded and returned in the
+    # 'attachment' attribute. This accessor returns the data in both cases.
+    def value
+      attributes['value'] ||
+      (attributes['attachment'] ? Base64.decode64(attributes['attachment']) : nil)
+    end
+    
+    def attach(data)
+      self.attachment = Base64.encode64(data)
+    end
+    
+    def destroy
+      connection.delete(element_path(prefix_options.merge(:asset => {:key => key})), self.class.headers)
+    end
+    
+    def new?
+      false
+    end
+    
+    def method_missing(method_symbol, *arguments) #:nodoc:
+      if %w{value= attachment= src= source_key=}.include?(method_symbol)
+        wipe_value_attributes
+      end
+      super
+    end
+    
+    private
+    
+    def wipe_value_attributes
+      %w{value attachment src source_key}.each do |attr|
+        attributes.delete(attr)
+      end
+    end
+  end
+end

+ 4 - 0
lib/shopify_api/billing_address.rb

@@ -0,0 +1,4 @@
+module ShopifyAPI
+  class BillingAddress < Base
+  end         
+end

+ 7 - 0
lib/shopify_api/blog.rb

@@ -0,0 +1,7 @@
+module ShopifyAPI
+  class Blog < Base
+    def articles
+      Article.find(:all, :params => { :blog_id => id })
+    end
+  end
+end

+ 5 - 0
lib/shopify_api/collect.rb

@@ -0,0 +1,5 @@
+module ShopifyAPI
+  # For adding/removing products from custom collections
+  class Collect < Base
+  end
+end

+ 14 - 0
lib/shopify_api/comment.rb

@@ -0,0 +1,14 @@
+module ShopifyAPI
+  class Comment < Base
+    def remove;   load_attributes_from_response(post(:remove, {}, only_id)); end
+    def ham;      load_attributes_from_response(post(:ham, {}, only_id)); end
+    def spam;     load_attributes_from_response(post(:spam, {}, only_id)); end
+    def approve;  load_attributes_from_response(post(:approve, {}, only_id)); end
+    def restore;  load_attributes_from_response(post(:restore, {}, only_id)); end
+    def not_spam; load_attributes_from_response(post(:not_spam, {}, only_id)); end
+
+    def only_id
+      encode(:only => :id)
+    end
+  end
+end

+ 4 - 0
lib/shopify_api/country.rb

@@ -0,0 +1,4 @@
+module ShopifyAPI
+  class Country < Base
+  end
+end

+ 18 - 0
lib/shopify_api/custom_collection.rb

@@ -0,0 +1,18 @@
+module ShopifyAPI
+  # Custom collection
+  #
+  class CustomCollection < 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                                                                 
+end

+ 4 - 0
lib/shopify_api/customer.rb

@@ -0,0 +1,4 @@
+module ShopifyAPI
+  class Customer < Base
+  end
+end

+ 4 - 0
lib/shopify_api/customer_group.rb

@@ -0,0 +1,4 @@
+module ShopifyAPI
+  class CustomerGroup < Base
+  end
+end

+ 16 - 0
lib/shopify_api/event.rb

@@ -0,0 +1,16 @@
+module ShopifyAPI
+  class Event < Base
+    self.prefix = "/admin/:resource/:resource_id/"
+    
+    # Hack to allow both Shop and other Events in through the same AR class
+    def self.prefix(options={})
+      options[:resource].nil? ? "/admin/" : "/admin/#{options[:resource]}/#{options[:resource_id]}/"
+    end
+  end
+  
+  module Events
+    def events
+      Event.find(:all, :params => {:resource => self.class.collection_name, :resource_id => id})
+    end
+  end
+end

+ 5 - 0
lib/shopify_api/fulfillment.rb

@@ -0,0 +1,5 @@
+module ShopifyAPI
+  class Fulfillment < Base
+    self.prefix = "/admin/orders/:order_id/"
+  end
+end

+ 16 - 0
lib/shopify_api/image.rb

@@ -0,0 +1,16 @@
+module ShopifyAPI
+  class Image < Base
+    self.prefix = "/admin/products/:product_id/"
+    
+    # generate a method for each possible image variant
+    [:pico, :icon, :thumb, :small, :compact, :medium, :large, :grande, :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
+end

+ 4 - 0
lib/shopify_api/line_item.rb

@@ -0,0 +1,4 @@
+module ShopifyAPI
+  class LineItem < Base 
+  end
+end

+ 32 - 0
lib/shopify_api/metafield.rb

@@ -0,0 +1,32 @@
+module ShopifyAPI
+  module Metafields
+    def metafields
+      Metafield.find(:all, :params => {:resource => self.class.collection_name, :resource_id => id})
+    end
+    
+    def add_metafield(metafield)
+      raise ArgumentError, "You can only add metafields to resource that has been saved" if new?
+      
+      metafield.prefix_options = {
+        :resource => self.class.collection_name,
+        :resource_id => id
+      }
+      metafield.save
+      metafield
+    end
+  end
+  
+  class Metafield < Base
+    self.prefix = "/admin/:resource/:resource_id/"
+    
+    # Hack to allow both Shop and other Metafields in through the same AR class
+    def self.prefix(options={})
+      options[:resource].nil? ? "/admin/" : "/admin/#{options[:resource]}/#{options[:resource_id]}/"
+    end
+            
+    def value
+      return if attributes["value"].nil?
+      attributes["value_type"] == "integer" ? attributes["value"].to_i : attributes["value"]
+    end
+  end
+end

+ 4 - 0
lib/shopify_api/note_attribute.rb

@@ -0,0 +1,4 @@
+module ShopifyAPI
+  class NoteAttribute < Base
+  end
+end

+ 22 - 0
lib/shopify_api/order.rb

@@ -0,0 +1,22 @@
+module ShopifyAPI
+  class Order < Base
+    def close;  load_attributes_from_response(post(:close, {}, only_id)); end
+    def open;   load_attributes_from_response(post(:open, {}, only_id)); end
+
+    def cancel(options = {})
+      load_attributes_from_response(post(:cancel, options, only_id))
+    end
+
+    def transactions
+      Transaction.find(:all, :params => { :order_id => id })
+    end
+
+    def capture(amount = "")
+      Transaction.create(:amount => amount, :kind => "capture", :order_id => id)
+    end
+
+    def only_id
+      encode(:only => :id, :include => [], :methods => [], :fields => [])
+    end
+  end
+end

+ 4 - 0
lib/shopify_api/page.rb

@@ -0,0 +1,4 @@
+module ShopifyAPI
+  class Page < Base
+  end
+end

+ 36 - 0
lib/shopify_api/product.rb

@@ -0,0 +1,36 @@
+module ShopifyAPI
+  class Product < 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
+end

+ 4 - 0
lib/shopify_api/product_search_engine.rb

@@ -0,0 +1,4 @@
+module ShopifyAPI
+  class ProductSearchEngine < Base
+  end
+end

+ 5 - 0
lib/shopify_api/province.rb

@@ -0,0 +1,5 @@
+module ShopifyAPI
+  class Province < Base
+    self.prefix = "/admin/countries/:country_id/"
+  end
+end

+ 17 - 0
lib/shopify_api/recurring_application_charge.rb

@@ -0,0 +1,17 @@
+module ShopifyAPI
+  class RecurringApplicationCharge < Base
+    undef_method :test
+
+    def self.current
+      find(:all).find{|charge| charge.status == 'active'}
+    end
+    
+    def cancel
+      load_attributes_from_response(self.destroy)
+    end
+    
+    def activate
+      load_attributes_from_response(post(:activate))
+    end
+  end
+end

+ 4 - 0
lib/shopify_api/redirect.rb

@@ -0,0 +1,4 @@
+module ShopifyAPI
+  class Redirect < Base
+  end
+end

+ 4 - 0
lib/shopify_api/script_tag.rb

@@ -0,0 +1,4 @@
+module ShopifyAPI
+  class ScriptTag < Base
+  end
+end

+ 148 - 0
lib/shopify_api/session.rb

@@ -0,0 +1,148 @@
+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:
+  # 
+  #    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?api_key=<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 Shopify::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, params = nil)
+      self.url, self.token = url, token
+
+      if params
+        unless self.class.validate_signature(params) && params[:timestamp].to_i > 24.hours.ago.utc.to_i
+          raise "Invalid Signature: Possible malicious login" 
+        end
+      end
+
+      self.class.prepare_url(self.url)
+    end
+    
+    def shop
+      Shop.current
+    end
+    
+    def create_permission_url
+      return nil if url.blank? || api_key.blank?
+      "http://#{url}/admin/api/auth?api_key=#{api_key}"
+    end
+
+    # Used by ActiveResource::Base to make all non-authentication API calls
+    # 
+    # (ShopifyAPI::Base.site set in ShopifyLoginProtection#shopify_session)
+    def site
+      "#{protocol}://#{api_key}:#{computed_password}@#{url}/admin"
+    end
+
+    def valid?
+      url.present? && token.present?
+    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
+    
+    def self.prepare_url(url)
+      return nil if url.blank?
+      url.gsub!(/https?:\/\//, '')                            # remove http:// or https://
+      url.concat(".myshopify.com") unless url.include?('.')   # extend url to myshopify.com if no host is given
+    end
+    
+    def self.validate_signature(params)
+      return false unless signature = params[:signature]
+
+      sorted_params = params.except(:signature, :action, :controller).collect{|k,v|"#{k}=#{v}"}.sort.join
+      Digest::MD5.hexdigest(secret + sorted_params) == signature
+    end
+  end
+end

+ 4 - 0
lib/shopify_api/shipping_address.rb

@@ -0,0 +1,4 @@
+module ShopifyAPI
+  class ShippingAddress < Base
+  end
+end

+ 4 - 0
lib/shopify_api/shipping_line.rb

@@ -0,0 +1,4 @@
+module ShopifyAPI
+  class ShippingLine < Base
+  end  
+end

+ 23 - 0
lib/shopify_api/shop.rb

@@ -0,0 +1,23 @@
+module ShopifyAPI
+  # Shop object. Use Shop.current to receive 
+  # the shop.
+  class Shop < Base
+    def self.current
+      find(:one, :from => "/admin/shop.#{format.extension}")
+    end
+    
+    def metafields
+      Metafield.find(:all)
+    end
+    
+    def add_metafield(metafield)
+      raise ArgumentError, "You can only add metafields to resource that has been saved" if new?      
+      metafield.save
+      metafield
+    end
+    
+    def events
+      Event.find(:all)
+    end
+  end               
+end

+ 7 - 0
lib/shopify_api/smart_collection.rb

@@ -0,0 +1,7 @@
+module ShopifyAPI
+  class SmartCollection < Base
+    def products
+      Product.find(:all, :params => {:collection_id => self.id})
+    end
+  end
+end

+ 4 - 0
lib/shopify_api/theme.rb

@@ -0,0 +1,4 @@
+module ShopifyAPI
+  class Theme < Base
+  end
+end

+ 5 - 0
lib/shopify_api/transaction.rb

@@ -0,0 +1,5 @@
+module ShopifyAPI
+  class Transaction < Base
+    self.prefix = "/admin/orders/:order_id/"
+  end
+end

+ 9 - 0
lib/shopify_api/variant.rb

@@ -0,0 +1,9 @@
+module ShopifyAPI
+  class Variant < Base
+    self.prefix = "/admin/products/:product_id/"
+    
+    def self.prefix(options={})
+      options[:product_id].nil? ? "/admin/" : "/admin/products/#{options[:product_id]}/"
+    end
+  end
+end

+ 4 - 0
lib/shopify_api/webhook.rb

@@ -0,0 +1,4 @@
+module ShopifyAPI
+  class Webhook < Base
+  end
+end