shopify_api 7.0.2 → 7.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (45) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop-https---shopify-github-io-ruby-style-guide-rubocop-yml +1027 -0
  3. data/CHANGELOG.md +7 -0
  4. data/README.md +30 -0
  5. data/Rakefile +1 -1
  6. data/lib/shopify_api.rb +2 -1
  7. data/lib/shopify_api/api_version.rb +17 -0
  8. data/lib/shopify_api/defined_versions.rb +1 -0
  9. data/lib/shopify_api/paginated_collection.rb +57 -0
  10. data/lib/shopify_api/pagination_link_headers.rb +33 -0
  11. data/lib/shopify_api/resources.rb +2 -1
  12. data/lib/shopify_api/resources/array_base.rb +13 -0
  13. data/lib/shopify_api/resources/base.rb +24 -21
  14. data/lib/shopify_api/resources/collect.rb +1 -0
  15. data/lib/shopify_api/resources/collection_listing.rb +10 -1
  16. data/lib/shopify_api/resources/customer_saved_search.rb +2 -0
  17. data/lib/shopify_api/resources/event.rb +1 -0
  18. data/lib/shopify_api/resources/metafield.rb +1 -0
  19. data/lib/shopify_api/resources/product.rb +2 -0
  20. data/lib/shopify_api/resources/product_listing.rb +8 -1
  21. data/lib/shopify_api/version.rb +1 -1
  22. data/shopify_api.gemspec +1 -1
  23. data/test/api_version_test.rb +14 -0
  24. data/test/base_test.rb +12 -6
  25. data/test/blog_test.rb +1 -1
  26. data/test/collection_listing_test.rb +38 -0
  27. data/test/collection_publication_test.rb +1 -1
  28. data/test/customer_test.rb +2 -2
  29. data/test/discount_code_test.rb +2 -2
  30. data/test/draft_order_test.rb +4 -4
  31. data/test/fixtures/collection_listing_product_ids2.json +1 -0
  32. data/test/fixtures/product_listing_product_ids.json +1 -1
  33. data/test/fixtures/product_listing_product_ids2.json +1 -0
  34. data/test/lib/webmock_extensions/last_request.rb +16 -0
  35. data/test/metafield_test.rb +1 -1
  36. data/test/pagination_test.rb +229 -0
  37. data/test/price_rule_test.rb +1 -1
  38. data/test/product_listing_test.rb +59 -2
  39. data/test/product_publication_test.rb +1 -1
  40. data/test/product_test.rb +1 -1
  41. data/test/recurring_application_charge_test.rb +3 -4
  42. data/test/shop_test.rb +1 -1
  43. data/test/tax_service_test.rb +2 -1
  44. data/test/test_helper.rb +83 -83
  45. metadata +12 -5
@@ -1,3 +1,10 @@
1
+ == Version 7.1.0
2
+
3
+ * Add 2019-10 to known API versions
4
+ * Add support for cursor pagination [#594](https://github.com/Shopify/shopify_api/pull/594) and
5
+ [#611](https://github.com/Shopify/shopify_api/pull/611)
6
+ * `ShopifyAPI::Base.api_version` now defaults to `ShopifyAPI::ApiVersion::NullVersion` instead of `nil`. Making requests without first setting an ApiVersion raises `ApiVersionNotSetError` instead of `NoMethodError: undefined method 'construct_api_path' for nil:NilClass'` [#605](https://github.com/Shopify/shopify_api/pull/605)
7
+
1
8
  == Version 7.0.2
2
9
 
3
10
  * Add 2019-07 to known API versions.
data/README.md CHANGED
@@ -365,6 +365,36 @@ ActiveResource is threadsafe as of version 4.1 (which works with Rails 4.x and a
365
365
 
366
366
  If you were previously using Shopify's [activeresource fork](https://github.com/shopify/activeresource) then you should remove it and use ActiveResource 4.1.
367
367
 
368
+ ## Pagination
369
+
370
+ Pagination can occur in one of two ways.
371
+
372
+ Page based pagination
373
+ ```ruby
374
+ page = 1
375
+ products = ShopifyAPI::Product.find(:all, params: { limit: 50, page: page })
376
+ process_products(products)
377
+ while(products.count == 50)
378
+ page += 1
379
+ products = ShopifyAPI::Product.find(:all, params: { limit: 50, page: page })
380
+ process_products(products)
381
+ end
382
+ ```
383
+
384
+ Page based pagination will be deprecated in the `2019-10` API version, in favor of the second method of pagination:
385
+
386
+ [Relative cursor based pagination](https://help.shopify.com/en/api/guides/paginated-rest-results)
387
+ ```ruby
388
+ products = ShopifyAPI::Product.find(:all, params: { limit: 50 })
389
+ process_products(products)
390
+ while products.next_page?
391
+ products = products.fetch_next_page
392
+ process_products(products)
393
+ end
394
+ ```
395
+
396
+ Relative cursor pagination is currently available for all endpoints using the `2019-10` and later API versions.
397
+
368
398
  ## Using Development Version
369
399
 
370
400
  Download the source code and run:
data/Rakefile CHANGED
@@ -5,7 +5,7 @@ require 'rake/testtask'
5
5
  Rake::TestTask.new(:test) do |test|
6
6
  test.libs << 'lib' << 'test'
7
7
  test.pattern = 'test/**/*_test.rb'
8
- test.verbose = true
8
+ test.warning = false
9
9
  end
10
10
 
11
11
  begin
@@ -1,5 +1,4 @@
1
1
  $:.unshift File.dirname(__FILE__)
2
-
3
2
  require 'active_resource'
4
3
  require 'active_support/core_ext/class/attribute_accessors'
5
4
  require 'digest/md5'
@@ -9,6 +8,7 @@ require 'shopify_api/limits'
9
8
  require 'shopify_api/defined_versions'
10
9
  require 'shopify_api/api_version'
11
10
  require 'active_resource/json_errors'
11
+ require 'shopify_api/paginated_collection'
12
12
  require 'shopify_api/disable_prefix_check'
13
13
 
14
14
  module ShopifyAPI
@@ -21,6 +21,7 @@ require 'shopify_api/countable'
21
21
  require 'shopify_api/resources'
22
22
  require 'shopify_api/session'
23
23
  require 'shopify_api/connection'
24
+ require 'shopify_api/pagination_link_headers'
24
25
 
25
26
  if ShopifyAPI::Base.respond_to?(:connection_class)
26
27
  ShopifyAPI::Base.connection_class = ShopifyAPI::Connection
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
  module ShopifyAPI
3
3
  class ApiVersion
4
+ class ApiVersionNotSetError < StandardError; end
4
5
  class UnknownVersion < StandardError; end
5
6
  class InvalidVersion < StandardError; end
6
7
 
@@ -109,5 +110,21 @@ module ShopifyAPI
109
110
  construct_api_path('graphql.json')
110
111
  end
111
112
  end
113
+
114
+ class NullVersion
115
+ class << self
116
+ def stable?
117
+ raise ApiVersionNotSetError, "You must set ShopifyAPI::Base.api_version before making a request."
118
+ end
119
+
120
+ def construct_api_path(*_path)
121
+ raise ApiVersionNotSetError, "You must set ShopifyAPI::Base.api_version before making a request."
122
+ end
123
+
124
+ def construct_graphql_path
125
+ raise ApiVersionNotSetError, "You must set ShopifyAPI::Base.api_version before making a request."
126
+ end
127
+ end
128
+ end
112
129
  end
113
130
  end
@@ -5,6 +5,7 @@ module ShopifyAPI
5
5
  define_version(ShopifyAPI::ApiVersion::Unstable.new)
6
6
  define_version(ShopifyAPI::ApiVersion::Release.new('2019-04'))
7
7
  define_version(ShopifyAPI::ApiVersion::Release.new('2019-07'))
8
+ define_version(ShopifyAPI::ApiVersion::Release.new('2019-10'))
8
9
  end
9
10
  end
10
11
  end
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ShopifyAPI
4
+ class PaginatedCollection < ActiveResource::Collection
5
+ module CollectionPagination
6
+ def initialize(args)
7
+ @next_url = pagination_link_headers.next_link&.url&.to_s
8
+ @previous_url = pagination_link_headers.previous_link&.url&.to_s
9
+ super(args)
10
+ end
11
+
12
+ def next_page?
13
+ ensure_available
14
+ @next_url.present?
15
+ end
16
+
17
+ def previous_page?
18
+ ensure_available
19
+ @previous_url.present?
20
+ end
21
+
22
+ def fetch_next_page
23
+ fetch_page(@next_url)
24
+ end
25
+
26
+ def fetch_previous_page
27
+ fetch_page(@previous_url)
28
+ end
29
+
30
+ private
31
+
32
+ AVAILABLE_IN_VERSION = ShopifyAPI::ApiVersion::Release.new('2019-10')
33
+ AVAILABLE_IN_VERSION_EARLY = ShopifyAPI::ApiVersion::Release.new('2019-07')
34
+
35
+ def fetch_page(url)
36
+ ensure_available
37
+ return [] unless url.present?
38
+
39
+ resource_class.all(from: url)
40
+ end
41
+
42
+ def pagination_link_headers
43
+ @pagination_link_headers ||= ShopifyAPI::PaginationLinkHeaders.new(
44
+ ShopifyAPI::Base.connection.response["Link"]
45
+ )
46
+ end
47
+
48
+ def ensure_available
49
+ return if ShopifyAPI::Base.api_version >= AVAILABLE_IN_VERSION
50
+ return if ShopifyAPI::Base.api_version >= AVAILABLE_IN_VERSION_EARLY && resource_class.early_july_pagination?
51
+ raise NotImplementedError
52
+ end
53
+ end
54
+
55
+ include CollectionPagination
56
+ end
57
+ end
@@ -0,0 +1,33 @@
1
+ module ShopifyAPI
2
+ class InvalidPaginationLinksError < StandardError; end
3
+
4
+ class PaginationLinkHeaders
5
+ LinkHeader = Struct.new(:url, :rel)
6
+ attr_reader :previous_link, :next_link
7
+
8
+ def initialize(link_header)
9
+ links = parse_link_header(link_header)
10
+ @previous_link = links.find { |link| link.rel == :previous }
11
+ @next_link = links.find { |link| link.rel == :next }
12
+
13
+ self
14
+ end
15
+
16
+ private
17
+
18
+ def parse_link_header(link_header)
19
+ return [] unless link_header.present?
20
+ links = link_header.split(',')
21
+ links.map do |link|
22
+ parts = link.split('; ')
23
+ raise ShopifyAPI::InvalidPaginationLinksError.new("Invalid link header: url and rel expected") unless parts.length == 2
24
+
25
+ url = parts[0][/<(.*)>/, 1]
26
+ rel = parts[1][/rel="(.*)"/, 1]&.to_sym
27
+
28
+ url = URI.parse(url)
29
+ LinkHeader.new(url, rel)
30
+ end
31
+ end
32
+ end
33
+ end
@@ -1,2 +1,3 @@
1
1
  require 'shopify_api/resources/base'
2
- Dir.glob("#{File.dirname(__FILE__)}/resources/*").each { |file| require(file) }
2
+ require 'shopify_api/resources/array_base'
3
+ Dir.glob("#{File.dirname(__FILE__)}/resources/*").each { |file| require(file) }
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ShopifyAPI
4
+ class ArrayBase < Base
5
+ class << self
6
+ private
7
+
8
+ def instantiate_record(record, *)
9
+ record
10
+ end
11
+ end
12
+ end
13
+ end
@@ -11,6 +11,8 @@ module ShopifyAPI
11
11
  "ActiveResource/#{ActiveResource::VERSION::STRING}",
12
12
  "Ruby/#{RUBY_VERSION}"].join(' ')
13
13
 
14
+ self.collection_parser = ShopifyAPI::PaginatedCollection
15
+
14
16
  def encode(options = {})
15
17
  same = dup
16
18
  same.attributes = {self.class.element_name => same.attributes} if self.class.format.extension == 'json'
@@ -30,26 +32,13 @@ module ShopifyAPI
30
32
 
31
33
  class << self
32
34
  threadsafe_attribute(:_api_version)
33
-
34
- if ActiveResource::Base.respond_to?(:_headers) && ActiveResource::Base.respond_to?(:_headers_defined?)
35
- def headers
36
- if _headers_defined?
37
- _headers
38
- elsif superclass != Object && superclass.headers
39
- superclass.headers
40
- else
41
- _headers ||= {}
42
- end
43
- end
44
- else
45
- def headers
46
- if defined?(@headers)
47
- @headers
48
- elsif superclass != Object && superclass.headers
49
- superclass.headers
50
- else
51
- @headers ||= {}
52
- end
35
+ def headers
36
+ if _headers_defined?
37
+ _headers
38
+ elsif superclass != Object && superclass.headers
39
+ superclass.headers
40
+ else
41
+ _headers ||= {}
53
42
  end
54
43
  end
55
44
 
@@ -73,11 +62,13 @@ module ShopifyAPI
73
62
  _api_version
74
63
  elsif superclass != Object && superclass.site
75
64
  superclass.api_version.dup.freeze
65
+ else
66
+ ApiVersion::NullVersion
76
67
  end
77
68
  end
78
69
 
79
70
  def api_version=(version)
80
- self._api_version = version.nil? ? nil : ApiVersion.coerce_to_version(version)
71
+ self._api_version = version.nil? ? ApiVersion::NullVersion : ApiVersion.coerce_to_version(version)
81
72
  end
82
73
 
83
74
  def prefix(options = {})
@@ -136,6 +127,18 @@ module ShopifyAPI
136
127
  @prefix_options[resource_id]
137
128
  end
138
129
  end
130
+
131
+ def early_july_pagination?
132
+ !!early_july_pagination
133
+ end
134
+
135
+ private
136
+
137
+ attr_accessor :early_july_pagination
138
+
139
+ def early_july_pagination_release!
140
+ self.early_july_pagination = true
141
+ end
139
142
  end
140
143
 
141
144
  def persisted?
@@ -1,5 +1,6 @@
1
1
  module ShopifyAPI
2
2
  # For adding/removing products from custom collections
3
3
  class Collect < Base
4
+ early_july_pagination_release!
4
5
  end
5
6
  end
@@ -2,8 +2,17 @@ module ShopifyAPI
2
2
  class CollectionListing < Base
3
3
  self.primary_key = :collection_id
4
4
 
5
+ early_july_pagination_release!
6
+
5
7
  def product_ids
6
- get(:product_ids)
8
+ ProductId.all(params: { collection_id: collection_id })
9
+ end
10
+
11
+ class ProductId < ArrayBase
12
+ self.resource_prefix = 'collection_listings/:collection_id/'
13
+
14
+ early_july_pagination_release!
7
15
  end
16
+ private_constant :ProductId
8
17
  end
9
18
  end
@@ -2,6 +2,8 @@ require 'shopify_api/resources/customer'
2
2
 
3
3
  module ShopifyAPI
4
4
  class CustomerSavedSearch < Base
5
+ early_july_pagination_release!
6
+
5
7
  def customers(params = {})
6
8
  Customer.search(params.merge({:customer_saved_search_id => self.id}))
7
9
  end
@@ -3,5 +3,6 @@ module ShopifyAPI
3
3
  include DisablePrefixCheck
4
4
 
5
5
  conditional_prefix :resource, true
6
+ early_july_pagination_release!
6
7
  end
7
8
  end
@@ -3,6 +3,7 @@ module ShopifyAPI
3
3
  include DisablePrefixCheck
4
4
 
5
5
  conditional_prefix :resource, true
6
+ early_july_pagination_release!
6
7
 
7
8
  def value
8
9
  return if attributes["value"].nil?
@@ -3,6 +3,8 @@ module ShopifyAPI
3
3
  include Events
4
4
  include Metafields
5
5
 
6
+ early_july_pagination_release!
7
+
6
8
  # compute the price range
7
9
  def price_range
8
10
  prices = variants.collect(&:price).collect(&:to_f)
@@ -2,8 +2,15 @@ module ShopifyAPI
2
2
  class ProductListing < Base
3
3
  self.primary_key = :product_id
4
4
 
5
+ early_july_pagination_release!
6
+
5
7
  def self.product_ids
6
- get(:product_ids)
8
+ ProductId.all
9
+ end
10
+
11
+ class ProductId < ArrayBase
12
+ self.resource_prefix = 'product_listings/'
7
13
  end
14
+ private_constant :ProductId
8
15
  end
9
16
  end
@@ -1,3 +1,3 @@
1
1
  module ShopifyAPI
2
- VERSION = "7.0.2"
2
+ VERSION = "7.1.0"
3
3
  end
@@ -30,7 +30,7 @@ Gem::Specification.new do |s|
30
30
  s.add_runtime_dependency("graphql-client")
31
31
 
32
32
  s.add_development_dependency("mocha", ">= 0.9.8")
33
- s.add_development_dependency("fakeweb")
33
+ s.add_development_dependency("webmock")
34
34
  s.add_development_dependency("minitest", ">= 4.0")
35
35
  s.add_development_dependency("rake")
36
36
  s.add_development_dependency("timecop")
@@ -136,6 +136,20 @@ class ApiVersionTest < Test::Unit::TestCase
136
136
  )
137
137
  end
138
138
 
139
+ test "NullVersion raises ApiVersionNotSetError" do
140
+ assert_raises(ShopifyAPI::ApiVersion::ApiVersionNotSetError) do
141
+ ShopifyAPI::ApiVersion::NullVersion.construct_api_path(:string)
142
+ end
143
+
144
+ assert_raises(ShopifyAPI::ApiVersion::ApiVersionNotSetError) do
145
+ ShopifyAPI::ApiVersion::NullVersion.construct_graphql_path
146
+ end
147
+
148
+ assert_raises(ShopifyAPI::ApiVersion::ApiVersionNotSetError) do
149
+ ShopifyAPI::ApiVersion::NullVersion.stable?
150
+ end
151
+ end
152
+
139
153
  class TestApiVersion < ShopifyAPI::ApiVersion
140
154
  def initialize(name)
141
155
  @version_name = name
@@ -33,9 +33,9 @@ class BaseTest < Test::Unit::TestCase
33
33
 
34
34
  ShopifyAPI::Base.clear_session
35
35
 
36
- assert_equal nil, ShopifyAPI::Base.user
37
- assert_equal nil, ShopifyAPI::Base.password
38
- assert_equal nil, ShopifyAPI::Base.site
36
+ assert_nil ShopifyAPI::Base.user
37
+ assert_nil ShopifyAPI::Base.password
38
+ assert_nil ShopifyAPI::Base.site
39
39
  end
40
40
 
41
41
  test '#clear_session should clear site and headers from Base' do
@@ -92,8 +92,9 @@ class BaseTest < Test::Unit::TestCase
92
92
  test "prefix= will forward to resource when the value does not start with admin" do
93
93
  session = ShopifyAPI::Session.new(domain: 'shop1.myshopify.com', token: 'token1', api_version: '2019-01')
94
94
  ShopifyAPI::Base.activate_session(session)
95
-
96
- TestResource.prefix = 'a/regular/path/'
95
+ silence_warnings do
96
+ TestResource.prefix = 'a/regular/path/'
97
+ end
97
98
 
98
99
  assert_equal('/admin/api/2019-01/a/regular/path/', TestResource.prefix)
99
100
  end
@@ -157,11 +158,16 @@ class BaseTest < Test::Unit::TestCase
157
158
  assert_equal 2, ShopifyAPI::Shop.current.id
158
159
  end
159
160
 
160
- test "#api_version should set ApiVersion" do
161
+ test "#api_version= should set ApiVersion" do
161
162
  ShopifyAPI::Base.api_version = '2019-04'
162
163
  assert_equal '2019-04', ShopifyAPI::Base.api_version.to_s
163
164
  end
164
165
 
166
+ test "#api_version= nil should set ApiVersion to ShopifyAPI::ApiVersion::NullVersion" do
167
+ ShopifyAPI::Base.api_version = nil
168
+ assert_equal ShopifyAPI::ApiVersion::NullVersion, ShopifyAPI::Base.api_version
169
+ end
170
+
165
171
  def clear_header(header)
166
172
  [ActiveResource::Base, ShopifyAPI::Base, ShopifyAPI::Product].each do |klass|
167
173
  klass.headers.delete(header)