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.
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'