th_shopify_api 1.2.6.pre
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/.document +5 -0
- data/.gitignore +5 -0
- data/CHANGELOG +57 -0
- data/LICENSE +20 -0
- data/README.rdoc +60 -0
- data/RELEASING +16 -0
- data/Rakefile +41 -0
- data/bin/shopify +4 -0
- data/lib/active_resource/connection_ext.rb +16 -0
- data/lib/shopify_api.rb +18 -0
- data/lib/shopify_api/countable.rb +7 -0
- data/lib/shopify_api/events.rb +7 -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 +5 -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/cli.rb +161 -0
- data/lib/shopify_api/resources/collect.rb +5 -0
- data/lib/shopify_api/resources/comment.rb +13 -0
- data/lib/shopify_api/resources/countable.rb +7 -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 +166 -0
- data/shopify_api.gemspec +35 -0
- data/test/cli_test.rb +109 -0
- data/test/limits_test.rb +37 -0
- data/test/order_test.rb +48 -0
- data/test/shopify_api_test.rb +55 -0
- data/test/test_helper.rb +29 -0
- metadata +153 -0
@@ -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,166 @@
|
|
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
|
+
return true if defined?(Rails) && Rails.env.test?
|
117
|
+
|
118
|
+
sorted_params = params.except(:signature, :action, :controller).collect{|k,v|"#{k}=#{v}"}.sort.join
|
119
|
+
Digest::MD5.hexdigest(secret + sorted_params) == signature
|
120
|
+
end
|
121
|
+
|
122
|
+
end
|
123
|
+
|
124
|
+
def initialize(url, token = nil, params = nil)
|
125
|
+
self.url, self.token = url, token
|
126
|
+
|
127
|
+
if params
|
128
|
+
unless self.class.validate_signature(params) && params[:timestamp].to_i > 24.hours.ago.utc.to_i
|
129
|
+
raise "Invalid Signature: Possible malicious login"
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
self.class.prepare_url(self.url)
|
134
|
+
end
|
135
|
+
|
136
|
+
def shop
|
137
|
+
Shop.current
|
138
|
+
end
|
139
|
+
|
140
|
+
def create_permission_url
|
141
|
+
return nil if url.blank? || api_key.blank?
|
142
|
+
"http://#{url}/admin/api/auth?api_key=#{api_key}"
|
143
|
+
end
|
144
|
+
|
145
|
+
# Used by ActiveResource::Base to make all non-authentication API calls
|
146
|
+
#
|
147
|
+
# (ShopifyAPI::Base.site set in ShopifyLoginProtection#shopify_session)
|
148
|
+
def site
|
149
|
+
"#{protocol}://#{api_key}:#{computed_password}@#{url}/admin"
|
150
|
+
end
|
151
|
+
|
152
|
+
def valid?
|
153
|
+
url.present? && token.present?
|
154
|
+
end
|
155
|
+
|
156
|
+
private
|
157
|
+
|
158
|
+
# The secret is computed by taking the shared_secret which we got when
|
159
|
+
# registring this third party application and concating the request_to it,
|
160
|
+
# and then calculating a MD5 hexdigest.
|
161
|
+
def computed_password
|
162
|
+
Digest::MD5.hexdigest(secret + token.to_s)
|
163
|
+
end
|
164
|
+
|
165
|
+
end
|
166
|
+
end
|
data/shopify_api.gemspec
ADDED
@@ -0,0 +1,35 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
$:.push File.expand_path("../lib", __FILE__)
|
3
|
+
|
4
|
+
Gem::Specification.new do |s|
|
5
|
+
s.name = "th_shopify_api"
|
6
|
+
s.version = "1.2.6.pre"
|
7
|
+
s.authors = ["TravisHaynes", "Shopify"]
|
8
|
+
s.email = ["travis.j.haynes@gmail.com", "developers@jadedpixel.com"]
|
9
|
+
s.homepage = %q{http://www.shopify.com/partners/apps}
|
10
|
+
s.summary = %q{The Shopify API gem is a lightweight gem for accessing the Shopify admin REST web services}
|
11
|
+
s.description = %q{The Shopify API gem allows Ruby developers to programmatically access the admin section of Shopify stores. The 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.}
|
12
|
+
|
13
|
+
s.extra_rdoc_files = [
|
14
|
+
"LICENSE",
|
15
|
+
"README.rdoc"
|
16
|
+
]
|
17
|
+
|
18
|
+
s.files = `git ls-files`.split("\n")
|
19
|
+
s.test_files = `git ls-files -- {test}/*`.split("\n")
|
20
|
+
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
21
|
+
s.require_paths = ["lib"]
|
22
|
+
|
23
|
+
s.rdoc_options = ["--charset=UTF-8"]
|
24
|
+
s.license = 'MIT'
|
25
|
+
|
26
|
+
s.add_dependency("activeresource", [">= 2.2.2"])
|
27
|
+
s.add_dependency("thor", [">= 0.14.4"])
|
28
|
+
|
29
|
+
if s.respond_to?(:add_development_dependency)
|
30
|
+
s.add_development_dependency("mocha", ">= 0.9.8")
|
31
|
+
else
|
32
|
+
s.add_dependency("mocha", ">= 0.9.8")
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
data/test/cli_test.rb
ADDED
@@ -0,0 +1,109 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
require 'shopify_api/cli'
|
3
|
+
require 'fileutils'
|
4
|
+
|
5
|
+
class CliTest < Test::Unit::TestCase
|
6
|
+
def setup
|
7
|
+
@test_home = File.join(File.expand_path(File.dirname(__FILE__)), 'files', 'home')
|
8
|
+
@shop_config_dir = File.join(@test_home, '.shopify', 'shops')
|
9
|
+
@default_symlink = File.join(@shop_config_dir, 'default')
|
10
|
+
`rm -rf #{@test_home}`
|
11
|
+
ENV['HOME'] = @test_home
|
12
|
+
@cli = ShopifyAPI::Cli.new
|
13
|
+
|
14
|
+
FileUtils.mkdir_p(@shop_config_dir)
|
15
|
+
File.open(config_file('foo'), 'w') do |file|
|
16
|
+
file.puts valid_options.merge('domain' => 'foo.myshopify.com').to_yaml
|
17
|
+
end
|
18
|
+
File.symlink(config_file('foo'), @default_symlink)
|
19
|
+
File.open(config_file('bar'), 'w') do |file|
|
20
|
+
file.puts valid_options.merge('domain' => 'bar.myshopify.com').to_yaml
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def teardown
|
25
|
+
`rm -rf #{@test_home}`
|
26
|
+
end
|
27
|
+
|
28
|
+
test "add with blank domain" do
|
29
|
+
`rm -rf #{@shop_config_dir}/*`
|
30
|
+
$stdout.expects(:print).with("Domain? (leave blank for foo.myshopify.com) ")
|
31
|
+
$stdout.expects(:print).with("API key? ")
|
32
|
+
$stdout.expects(:print).with("Password? ")
|
33
|
+
$stdin.expects(:gets).times(3).returns("", "key", "pass")
|
34
|
+
@cli.expects(:puts).with("\nopen https://foo.myshopify.com/admin/api in your browser to get API credentials\n")
|
35
|
+
@cli.expects(:puts).with("Default connection is foo")
|
36
|
+
|
37
|
+
@cli.add('foo')
|
38
|
+
|
39
|
+
config = YAML.load(File.read(config_file('foo')))
|
40
|
+
assert_equal 'foo.myshopify.com', config['domain']
|
41
|
+
assert_equal 'key', config['api_key']
|
42
|
+
assert_equal 'pass', config['password']
|
43
|
+
assert_equal 'https', config['protocol']
|
44
|
+
assert_equal config_file('foo'), File.readlink(@default_symlink)
|
45
|
+
end
|
46
|
+
|
47
|
+
test "add with explicit domain" do
|
48
|
+
`rm -rf #{@shop_config_dir}/*`
|
49
|
+
$stdout.expects(:print).with("Domain? (leave blank for foo.myshopify.com) ")
|
50
|
+
$stdout.expects(:print).with("API key? ")
|
51
|
+
$stdout.expects(:print).with("Password? ")
|
52
|
+
$stdin.expects(:gets).times(3).returns("bar.myshopify.com", "key", "pass")
|
53
|
+
@cli.expects(:puts).with("\nopen https://bar.myshopify.com/admin/api in your browser to get API credentials\n")
|
54
|
+
@cli.expects(:puts).with("Default connection is foo")
|
55
|
+
|
56
|
+
@cli.add('foo')
|
57
|
+
|
58
|
+
config = YAML.load(File.read(config_file('foo')))
|
59
|
+
assert_equal 'bar.myshopify.com', config['domain']
|
60
|
+
end
|
61
|
+
|
62
|
+
test "list" do
|
63
|
+
@cli.expects(:puts).with(" bar")
|
64
|
+
@cli.expects(:puts).with(" * foo")
|
65
|
+
|
66
|
+
@cli.list
|
67
|
+
end
|
68
|
+
|
69
|
+
test "show default" do
|
70
|
+
@cli.expects(:puts).with("Default connection is foo")
|
71
|
+
|
72
|
+
@cli.default
|
73
|
+
end
|
74
|
+
|
75
|
+
test "set default" do
|
76
|
+
@cli.expects(:puts).with("Default connection is bar")
|
77
|
+
|
78
|
+
@cli.default('bar')
|
79
|
+
|
80
|
+
assert_equal config_file('bar'), File.readlink(@default_symlink)
|
81
|
+
end
|
82
|
+
|
83
|
+
test "remove default connection" do
|
84
|
+
@cli.remove('foo')
|
85
|
+
|
86
|
+
assert !File.exist?(@default_symlink)
|
87
|
+
assert !File.exist?(config_file('foo'))
|
88
|
+
assert File.exist?(config_file('bar'))
|
89
|
+
end
|
90
|
+
|
91
|
+
test "remove non-default connection" do
|
92
|
+
@cli.remove('bar')
|
93
|
+
|
94
|
+
assert_equal config_file('foo'), File.readlink(@default_symlink)
|
95
|
+
assert File.exist?(config_file('foo'))
|
96
|
+
assert !File.exist?(config_file('bar'))
|
97
|
+
end
|
98
|
+
|
99
|
+
private
|
100
|
+
|
101
|
+
def valid_options
|
102
|
+
{'domain' => 'snowdevil.myshopify.com', 'api_key' => 'key', 'password' => 'pass', 'protocol' => 'https'}
|
103
|
+
end
|
104
|
+
|
105
|
+
def config_file(connection)
|
106
|
+
File.join(@shop_config_dir, "#{connection}.yml")
|
107
|
+
end
|
108
|
+
|
109
|
+
end
|
data/test/limits_test.rb
ADDED
@@ -0,0 +1,37 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
require 'mocha'
|
3
|
+
|
4
|
+
class LimitsTest < Test::Unit::TestCase
|
5
|
+
def setup
|
6
|
+
ShopifyAPI::Base.site = "test.myshopify.com"
|
7
|
+
@header_hash = {'http_x_shopify_api_call_limit' => '150/3000',
|
8
|
+
'http_x_shopify_shop_api_call_limit' => '100/300'}
|
9
|
+
ShopifyAPI::Base.connection.expects(:response).at_least(0).returns(@header_hash)
|
10
|
+
end
|
11
|
+
|
12
|
+
context "Limits" do
|
13
|
+
should "fetch limit total" do
|
14
|
+
assert_equal(299, ShopifyAPI.credit_limit(:shop))
|
15
|
+
assert_equal(2999, ShopifyAPI.credit_limit(:global))
|
16
|
+
end
|
17
|
+
|
18
|
+
should "fetch used calls" do
|
19
|
+
assert_equal(100, ShopifyAPI.credit_used(:shop))
|
20
|
+
assert_equal(150, ShopifyAPI.credit_used(:global))
|
21
|
+
end
|
22
|
+
|
23
|
+
should "calculate remaining calls" do
|
24
|
+
assert_equal(199, ShopifyAPI.credit_left)
|
25
|
+
end
|
26
|
+
|
27
|
+
should "flag maxed out credits" do
|
28
|
+
assert !ShopifyAPI.maxed?
|
29
|
+
@header_hash = {'http_x_shopify_api_call_limit' => '2999/3000',
|
30
|
+
'http_x_shopify_shop_api_call_limit' => '299/300'}
|
31
|
+
ShopifyAPI::Base.connection.expects(:response).at_least(1).returns(@header_hash)
|
32
|
+
assert ShopifyAPI.maxed?
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
|
37
|
+
end
|