shopify_api 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.document +5 -0
- data/.gitignore +5 -0
- data/CHANGELOG +5 -0
- data/LICENSE +20 -0
- data/README.rdoc +55 -0
- data/Rakefile +59 -0
- data/VERSION +1 -0
- data/lib/shopify_api.rb +463 -0
- data/shopify_api.gemspec +53 -0
- data/test/shopify_api_test.rb +21 -0
- data/test/test_helper.rb +28 -0
- metadata +78 -0
data/.document
ADDED
data/CHANGELOG
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2009 "JadedPixel inc."
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.rdoc
ADDED
@@ -0,0 +1,55 @@
|
|
1
|
+
= Shopify API
|
2
|
+
|
3
|
+
The Shopify API gem allows Ruby developers to programmatically access the admin section of Shopify stores.
|
4
|
+
|
5
|
+
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. In other words, we’ve tried to make the API follow the REST principles as much as possible.
|
6
|
+
|
7
|
+
|
8
|
+
== Usage
|
9
|
+
|
10
|
+
=== Requirements
|
11
|
+
|
12
|
+
All API usage happens through Shopify applications, created by either shop owners for their own shops, or by Shopify Partners for use by other shop owners:
|
13
|
+
|
14
|
+
* Shop owners can create applications for themselves through their own admin (under the Preferences > Applications tab).
|
15
|
+
* Shopify Partners create applications through their admin: http://app.shopify.com/services/partners
|
16
|
+
|
17
|
+
For more information and detailed documentation about the API visit http://api.shopify.com
|
18
|
+
|
19
|
+
=== Getting Started
|
20
|
+
|
21
|
+
ShopifyAPI uses ActiveResource to communicate with the REST web service. ActiveResource has to be configured with a fully authorized URL of a particular store first. To obtain that URL you can follow these steps:
|
22
|
+
|
23
|
+
1. First create a new application in either the partners admin or your store admin and write down your API_KEY and SHARED_SECRET.
|
24
|
+
|
25
|
+
2. You will need to supply two parameters to the Session class before you instantiate it:
|
26
|
+
|
27
|
+
ShopifyAPI::Session.setup({:api_key => API_KEY, :secret => SHARED_SECRET})
|
28
|
+
|
29
|
+
3. Create a new Session for a specific shop. That session is not fully valid yet, but it can be used to create a URL that you will redirect your users to:
|
30
|
+
|
31
|
+
session = ShopifyAPI::Session.new("yourshopname.myshopify.com")
|
32
|
+
session.valid? # returns false
|
33
|
+
|
34
|
+
4. To access the API shop owners need a "token" from that specific shop. In order to get this token they need to authorize with that shop first. The URL to redirect your user to can be generated via:
|
35
|
+
|
36
|
+
url = session.create_permission_url
|
37
|
+
|
38
|
+
5. After visiting this URL, the shop redirects the owner to a custom URL of your application where the "token" gets sent to (it's param name is just "t"). Use that token to instantiate a "valid" session, that is ready to make calls to that particular shop.
|
39
|
+
|
40
|
+
token = params[:t]
|
41
|
+
session = ShopifyAPI::Session.new("yourshopname.myshopify.com", token)
|
42
|
+
session.valid? # returns true
|
43
|
+
|
44
|
+
6. Now you can finally get the fully authorized URL for that shop. Use that URL to configure ActiveResource and you're set:
|
45
|
+
|
46
|
+
ActiveResource::Base.site = session.site
|
47
|
+
|
48
|
+
7. Get data from that shop (returns ActiveResource instances):
|
49
|
+
|
50
|
+
shop = ShopifyAPI::Shop.current
|
51
|
+
latest_orders = ShopifyAPI::Order.find(:all)
|
52
|
+
|
53
|
+
== Copyright
|
54
|
+
|
55
|
+
Copyright (c) 2009 "JadedPixel inc.". See LICENSE for details.
|
data/Rakefile
ADDED
@@ -0,0 +1,59 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'rake'
|
3
|
+
|
4
|
+
begin
|
5
|
+
require 'jeweler'
|
6
|
+
Jeweler::Tasks.new do |gem|
|
7
|
+
gem.name = "shopify_api"
|
8
|
+
gem.summary = "ShopifyAPI is a lightweight gem for accessing the Shopify admin REST web services"
|
9
|
+
gem.description = File.read(File.dirname(__FILE__) + "/README.rdoc")
|
10
|
+
gem.email = "developers@jadedpixel.com"
|
11
|
+
gem.homepage = "http://github.com/Shopify/shopify_api"
|
12
|
+
gem.authors = ["Tobias Lütke", "Cody Fauser", "Dennis Theisen"]
|
13
|
+
gem.rubyforge_project = "shopify-api"
|
14
|
+
gem.add_dependency('activeresource', '>= 2.2.2')
|
15
|
+
# gem is a Gem::Specification... see http://www.rubygems.org/read/chapter/20 for additional settings
|
16
|
+
end
|
17
|
+
|
18
|
+
Jeweler::RubyforgeTasks.new
|
19
|
+
rescue LoadError
|
20
|
+
puts "Jeweler (or a dependency) not available. Install it with: sudo gem install jeweler"
|
21
|
+
end
|
22
|
+
|
23
|
+
require 'rake/testtask'
|
24
|
+
Rake::TestTask.new(:test) do |test|
|
25
|
+
test.libs << 'lib' << 'test'
|
26
|
+
test.pattern = 'test/**/*_test.rb'
|
27
|
+
test.verbose = true
|
28
|
+
end
|
29
|
+
|
30
|
+
begin
|
31
|
+
require 'rcov/rcovtask'
|
32
|
+
Rcov::RcovTask.new do |test|
|
33
|
+
test.libs << 'test'
|
34
|
+
test.pattern = 'test/**/*_test.rb'
|
35
|
+
test.verbose = true
|
36
|
+
end
|
37
|
+
rescue LoadError
|
38
|
+
task :rcov do
|
39
|
+
abort "RCov is not available. In order to run rcov, you must: sudo gem install spicycode-rcov"
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
|
44
|
+
task :default => :test
|
45
|
+
|
46
|
+
require 'rake/rdoctask'
|
47
|
+
Rake::RDocTask.new do |rdoc|
|
48
|
+
if File.exist?('VERSION.yml')
|
49
|
+
config = YAML.load(File.read('VERSION.yml'))
|
50
|
+
version = "#{config[:major]}.#{config[:minor]}.#{config[:patch]}"
|
51
|
+
else
|
52
|
+
version = ""
|
53
|
+
end
|
54
|
+
|
55
|
+
rdoc.rdoc_dir = 'rdoc'
|
56
|
+
rdoc.title = "shopify_api #{version}"
|
57
|
+
rdoc.rdoc_files.include('README*')
|
58
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
59
|
+
end
|
data/VERSION
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
1.0.0
|
data/lib/shopify_api.rb
ADDED
@@ -0,0 +1,463 @@
|
|
1
|
+
require 'active_resource'
|
2
|
+
require 'digest/md5'
|
3
|
+
|
4
|
+
module ShopifyAPI
|
5
|
+
METAFIELD_ENABLED_CLASSES = %w( Order Product CustomCollection SmartCollection Page Blog Article )
|
6
|
+
|
7
|
+
module Countable
|
8
|
+
def count(options = {})
|
9
|
+
Integer(get(:count, options))
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
module Metafields
|
14
|
+
def metafields
|
15
|
+
Metafield.find(:all, :params => {:resource => self.class.collection_name, :resource_id => id})
|
16
|
+
end
|
17
|
+
|
18
|
+
def add_metafield(metafield)
|
19
|
+
raise ArgumentError, "You can only add metafields to resource that has been saved" if new?
|
20
|
+
|
21
|
+
metafield.prefix_options = {
|
22
|
+
:resource => self.class.collection_name,
|
23
|
+
:resource_id => id
|
24
|
+
}
|
25
|
+
metafield.save
|
26
|
+
metafield
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
#
|
31
|
+
# The Shopify API authenticates each call via HTTP Authentication, using
|
32
|
+
# * the application's API key as the username, and
|
33
|
+
# * a hex digest of the application's shared secret and an
|
34
|
+
# authentication token as the password.
|
35
|
+
#
|
36
|
+
# Generation & acquisition of the beforementioned looks like this:
|
37
|
+
#
|
38
|
+
# 0. Developer (that's you) registers Application (and provides a
|
39
|
+
# callback url) and receives an API key and a shared secret
|
40
|
+
#
|
41
|
+
# 1. User visits Application and are told they need to authenticate the
|
42
|
+
# application first for read/write permission to their data (needs to
|
43
|
+
# happen only once). User is asked for their shop url.
|
44
|
+
#
|
45
|
+
# 2. Application redirects to Shopify : GET <user's shop url>/admin/api/auth?api_key=<API key>
|
46
|
+
# (See Session#create_permission_url)
|
47
|
+
#
|
48
|
+
# 3. User logs-in to Shopify, approves application permission request
|
49
|
+
#
|
50
|
+
# 4. Shopify redirects to the Application's callback url (provided during
|
51
|
+
# registration), including the shop's name, and an authentication token in the parameters:
|
52
|
+
# GET client.com/customers?shop=snake-oil.myshopify.com&t=a94a110d86d2452eb3e2af4cfb8a3828
|
53
|
+
#
|
54
|
+
# 5. Authentication password computed using the shared secret and the
|
55
|
+
# authentication token (see Session#computed_password)
|
56
|
+
#
|
57
|
+
# 6. Profit!
|
58
|
+
# (API calls can now authenticate through HTTP using the API key, and
|
59
|
+
# computed password)
|
60
|
+
#
|
61
|
+
# LoginController and ShopifyLoginProtection use the Session class to set Shopify::Base.site
|
62
|
+
# so that all API calls are authorized transparently and end up just looking like this:
|
63
|
+
#
|
64
|
+
# # get 3 products
|
65
|
+
# @products = ShopifyAPI::Product.find(:all, :params => {:limit => 3})
|
66
|
+
#
|
67
|
+
# # get latest 3 orders
|
68
|
+
# @orders = ShopifyAPI::Order.find(:all, :params => {:limit => 3, :order => "created_at DESC" })
|
69
|
+
#
|
70
|
+
# As an example of what your LoginController should look like, take a look
|
71
|
+
# at the following:
|
72
|
+
#
|
73
|
+
# class LoginController < ApplicationController
|
74
|
+
# def index
|
75
|
+
# # Ask user for their #{shop}.myshopify.com address
|
76
|
+
# end
|
77
|
+
#
|
78
|
+
# def authenticate
|
79
|
+
# redirect_to ShopifyAPI::Session.new(params[:shop]).create_permission_url
|
80
|
+
# end
|
81
|
+
#
|
82
|
+
# # Shopify redirects the logged-in user back to this action along with
|
83
|
+
# # the authorization token t.
|
84
|
+
# #
|
85
|
+
# # This token is later combined with the developer's shared secret to form
|
86
|
+
# # the password used to call API methods.
|
87
|
+
# def finalize
|
88
|
+
# shopify_session = ShopifyAPI::Session.new(params[:shop], params[:t])
|
89
|
+
# if shopify_session.valid?
|
90
|
+
# session[:shopify] = shopify_session
|
91
|
+
# flash[:notice] = "Logged in to shopify store."
|
92
|
+
#
|
93
|
+
# return_address = session[:return_to] || '/home'
|
94
|
+
# session[:return_to] = nil
|
95
|
+
# redirect_to return_address
|
96
|
+
# else
|
97
|
+
# flash[:error] = "Could not log in to Shopify store."
|
98
|
+
# redirect_to :action => 'index'
|
99
|
+
# end
|
100
|
+
# end
|
101
|
+
#
|
102
|
+
# def logout
|
103
|
+
# session[:shopify] = nil
|
104
|
+
# flash[:notice] = "Successfully logged out."
|
105
|
+
#
|
106
|
+
# redirect_to :action => 'index'
|
107
|
+
# end
|
108
|
+
# end
|
109
|
+
#
|
110
|
+
class Session
|
111
|
+
cattr_accessor :api_key
|
112
|
+
cattr_accessor :secret
|
113
|
+
cattr_accessor :protocol
|
114
|
+
self.protocol = 'https'
|
115
|
+
|
116
|
+
attr_accessor :url, :token, :name
|
117
|
+
|
118
|
+
def self.setup(params)
|
119
|
+
params.each { |k,value| send("#{k}=", value) }
|
120
|
+
end
|
121
|
+
|
122
|
+
def initialize(url, token = nil, params = nil)
|
123
|
+
self.url, self.token = url, token
|
124
|
+
|
125
|
+
if params && params[:signature]
|
126
|
+
unless self.class.validate_signature(params) && params[:timestamp].to_i > 24.hours.ago.utc.to_i
|
127
|
+
raise "Invalid Signature: Possible malicious login"
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
self.class.prepare_url(self.url)
|
132
|
+
end
|
133
|
+
|
134
|
+
def shop
|
135
|
+
Shop.current
|
136
|
+
end
|
137
|
+
|
138
|
+
def create_permission_url
|
139
|
+
"http://#{url}/admin/api/auth?api_key=#{api_key}"
|
140
|
+
end
|
141
|
+
|
142
|
+
# Used by ActiveResource::Base to make all non-authentication API calls
|
143
|
+
#
|
144
|
+
# (ShopifyAPI::Base.site set in ShopifyLoginProtection#shopify_session)
|
145
|
+
def site
|
146
|
+
"#{protocol}://#{api_key}:#{computed_password}@#{url}/admin"
|
147
|
+
end
|
148
|
+
|
149
|
+
def valid?
|
150
|
+
[url, token].all?
|
151
|
+
end
|
152
|
+
|
153
|
+
private
|
154
|
+
|
155
|
+
# The secret is computed by taking the shared_secret which we got when
|
156
|
+
# registring this third party application and concating the request_to it,
|
157
|
+
# and then calculating a MD5 hexdigest.
|
158
|
+
def computed_password
|
159
|
+
Digest::MD5.hexdigest(secret + token.to_s)
|
160
|
+
end
|
161
|
+
|
162
|
+
def self.prepare_url(url)
|
163
|
+
url.gsub!(/https?:\/\//, '') # remove http:// or https://
|
164
|
+
url.concat(".myshopify.com") unless url.include?('.') # extend url to myshopify.com if no host is given
|
165
|
+
end
|
166
|
+
|
167
|
+
def self.validate_signature(params)
|
168
|
+
return false unless signature = params[:signature]
|
169
|
+
|
170
|
+
sorted_params = params.except(:signature, :action, :controller).collect{|k,v|"#{k}=#{v}"}.sort.join
|
171
|
+
Digest::MD5.hexdigest(secret + sorted_params) == signature
|
172
|
+
end
|
173
|
+
end
|
174
|
+
|
175
|
+
class Base < ActiveResource::Base
|
176
|
+
extend Countable
|
177
|
+
end
|
178
|
+
|
179
|
+
# Shop object. Use Shop.current to receive
|
180
|
+
# the shop.
|
181
|
+
class Shop < Base
|
182
|
+
def self.current
|
183
|
+
find(:one, :from => "/admin/shop.xml")
|
184
|
+
end
|
185
|
+
|
186
|
+
def metafields
|
187
|
+
Metafield.find(:all)
|
188
|
+
end
|
189
|
+
|
190
|
+
def add_metafield(metafield)
|
191
|
+
raise ArgumentError, "You can only add metafields to resource that has been saved" if new?
|
192
|
+
metafield.save
|
193
|
+
metafield
|
194
|
+
end
|
195
|
+
end
|
196
|
+
|
197
|
+
# Custom collection
|
198
|
+
#
|
199
|
+
class CustomCollection < Base
|
200
|
+
def products
|
201
|
+
Product.find(:all, :params => {:collection_id => self.id})
|
202
|
+
end
|
203
|
+
|
204
|
+
def add_product(product)
|
205
|
+
Collect.create(:collection_id => self.id, :product_id => product.id)
|
206
|
+
end
|
207
|
+
|
208
|
+
def remove_product(product)
|
209
|
+
collect = Collect.find(:first, :params => {:collection_id => self.id, :product_id => product.id})
|
210
|
+
collect.destroy if collect
|
211
|
+
end
|
212
|
+
end
|
213
|
+
|
214
|
+
class SmartCollection < Base
|
215
|
+
def products
|
216
|
+
Product.find(:all, :params => {:collection_id => self.id})
|
217
|
+
end
|
218
|
+
end
|
219
|
+
|
220
|
+
# For adding/removing products from custom collections
|
221
|
+
class Collect < Base
|
222
|
+
end
|
223
|
+
|
224
|
+
class ShippingAddress < Base
|
225
|
+
end
|
226
|
+
|
227
|
+
class BillingAddress < Base
|
228
|
+
end
|
229
|
+
|
230
|
+
class LineItem < Base
|
231
|
+
end
|
232
|
+
|
233
|
+
class ShippingLine < Base
|
234
|
+
end
|
235
|
+
|
236
|
+
class NoteAttribute < Base
|
237
|
+
end
|
238
|
+
|
239
|
+
class Order < Base
|
240
|
+
def close; load_attributes_from_response(post(:close)); end
|
241
|
+
|
242
|
+
def open; load_attributes_from_response(post(:open)); end
|
243
|
+
|
244
|
+
def transactions
|
245
|
+
Transaction.find(:all, :params => { :order_id => id })
|
246
|
+
end
|
247
|
+
|
248
|
+
def capture(amount = "")
|
249
|
+
Transaction.create(:amount => amount, :kind => "capture", :order_id => id)
|
250
|
+
end
|
251
|
+
end
|
252
|
+
|
253
|
+
class Product < Base
|
254
|
+
|
255
|
+
# Share all items of this store with the
|
256
|
+
# shopify marketplace
|
257
|
+
def self.share; post :share; end
|
258
|
+
def self.unshare; delete :share; end
|
259
|
+
|
260
|
+
# compute the price range
|
261
|
+
def price_range
|
262
|
+
prices = variants.collect(&:price)
|
263
|
+
format = "%0.2f"
|
264
|
+
if prices.min != prices.max
|
265
|
+
"#{format % prices.min} - #{format % prices.max}"
|
266
|
+
else
|
267
|
+
format % prices.min
|
268
|
+
end
|
269
|
+
end
|
270
|
+
|
271
|
+
def collections
|
272
|
+
CustomCollection.find(:all, :params => {:product_id => self.id})
|
273
|
+
end
|
274
|
+
|
275
|
+
def smart_collections
|
276
|
+
SmartCollection.find(:all, :params => {:product_id => self.id})
|
277
|
+
end
|
278
|
+
|
279
|
+
def add_to_collection(collection)
|
280
|
+
collection.add_product(self)
|
281
|
+
end
|
282
|
+
|
283
|
+
def remove_from_collection(collection)
|
284
|
+
collection.remove_product(self)
|
285
|
+
end
|
286
|
+
end
|
287
|
+
|
288
|
+
class Variant < Base
|
289
|
+
self.prefix = "/admin/products/:product_id/"
|
290
|
+
end
|
291
|
+
|
292
|
+
class Image < Base
|
293
|
+
self.prefix = "/admin/products/:product_id/"
|
294
|
+
|
295
|
+
# generate a method for each possible image variant
|
296
|
+
[:pico, :icon, :thumb, :small, :medium, :large, :original].each do |m|
|
297
|
+
reg_exp_match = "/\\1_#{m}.\\2"
|
298
|
+
define_method(m) { src.gsub(/\/(.*)\.(\w{2,4})/, reg_exp_match) }
|
299
|
+
end
|
300
|
+
|
301
|
+
def attach_image(data, filename = nil)
|
302
|
+
attributes['attachment'] = Base64.encode64(data)
|
303
|
+
attributes['filename'] = filename unless filename.nil?
|
304
|
+
end
|
305
|
+
end
|
306
|
+
|
307
|
+
class Transaction < Base
|
308
|
+
self.prefix = "/admin/orders/:order_id/"
|
309
|
+
end
|
310
|
+
|
311
|
+
class Fulfillment < Base
|
312
|
+
self.prefix = "/admin/orders/:order_id/"
|
313
|
+
end
|
314
|
+
|
315
|
+
class Country < Base
|
316
|
+
end
|
317
|
+
|
318
|
+
class Page < Base
|
319
|
+
end
|
320
|
+
|
321
|
+
class Blog < Base
|
322
|
+
def articles
|
323
|
+
Article.find(:all, :params => { :blog_id => id })
|
324
|
+
end
|
325
|
+
end
|
326
|
+
|
327
|
+
class Article < Base
|
328
|
+
self.prefix = "/admin/blogs/:blog_id/"
|
329
|
+
end
|
330
|
+
|
331
|
+
class Metafield < Base
|
332
|
+
self.prefix = "/admin/:resource/:resource_id/"
|
333
|
+
|
334
|
+
# Hack to allow both Shop and other Metafields in through the same AR class
|
335
|
+
def self.prefix(options={})
|
336
|
+
options[:resource].nil? ? "/admin/" : "/admin/#{options[:resource]}/#{options[:resource_id]}/"
|
337
|
+
end
|
338
|
+
|
339
|
+
def value
|
340
|
+
return if attributes["value"].nil?
|
341
|
+
attributes["value_type"] == "integer" ? attributes["value"].to_i : attributes["value"]
|
342
|
+
end
|
343
|
+
|
344
|
+
end
|
345
|
+
|
346
|
+
class Comment < Base
|
347
|
+
def remove; load_attributes_from_response(post(:remove)); end
|
348
|
+
def ham; load_attributes_from_response(post(:ham)); end
|
349
|
+
def spam; load_attributes_from_response(post(:spam)); end
|
350
|
+
def approve; load_attributes_from_response(post(:approve)); end
|
351
|
+
end
|
352
|
+
|
353
|
+
class Province < Base
|
354
|
+
self.prefix = "/admin/countries/:country_id/"
|
355
|
+
end
|
356
|
+
|
357
|
+
class Redirect < Base
|
358
|
+
end
|
359
|
+
|
360
|
+
|
361
|
+
# Assets represent the files that comprise your theme.
|
362
|
+
# There are different buckets which hold different kinds
|
363
|
+
# of assets, each corresponding to one of the folders
|
364
|
+
# within a theme's zip file: layout, templates, and
|
365
|
+
# assets. The full key of an asset always starts with the
|
366
|
+
# bucket name, and the path separator is a forward slash,
|
367
|
+
# like layout/theme.liquid or assets/bg-body.gif.
|
368
|
+
#
|
369
|
+
# Initialize with a key:
|
370
|
+
# asset = ShopifyAPI::Asset.new(:key => 'assets/special.css')
|
371
|
+
#
|
372
|
+
# Find by key:
|
373
|
+
# asset = ShopifyAPI::Asset.find('assets/image.png')
|
374
|
+
#
|
375
|
+
# Get the text or binary value:
|
376
|
+
# asset.value # decodes from attachment attribute if necessary
|
377
|
+
#
|
378
|
+
# You can provide new data for assets in a few different ways:
|
379
|
+
#
|
380
|
+
# * assign text data for the value directly:
|
381
|
+
# asset.value = "div.special {color:red;}"
|
382
|
+
#
|
383
|
+
# * provide binary data for the value:
|
384
|
+
# asset.attach(File.read('image.png'))
|
385
|
+
#
|
386
|
+
# * set a URL from which Shopify will fetch the value:
|
387
|
+
# asset.src = "http://mysite.com/image.png"
|
388
|
+
#
|
389
|
+
# * set a source key of another of your assets from which
|
390
|
+
# the value will be copied:
|
391
|
+
# asset.source_key = "assets/another_image.png"
|
392
|
+
class Asset < Base
|
393
|
+
self.primary_key = 'key'
|
394
|
+
|
395
|
+
# find an asset by key:
|
396
|
+
# ShopifyAPI::Asset.find('layout/theme.liquid')
|
397
|
+
def self.find(*args)
|
398
|
+
if args[0].is_a?(Symbol)
|
399
|
+
super
|
400
|
+
else
|
401
|
+
find(:one, :from => "/admin/assets.xml", :params => {:asset => {:key => args[0]}})
|
402
|
+
end
|
403
|
+
end
|
404
|
+
|
405
|
+
# For text assets, Shopify returns the data in the 'value' attribute.
|
406
|
+
# For binary assets, the data is base-64-encoded and returned in the
|
407
|
+
# 'attachment' attribute. This accessor returns the data in both cases.
|
408
|
+
def value
|
409
|
+
attributes['value'] ||
|
410
|
+
(attributes['attachment'] ? Base64.decode64(attributes['attachment']) : nil)
|
411
|
+
end
|
412
|
+
|
413
|
+
def attach(data)
|
414
|
+
self.attachment = Base64.encode64(data)
|
415
|
+
end
|
416
|
+
|
417
|
+
def destroy #:nodoc:
|
418
|
+
connection.delete(element_path(:asset => {:key => key}), self.class.headers)
|
419
|
+
end
|
420
|
+
|
421
|
+
def new? #:nodoc:
|
422
|
+
false
|
423
|
+
end
|
424
|
+
|
425
|
+
def self.element_path(id, prefix_options = {}, query_options = nil) #:nodoc:
|
426
|
+
prefix_options, query_options = split_options(prefix_options) if query_options.nil?
|
427
|
+
"#{prefix(prefix_options)}#{collection_name}.#{format.extension}#{query_string(query_options)}"
|
428
|
+
end
|
429
|
+
|
430
|
+
def method_missing(method_symbol, *arguments) #:nodoc:
|
431
|
+
if %w{value= attachment= src= source_key=}.include?(method_symbol)
|
432
|
+
wipe_value_attributes
|
433
|
+
end
|
434
|
+
super
|
435
|
+
end
|
436
|
+
|
437
|
+
private
|
438
|
+
|
439
|
+
def wipe_value_attributes
|
440
|
+
%w{value attachment src source_key}.each do |attr|
|
441
|
+
attributes.delete(attr)
|
442
|
+
end
|
443
|
+
end
|
444
|
+
end
|
445
|
+
|
446
|
+
class RecurringApplicationCharge < Base
|
447
|
+
def self.current
|
448
|
+
find(:all).find{|charge| charge.status == 'active'}
|
449
|
+
end
|
450
|
+
|
451
|
+
def cancel
|
452
|
+
load_attributes_from_response(self.destroy)
|
453
|
+
end
|
454
|
+
end
|
455
|
+
|
456
|
+
class ApplicationCharge < Base
|
457
|
+
end
|
458
|
+
|
459
|
+
# Include Metafields module in all enabled classes
|
460
|
+
METAFIELD_ENABLED_CLASSES.each do |klass|
|
461
|
+
"ShopifyAPI::#{klass}".constantize.send(:include, Metafields)
|
462
|
+
end
|
463
|
+
end
|
data/shopify_api.gemspec
ADDED
@@ -0,0 +1,53 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
|
3
|
+
Gem::Specification.new do |s|
|
4
|
+
s.name = %q{shopify_api}
|
5
|
+
s.version = "1.0.0"
|
6
|
+
|
7
|
+
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
|
8
|
+
s.authors = ["Tobias Lütke", "Cody Fauser", "Dennis Theisen"]
|
9
|
+
s.date = %q{2009-06-15}
|
10
|
+
s.description = %q{= Shopify API 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. In other words, we’ve tried to make the API follow the REST principles as much as possible. == Usage === Requirements All API usage happens through Shopify applications, created by either shop owners for their own shops, or by Shopify Partners for use by shop owners: * Shop owners can create applications for themselves through their own admin (under the Preferences > Applications tab). * Shopify Partners create applications through their admin. For more information and detailed documentation visit http://api.shopify.com === Getting Started ShopifyAPI uses ActiveResource to communicate with the REST web service. ActiveResource has to be configured with a fully authorized URL of a particular store first. To obtain that URL you can follow these steps: 1. First create a new application in either the partners admin or your store admin and write down your API_KEY and SHARED_SECRET. 2. You will need to supply two parameters to the Session class before you instantiate it: ShopifyAPI::Session.setup({:api_key => API_KEY, :secret => SHARED_SECRET}) 3. Create a new Session for a specific shop. That session is not fully valid yet, but it can be used to create a URL that you will redirect your users to: session = ShopifyAPI::Session.new("yourshopname.myshopify.com") session.valid? # returns false 4. To access the API shop owners need a "token" from that specific shop. In order to get this token they need to authorize with that shop first. The URL to redirect your user to can be generated via: url = session.create_permission_url 5. After visiting this URL, the shop redirects the owner to a custom URL of your application where the "token" gets sent to (it's param name is just "t"). Use that token to instantiate a "valid" session, that is ready to make calls to that particular shop. token = params[:t] session = ShopifyAPI::Session.new("yourshopname.myshopify.com", token) session.valid? # returns true 6. Now you can finally get the fully authorized URL for that shop. Use that URL to configure ActiveResource and you're set: ActiveResource::Base.site = session.site 7. Get data from that shop (returns ActiveResource instances): shop = ShopifyAPI::Shop.current latest_orders = ShopifyAPI::Order.find(:all) == Copyright Copyright (c) 2009 "JadedPixel inc.". See LICENSE for details.}
|
11
|
+
s.email = %q{developers@jadedpixel.com}
|
12
|
+
s.extra_rdoc_files = [
|
13
|
+
"LICENSE",
|
14
|
+
"README.rdoc"
|
15
|
+
]
|
16
|
+
s.files = [
|
17
|
+
".document",
|
18
|
+
".gitignore",
|
19
|
+
"CHANGELOG",
|
20
|
+
"LICENSE",
|
21
|
+
"README.rdoc",
|
22
|
+
"Rakefile",
|
23
|
+
"VERSION",
|
24
|
+
"lib/shopify_api.rb",
|
25
|
+
"shopify_api.gemspec",
|
26
|
+
"test/shopify_api_test.rb",
|
27
|
+
"test/test_helper.rb"
|
28
|
+
]
|
29
|
+
s.has_rdoc = true
|
30
|
+
s.homepage = %q{http://github.com/Shopify/shopify_api}
|
31
|
+
s.rdoc_options = ["--charset=UTF-8"]
|
32
|
+
s.require_paths = ["lib"]
|
33
|
+
s.rubyforge_project = %q{shopify-api}
|
34
|
+
s.rubygems_version = %q{1.3.1}
|
35
|
+
s.summary = %q{ShopifyAPI is a lightweight gem for accessing the Shopify admin REST web services}
|
36
|
+
s.test_files = [
|
37
|
+
"test/shopify_api_test.rb",
|
38
|
+
"test/test_helper.rb"
|
39
|
+
]
|
40
|
+
|
41
|
+
if s.respond_to? :specification_version then
|
42
|
+
current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
|
43
|
+
s.specification_version = 2
|
44
|
+
|
45
|
+
if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then
|
46
|
+
s.add_runtime_dependency(%q<activeresource>, [">= 2.2.2"])
|
47
|
+
else
|
48
|
+
s.add_dependency(%q<activeresource>, [">= 2.2.2"])
|
49
|
+
end
|
50
|
+
else
|
51
|
+
s.add_dependency(%q<activeresource>, [">= 2.2.2"])
|
52
|
+
end
|
53
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
|
3
|
+
class ShopifyApiTest < Test::Unit::TestCase
|
4
|
+
|
5
|
+
context "Session" do
|
6
|
+
should "raise error when blank shop url is provided" do
|
7
|
+
assert_raise(ArgumentError) { ShopifyAPI::Session.new("") }
|
8
|
+
end
|
9
|
+
|
10
|
+
should "not be valid without token" do
|
11
|
+
session = ShopifyAPI::Session.new("testshop.myshopify.com")
|
12
|
+
assert_not session.valid?
|
13
|
+
end
|
14
|
+
|
15
|
+
should "be valid with any token" do
|
16
|
+
session = ShopifyAPI::Session.new("testshop.myshopify.com", "any-token")
|
17
|
+
assert session.valid?
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
end
|
data/test/test_helper.rb
ADDED
@@ -0,0 +1,28 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'test/unit'
|
3
|
+
|
4
|
+
$LOAD_PATH.unshift(File.dirname(__FILE__))
|
5
|
+
$LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
|
6
|
+
require 'shopify_api'
|
7
|
+
|
8
|
+
# setup ShopifyAPI with fake api_key and secret
|
9
|
+
ShopifyAPI::Session.setup(:api_key => "API Test key", :secret => "API Test secret")
|
10
|
+
|
11
|
+
class Test::Unit::TestCase
|
12
|
+
def self.test(string, &block)
|
13
|
+
define_method("test:#{string}", &block)
|
14
|
+
end
|
15
|
+
|
16
|
+
def self.should(string, &block)
|
17
|
+
self.test("should_#{string}", &block)
|
18
|
+
end
|
19
|
+
|
20
|
+
def self.context(string)
|
21
|
+
yield
|
22
|
+
end
|
23
|
+
|
24
|
+
# Custom Assertions
|
25
|
+
def assert_not(expression)
|
26
|
+
assert_block("Expected <#{expression}> to be false!") { not expression }
|
27
|
+
end
|
28
|
+
end
|
metadata
ADDED
@@ -0,0 +1,78 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: shopify_api
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 1.0.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- "Tobias L\xC3\xBCtke"
|
8
|
+
- Cody Fauser
|
9
|
+
- Dennis Theisen
|
10
|
+
autorequire:
|
11
|
+
bindir: bin
|
12
|
+
cert_chain: []
|
13
|
+
|
14
|
+
date: 2009-06-15 00:00:00 -04:00
|
15
|
+
default_executable:
|
16
|
+
dependencies:
|
17
|
+
- !ruby/object:Gem::Dependency
|
18
|
+
name: activeresource
|
19
|
+
type: :runtime
|
20
|
+
version_requirement:
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
22
|
+
requirements:
|
23
|
+
- - ">="
|
24
|
+
- !ruby/object:Gem::Version
|
25
|
+
version: 2.2.2
|
26
|
+
version:
|
27
|
+
description: "= Shopify API 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. In other words, we\xE2\x80\x99ve tried to make the API follow the REST principles as much as possible. == Usage === Requirements All API usage happens through Shopify applications, created by either shop owners for their own shops, or by Shopify Partners for use by shop owners: * Shop owners can create applications for themselves through their own admin (under the Preferences > Applications tab). * Shopify Partners create applications through their admin. For more information and detailed documentation visit http://api.shopify.com === Getting Started ShopifyAPI uses ActiveResource to communicate with the REST web service. ActiveResource has to be configured with a fully authorized URL of a particular store first. To obtain that URL you can follow these steps: 1. First create a new application in either the partners admin or your store admin and write down your API_KEY and SHARED_SECRET. 2. You will need to supply two parameters to the Session class before you instantiate it: ShopifyAPI::Session.setup({:api_key => API_KEY, :secret => SHARED_SECRET}) 3. Create a new Session for a specific shop. That session is not fully valid yet, but it can be used to create a URL that you will redirect your users to: session = ShopifyAPI::Session.new(\"yourshopname.myshopify.com\") session.valid? # returns false 4. To access the API shop owners need a \"token\" from that specific shop. In order to get this token they need to authorize with that shop first. The URL to redirect your user to can be generated via: url = session.create_permission_url 5. After visiting this URL, the shop redirects the owner to a custom URL of your application where the \"token\" gets sent to (it's param name is just \"t\"). Use that token to instantiate a \"valid\" session, that is ready to make calls to that particular shop. token = params[:t] session = ShopifyAPI::Session.new(\"yourshopname.myshopify.com\", token) session.valid? # returns true 6. Now you can finally get the fully authorized URL for that shop. Use that URL to configure ActiveResource and you're set: ActiveResource::Base.site = session.site 7. Get data from that shop (returns ActiveResource instances): shop = ShopifyAPI::Shop.current latest_orders = ShopifyAPI::Order.find(:all) == Copyright Copyright (c) 2009 \"JadedPixel inc.\". See LICENSE for details."
|
28
|
+
email: developers@jadedpixel.com
|
29
|
+
executables: []
|
30
|
+
|
31
|
+
extensions: []
|
32
|
+
|
33
|
+
extra_rdoc_files:
|
34
|
+
- LICENSE
|
35
|
+
- README.rdoc
|
36
|
+
files:
|
37
|
+
- .document
|
38
|
+
- .gitignore
|
39
|
+
- CHANGELOG
|
40
|
+
- LICENSE
|
41
|
+
- README.rdoc
|
42
|
+
- Rakefile
|
43
|
+
- VERSION
|
44
|
+
- lib/shopify_api.rb
|
45
|
+
- shopify_api.gemspec
|
46
|
+
- test/shopify_api_test.rb
|
47
|
+
- test/test_helper.rb
|
48
|
+
has_rdoc: true
|
49
|
+
homepage: http://github.com/Shopify/shopify_api
|
50
|
+
licenses: []
|
51
|
+
|
52
|
+
post_install_message:
|
53
|
+
rdoc_options:
|
54
|
+
- --charset=UTF-8
|
55
|
+
require_paths:
|
56
|
+
- lib
|
57
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - ">="
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: "0"
|
62
|
+
version:
|
63
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
64
|
+
requirements:
|
65
|
+
- - ">="
|
66
|
+
- !ruby/object:Gem::Version
|
67
|
+
version: "0"
|
68
|
+
version:
|
69
|
+
requirements: []
|
70
|
+
|
71
|
+
rubyforge_project: shopify-api
|
72
|
+
rubygems_version: 1.3.5
|
73
|
+
signing_key:
|
74
|
+
specification_version: 2
|
75
|
+
summary: ShopifyAPI is a lightweight gem for accessing the Shopify admin REST web services
|
76
|
+
test_files:
|
77
|
+
- test/shopify_api_test.rb
|
78
|
+
- test/test_helper.rb
|