Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add ApiVersion coercion_modes and fetch_known_versions from Shopify #600

Merged
merged 6 commits into from
Sep 3, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I might call this fetch_known_versions to be explicit that it involves a remote lookup

Copy link
Contributor Author

@jtgrenz jtgrenz Aug 15, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah I think I had that at first and at some point changed it back to minimize the number of changes or something? 🤔 It sorta made sense at the time, but I think you're right that fetch is a better choice.

Not sure if I should just rename this or alias it and issue a depreciation warning

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.'
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a recommended way to do this same thing after this method is removed? Is this just discouraged going forward?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we wanted to discourage anyone trying to set the version to the latest version automatically since it's likely to break apps. Bumping to the latest version should hopefully be intentional.

It can still be computed however since each ApiVersion has a latest_supported attribute, its just a little more work.

ShopifyAPI::Meta.admin_versions.find(&:latest_supported?) works at the expense of another api call, but we could also cache the versions in Meta.admin_versions

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

latest_supported_version is used by the ShopifyApp gem https://github.com/Shopify/shopify_app/blob/0900ddcb2f268f6efe3e184739ee83eeab3a0020/lib/generators/shopify_app/install/install_generator.rb#L12 to run the generators.

It can now use ShopifyAPI::Meta.admin_versions.find(&:latest_supported?) to find that info.

)
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
jtgrenz marked this conversation as resolved.
Show resolved Hide resolved
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