shopify_api 6.0.0 → 7.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (57) hide show
  1. checksums.yaml +4 -4
  2. data/.travis.yml +10 -19
  3. data/CHANGELOG +11 -0
  4. data/Gemfile +1 -2
  5. data/Gemfile_ar41 +5 -0
  6. data/Gemfile_ar50 +5 -0
  7. data/Gemfile_ar51 +5 -0
  8. data/Gemfile_ar_master +0 -1
  9. data/README.md +116 -9
  10. data/RELEASING +2 -5
  11. data/lib/shopify_api.rb +4 -4
  12. data/lib/shopify_api/api_version.rb +116 -0
  13. data/lib/shopify_api/disable_prefix_check.rb +31 -0
  14. data/lib/shopify_api/limits.rb +5 -5
  15. data/lib/shopify_api/resources/access_scope.rb +6 -1
  16. data/lib/shopify_api/resources/asset.rb +15 -11
  17. data/lib/shopify_api/resources/base.rb +63 -1
  18. data/lib/shopify_api/resources/fulfillment_event.rb +1 -1
  19. data/lib/shopify_api/resources/graphql.rb +1 -1
  20. data/lib/shopify_api/resources/inventory_level.rb +2 -2
  21. data/lib/shopify_api/resources/location.rb +1 -1
  22. data/lib/shopify_api/resources/marketing_event.rb +2 -0
  23. data/lib/shopify_api/resources/payment.rb +1 -1
  24. data/lib/shopify_api/resources/refund.rb +4 -3
  25. data/lib/shopify_api/resources/shipping_rate.rb +1 -1
  26. data/lib/shopify_api/resources/shop.rb +4 -2
  27. data/lib/shopify_api/resources/smart_collection.rb +1 -1
  28. data/lib/shopify_api/session.rb +45 -16
  29. data/lib/shopify_api/version.rb +1 -1
  30. data/shopify_api.gemspec +3 -2
  31. data/test/access_scope_test.rb +23 -0
  32. data/test/api_version_test.rb +144 -0
  33. data/test/base_test.rb +75 -32
  34. data/test/detailed_log_subscriber_test.rb +51 -12
  35. data/test/fixtures/access_scopes.json +10 -0
  36. data/test/limits_test.rb +2 -2
  37. data/test/marketing_event_test.rb +1 -1
  38. data/test/recurring_application_charge_test.rb +3 -9
  39. data/test/session_test.rb +158 -32
  40. data/test/test_helper.rb +27 -11
  41. metadata +33 -21
  42. data/Gemfile_ar30 +0 -6
  43. data/Gemfile_ar31 +0 -6
  44. data/Gemfile_ar32 +0 -6
  45. data/Gemfile_ar40 +0 -6
  46. data/lib/active_resource/base_ext.rb +0 -21
  47. data/lib/active_resource/disable_prefix_check.rb +0 -36
  48. data/lib/active_resource/to_query.rb +0 -10
  49. data/lib/shopify_api/json_format.rb +0 -18
  50. data/lib/shopify_api/resources/o_auth.rb +0 -17
  51. data/lib/shopify_api/resources/ping/conversation.rb +0 -42
  52. data/lib/shopify_api/resources/ping/delivery_confirmation_details.rb +0 -10
  53. data/lib/shopify_api/resources/ping/message.rb +0 -8
  54. data/test/fixtures/o_auth_revoke.json +0 -5
  55. data/test/o_auth_test.rb +0 -8
  56. data/test/ping/conversation_test.rb +0 -71
  57. data/test/ping/message_test.rb +0 -23
@@ -7,14 +7,14 @@ module ShopifyAPI
7
7
  end
8
8
 
9
9
  module ClassMethods
10
-
11
- # Takes form num_requests_executed/max_requests
12
- # Eg: 101/3000
10
+ # Takes form <call count>/<bucket size>
11
+ # See https://help.shopify.com/en/api/getting-started/api-call-limit
12
+ # Eg: 2/40
13
13
  CREDIT_LIMIT_HEADER_PARAM = {
14
- :shop => 'http_x_shopify_shop_api_call_limit'
14
+ shop: 'X-Shopify-Shop-Api-Call-Limit',
15
15
  }
16
16
 
17
- ##
17
+ ##
18
18
  # How many more API calls can I make?
19
19
  # @return {Integer}
20
20
  #
@@ -1,5 +1,10 @@
1
+ # frozen_string_literal: true
1
2
  module ShopifyAPI
2
3
  class AccessScope < Base
3
- self.prefix = '/admin/oauth/'
4
+ class << self
5
+ def prefix(_options={})
6
+ '/admin/oauth/'
7
+ end
8
+ end
4
9
  end
5
10
  end
@@ -10,24 +10,24 @@ module ShopifyAPI
10
10
  #
11
11
  # Initialize with a key:
12
12
  # asset = ShopifyAPI::Asset.new(:key => 'assets/special.css', :theme_id => 12345)
13
- #
13
+ #
14
14
  # Find by key:
15
15
  # asset = ShopifyAPI::Asset.find('assets/image.png', :params => {:theme_id => 12345})
16
- #
16
+ #
17
17
  # Get the text or binary value:
18
18
  # asset.value # decodes from attachment attribute if necessary
19
- #
19
+ #
20
20
  # You can provide new data for assets in a few different ways:
21
- #
21
+ #
22
22
  # * assign text data for the value directly:
23
23
  # asset.value = "div.special {color:red;}"
24
- #
24
+ #
25
25
  # * provide binary data for the value:
26
26
  # asset.attach(File.read('image.png'))
27
- #
27
+ #
28
28
  # * set a URL from which Shopify will fetch the value:
29
29
  # asset.src = "http://mysite.com/image.png"
30
- #
30
+ #
31
31
  # * set a source key of another of your assets from which
32
32
  # the value will be copied:
33
33
  # asset.source_key = "assets/another_image.png"
@@ -44,15 +44,19 @@ module ShopifyAPI
44
44
  end
45
45
 
46
46
  # find an asset by key:
47
- # ShopifyAPI::Asset.find('layout/theme.liquid', :params => {:theme_id => 99})
47
+ # ShopifyAPI::Asset.find('layout/theme.liquid', :params => { theme_id: 99 })
48
48
  def self.find(*args)
49
49
  if args[0].is_a?(Symbol)
50
50
  super
51
51
  else
52
- params = {:asset => {:key => args[0]}}
52
+ params = { asset: { key: args[0] } }
53
53
  params = params.merge(args[1][:params]) if args[1] && args[1][:params]
54
- path_prefix = params[:theme_id] ? "/admin/themes/#{params[:theme_id]}" : "/admin"
55
- resource = find(:one, :from => "#{path_prefix}/assets.#{format.extension}", :params => params)
54
+ path_prefix = params[:theme_id] ? "themes/#{params[:theme_id]}/" : ""
55
+ resource = find(
56
+ :one,
57
+ from: api_version.construct_api_path("#{path_prefix}assets.#{format.extension}"),
58
+ params: params
59
+ )
56
60
  resource.prefix_options[:theme_id] = params[:theme_id] if resource && params[:theme_id]
57
61
  resource
58
62
  end
@@ -4,6 +4,7 @@ module ShopifyAPI
4
4
  class Base < ActiveResource::Base
5
5
  class InvalidSessionError < StandardError; end
6
6
  extend Countable
7
+
7
8
  self.timeout = 90
8
9
  self.include_root_in_json = false
9
10
  self.headers['User-Agent'] = ["ShopifyAPI/#{ShopifyAPI::VERSION}",
@@ -28,6 +29,8 @@ module ShopifyAPI
28
29
  end
29
30
 
30
31
  class << self
32
+ threadsafe_attribute(:_api_version)
33
+
31
34
  if ActiveResource::Base.respond_to?(:_headers) && ActiveResource::Base.respond_to?(:_headers_defined?)
32
35
  def headers
33
36
  if _headers_defined?
@@ -54,21 +57,80 @@ module ShopifyAPI
54
57
  raise InvalidSessionError.new("Session cannot be nil") if session.nil?
55
58
  self.site = session.site
56
59
  self.headers.merge!('X-Shopify-Access-Token' => session.token)
60
+ self.api_version = session.api_version
57
61
  end
58
62
 
59
63
  def clear_session
60
64
  self.site = nil
61
65
  self.password = nil
62
66
  self.user = nil
67
+ self.api_version = nil
63
68
  self.headers.delete('X-Shopify-Access-Token')
64
69
  end
65
70
 
71
+ def api_version
72
+ if _api_version_defined?
73
+ _api_version
74
+ elsif superclass != Object && superclass.site
75
+ superclass.api_version.dup.freeze
76
+ end
77
+ end
78
+
79
+ def api_version=(value)
80
+ self._api_version = value
81
+ end
82
+
83
+ def prefix(options = {})
84
+ api_version.construct_api_path(resource_prefix(options))
85
+ end
86
+
87
+ def prefix_source
88
+ ''
89
+ end
90
+
91
+ def resource_prefix(_options = {})
92
+ ''
93
+ end
94
+
95
+ # Sets the \prefix for a resource's nested URL (e.g., <tt>prefix/collectionname/1.json</tt>).
96
+ # Default value is <tt>site.path</tt>.
97
+ def resource_prefix=(value)
98
+ @prefix_parameters = nil
99
+
100
+ resource_prefix_call = value.gsub(/:\w+/) { |key| "\#{URI.parser.escape options[#{key}].to_s}" }
101
+
102
+ silence_warnings do
103
+ # Redefine the new methods.
104
+ instance_eval <<-RUBY_EVAL, __FILE__, __LINE__ + 1
105
+ def prefix_source() "#{value}" end
106
+ def resource_prefix(options={}) "#{resource_prefix_call}" end
107
+ RUBY_EVAL
108
+ end
109
+ rescue => e
110
+ logger&.error("Couldn't set prefix: #{e}\n #{code}")
111
+ raise
112
+ end
113
+
114
+ def prefix=(value)
115
+ if value.start_with?('/admin')
116
+ raise ArgumentError, "'#{value}' can no longer start /admin/. Change to using resource_prefix="
117
+ end
118
+
119
+ warn(
120
+ '[DEPRECATED] ShopifyAPI::Base#prefix= is deprecated and will be removed in a future version. ' \
121
+ 'Use `self.resource_prefix=` instead.'
122
+ )
123
+ self.resource_prefix = value
124
+ end
125
+
126
+ alias_method :set_prefix, :prefix=
127
+
66
128
  def init_prefix(resource)
67
129
  init_prefix_explicit(resource.to_s.pluralize, "#{resource}_id")
68
130
  end
69
131
 
70
132
  def init_prefix_explicit(resource_type, resource_id)
71
- self.prefix = "/admin/#{resource_type}/:#{resource_id}/"
133
+ self.resource_prefix = "#{resource_type}/:#{resource_id}/"
72
134
 
73
135
  define_method resource_id.to_sym do
74
136
  @prefix_options[resource_id]
@@ -1,6 +1,6 @@
1
1
  module ShopifyAPI
2
2
  class FulfillmentEvent < Base
3
- self.prefix = '/admin/orders/:order_id/fulfillments/:fulfillment_id/'
3
+ self.resource_prefix = "orders/:order_id/fulfillments/:fulfillment_id/"
4
4
  self.collection_name = 'events'
5
5
  self.element_name = 'event'
6
6
 
@@ -7,7 +7,7 @@ module ShopifyAPI
7
7
  class GraphQL
8
8
  def initialize
9
9
  uri = Base.site.dup
10
- uri.path = '/admin/api/graphql.json'
10
+ uri.path = Base.api_version.construct_graphql_path
11
11
  @http = ::GraphQL::Client::HTTP.new(uri.to_s) do
12
12
  define_method(:headers) do |_context|
13
13
  Base.headers
@@ -4,11 +4,11 @@ module ShopifyAPI
4
4
  class InventoryLevel < Base
5
5
 
6
6
  # The default path structure in ActiveResource for delete would result in:
7
- # /admin/inventory_levels/#{ inventory_level.id }.json?#{ params }, but since
7
+ # /admin/api/<version>/inventory_levels/#{ inventory_level.id }.json?#{ params }, but since
8
8
  # InventroyLevels are a second class resource made up of a Where and a What
9
9
  # (Location and InventoryItem), it does not have a resource ID. Here we
10
10
  # redefine element_path to remove the id so HTTP DELETE requests go to
11
- # /admin/inventory_levels.json?#{ params } instead.
11
+ # /admin/api/<version>/inventory_levels.json?#{ params } instead.
12
12
  #
13
13
  def self.element_path(prefix_options = {}, query_options = nil)
14
14
  prefix_options, query_options = split_options(prefix_options) if query_options.nil?
@@ -2,7 +2,7 @@ module ShopifyAPI
2
2
  class Location < Base
3
3
 
4
4
  def inventory_levels
5
- ShopifyAPI::InventoryLevel.find(:all, from: "/admin/locations/#{id}/inventory_levels.json")
5
+ ShopifyAPI::InventoryLevel.find(:all, from: "#{self.class.prefix}locations/#{id}/inventory_levels.json")
6
6
  end
7
7
  end
8
8
  end
@@ -1,5 +1,7 @@
1
1
  module ShopifyAPI
2
2
  class MarketingEvent < Base
3
+ include Countable
4
+
3
5
  def add_engagements(engagements)
4
6
  engagements = { engagements: Array.wrap(engagements) }
5
7
  post(:engagements, {}, engagements.to_json)
@@ -2,6 +2,6 @@
2
2
 
3
3
  module ShopifyAPI
4
4
  class Payment < Base
5
- self.prefix = '/admin/checkouts/:checkout_id/'
5
+ self.resource_prefix = "checkouts/:checkout_id/"
6
6
  end
7
7
  end
@@ -4,9 +4,10 @@ module ShopifyAPI
4
4
 
5
5
  def self.calculate(*args)
6
6
  options = { :refund => args[0] }
7
- params = options.merge(args[1][:params]) if args[1] && args[1][:params]
8
- self.prefix = "/admin/orders/#{params[:order_id]}/"
9
- resource = post(:calculate, {}, options.to_json)
7
+ params = {}
8
+ params = args[1][:params] if args[1] && args[1][:params]
9
+
10
+ resource = post(:calculate, params, options.to_json)
10
11
  instantiate_record(format.decode(resource.body), {})
11
12
  end
12
13
  end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module ShopifyAPI
4
4
  class ShippingRate < Base
5
- self.prefix = '/admin/checkouts/:checkout_id/'
5
+ self.resource_prefix = "checkouts/:checkout_id/"
6
6
  end
7
7
  end
@@ -2,8 +2,10 @@ module ShopifyAPI
2
2
  # Shop object. Use Shop.current to receive
3
3
  # the shop.
4
4
  class Shop < Base
5
- def self.current(options={})
6
- find(:one, options.merge({from: "/admin/shop.#{format.extension}"}))
5
+ include ActiveResource::Singleton
6
+
7
+ def self.current(options = {})
8
+ find(options)
7
9
  end
8
10
 
9
11
  def metafields(**options)
@@ -5,7 +5,7 @@ module ShopifyAPI
5
5
 
6
6
  def products(options = {})
7
7
  if options.present?
8
- Product.find(:all, from: "/admin/smart_collections/#{id}/products.json", params: options)
8
+ Product.find(:all, from: "#{self.class.prefix}smart_collections/#{id}/products.json", params: options)
9
9
  else
10
10
  Product.find(:all, params: { collection_id: id })
11
11
  end
@@ -10,7 +10,9 @@ module ShopifyAPI
10
10
  cattr_accessor :api_key, :secret, :myshopify_domain
11
11
  self.myshopify_domain = 'myshopify.com'
12
12
 
13
- attr_accessor :url, :token, :name, :extra
13
+ attr_accessor :domain, :token, :name, :extra
14
+ attr_reader :api_version
15
+ alias_method :url, :domain
14
16
 
15
17
  class << self
16
18
 
@@ -18,11 +20,14 @@ module ShopifyAPI
18
20
  params.each { |k,value| public_send("#{k}=", value) }
19
21
  end
20
22
 
21
- def temp(domain, token, &block)
22
- session = new(domain, token)
23
- original_site = ShopifyAPI::Base.site.to_s
24
- original_token = ShopifyAPI::Base.headers['X-Shopify-Access-Token']
25
- original_session = new(original_site, original_token)
23
+ def temp(domain:, token:, api_version:, &block)
24
+ session = new(domain: domain, token: token, api_version: api_version)
25
+
26
+ with_session(session, &block)
27
+ end
28
+
29
+ def with_session(session, &_block)
30
+ original_session = extract_current_session
26
31
 
27
32
  begin
28
33
  ShopifyAPI::Base.activate_session(session)
@@ -32,12 +37,19 @@ module ShopifyAPI
32
37
  end
33
38
  end
34
39
 
35
- def prepare_url(url)
36
- return nil if url.blank?
40
+ def with_version(api_version, &block)
41
+ original_session = extract_current_session
42
+ session = new(domain: original_session.site, token: original_session.token, api_version: api_version)
43
+
44
+ with_session(session, &block)
45
+ end
46
+
47
+ def prepare_domain(domain)
48
+ return nil if domain.blank?
37
49
  # remove http:// or https://
38
- url = url.strip.gsub(/\Ahttps?:\/\//, '')
50
+ domain = domain.strip.gsub(%r{\Ahttps?://}, '')
39
51
  # extract host, removing any username, password or path
40
- shop = URI.parse("https://#{url}").host
52
+ shop = URI.parse("https://#{domain}").host
41
53
  # extract subdomain of .myshopify.com
42
54
  if idx = shop.index(".")
43
55
  shop = shop.slice(0, idx)
@@ -63,10 +75,18 @@ module ShopifyAPI
63
75
  params = params.except(:signature, :hmac, :action, :controller)
64
76
  params.map{|k,v| "#{URI.escape(k.to_s, '&=%')}=#{URI.escape(v.to_s, '&%')}"}.sort.join('&')
65
77
  end
78
+
79
+ def extract_current_session
80
+ site = ShopifyAPI::Base.site.to_s
81
+ token = ShopifyAPI::Base.headers['X-Shopify-Access-Token']
82
+ version = ShopifyAPI::Base.api_version
83
+ new(domain: site, token: token, api_version: version)
84
+ end
66
85
  end
67
86
 
68
- def initialize(url, token = nil, extra = {})
69
- self.url = self.class.prepare_url(url)
87
+ def initialize(domain:, token:, api_version:, extra: {})
88
+ self.domain = self.class.prepare_domain(domain)
89
+ self.api_version = api_version
70
90
  self.token = token
71
91
  self.extra = extra
72
92
  end
@@ -74,7 +94,7 @@ module ShopifyAPI
74
94
  def create_permission_url(scope, redirect_uri = nil)
75
95
  params = {:client_id => api_key, :scope => scope.join(',')}
76
96
  params[:redirect_uri] = redirect_uri if redirect_uri
77
- "#{site}/oauth/authorize?#{parameterize(params)}"
97
+ construct_oauth_url("authorize", params)
78
98
  end
79
99
 
80
100
  def request_token(params)
@@ -103,11 +123,15 @@ module ShopifyAPI
103
123
  end
104
124
 
105
125
  def site
106
- "https://#{url}/admin"
126
+ "https://#{domain}"
127
+ end
128
+
129
+ def api_version=(version)
130
+ @api_version = version.nil? ? nil : ApiVersion.coerce_to_version(version)
107
131
  end
108
132
 
109
133
  def valid?
110
- url.present? && token.present?
134
+ domain.present? && token.present? && api_version.present?
111
135
  end
112
136
 
113
137
  def expires_in
@@ -132,12 +156,17 @@ module ShopifyAPI
132
156
  end
133
157
 
134
158
  def access_token_request(code)
135
- uri = URI.parse("https://#{url}/admin/oauth/access_token")
159
+ uri = URI.parse(construct_oauth_url('access_token'))
136
160
  https = Net::HTTP.new(uri.host, uri.port)
137
161
  https.use_ssl = true
138
162
  request = Net::HTTP::Post.new(uri.request_uri)
139
163
  request.set_form_data('client_id' => api_key, 'client_secret' => secret, 'code' => code)
140
164
  https.request(request)
141
165
  end
166
+
167
+ def construct_oauth_url(path, query_params = {})
168
+ query_string = "?#{parameterize(query_params)}" unless query_params.empty?
169
+ "https://#{domain}/admin/oauth/#{path}#{query_string}"
170
+ end
142
171
  end
143
172
  end
@@ -1,3 +1,3 @@
1
1
  module ShopifyAPI
2
- VERSION = "6.0.0"
2
+ VERSION = "7.0.0"
3
3
  end
@@ -23,9 +23,9 @@ Gem::Specification.new do |s|
23
23
  s.summary = %q{ShopifyAPI is a lightweight gem for accessing the Shopify admin REST web services}
24
24
  s.license = "MIT"
25
25
 
26
- s.required_ruby_version = ">= 2.1"
26
+ s.required_ruby_version = ">= 2.4"
27
27
 
28
- s.add_runtime_dependency("activeresource", ">= 3.0.0")
28
+ s.add_runtime_dependency("activeresource", ">= 4.1.0", "< 6.0.0")
29
29
  s.add_runtime_dependency("rack")
30
30
  s.add_runtime_dependency("graphql-client")
31
31
 
@@ -34,6 +34,7 @@ Gem::Specification.new do |s|
34
34
  s.add_development_dependency("minitest", ">= 4.0")
35
35
  s.add_development_dependency("rake")
36
36
  s.add_development_dependency("timecop")
37
+ s.add_development_dependency("rubocop")
37
38
  s.add_development_dependency("pry")
38
39
  s.add_development_dependency("pry-byebug")
39
40
  end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+ require 'test_helper'
3
+
4
+ class AccessScopeTest < Test::Unit::TestCase
5
+ test 'access scope does not use the versioned resource urls' do
6
+ fake(
7
+ 'access_scopes',
8
+ url: 'https://shop2.myshopify.com/admin/oauth/access_scopes.json',
9
+ method: :get,
10
+ status: 201,
11
+ body: load_fixture('access_scopes'),
12
+ extension: false
13
+ )
14
+
15
+ unstable_version = ShopifyAPI::Session.new(domain: 'shop2.myshopify.com', token: 'token2', api_version: :unstable)
16
+
17
+ ShopifyAPI::Base.activate_session(unstable_version)
18
+
19
+ scope_handles = ShopifyAPI::AccessScope.find(:all).map(&:handle)
20
+
21
+ assert_equal(['write_product_listings', 'read_shipping'], scope_handles)
22
+ end
23
+ end