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