shopify_api 7.0.2 → 7.1.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 (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)