shopify_api 1.2.5 → 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (58) hide show
  1. data/CHANGELOG +10 -0
  2. data/README.rdoc +6 -1
  3. data/RELEASING +16 -0
  4. data/lib/active_resource/connection_ext.rb +16 -0
  5. data/lib/shopify_api.rb +12 -533
  6. data/lib/shopify_api/cli.rb +9 -9
  7. data/lib/shopify_api/countable.rb +7 -0
  8. data/lib/shopify_api/events.rb +7 -0
  9. data/lib/shopify_api/json_format.rb +23 -0
  10. data/lib/shopify_api/limits.rb +76 -0
  11. data/lib/shopify_api/metafields.rb +18 -0
  12. data/lib/shopify_api/resources.rb +40 -0
  13. data/lib/shopify_api/resources/address.rb +4 -0
  14. data/lib/shopify_api/resources/application_charge.rb +9 -0
  15. data/lib/shopify_api/resources/article.rb +12 -0
  16. data/lib/shopify_api/resources/asset.rb +95 -0
  17. data/lib/shopify_api/resources/base.rb +6 -0
  18. data/lib/shopify_api/resources/billing_address.rb +4 -0
  19. data/lib/shopify_api/resources/blog.rb +10 -0
  20. data/lib/shopify_api/resources/collect.rb +5 -0
  21. data/lib/shopify_api/resources/comment.rb +13 -0
  22. data/lib/shopify_api/resources/country.rb +4 -0
  23. data/lib/shopify_api/resources/custom_collection.rb +19 -0
  24. data/lib/shopify_api/resources/customer.rb +4 -0
  25. data/lib/shopify_api/resources/customer_group.rb +4 -0
  26. data/lib/shopify_api/resources/event.rb +10 -0
  27. data/lib/shopify_api/resources/fulfillment.rb +5 -0
  28. data/lib/shopify_api/resources/image.rb +16 -0
  29. data/lib/shopify_api/resources/line_item.rb +4 -0
  30. data/lib/shopify_api/resources/metafield.rb +15 -0
  31. data/lib/shopify_api/resources/note_attribute.rb +4 -0
  32. data/lib/shopify_api/resources/option.rb +4 -0
  33. data/lib/shopify_api/resources/order.rb +25 -0
  34. data/lib/shopify_api/resources/page.rb +6 -0
  35. data/lib/shopify_api/resources/payment_details.rb +4 -0
  36. data/lib/shopify_api/resources/product.rb +33 -0
  37. data/lib/shopify_api/resources/product_search_engine.rb +4 -0
  38. data/lib/shopify_api/resources/province.rb +5 -0
  39. data/lib/shopify_api/resources/receipt.rb +4 -0
  40. data/lib/shopify_api/resources/recurring_application_charge.rb +23 -0
  41. data/lib/shopify_api/resources/redirect.rb +4 -0
  42. data/lib/shopify_api/resources/rule.rb +4 -0
  43. data/lib/shopify_api/resources/script_tag.rb +4 -0
  44. data/lib/shopify_api/resources/shipping_address.rb +4 -0
  45. data/lib/shopify_api/resources/shipping_line.rb +4 -0
  46. data/lib/shopify_api/resources/shop.rb +23 -0
  47. data/lib/shopify_api/resources/smart_collection.rb +10 -0
  48. data/lib/shopify_api/resources/tax_line.rb +4 -0
  49. data/lib/shopify_api/resources/theme.rb +4 -0
  50. data/lib/shopify_api/resources/transaction.rb +5 -0
  51. data/lib/shopify_api/resources/variant.rb +11 -0
  52. data/lib/shopify_api/resources/webhook.rb +4 -0
  53. data/lib/shopify_api/session.rb +165 -0
  54. data/shopify_api.gemspec +13 -92
  55. data/test/cli_test.rb +109 -0
  56. data/test/limits_test.rb +37 -0
  57. data/test/shopify_api_test.rb +13 -1
  58. metadata +76 -82
data/CHANGELOG CHANGED
@@ -1,3 +1,13 @@
1
+ == Version 2.0.0
2
+
3
+ * Bump to 2.0.0 as this release breaks Rails 2 compatibility; we're now officially only supporting Rails 3. Rails 2 devs can follow the rails2 tag in this repo to know where we broke off
4
+ * Refactored resources into their own source files
5
+ * Added API limits functionality
6
+ * Patched ActiveResource issue with roots in JSON
7
+ * Added pending, cancelled, accepted, and declined convenience methods to ShopifyAPI::RecurringApplicationCharge
8
+ * ShopifyAPI::Session#temp now available as a convenience method to support temporarily switching to other shops when making calls
9
+ * Fixes to `shopify console` CLI tool
10
+
1
11
  == Version 1.2.5
2
12
 
3
13
  * Fix for Article#comments
data/README.rdoc CHANGED
@@ -50,6 +50,11 @@ ShopifyAPI uses ActiveResource to communicate with the REST web service. ActiveR
50
50
  shop = ShopifyAPI::Shop.current
51
51
  latest_orders = ShopifyAPI::Order.find(:all)
52
52
 
53
+ Alternatively, you can use #temp to initialize a Session and execute a command which also handles temporarily setting ActiveResource::Base.site:
54
+
55
+ latest_orders = ShopifyAPI::Session.temp("yourshopname.myshopify.com", token) { ShopifyAPI::Order.find(:all) }
56
+
57
+
53
58
  == Copyright
54
59
 
55
- Copyright (c) 2009 "JadedPixel inc.". See LICENSE for details.
60
+ Copyright (c) 2011 "JadedPixel inc.". See LICENSE for details.
data/RELEASING ADDED
@@ -0,0 +1,16 @@
1
+ Releasing ShopifyAPI
2
+
3
+ 1. Check the Semantic Versioning page for info on how to version the new release: http://semver.org
4
+ 2. Update the version of ShopifyAPI in shopify_api.gemspec
5
+ 3. Add a CHANGELOG entry for the new release with the date
6
+ 4. Commit the changes with a commit message like "Packaging for release X.Y.Z"
7
+ 5. Tag the release with the version (Leave REV blank for HEAD or provide a SHA)
8
+ $ git tag vX.Y.Z REV
9
+ 6. Push out the changes
10
+ $ git push
11
+ 7. Push out the tags
12
+ $ git push --tags
13
+ 8. Build the new .gem from the updated .gemspec
14
+ $ gem build shopify_api.gemspec
15
+ 9. Publish the Gem to gemcutter
16
+ $ gem push shopify_api-X.Y.Z.gem
@@ -0,0 +1,16 @@
1
+ require 'active_support/core_ext/module/aliasing'
2
+
3
+ module ActiveResource
4
+ class Connection
5
+
6
+ attr_reader :response
7
+
8
+ def handle_response_with_response_capture(response)
9
+ @response = handle_response_without_response_capture(response)
10
+ end
11
+
12
+ alias_method_chain :handle_response, :response_capture
13
+ # alias_method :handle_response_without_instance, :handle_response
14
+ # alias_method :handle_response, :handle_response_with_instance
15
+ end
16
+ end
data/lib/shopify_api.rb CHANGED
@@ -1,540 +1,19 @@
1
+ $:.unshift File.dirname(__FILE__)
2
+
1
3
  require 'active_resource'
2
4
  require 'active_support/core_ext/class/attribute_accessors'
3
5
  require 'digest/md5'
4
6
  require 'base64'
7
+ require 'active_resource/connection_ext'
8
+ require 'shopify_api/limits'
9
+ require 'shopify_api/json_format'
5
10
 
6
11
  module ShopifyAPI
7
- METAFIELD_ENABLED_CLASSES = %w( Order Product CustomCollection SmartCollection Page Blog Article Variant)
8
- EVENT_ENABLED_CLASSES = %w( Order Product CustomCollection SmartCollection Page Blog Article )
9
-
10
- module Countable
11
- def count(options = {})
12
- Integer(get(:count, options))
13
- end
14
- end
15
-
16
- module Metafields
17
- def metafields
18
- Metafield.find(:all, :params => {:resource => self.class.collection_name, :resource_id => id})
19
- end
20
-
21
- def add_metafield(metafield)
22
- raise ArgumentError, "You can only add metafields to resource that has been saved" if new?
23
-
24
- metafield.prefix_options = {
25
- :resource => self.class.collection_name,
26
- :resource_id => id
27
- }
28
- metafield.save
29
- metafield
30
- end
31
- end
32
-
33
- module Events
34
- def events
35
- Event.find(:all, :params => {:resource => self.class.collection_name, :resource_id => id})
36
- end
37
- end
38
-
39
- #
40
- # The Shopify API authenticates each call via HTTP Authentication, using
41
- # * the application's API key as the username, and
42
- # * a hex digest of the application's shared secret and an
43
- # authentication token as the password.
44
- #
45
- # Generation & acquisition of the beforementioned looks like this:
46
- #
47
- # 0. Developer (that's you) registers Application (and provides a
48
- # callback url) and receives an API key and a shared secret
49
- #
50
- # 1. User visits Application and are told they need to authenticate the
51
- # application first for read/write permission to their data (needs to
52
- # happen only once). User is asked for their shop url.
53
- #
54
- # 2. Application redirects to Shopify : GET <user's shop url>/admin/api/auth?api_key=<API key>
55
- # (See Session#create_permission_url)
56
- #
57
- # 3. User logs-in to Shopify, approves application permission request
58
- #
59
- # 4. Shopify redirects to the Application's callback url (provided during
60
- # registration), including the shop's name, and an authentication token in the parameters:
61
- # GET client.com/customers?shop=snake-oil.myshopify.com&t=a94a110d86d2452eb3e2af4cfb8a3828
62
- #
63
- # 5. Authentication password computed using the shared secret and the
64
- # authentication token (see Session#computed_password)
65
- #
66
- # 6. Profit!
67
- # (API calls can now authenticate through HTTP using the API key, and
68
- # computed password)
69
- #
70
- # LoginController and ShopifyLoginProtection use the Session class to set Shopify::Base.site
71
- # so that all API calls are authorized transparently and end up just looking like this:
72
- #
73
- # # get 3 products
74
- # @products = ShopifyAPI::Product.find(:all, :params => {:limit => 3})
75
- #
76
- # # get latest 3 orders
77
- # @orders = ShopifyAPI::Order.find(:all, :params => {:limit => 3, :order => "created_at DESC" })
78
- #
79
- # As an example of what your LoginController should look like, take a look
80
- # at the following:
81
- #
82
- # class LoginController < ApplicationController
83
- # def index
84
- # # Ask user for their #{shop}.myshopify.com address
85
- # end
86
- #
87
- # def authenticate
88
- # redirect_to ShopifyAPI::Session.new(params[:shop]).create_permission_url
89
- # end
90
- #
91
- # # Shopify redirects the logged-in user back to this action along with
92
- # # the authorization token t.
93
- # #
94
- # # This token is later combined with the developer's shared secret to form
95
- # # the password used to call API methods.
96
- # def finalize
97
- # shopify_session = ShopifyAPI::Session.new(params[:shop], params[:t])
98
- # if shopify_session.valid?
99
- # session[:shopify] = shopify_session
100
- # flash[:notice] = "Logged in to shopify store."
101
- #
102
- # return_address = session[:return_to] || '/home'
103
- # session[:return_to] = nil
104
- # redirect_to return_address
105
- # else
106
- # flash[:error] = "Could not log in to Shopify store."
107
- # redirect_to :action => 'index'
108
- # end
109
- # end
110
- #
111
- # def logout
112
- # session[:shopify] = nil
113
- # flash[:notice] = "Successfully logged out."
114
- #
115
- # redirect_to :action => 'index'
116
- # end
117
- # end
118
- #
119
- class Session
120
- cattr_accessor :api_key
121
- cattr_accessor :secret
122
- cattr_accessor :protocol
123
- self.protocol = 'https'
124
-
125
- attr_accessor :url, :token, :name
126
-
127
- def self.setup(params)
128
- params.each { |k,value| send("#{k}=", value) }
129
- end
130
-
131
- def initialize(url, token = nil, params = nil)
132
- self.url, self.token = url, token
133
-
134
- if params
135
- unless self.class.validate_signature(params) && params[:timestamp].to_i > 24.hours.ago.utc.to_i
136
- raise "Invalid Signature: Possible malicious login"
137
- end
138
- end
139
-
140
- self.class.prepare_url(self.url)
141
- end
142
-
143
- def shop
144
- Shop.current
145
- end
146
-
147
- def create_permission_url
148
- return nil if url.blank? || api_key.blank?
149
- "http://#{url}/admin/api/auth?api_key=#{api_key}"
150
- end
151
-
152
- # Used by ActiveResource::Base to make all non-authentication API calls
153
- #
154
- # (ShopifyAPI::Base.site set in ShopifyLoginProtection#shopify_session)
155
- def site
156
- "#{protocol}://#{api_key}:#{computed_password}@#{url}/admin"
157
- end
158
-
159
- def valid?
160
- url.present? && token.present?
161
- end
162
-
163
- private
164
-
165
- # The secret is computed by taking the shared_secret which we got when
166
- # registring this third party application and concating the request_to it,
167
- # and then calculating a MD5 hexdigest.
168
- def computed_password
169
- Digest::MD5.hexdigest(secret + token.to_s)
170
- end
171
-
172
- def self.prepare_url(url)
173
- return nil if url.blank?
174
- url.gsub!(/https?:\/\//, '') # remove http:// or https://
175
- url.concat(".myshopify.com") unless url.include?('.') # extend url to myshopify.com if no host is given
176
- end
177
-
178
- def self.validate_signature(params)
179
- return false unless signature = params[:signature]
180
-
181
- sorted_params = params.except(:signature, :action, :controller).collect{|k,v|"#{k}=#{v}"}.sort.join
182
- Digest::MD5.hexdigest(secret + sorted_params) == signature
183
- end
184
- end
185
-
186
- class Base < ActiveResource::Base
187
- extend Countable
188
- end
189
-
190
- # Shop object. Use Shop.current to receive
191
- # the shop.
192
- class Shop < Base
193
- def self.current
194
- find(:one, :from => "/admin/shop.#{format.extension}")
195
- end
196
-
197
- def metafields
198
- Metafield.find(:all)
199
- end
200
-
201
- def add_metafield(metafield)
202
- raise ArgumentError, "You can only add metafields to resource that has been saved" if new?
203
- metafield.save
204
- metafield
205
- end
206
-
207
- def events
208
- Event.find(:all)
209
- end
210
- end
211
-
212
- # Custom collection
213
- #
214
- class CustomCollection < Base
215
- def products
216
- Product.find(:all, :params => {:collection_id => self.id})
217
- end
218
-
219
- def add_product(product)
220
- Collect.create(:collection_id => self.id, :product_id => product.id)
221
- end
222
-
223
- def remove_product(product)
224
- collect = Collect.find(:first, :params => {:collection_id => self.id, :product_id => product.id})
225
- collect.destroy if collect
226
- end
227
- end
228
-
229
- class SmartCollection < Base
230
- def products
231
- Product.find(:all, :params => {:collection_id => self.id})
232
- end
233
- end
234
-
235
- # For adding/removing products from custom collections
236
- class Collect < Base
237
- end
238
-
239
- class ShippingAddress < Base
240
- end
241
-
242
- class BillingAddress < Base
243
- end
244
-
245
- class LineItem < Base
246
- end
247
-
248
- class ShippingLine < Base
249
- end
250
-
251
- class NoteAttribute < Base
252
- end
253
-
254
- class Order < Base
255
- def close; load_attributes_from_response(post(:close, {}, only_id)); end
256
- def open; load_attributes_from_response(post(:open, {}, only_id)); end
257
-
258
- def cancel(options = {})
259
- load_attributes_from_response(post(:cancel, options, only_id))
260
- end
261
-
262
- def transactions
263
- Transaction.find(:all, :params => { :order_id => id })
264
- end
265
-
266
- def capture(amount = "")
267
- Transaction.create(:amount => amount, :kind => "capture", :order_id => id)
268
- end
269
-
270
- def only_id
271
- encode(:only => :id, :include => [], :methods => [], :fields => [])
272
- end
273
- end
274
-
275
- class Product < Base
276
-
277
- # Share all items of this store with the
278
- # shopify marketplace
279
- def self.share; post :share; end
280
- def self.unshare; delete :share; end
281
-
282
- # compute the price range
283
- def price_range
284
- prices = variants.collect(&:price)
285
- format = "%0.2f"
286
- if prices.min != prices.max
287
- "#{format % prices.min} - #{format % prices.max}"
288
- else
289
- format % prices.min
290
- end
291
- end
292
-
293
- def collections
294
- CustomCollection.find(:all, :params => {:product_id => self.id})
295
- end
296
-
297
- def smart_collections
298
- SmartCollection.find(:all, :params => {:product_id => self.id})
299
- end
300
-
301
- def add_to_collection(collection)
302
- collection.add_product(self)
303
- end
304
-
305
- def remove_from_collection(collection)
306
- collection.remove_product(self)
307
- end
308
- end
309
-
310
- class Variant < Base
311
- self.prefix = "/admin/products/:product_id/"
312
-
313
- def self.prefix(options={})
314
- options[:product_id].nil? ? "/admin/" : "/admin/products/#{options[:product_id]}/"
315
- end
316
- end
317
-
318
- class Image < Base
319
- self.prefix = "/admin/products/:product_id/"
320
-
321
- # generate a method for each possible image variant
322
- [:pico, :icon, :thumb, :small, :compact, :medium, :large, :grande, :original].each do |m|
323
- reg_exp_match = "/\\1_#{m}.\\2"
324
- define_method(m) { src.gsub(/\/(.*)\.(\w{2,4})/, reg_exp_match) }
325
- end
326
-
327
- def attach_image(data, filename = nil)
328
- attributes['attachment'] = Base64.encode64(data)
329
- attributes['filename'] = filename unless filename.nil?
330
- end
331
- end
332
-
333
- class Transaction < Base
334
- self.prefix = "/admin/orders/:order_id/"
335
- end
336
-
337
- class Fulfillment < Base
338
- self.prefix = "/admin/orders/:order_id/"
339
- end
340
-
341
- class Country < Base
342
- end
343
-
344
- class Page < Base
345
- end
346
-
347
- class Blog < Base
348
- def articles
349
- Article.find(:all, :params => { :blog_id => id })
350
- end
351
- end
352
-
353
- class Article < Base
354
- self.prefix = "/admin/blogs/:blog_id/"
355
-
356
- def comments
357
- Comment.find(:all, :params => { :article_id => id })
358
- end
359
- end
360
-
361
- class Metafield < Base
362
- self.prefix = "/admin/:resource/:resource_id/"
363
-
364
- # Hack to allow both Shop and other Metafields in through the same AR class
365
- def self.prefix(options={})
366
- options[:resource].nil? ? "/admin/" : "/admin/#{options[:resource]}/#{options[:resource_id]}/"
367
- end
368
-
369
- def value
370
- return if attributes["value"].nil?
371
- attributes["value_type"] == "integer" ? attributes["value"].to_i : attributes["value"]
372
- end
373
-
374
- end
375
-
376
- class Comment < Base
377
- def remove; load_attributes_from_response(post(:remove, {}, only_id)); end
378
- def ham; load_attributes_from_response(post(:ham, {}, only_id)); end
379
- def spam; load_attributes_from_response(post(:spam, {}, only_id)); end
380
- def approve; load_attributes_from_response(post(:approve, {}, only_id)); end
381
- def restore; load_attributes_from_response(post(:restore, {}, only_id)); end
382
- def not_spam; load_attributes_from_response(post(:not_spam, {}, only_id)); end
383
-
384
- def only_id
385
- encode(:only => :id)
386
- end
387
- end
388
-
389
- class Province < Base
390
- self.prefix = "/admin/countries/:country_id/"
391
- end
392
-
393
- class Redirect < Base
394
- end
395
-
396
- class Webhook < Base
397
- end
398
-
399
- class Event < Base
400
- self.prefix = "/admin/:resource/:resource_id/"
401
-
402
- # Hack to allow both Shop and other Events in through the same AR class
403
- def self.prefix(options={})
404
- options[:resource].nil? ? "/admin/" : "/admin/#{options[:resource]}/#{options[:resource_id]}/"
405
- end
406
- end
407
-
408
- class Customer < Base
409
- end
410
-
411
- class CustomerGroup < Base
412
- end
413
-
414
- # Assets represent the files that comprise your theme.
415
- # There are different buckets which hold different kinds
416
- # of assets, each corresponding to one of the folders
417
- # within a theme's zip file: layout, templates, and
418
- # assets. The full key of an asset always starts with the
419
- # bucket name, and the path separator is a forward slash,
420
- # like layout/theme.liquid or assets/bg-body.gif.
421
- #
422
- # Initialize with a key:
423
- # asset = ShopifyAPI::Asset.new(:key => 'assets/special.css')
424
- #
425
- # Find by key:
426
- # asset = ShopifyAPI::Asset.find('assets/image.png')
427
- #
428
- # Get the text or binary value:
429
- # asset.value # decodes from attachment attribute if necessary
430
- #
431
- # You can provide new data for assets in a few different ways:
432
- #
433
- # * assign text data for the value directly:
434
- # asset.value = "div.special {color:red;}"
435
- #
436
- # * provide binary data for the value:
437
- # asset.attach(File.read('image.png'))
438
- #
439
- # * set a URL from which Shopify will fetch the value:
440
- # asset.src = "http://mysite.com/image.png"
441
- #
442
- # * set a source key of another of your assets from which
443
- # the value will be copied:
444
- # asset.source_key = "assets/another_image.png"
445
- class Asset < Base
446
- self.primary_key = 'key'
447
-
448
- # find an asset by key:
449
- # ShopifyAPI::Asset.find('layout/theme.liquid')
450
- def self.find(*args)
451
- if args[0].is_a?(Symbol)
452
- super
453
- else
454
- params = {:asset => {:key => args[0]}}
455
- params = params.merge(args[1][:params]) if args[1] && args[1][:params]
456
- find(:one, :from => "/admin/assets.#{format.extension}", :params => params)
457
- end
458
- end
459
-
460
- # For text assets, Shopify returns the data in the 'value' attribute.
461
- # For binary assets, the data is base-64-encoded and returned in the
462
- # 'attachment' attribute. This accessor returns the data in both cases.
463
- def value
464
- attributes['value'] ||
465
- (attributes['attachment'] ? Base64.decode64(attributes['attachment']) : nil)
466
- end
467
-
468
- def attach(data)
469
- self.attachment = Base64.encode64(data)
470
- end
471
-
472
- def destroy #:nodoc:
473
- connection.delete(element_path(:asset => {:key => key}), self.class.headers)
474
- end
475
-
476
- def new? #:nodoc:
477
- false
478
- end
479
-
480
- def self.element_path(id, prefix_options = {}, query_options = nil) #:nodoc:
481
- prefix_options, query_options = split_options(prefix_options) if query_options.nil?
482
- "#{prefix(prefix_options)}#{collection_name}.#{format.extension}#{query_string(query_options)}"
483
- end
484
-
485
- def method_missing(method_symbol, *arguments) #:nodoc:
486
- if %w{value= attachment= src= source_key=}.include?(method_symbol)
487
- wipe_value_attributes
488
- end
489
- super
490
- end
491
-
492
- private
493
-
494
- def wipe_value_attributes
495
- %w{value attachment src source_key}.each do |attr|
496
- attributes.delete(attr)
497
- end
498
- end
499
- end
500
-
501
- class RecurringApplicationCharge < Base
502
- undef_method :test
503
-
504
- def self.current
505
- find(:all).find{|charge| charge.status == 'active'}
506
- end
507
-
508
- def cancel
509
- load_attributes_from_response(self.destroy)
510
- end
511
-
512
- def activate
513
- load_attributes_from_response(post(:activate))
514
- end
515
- end
516
-
517
- class ApplicationCharge < Base
518
- undef_method :test
519
-
520
- def activate
521
- load_attributes_from_response(post(:activate))
522
- end
523
- end
524
-
525
- class ProductSearchEngine < Base
526
- end
527
-
528
- class ScriptTag < Base
529
- end
530
-
531
- # Include Metafields module in all enabled classes
532
- METAFIELD_ENABLED_CLASSES.each do |klass|
533
- "ShopifyAPI::#{klass}".constantize.send(:include, Metafields)
534
- end
535
-
536
- # Include Events module in all enabled classes
537
- EVENT_ENABLED_CLASSES.each do |klass|
538
- "ShopifyAPI::#{klass}".constantize.send(:include, Events)
539
- end
12
+ include Limits
540
13
  end
14
+
15
+ require 'shopify_api/events'
16
+ require 'shopify_api/metafields'
17
+ require 'shopify_api/countable'
18
+ require 'shopify_api/resources'
19
+ require 'shopify_api/session'