Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

Split shopify_api.rb into separate files

  • Loading branch information...
commit fa8f791ce4e24c808b1fef28ad63ab7a1893e7b6 1 parent 4ffdb62
@travishaynes travishaynes authored odorcicd committed
Showing with 558 additions and 520 deletions.
  1. +4 −520 lib/shopify_api.rb
  2. +9 −0 lib/shopify_api/application_charge.rb
  3. +9 −0 lib/shopify_api/article.rb
  4. +95 −0 lib/shopify_api/asset.rb
  5. +4 −0 lib/shopify_api/billing_address.rb
  6. +7 −0 lib/shopify_api/blog.rb
  7. +5 −0 lib/shopify_api/collect.rb
  8. +14 −0 lib/shopify_api/comment.rb
  9. +4 −0 lib/shopify_api/country.rb
  10. +18 −0 lib/shopify_api/custom_collection.rb
  11. +4 −0 lib/shopify_api/customer.rb
  12. +4 −0 lib/shopify_api/customer_group.rb
  13. +16 −0 lib/shopify_api/event.rb
  14. +5 −0 lib/shopify_api/fulfillment.rb
  15. +16 −0 lib/shopify_api/image.rb
  16. +4 −0 lib/shopify_api/line_item.rb
  17. +32 −0 lib/shopify_api/metafield.rb
  18. +4 −0 lib/shopify_api/note_attribute.rb
  19. +22 −0 lib/shopify_api/order.rb
  20. +4 −0 lib/shopify_api/page.rb
  21. +36 −0 lib/shopify_api/product.rb
  22. +4 −0 lib/shopify_api/product_search_engine.rb
  23. +5 −0 lib/shopify_api/province.rb
  24. +17 −0 lib/shopify_api/recurring_application_charge.rb
  25. +4 −0 lib/shopify_api/redirect.rb
  26. +4 −0 lib/shopify_api/script_tag.rb
  27. +148 −0 lib/shopify_api/session.rb
  28. +4 −0 lib/shopify_api/shipping_address.rb
  29. +4 −0 lib/shopify_api/shipping_line.rb
  30. +23 −0 lib/shopify_api/shop.rb
  31. +7 −0 lib/shopify_api/smart_collection.rb
  32. +4 −0 lib/shopify_api/theme.rb
  33. +5 −0 lib/shopify_api/transaction.rb
  34. +9 −0 lib/shopify_api/variant.rb
  35. +4 −0 lib/shopify_api/webhook.rb
View
524 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
View
9 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
View
9 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
View
95 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
View
4 lib/shopify_api/billing_address.rb
@@ -0,0 +1,4 @@
+module ShopifyAPI
+ class BillingAddress < Base
+ end
+end
View
7 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
View
5 lib/shopify_api/collect.rb
@@ -0,0 +1,5 @@
+module ShopifyAPI
+ # For adding/removing products from custom collections
+ class Collect < Base
+ end
+end
View
14 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
View
4 lib/shopify_api/country.rb
@@ -0,0 +1,4 @@
+module ShopifyAPI
+ class Country < Base
+ end
+end
View
18 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
View
4 lib/shopify_api/customer.rb
@@ -0,0 +1,4 @@
+module ShopifyAPI
+ class Customer < Base
+ end
+end
View
4 lib/shopify_api/customer_group.rb
@@ -0,0 +1,4 @@
+module ShopifyAPI
+ class CustomerGroup < Base
+ end
+end
View
16 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
View
5 lib/shopify_api/fulfillment.rb
@@ -0,0 +1,5 @@
+module ShopifyAPI
+ class Fulfillment < Base
+ self.prefix = "/admin/orders/:order_id/"
+ end
+end
View
16 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
View
4 lib/shopify_api/line_item.rb
@@ -0,0 +1,4 @@
+module ShopifyAPI
+ class LineItem < Base
+ end
+end
View
32 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
View
4 lib/shopify_api/note_attribute.rb
@@ -0,0 +1,4 @@
+module ShopifyAPI
+ class NoteAttribute < Base
+ end
+end
View
22 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
View
4 lib/shopify_api/page.rb
@@ -0,0 +1,4 @@
+module ShopifyAPI
+ class Page < Base
+ end
+end
View
36 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
View
4 lib/shopify_api/product_search_engine.rb
@@ -0,0 +1,4 @@
+module ShopifyAPI
+ class ProductSearchEngine < Base
+ end
+end
View
5 lib/shopify_api/province.rb
@@ -0,0 +1,5 @@
+module ShopifyAPI
+ class Province < Base
+ self.prefix = "/admin/countries/:country_id/"
+ end
+end
View
17 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
View
4 lib/shopify_api/redirect.rb
@@ -0,0 +1,4 @@
+module ShopifyAPI
+ class Redirect < Base
+ end
+end
View
4 lib/shopify_api/script_tag.rb
@@ -0,0 +1,4 @@
+module ShopifyAPI
+ class ScriptTag < Base
+ end
+end
View
148 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
View
4 lib/shopify_api/shipping_address.rb
@@ -0,0 +1,4 @@
+module ShopifyAPI
+ class ShippingAddress < Base
+ end
+end
View
4 lib/shopify_api/shipping_line.rb
@@ -0,0 +1,4 @@
+module ShopifyAPI
+ class ShippingLine < Base
+ end
+end
View
23 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
View
7 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
View
4 lib/shopify_api/theme.rb
@@ -0,0 +1,4 @@
+module ShopifyAPI
+ class Theme < Base
+ end
+end
View
5 lib/shopify_api/transaction.rb
@@ -0,0 +1,5 @@
+module ShopifyAPI
+ class Transaction < Base
+ self.prefix = "/admin/orders/:order_id/"
+ end
+end
View
9 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
View
4 lib/shopify_api/webhook.rb
@@ -0,0 +1,4 @@
+module ShopifyAPI
+ class Webhook < Base
+ end
+end
Please sign in to comment.
Something went wrong with that request. Please try again.