shopify_api 6.0.0 → 7.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 (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