shopify_api 1.2.5 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/CHANGELOG +10 -0
- data/README.rdoc +6 -1
- data/RELEASING +16 -0
- data/lib/active_resource/connection_ext.rb +16 -0
- data/lib/shopify_api.rb +12 -533
- data/lib/shopify_api/cli.rb +9 -9
- data/lib/shopify_api/countable.rb +7 -0
- data/lib/shopify_api/events.rb +7 -0
- data/lib/shopify_api/json_format.rb +23 -0
- data/lib/shopify_api/limits.rb +76 -0
- data/lib/shopify_api/metafields.rb +18 -0
- data/lib/shopify_api/resources.rb +40 -0
- data/lib/shopify_api/resources/address.rb +4 -0
- data/lib/shopify_api/resources/application_charge.rb +9 -0
- data/lib/shopify_api/resources/article.rb +12 -0
- data/lib/shopify_api/resources/asset.rb +95 -0
- data/lib/shopify_api/resources/base.rb +6 -0
- data/lib/shopify_api/resources/billing_address.rb +4 -0
- data/lib/shopify_api/resources/blog.rb +10 -0
- data/lib/shopify_api/resources/collect.rb +5 -0
- data/lib/shopify_api/resources/comment.rb +13 -0
- data/lib/shopify_api/resources/country.rb +4 -0
- data/lib/shopify_api/resources/custom_collection.rb +19 -0
- data/lib/shopify_api/resources/customer.rb +4 -0
- data/lib/shopify_api/resources/customer_group.rb +4 -0
- data/lib/shopify_api/resources/event.rb +10 -0
- data/lib/shopify_api/resources/fulfillment.rb +5 -0
- data/lib/shopify_api/resources/image.rb +16 -0
- data/lib/shopify_api/resources/line_item.rb +4 -0
- data/lib/shopify_api/resources/metafield.rb +15 -0
- data/lib/shopify_api/resources/note_attribute.rb +4 -0
- data/lib/shopify_api/resources/option.rb +4 -0
- data/lib/shopify_api/resources/order.rb +25 -0
- data/lib/shopify_api/resources/page.rb +6 -0
- data/lib/shopify_api/resources/payment_details.rb +4 -0
- data/lib/shopify_api/resources/product.rb +33 -0
- data/lib/shopify_api/resources/product_search_engine.rb +4 -0
- data/lib/shopify_api/resources/province.rb +5 -0
- data/lib/shopify_api/resources/receipt.rb +4 -0
- data/lib/shopify_api/resources/recurring_application_charge.rb +23 -0
- data/lib/shopify_api/resources/redirect.rb +4 -0
- data/lib/shopify_api/resources/rule.rb +4 -0
- data/lib/shopify_api/resources/script_tag.rb +4 -0
- data/lib/shopify_api/resources/shipping_address.rb +4 -0
- data/lib/shopify_api/resources/shipping_line.rb +4 -0
- data/lib/shopify_api/resources/shop.rb +23 -0
- data/lib/shopify_api/resources/smart_collection.rb +10 -0
- data/lib/shopify_api/resources/tax_line.rb +4 -0
- data/lib/shopify_api/resources/theme.rb +4 -0
- data/lib/shopify_api/resources/transaction.rb +5 -0
- data/lib/shopify_api/resources/variant.rb +11 -0
- data/lib/shopify_api/resources/webhook.rb +4 -0
- data/lib/shopify_api/session.rb +165 -0
- data/shopify_api.gemspec +13 -92
- data/test/cli_test.rb +109 -0
- data/test/limits_test.rb +37 -0
- data/test/shopify_api_test.rb +13 -1
- metadata +76 -82
@@ -0,0 +1,10 @@
|
|
1
|
+
module ShopifyAPI
|
2
|
+
class Event < Base
|
3
|
+
self.prefix = "/admin/:resource/:resource_id/"
|
4
|
+
|
5
|
+
# Hack to allow both Shop and other Events in through the same AR class
|
6
|
+
def self.prefix(options={})
|
7
|
+
options[:resource].nil? ? "/admin/" : "/admin/#{options[:resource]}/#{options[:resource_id]}/"
|
8
|
+
end
|
9
|
+
end
|
10
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
module ShopifyAPI
|
2
|
+
class Image < Base
|
3
|
+
self.prefix = "/admin/products/:product_id/"
|
4
|
+
|
5
|
+
# generate a method for each possible image variant
|
6
|
+
[:pico, :icon, :thumb, :small, :compact, :medium, :large, :grande, :original].each do |m|
|
7
|
+
reg_exp_match = "/\\1_#{m}.\\2"
|
8
|
+
define_method(m) { src.gsub(/\/(.*)\.(\w{2,4})/, reg_exp_match) }
|
9
|
+
end
|
10
|
+
|
11
|
+
def attach_image(data, filename = nil)
|
12
|
+
attributes['attachment'] = Base64.encode64(data)
|
13
|
+
attributes['filename'] = filename unless filename.nil?
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
module ShopifyAPI
|
2
|
+
class Metafield < Base
|
3
|
+
self.prefix = "/admin/:resource/:resource_id/"
|
4
|
+
|
5
|
+
# Hack to allow both Shop and other Metafields in through the same AR class
|
6
|
+
def self.prefix(options={})
|
7
|
+
options[:resource].nil? ? "/admin/" : "/admin/#{options[:resource]}/#{options[:resource_id]}/"
|
8
|
+
end
|
9
|
+
|
10
|
+
def value
|
11
|
+
return if attributes["value"].nil?
|
12
|
+
attributes["value_type"] == "integer" ? attributes["value"].to_i : attributes["value"]
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
module ShopifyAPI
|
2
|
+
class Order < Base
|
3
|
+
include Events
|
4
|
+
include Metafields
|
5
|
+
|
6
|
+
def close; load_attributes_from_response(post(:close, {}, only_id)); end
|
7
|
+
def open; load_attributes_from_response(post(:open, {}, only_id)); end
|
8
|
+
|
9
|
+
def cancel(options = {})
|
10
|
+
load_attributes_from_response(post(:cancel, options, only_id))
|
11
|
+
end
|
12
|
+
|
13
|
+
def transactions
|
14
|
+
Transaction.find(:all, :params => { :order_id => id })
|
15
|
+
end
|
16
|
+
|
17
|
+
def capture(amount = "")
|
18
|
+
Transaction.create(:amount => amount, :kind => "capture", :order_id => id)
|
19
|
+
end
|
20
|
+
|
21
|
+
def only_id
|
22
|
+
encode(:only => :id, :include => [], :methods => [], :fields => [])
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
module ShopifyAPI
|
2
|
+
class Product < Base
|
3
|
+
include Events
|
4
|
+
include Metafields
|
5
|
+
|
6
|
+
# compute the price range
|
7
|
+
def price_range
|
8
|
+
prices = variants.collect(&:price)
|
9
|
+
format = "%0.2f"
|
10
|
+
if prices.min != prices.max
|
11
|
+
"#{format % prices.min} - #{format % prices.max}"
|
12
|
+
else
|
13
|
+
format % prices.min
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
def collections
|
18
|
+
CustomCollection.find(:all, :params => {:product_id => self.id})
|
19
|
+
end
|
20
|
+
|
21
|
+
def smart_collections
|
22
|
+
SmartCollection.find(:all, :params => {:product_id => self.id})
|
23
|
+
end
|
24
|
+
|
25
|
+
def add_to_collection(collection)
|
26
|
+
collection.add_product(self)
|
27
|
+
end
|
28
|
+
|
29
|
+
def remove_from_collection(collection)
|
30
|
+
collection.remove_product(self)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
module ShopifyAPI
|
2
|
+
class RecurringApplicationCharge < Base
|
3
|
+
undef_method :test
|
4
|
+
|
5
|
+
class << self
|
6
|
+
def current
|
7
|
+
all.find { |c| c.status == 'active' }
|
8
|
+
end
|
9
|
+
|
10
|
+
[:pending, :cancelled, :accepted, :declined].each do |status|
|
11
|
+
define_method(status) { all.select { |c| c.status == status.to_s } }
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
def cancel
|
16
|
+
load_attributes_from_response(self.destroy)
|
17
|
+
end
|
18
|
+
|
19
|
+
def activate
|
20
|
+
load_attributes_from_response(post(:activate))
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
module ShopifyAPI
|
2
|
+
# Shop object. Use Shop.current to receive
|
3
|
+
# the shop.
|
4
|
+
class Shop < Base
|
5
|
+
def self.current
|
6
|
+
find(:one, :from => "/admin/shop.#{format.extension}")
|
7
|
+
end
|
8
|
+
|
9
|
+
def metafields
|
10
|
+
Metafield.find(:all)
|
11
|
+
end
|
12
|
+
|
13
|
+
def add_metafield(metafield)
|
14
|
+
raise ArgumentError, "You can only add metafields to resource that has been saved" if new?
|
15
|
+
metafield.save
|
16
|
+
metafield
|
17
|
+
end
|
18
|
+
|
19
|
+
def events
|
20
|
+
Event.find(:all)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,165 @@
|
|
1
|
+
module ShopifyAPI
|
2
|
+
#
|
3
|
+
# The Shopify API authenticates each call via HTTP Authentication, using
|
4
|
+
# * the application's API key as the username, and
|
5
|
+
# * a hex digest of the application's shared secret and an
|
6
|
+
# authentication token as the password.
|
7
|
+
#
|
8
|
+
# Generation & acquisition of the beforementioned looks like this:
|
9
|
+
#
|
10
|
+
# 0. Developer (that's you) registers Application (and provides a
|
11
|
+
# callback url) and receives an API key and a shared secret
|
12
|
+
#
|
13
|
+
# 1. User visits Application and are told they need to authenticate the
|
14
|
+
# application first for read/write permission to their data (needs to
|
15
|
+
# happen only once). User is asked for their shop url.
|
16
|
+
#
|
17
|
+
# 2. Application redirects to Shopify : GET <user's shop url>/admin/api/auth?api_key=<API key>
|
18
|
+
# (See Session#create_permission_url)
|
19
|
+
#
|
20
|
+
# 3. User logs-in to Shopify, approves application permission request
|
21
|
+
#
|
22
|
+
# 4. Shopify redirects to the Application's callback url (provided during
|
23
|
+
# registration), including the shop's name, and an authentication token in the parameters:
|
24
|
+
# GET client.com/customers?shop=snake-oil.myshopify.com&t=a94a110d86d2452eb3e2af4cfb8a3828
|
25
|
+
#
|
26
|
+
# 5. Authentication password computed using the shared secret and the
|
27
|
+
# authentication token (see Session#computed_password)
|
28
|
+
#
|
29
|
+
# 6. Profit!
|
30
|
+
# (API calls can now authenticate through HTTP using the API key, and
|
31
|
+
# computed password)
|
32
|
+
#
|
33
|
+
# LoginController and ShopifyLoginProtection use the Session class to set Shopify::Base.site
|
34
|
+
# so that all API calls are authorized transparently and end up just looking like this:
|
35
|
+
#
|
36
|
+
# # get 3 products
|
37
|
+
# @products = ShopifyAPI::Product.find(:all, :params => {:limit => 3})
|
38
|
+
#
|
39
|
+
# # get latest 3 orders
|
40
|
+
# @orders = ShopifyAPI::Order.find(:all, :params => {:limit => 3, :order => "created_at DESC" })
|
41
|
+
#
|
42
|
+
# As an example of what your LoginController should look like, take a look
|
43
|
+
# at the following:
|
44
|
+
#
|
45
|
+
# class LoginController < ApplicationController
|
46
|
+
# def index
|
47
|
+
# # Ask user for their #{shop}.myshopify.com address
|
48
|
+
# end
|
49
|
+
#
|
50
|
+
# def authenticate
|
51
|
+
# redirect_to ShopifyAPI::Session.new(params[:shop]).create_permission_url
|
52
|
+
# end
|
53
|
+
#
|
54
|
+
# # Shopify redirects the logged-in user back to this action along with
|
55
|
+
# # the authorization token t.
|
56
|
+
# #
|
57
|
+
# # This token is later combined with the developer's shared secret to form
|
58
|
+
# # the password used to call API methods.
|
59
|
+
# def finalize
|
60
|
+
# shopify_session = ShopifyAPI::Session.new(params[:shop], params[:t])
|
61
|
+
# if shopify_session.valid?
|
62
|
+
# session[:shopify] = shopify_session
|
63
|
+
# flash[:notice] = "Logged in to shopify store."
|
64
|
+
#
|
65
|
+
# return_address = session[:return_to] || '/home'
|
66
|
+
# session[:return_to] = nil
|
67
|
+
# redirect_to return_address
|
68
|
+
# else
|
69
|
+
# flash[:error] = "Could not log in to Shopify store."
|
70
|
+
# redirect_to :action => 'index'
|
71
|
+
# end
|
72
|
+
# end
|
73
|
+
#
|
74
|
+
# def logout
|
75
|
+
# session[:shopify] = nil
|
76
|
+
# flash[:notice] = "Successfully logged out."
|
77
|
+
#
|
78
|
+
# redirect_to :action => 'index'
|
79
|
+
# end
|
80
|
+
# end
|
81
|
+
#
|
82
|
+
class Session
|
83
|
+
cattr_accessor :api_key
|
84
|
+
cattr_accessor :secret
|
85
|
+
cattr_accessor :protocol
|
86
|
+
self.protocol = 'https'
|
87
|
+
|
88
|
+
attr_accessor :url, :token, :name
|
89
|
+
|
90
|
+
class << self
|
91
|
+
|
92
|
+
def setup(params)
|
93
|
+
params.each { |k,value| send("#{k}=", value) }
|
94
|
+
end
|
95
|
+
|
96
|
+
def temp(domain, token, &block)
|
97
|
+
session = new(domain, token)
|
98
|
+
|
99
|
+
original_site = ShopifyAPI::Base.site
|
100
|
+
begin
|
101
|
+
ShopifyAPI::Base.site = session.site
|
102
|
+
yield
|
103
|
+
ensure
|
104
|
+
ShopifyAPI::Base.site = original_site
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
def prepare_url(url)
|
109
|
+
return nil if url.blank?
|
110
|
+
url.gsub!(/https?:\/\//, '') # remove http:// or https://
|
111
|
+
url.concat(".myshopify.com") unless url.include?('.') # extend url to myshopify.com if no host is given
|
112
|
+
end
|
113
|
+
|
114
|
+
def validate_signature(params)
|
115
|
+
return false unless signature = params[:signature]
|
116
|
+
|
117
|
+
sorted_params = params.except(:signature, :action, :controller).collect{|k,v|"#{k}=#{v}"}.sort.join
|
118
|
+
Digest::MD5.hexdigest(secret + sorted_params) == signature
|
119
|
+
end
|
120
|
+
|
121
|
+
end
|
122
|
+
|
123
|
+
def initialize(url, token = nil, params = nil)
|
124
|
+
self.url, self.token = url, token
|
125
|
+
|
126
|
+
if params
|
127
|
+
unless self.class.validate_signature(params) && params[:timestamp].to_i > 24.hours.ago.utc.to_i
|
128
|
+
raise "Invalid Signature: Possible malicious login"
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
132
|
+
self.class.prepare_url(self.url)
|
133
|
+
end
|
134
|
+
|
135
|
+
def shop
|
136
|
+
Shop.current
|
137
|
+
end
|
138
|
+
|
139
|
+
def create_permission_url
|
140
|
+
return nil if url.blank? || api_key.blank?
|
141
|
+
"http://#{url}/admin/api/auth?api_key=#{api_key}"
|
142
|
+
end
|
143
|
+
|
144
|
+
# Used by ActiveResource::Base to make all non-authentication API calls
|
145
|
+
#
|
146
|
+
# (ShopifyAPI::Base.site set in ShopifyLoginProtection#shopify_session)
|
147
|
+
def site
|
148
|
+
"#{protocol}://#{api_key}:#{computed_password}@#{url}/admin"
|
149
|
+
end
|
150
|
+
|
151
|
+
def valid?
|
152
|
+
url.present? && token.present?
|
153
|
+
end
|
154
|
+
|
155
|
+
private
|
156
|
+
|
157
|
+
# The secret is computed by taking the shared_secret which we got when
|
158
|
+
# registring this third party application and concating the request_to it,
|
159
|
+
# and then calculating a MD5 hexdigest.
|
160
|
+
def computed_password
|
161
|
+
Digest::MD5.hexdigest(secret + token.to_s)
|
162
|
+
end
|
163
|
+
|
164
|
+
end
|
165
|
+
end
|