th_shopify_api 1.2.6.pre

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) hide show
  1. data/.document +5 -0
  2. data/.gitignore +5 -0
  3. data/CHANGELOG +57 -0
  4. data/LICENSE +20 -0
  5. data/README.rdoc +60 -0
  6. data/RELEASING +16 -0
  7. data/Rakefile +41 -0
  8. data/bin/shopify +4 -0
  9. data/lib/active_resource/connection_ext.rb +16 -0
  10. data/lib/shopify_api.rb +18 -0
  11. data/lib/shopify_api/countable.rb +7 -0
  12. data/lib/shopify_api/events.rb +7 -0
  13. data/lib/shopify_api/limits.rb +76 -0
  14. data/lib/shopify_api/metafields.rb +18 -0
  15. data/lib/shopify_api/resources.rb +40 -0
  16. data/lib/shopify_api/resources/address.rb +4 -0
  17. data/lib/shopify_api/resources/application_charge.rb +9 -0
  18. data/lib/shopify_api/resources/article.rb +12 -0
  19. data/lib/shopify_api/resources/asset.rb +95 -0
  20. data/lib/shopify_api/resources/base.rb +5 -0
  21. data/lib/shopify_api/resources/billing_address.rb +4 -0
  22. data/lib/shopify_api/resources/blog.rb +10 -0
  23. data/lib/shopify_api/resources/cli.rb +161 -0
  24. data/lib/shopify_api/resources/collect.rb +5 -0
  25. data/lib/shopify_api/resources/comment.rb +13 -0
  26. data/lib/shopify_api/resources/countable.rb +7 -0
  27. data/lib/shopify_api/resources/country.rb +4 -0
  28. data/lib/shopify_api/resources/custom_collection.rb +19 -0
  29. data/lib/shopify_api/resources/customer.rb +4 -0
  30. data/lib/shopify_api/resources/customer_group.rb +4 -0
  31. data/lib/shopify_api/resources/event.rb +10 -0
  32. data/lib/shopify_api/resources/fulfillment.rb +5 -0
  33. data/lib/shopify_api/resources/image.rb +16 -0
  34. data/lib/shopify_api/resources/line_item.rb +4 -0
  35. data/lib/shopify_api/resources/metafield.rb +15 -0
  36. data/lib/shopify_api/resources/note_attribute.rb +4 -0
  37. data/lib/shopify_api/resources/option.rb +4 -0
  38. data/lib/shopify_api/resources/order.rb +25 -0
  39. data/lib/shopify_api/resources/page.rb +6 -0
  40. data/lib/shopify_api/resources/payment_details.rb +4 -0
  41. data/lib/shopify_api/resources/product.rb +33 -0
  42. data/lib/shopify_api/resources/product_search_engine.rb +4 -0
  43. data/lib/shopify_api/resources/province.rb +5 -0
  44. data/lib/shopify_api/resources/receipt.rb +4 -0
  45. data/lib/shopify_api/resources/recurring_application_charge.rb +23 -0
  46. data/lib/shopify_api/resources/redirect.rb +4 -0
  47. data/lib/shopify_api/resources/rule.rb +4 -0
  48. data/lib/shopify_api/resources/script_tag.rb +4 -0
  49. data/lib/shopify_api/resources/shipping_address.rb +4 -0
  50. data/lib/shopify_api/resources/shipping_line.rb +4 -0
  51. data/lib/shopify_api/resources/shop.rb +23 -0
  52. data/lib/shopify_api/resources/smart_collection.rb +10 -0
  53. data/lib/shopify_api/resources/tax_line.rb +4 -0
  54. data/lib/shopify_api/resources/theme.rb +4 -0
  55. data/lib/shopify_api/resources/transaction.rb +5 -0
  56. data/lib/shopify_api/resources/variant.rb +11 -0
  57. data/lib/shopify_api/resources/webhook.rb +4 -0
  58. data/lib/shopify_api/session.rb +166 -0
  59. data/shopify_api.gemspec +35 -0
  60. data/test/cli_test.rb +109 -0
  61. data/test/limits_test.rb +37 -0
  62. data/test/order_test.rb +48 -0
  63. data/test/shopify_api_test.rb +55 -0
  64. data/test/test_helper.rb +29 -0
  65. metadata +153 -0
@@ -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,166 @@
1
+ module ShopifyAPI
2
+ #
3
+ # The Shopify API authenticates each call via HTTP Authentication, using
4
+ # * the application's API key as the username, and
5
+ # * a hex digest of the application's shared secret and an
6
+ # authentication token as the password.
7
+ #
8
+ # Generation & acquisition of the beforementioned looks like this:
9
+ #
10
+ # 0. Developer (that's you) registers Application (and provides a
11
+ # callback url) and receives an API key and a shared secret
12
+ #
13
+ # 1. User visits Application and are told they need to authenticate the
14
+ # application first for read/write permission to their data (needs to
15
+ # happen only once). User is asked for their shop url.
16
+ #
17
+ # 2. Application redirects to Shopify : GET <user's shop url>/admin/api/auth?api_key=<API key>
18
+ # (See Session#create_permission_url)
19
+ #
20
+ # 3. User logs-in to Shopify, approves application permission request
21
+ #
22
+ # 4. Shopify redirects to the Application's callback url (provided during
23
+ # registration), including the shop's name, and an authentication token in the parameters:
24
+ # GET client.com/customers?shop=snake-oil.myshopify.com&t=a94a110d86d2452eb3e2af4cfb8a3828
25
+ #
26
+ # 5. Authentication password computed using the shared secret and the
27
+ # authentication token (see Session#computed_password)
28
+ #
29
+ # 6. Profit!
30
+ # (API calls can now authenticate through HTTP using the API key, and
31
+ # computed password)
32
+ #
33
+ # LoginController and ShopifyLoginProtection use the Session class to set Shopify::Base.site
34
+ # so that all API calls are authorized transparently and end up just looking like this:
35
+ #
36
+ # # get 3 products
37
+ # @products = ShopifyAPI::Product.find(:all, :params => {:limit => 3})
38
+ #
39
+ # # get latest 3 orders
40
+ # @orders = ShopifyAPI::Order.find(:all, :params => {:limit => 3, :order => "created_at DESC" })
41
+ #
42
+ # As an example of what your LoginController should look like, take a look
43
+ # at the following:
44
+ #
45
+ # class LoginController < ApplicationController
46
+ # def index
47
+ # # Ask user for their #{shop}.myshopify.com address
48
+ # end
49
+ #
50
+ # def authenticate
51
+ # redirect_to ShopifyAPI::Session.new(params[:shop]).create_permission_url
52
+ # end
53
+ #
54
+ # # Shopify redirects the logged-in user back to this action along with
55
+ # # the authorization token t.
56
+ # #
57
+ # # This token is later combined with the developer's shared secret to form
58
+ # # the password used to call API methods.
59
+ # def finalize
60
+ # shopify_session = ShopifyAPI::Session.new(params[:shop], params[:t])
61
+ # if shopify_session.valid?
62
+ # session[:shopify] = shopify_session
63
+ # flash[:notice] = "Logged in to shopify store."
64
+ #
65
+ # return_address = session[:return_to] || '/home'
66
+ # session[:return_to] = nil
67
+ # redirect_to return_address
68
+ # else
69
+ # flash[:error] = "Could not log in to Shopify store."
70
+ # redirect_to :action => 'index'
71
+ # end
72
+ # end
73
+ #
74
+ # def logout
75
+ # session[:shopify] = nil
76
+ # flash[:notice] = "Successfully logged out."
77
+ #
78
+ # redirect_to :action => 'index'
79
+ # end
80
+ # end
81
+ #
82
+ class Session
83
+ cattr_accessor :api_key
84
+ cattr_accessor :secret
85
+ cattr_accessor :protocol
86
+ self.protocol = 'https'
87
+
88
+ attr_accessor :url, :token, :name
89
+
90
+ class << self
91
+
92
+ def setup(params)
93
+ params.each { |k,value| send("#{k}=", value) }
94
+ end
95
+
96
+ def temp(domain, token, &block)
97
+ session = new(domain, token)
98
+
99
+ original_site = ShopifyAPI::Base.site
100
+ begin
101
+ ShopifyAPI::Base.site = session.site
102
+ yield
103
+ ensure
104
+ ShopifyAPI::Base.site = original_site
105
+ end
106
+ end
107
+
108
+ def prepare_url(url)
109
+ return nil if url.blank?
110
+ url.gsub!(/https?:\/\//, '') # remove http:// or https://
111
+ url.concat(".myshopify.com") unless url.include?('.') # extend url to myshopify.com if no host is given
112
+ end
113
+
114
+ def validate_signature(params)
115
+ return false unless signature = params[:signature]
116
+ return true if defined?(Rails) && Rails.env.test?
117
+
118
+ sorted_params = params.except(:signature, :action, :controller).collect{|k,v|"#{k}=#{v}"}.sort.join
119
+ Digest::MD5.hexdigest(secret + sorted_params) == signature
120
+ end
121
+
122
+ end
123
+
124
+ def initialize(url, token = nil, params = nil)
125
+ self.url, self.token = url, token
126
+
127
+ if params
128
+ unless self.class.validate_signature(params) && params[:timestamp].to_i > 24.hours.ago.utc.to_i
129
+ raise "Invalid Signature: Possible malicious login"
130
+ end
131
+ end
132
+
133
+ self.class.prepare_url(self.url)
134
+ end
135
+
136
+ def shop
137
+ Shop.current
138
+ end
139
+
140
+ def create_permission_url
141
+ return nil if url.blank? || api_key.blank?
142
+ "http://#{url}/admin/api/auth?api_key=#{api_key}"
143
+ end
144
+
145
+ # Used by ActiveResource::Base to make all non-authentication API calls
146
+ #
147
+ # (ShopifyAPI::Base.site set in ShopifyLoginProtection#shopify_session)
148
+ def site
149
+ "#{protocol}://#{api_key}:#{computed_password}@#{url}/admin"
150
+ end
151
+
152
+ def valid?
153
+ url.present? && token.present?
154
+ end
155
+
156
+ private
157
+
158
+ # The secret is computed by taking the shared_secret which we got when
159
+ # registring this third party application and concating the request_to it,
160
+ # and then calculating a MD5 hexdigest.
161
+ def computed_password
162
+ Digest::MD5.hexdigest(secret + token.to_s)
163
+ end
164
+
165
+ end
166
+ end
@@ -0,0 +1,35 @@
1
+ # -*- encoding: utf-8 -*-
2
+ $:.push File.expand_path("../lib", __FILE__)
3
+
4
+ Gem::Specification.new do |s|
5
+ s.name = "th_shopify_api"
6
+ s.version = "1.2.6.pre"
7
+ s.authors = ["TravisHaynes", "Shopify"]
8
+ s.email = ["travis.j.haynes@gmail.com", "developers@jadedpixel.com"]
9
+ s.homepage = %q{http://www.shopify.com/partners/apps}
10
+ s.summary = %q{The Shopify API gem is a lightweight gem for accessing the Shopify admin REST web services}
11
+ s.description = %q{The Shopify API gem allows Ruby developers to programmatically access the admin section of Shopify stores. The API is implemented as XML over HTTP using all four verbs (GET/POST/PUT/DELETE). Each resource, like Order, Product, or Collection, has its own URL and is manipulated in isolation.}
12
+
13
+ s.extra_rdoc_files = [
14
+ "LICENSE",
15
+ "README.rdoc"
16
+ ]
17
+
18
+ s.files = `git ls-files`.split("\n")
19
+ s.test_files = `git ls-files -- {test}/*`.split("\n")
20
+ s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
21
+ s.require_paths = ["lib"]
22
+
23
+ s.rdoc_options = ["--charset=UTF-8"]
24
+ s.license = 'MIT'
25
+
26
+ s.add_dependency("activeresource", [">= 2.2.2"])
27
+ s.add_dependency("thor", [">= 0.14.4"])
28
+
29
+ if s.respond_to?(:add_development_dependency)
30
+ s.add_development_dependency("mocha", ">= 0.9.8")
31
+ else
32
+ s.add_dependency("mocha", ">= 0.9.8")
33
+ end
34
+ end
35
+
@@ -0,0 +1,109 @@
1
+ require 'test_helper'
2
+ require 'shopify_api/cli'
3
+ require 'fileutils'
4
+
5
+ class CliTest < Test::Unit::TestCase
6
+ def setup
7
+ @test_home = File.join(File.expand_path(File.dirname(__FILE__)), 'files', 'home')
8
+ @shop_config_dir = File.join(@test_home, '.shopify', 'shops')
9
+ @default_symlink = File.join(@shop_config_dir, 'default')
10
+ `rm -rf #{@test_home}`
11
+ ENV['HOME'] = @test_home
12
+ @cli = ShopifyAPI::Cli.new
13
+
14
+ FileUtils.mkdir_p(@shop_config_dir)
15
+ File.open(config_file('foo'), 'w') do |file|
16
+ file.puts valid_options.merge('domain' => 'foo.myshopify.com').to_yaml
17
+ end
18
+ File.symlink(config_file('foo'), @default_symlink)
19
+ File.open(config_file('bar'), 'w') do |file|
20
+ file.puts valid_options.merge('domain' => 'bar.myshopify.com').to_yaml
21
+ end
22
+ end
23
+
24
+ def teardown
25
+ `rm -rf #{@test_home}`
26
+ end
27
+
28
+ test "add with blank domain" do
29
+ `rm -rf #{@shop_config_dir}/*`
30
+ $stdout.expects(:print).with("Domain? (leave blank for foo.myshopify.com) ")
31
+ $stdout.expects(:print).with("API key? ")
32
+ $stdout.expects(:print).with("Password? ")
33
+ $stdin.expects(:gets).times(3).returns("", "key", "pass")
34
+ @cli.expects(:puts).with("\nopen https://foo.myshopify.com/admin/api in your browser to get API credentials\n")
35
+ @cli.expects(:puts).with("Default connection is foo")
36
+
37
+ @cli.add('foo')
38
+
39
+ config = YAML.load(File.read(config_file('foo')))
40
+ assert_equal 'foo.myshopify.com', config['domain']
41
+ assert_equal 'key', config['api_key']
42
+ assert_equal 'pass', config['password']
43
+ assert_equal 'https', config['protocol']
44
+ assert_equal config_file('foo'), File.readlink(@default_symlink)
45
+ end
46
+
47
+ test "add with explicit domain" do
48
+ `rm -rf #{@shop_config_dir}/*`
49
+ $stdout.expects(:print).with("Domain? (leave blank for foo.myshopify.com) ")
50
+ $stdout.expects(:print).with("API key? ")
51
+ $stdout.expects(:print).with("Password? ")
52
+ $stdin.expects(:gets).times(3).returns("bar.myshopify.com", "key", "pass")
53
+ @cli.expects(:puts).with("\nopen https://bar.myshopify.com/admin/api in your browser to get API credentials\n")
54
+ @cli.expects(:puts).with("Default connection is foo")
55
+
56
+ @cli.add('foo')
57
+
58
+ config = YAML.load(File.read(config_file('foo')))
59
+ assert_equal 'bar.myshopify.com', config['domain']
60
+ end
61
+
62
+ test "list" do
63
+ @cli.expects(:puts).with(" bar")
64
+ @cli.expects(:puts).with(" * foo")
65
+
66
+ @cli.list
67
+ end
68
+
69
+ test "show default" do
70
+ @cli.expects(:puts).with("Default connection is foo")
71
+
72
+ @cli.default
73
+ end
74
+
75
+ test "set default" do
76
+ @cli.expects(:puts).with("Default connection is bar")
77
+
78
+ @cli.default('bar')
79
+
80
+ assert_equal config_file('bar'), File.readlink(@default_symlink)
81
+ end
82
+
83
+ test "remove default connection" do
84
+ @cli.remove('foo')
85
+
86
+ assert !File.exist?(@default_symlink)
87
+ assert !File.exist?(config_file('foo'))
88
+ assert File.exist?(config_file('bar'))
89
+ end
90
+
91
+ test "remove non-default connection" do
92
+ @cli.remove('bar')
93
+
94
+ assert_equal config_file('foo'), File.readlink(@default_symlink)
95
+ assert File.exist?(config_file('foo'))
96
+ assert !File.exist?(config_file('bar'))
97
+ end
98
+
99
+ private
100
+
101
+ def valid_options
102
+ {'domain' => 'snowdevil.myshopify.com', 'api_key' => 'key', 'password' => 'pass', 'protocol' => 'https'}
103
+ end
104
+
105
+ def config_file(connection)
106
+ File.join(@shop_config_dir, "#{connection}.yml")
107
+ end
108
+
109
+ end
@@ -0,0 +1,37 @@
1
+ require 'test_helper'
2
+ require 'mocha'
3
+
4
+ class LimitsTest < Test::Unit::TestCase
5
+ def setup
6
+ ShopifyAPI::Base.site = "test.myshopify.com"
7
+ @header_hash = {'http_x_shopify_api_call_limit' => '150/3000',
8
+ 'http_x_shopify_shop_api_call_limit' => '100/300'}
9
+ ShopifyAPI::Base.connection.expects(:response).at_least(0).returns(@header_hash)
10
+ end
11
+
12
+ context "Limits" do
13
+ should "fetch limit total" do
14
+ assert_equal(299, ShopifyAPI.credit_limit(:shop))
15
+ assert_equal(2999, ShopifyAPI.credit_limit(:global))
16
+ end
17
+
18
+ should "fetch used calls" do
19
+ assert_equal(100, ShopifyAPI.credit_used(:shop))
20
+ assert_equal(150, ShopifyAPI.credit_used(:global))
21
+ end
22
+
23
+ should "calculate remaining calls" do
24
+ assert_equal(199, ShopifyAPI.credit_left)
25
+ end
26
+
27
+ should "flag maxed out credits" do
28
+ assert !ShopifyAPI.maxed?
29
+ @header_hash = {'http_x_shopify_api_call_limit' => '2999/3000',
30
+ 'http_x_shopify_shop_api_call_limit' => '299/300'}
31
+ ShopifyAPI::Base.connection.expects(:response).at_least(1).returns(@header_hash)
32
+ assert ShopifyAPI.maxed?
33
+ end
34
+ end
35
+
36
+
37
+ end