Skip to content

Commit

Permalink
Merge pull request #600 from Shopify/auto-define-api-versions
Browse files Browse the repository at this point in the history
Add ApiVersion coercion_modes and fetch_known_versions from Shopify
  • Loading branch information
jtgrenz authored Sep 3, 2019
2 parents c8f02c0 + 0ee8d7e commit 7c5a458
Show file tree
Hide file tree
Showing 18 changed files with 398 additions and 225 deletions.
25 changes: 9 additions & 16 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,15 @@ ShopifyAPI::Session.temp(domain: domain, token: token, api_version: api_version)
end
```

The `api_version` attribute can take the string or symbol name of any known version and correctly coerce it to a `ShopifyAPI::ApiVersion`. You can find the currently defined versions [here](https://github.com/Shopify/shopify_api/blob/master/lib/shopify_api/defined_versions.rb), follow these [instructions](#adding-additional-api-versions) to add additional version definitions if needed.
The `api_version` attribute takes a version handle (ie `'2019-07'` or `:unstable`) and sets an instance of `ShopifyAPI::ApiVersion` matching the handle.
By default, any handle will naïvely create a new `ApiVersion` if the version is not in the known versions returned by `ShopifyAPI::ApiVersion.versions`. To ensure only known and active versions can be set, call

```ruby
ShopifyAPI::ApiVersion.version_lookup_mode = :raise_on_unknown
ShopifyAPI::ApiVersion.fetch_known_versions
```

Known and active versions are fetched from https://app.shopify.com/services/apis.json and cached. Trying to use a version outside this cached set will raise an error. To switch back to naïve lookup and create a version if its not found, call `ShopifyAPI::ApiVersion.version_lookup_mode = :define_on_unknown` (this is the default mode).

For example if you want to use the `2019-04` version you would create a session like this:
```ruby
Expand Down Expand Up @@ -344,21 +352,6 @@ result = client.query(SHOP_NAME_QUERY)
result.data.shop.name
```

## Adding additional API versions
We will release a gem update every time we release a new version of the API. Most of the time upgrading the gem will be all you need to do.

If you want access to a newer version without upgrading you can define an api version.
For example if you wanted to add an `ApiVersion` '2022-03', you would add the following to the initialization of your application:
```ruby
ShopifyAPI::ApiVersion.define_version(ShopifyAPI::ApiVersion::Release.new('2022-03'))
```
Once you have done that you can now set this version in a Sesssion like this:

```ruby
ShopifyAPI::Session.new(domain: domain, token: token, api_version: '2022-03')
```


## Threadsafety

ActiveResource is threadsafe as of version 4.1 (which works with Rails 4.x and above).
Expand Down
4 changes: 1 addition & 3 deletions lib/shopify_api.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@
require 'base64'
require 'active_resource/detailed_log_subscriber'
require 'shopify_api/limits'
require 'shopify_api/defined_versions'
require 'shopify_api/api_version'
require 'shopify_api/meta'
require 'active_resource/json_errors'
require 'shopify_api/paginated_collection'
require 'shopify_api/disable_prefix_check'
Expand All @@ -28,5 +28,3 @@ module ShopifyAPI
else
require 'active_resource/connection_ext'
end

ShopifyAPI::ApiVersion.define_known_versions
228 changes: 147 additions & 81 deletions lib/shopify_api/api_version.rb
Original file line number Diff line number Diff line change
@@ -1,129 +1,195 @@
# frozen_string_literal: true
module ShopifyAPI
class ApiVersion
class ApiVersionNotSetError < StandardError; end
class UnknownVersion < StandardError; end
class InvalidVersion < StandardError; end
class ApiVersionNotSetError < StandardError; end
include Comparable

extend DefinedVersions
HANDLE_FORMAT = /^\d{4}-\d{2}$/.freeze
UNSTABLE_HANDLE = 'unstable'
UNSTABLE_AS_DATE = Time.utc(3000, 1, 1)
API_PREFIX = '/admin/api/'
LOOKUP_MODES = [:raise_on_unknown, :define_on_unknown].freeze

include Comparable
class << self
attr_reader :versions

def self.coerce_to_version(version_or_name)
return version_or_name if version_or_name.is_a?(ApiVersion)
def version_lookup_mode
@version_lookup_mode ||= :define_on_unknown
end

@versions ||= {}
@versions.fetch(version_or_name.to_s) do
raise UnknownVersion, "#{version_or_name} is not in the defined version set: #{@versions.keys.join(', ')}"
def version_lookup_mode=(mode)
raise ArgumentError, "Mode must be one of #{LOOKUP_MODES}" unless LOOKUP_MODES.include?(mode)
sanitize_known_versions if mode == :raise_on_unknown
@version_lookup_mode = mode
end
end

def self.define_version(version)
@versions ||= {}
def find_version(version_or_handle)
return version_or_handle if version_or_handle.is_a?(ApiVersion)
handle = version_or_handle.to_s
@versions ||= {}
@versions.fetch(handle) do
if @version_lookup_mode == :raise_on_unknown
raise UnknownVersion, unknown_version_error_message(handle)
else
add_to_known_versions(ApiVersion.new(handle: handle))
end
end
end

@versions[version.name] = version
end
def coerce_to_version(version_or_handle)
warn(
'[DEPRECATED] ShopifyAPI::ApiVersion.coerce_to_version be removed in a future version. ' \
'Use `find_version` instead.'
)
find_version(version_or_handle)
end

def fetch_known_versions
@versions = Meta.admin_versions.map do |version|
[version.handle, ApiVersion.new(version.attributes.merge(verified: version.persisted?))]
end.to_h
end

def self.clear_defined_versions
@versions = {}
def define_known_versions
warn(
'[DEPRECATED] ShopifyAPI::ApiVersion.define_known_versions is deprecated and will be removed in a future version. ' \
'Use `fetch_known_versions` instead.'
)
fetch_known_versions
end

def add_to_known_versions(version)
@versions[version.handle] = version
end

def clear_known_versions
@versions = {}
end

def clear_defined_versions
warn(
'[DEPRECATED] ShopifyAPI::ApiVersion.clear_defined_versions is deprecated and will be removed in a future version. ' \
'Use `clear_known_versions` instead.'
)
clear_known_versions
end

def latest_stable_version
warn(
'[DEPRECATED] ShopifyAPI::ApiVersion.latest_stable_version is deprecated and will be removed in a future version.'
)
versions.values.find(&:latest_supported?)
end

private

def sanitize_known_versions
return if @versions.nil?
@versions = @versions.keys.map do |handle|
next unless @versions[handle].verified?
[handle, @versions[handle]]
end.compact.to_h
end

def unknown_version_error_message(handle)
msg = "ApiVersion.version_lookup_mode is set to `:raise_on_unknown`. \n"
return msg + "No versions defined. You must call `ApiVersion.fetch_known_versions` first." if @versions.empty?
msg + "`#{handle}` is not in the defined version set. Available versions: #{@versions.keys}"
end
end

def self.latest_stable_version
@versions.values.select(&:stable?).sort.last
attr_reader :handle, :display_name, :supported, :latest_supported, :verified

def initialize(attributes)
attributes = ActiveSupport::HashWithIndifferentAccess.new(attributes)
@handle = attributes[:handle].to_s
@display_name = attributes.fetch(:display_name, attributes[:handle].to_s)
@supported = attributes.fetch(:supported, false)
@latest_supported = attributes.fetch(:latest_supported, false)
@verified = attributes.fetch(:verified, false)
end

def to_s
@version_name
handle
end
alias_method :name, :to_s

def inspect
@version_name
def latest_supported?
latest_supported
end

def ==(other)
other.class == self.class && to_s == other.to_s
def supported?
supported
end

def hash
@version_name.hash
def verified?
verified
end

def <=>(other)
numeric_version <=> other.numeric_version
handle_as_date <=> other.handle_as_date
end

def stable?
false
def ==(other)
other.class == self.class && handle == other.handle
end

def construct_api_path(_path)
raise NotImplementedError
def hash
handle.hash
end

def construct_graphql_path
raise NotImplementedError
def construct_api_path(path)
"#{API_PREFIX}#{handle}/#{path}"
end

protected

attr_reader :numeric_version

class Unstable < ApiVersion
API_PREFIX = '/admin/api/unstable/'

def initialize
@version_name = "unstable"
@url = API_PREFIX
@numeric_version = 9_000_00
end

def construct_api_path(path)
"#{@url}#{path}"
end

def construct_graphql_path
construct_api_path("graphql.json")
end
def construct_graphql_path
construct_api_path('graphql.json')
end

class Release < ApiVersion
FORMAT = /^\d{4}-\d{2}$/.freeze
API_PREFIX = '/admin/api/'

def initialize(version_number)
raise InvalidVersion, version_number unless version_number.match(FORMAT)
@version_name = version_number
@url = "#{API_PREFIX}#{version_number}/"
@numeric_version = version_number.tr('-', '').to_i
end
def name
warn(
'[DEPRECATED] ShopifyAPI::ApiVersion#name is deprecated and will be removed in a future version. ' \
'Use `handle` instead.'
)
handle
end

def stable?
true
end
def stable?
warn(
'[DEPRECATED] ShopifyAPI::ApiVersion#stable? is deprecated and will be removed in a future version. ' \
'Use `supported?` instead.'
)
supported?
end

def construct_api_path(path)
"#{@url}#{path}"
end
def unstable?
handle == UNSTABLE_HANDLE
end

def construct_graphql_path
construct_api_path('graphql.json')
end
def handle_as_date
return UNSTABLE_AS_DATE if unstable?
year, month, day = handle.split('-')
Time.utc(year, month, day)
end

class NullVersion
class << self
def stable?
raise ApiVersionNotSetError, "You must set ShopifyAPI::Base.api_version before making a request."
end

def construct_api_path(*_path)
raise ApiVersionNotSetError, "You must set ShopifyAPI::Base.api_version before making a request."
end

def construct_graphql_path
def raise_not_set_error(*_args)
raise ApiVersionNotSetError, "You must set ShopifyAPI::Base.api_version before making a request."
end
alias_method :stable?, :raise_not_set_error
alias_method :construct_api_path, :raise_not_set_error
alias_method :construct_graphql_path, :raise_not_set_error
alias_method :latest_supported?, :raise_not_set_error
alias_method :supported?, :raise_not_set_error
alias_method :verified?, :raise_not_set_error
alias_method :unstable?, :raise_not_set_error
alias_method :handle, :raise_not_set_error
alias_method :display_name, :raise_not_set_error
alias_method :supported, :raise_not_set_error
alias_method :verified, :raise_not_set_error
alias_method :latest_supported, :raise_not_set_error
alias_method :name, :raise_not_set_error
end
end
end
Expand Down
11 changes: 0 additions & 11 deletions lib/shopify_api/defined_versions.rb

This file was deleted.

15 changes: 15 additions & 0 deletions lib/shopify_api/meta.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@

# frozen_string_literal: true
module ShopifyAPI
class Meta < ActiveResource::Base
self.site = "https://app.shopify.com/services/"
self.element_name = 'api'
self.primary_key = :handle
self.timeout = 5

def self.admin_versions
all.find { |api| api.handle = :admin }.versions
end
end
end

4 changes: 2 additions & 2 deletions lib/shopify_api/paginated_collection.rb
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,8 @@ def fetch_previous_page

private

AVAILABLE_IN_VERSION = ShopifyAPI::ApiVersion::Release.new('2019-10')
AVAILABLE_IN_VERSION_EARLY = ShopifyAPI::ApiVersion::Release.new('2019-07')
AVAILABLE_IN_VERSION = ShopifyAPI::ApiVersion.find_version('2019-10')
AVAILABLE_IN_VERSION_EARLY = ShopifyAPI::ApiVersion.find_version('2019-07')

def fetch_page(url)
ensure_available
Expand Down
2 changes: 1 addition & 1 deletion lib/shopify_api/resources/base.rb
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ def api_version
end

def api_version=(version)
self._api_version = version.nil? ? ApiVersion::NullVersion : ApiVersion.coerce_to_version(version)
self._api_version = version.nil? ? ApiVersion::NullVersion : ApiVersion.find_version(version)
end

def prefix(options = {})
Expand Down
2 changes: 1 addition & 1 deletion lib/shopify_api/session.rb
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,7 @@ def site
end

def api_version=(version)
@api_version = version.nil? ? nil : ApiVersion.coerce_to_version(version)
@api_version = version.nil? ? nil : ApiVersion.find_version(version)
end

def valid?
Expand Down
Loading

0 comments on commit 7c5a458

Please sign in to comment.