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 api versions for url prefix control #548

Merged
merged 5 commits into from
Apr 1, 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
3 changes: 3 additions & 0 deletions lib/shopify_api.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
require 'base64'
require 'active_resource/detailed_log_subscriber'
require 'shopify_api/limits'
require 'shopify_api/api_version'
require 'shopify_api/json_format'
require 'active_resource/json_errors'
require 'shopify_api/disable_prefix_check'
Expand All @@ -27,3 +28,5 @@ module ShopifyAPI
else
require 'active_resource/connection_ext'
end

ShopifyAPI::ApiVersion.define_known_versions
88 changes: 88 additions & 0 deletions lib/shopify_api/api_version.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
# frozen_string_literal: true
module ShopifyAPI
class ApiVersion
class UnknownVersion < StandardError; end

class NoVersion < ApiVersion
API_PREFIX = '/admin/'
GRAPHQL_PATH = '/admin/api/graphql.json'

def initialize
@version_name = "no_version"
end

def construct_api_path(path)
"#{API_PREFIX}#{path}"
end

def construct_graphql_path
GRAPHQL_PATH
end
end

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

def initialize
@version_name = "unstable"
end

def construct_api_path(path)
"#{API_PREFIX}#{path}"
end

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

def self.coerce_to_version(version_or_name)
return version_or_name if version_or_name.is_a?(ApiVersion)

@versions ||= {}
@versions.fetch(version_or_name.to_s) do
raise UnknownVersion, "#{version_or_name} is not in the defined version set: #{@versions.keys.join(', ')}"
end
end

def self.define_version(version)
@versions ||= {}

@versions[version.name] = version
end

def self.clear_defined_versions
@versions = {}
end

def self.define_known_versions
define_version(NoVersion.new)
define_version(Unstable.new)
end

def to_s
@version_name
end
alias_method :name, :to_s

def inspect
@version_name
end

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

def hash
version_name.hash
end

def construct_api_path(_path)
raise NotImplementedError
end

def construct_graphql_path
raise NotImplementedError
end
end
end
2 changes: 1 addition & 1 deletion lib/shopify_api/resources/asset.rb
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ def self.find(*args)
path_prefix = params[:theme_id] ? "themes/#{params[:theme_id]}/" : ""
resource = find(
:one,
from: "#{api_prefix}#{path_prefix}assets.#{format.extension}",
from: api_version.construct_api_path("#{path_prefix}assets.#{format.extension}"),
params: params
)
resource.prefix_options[:theme_id] = params[:theme_id] if resource && params[:theme_id]
Expand Down
34 changes: 29 additions & 5 deletions lib/shopify_api/resources/base.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,6 @@ class InvalidSessionError < StandardError; end
"ActiveResource/#{ActiveResource::VERSION::STRING}",
"Ruby/#{RUBY_VERSION}"].join(' ')

API_PREFIX = '/admin/'.freeze

def encode(options = {})
same = dup
same.attributes = {self.class.element_name => same.attributes} if self.class.format.extension == 'json'
Expand All @@ -31,6 +29,22 @@ def as_json(options = nil)
end

class << self
if respond_to?(:threadsafe_attribute)
threadsafe_attribute(:_api_version)
else
def _api_version
@api_version
end

def _api_version_defined?
defined?(@api_version)
end

def _api_version=(value)
@api_version = value
end
end

if ActiveResource::Base.respond_to?(:_headers) && ActiveResource::Base.respond_to?(:_headers_defined?)
def headers
if _headers_defined?
Expand All @@ -57,21 +71,31 @@ def activate_session(session)
raise InvalidSessionError.new("Session cannot be nil") if session.nil?
self.site = session.site
self.headers.merge!('X-Shopify-Access-Token' => session.token)
self.api_version = session.api_version
end

def clear_session
self.site = nil
self.password = nil
self.user = nil
self.api_version = nil
self.headers.delete('X-Shopify-Access-Token')
end

def api_prefix
API_PREFIX
def api_version
if _api_version_defined?
_api_version
elsif superclass != Object && superclass.site
chrisbutcher marked this conversation as resolved.
Show resolved Hide resolved
superclass.api_version.dup.freeze
end
end

def api_version=(value)
self._api_version = value
end

def prefix(options = {})
"#{api_prefix}#{resource_prefix(options)}"
api_version.construct_api_path(resource_prefix(options))
end

def prefix_source
Expand Down
2 changes: 1 addition & 1 deletion lib/shopify_api/resources/graphql.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ module ShopifyAPI
class GraphQL
def initialize
uri = Base.site.dup
uri.path = '/admin/api/graphql.json'
uri.path = Base.api_version.construct_graphql_path
@http = ::GraphQL::Client::HTTP.new(uri.to_s) do
define_method(:headers) do |_context|
Base.headers
Expand Down
4 changes: 3 additions & 1 deletion lib/shopify_api/resources/shop.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,9 @@ def self.current(options = {})
end
else
def self.current(options = {})
find(:one, options.merge(from: "#{api_prefix}shop.#{format.extension}"))
find(:one, options.merge(
from: api_version.construct_api_path("shop.#{format.extension}")
))
end
end

Expand Down
15 changes: 11 additions & 4 deletions lib/shopify_api/session.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,18 +11,20 @@ class Session
self.myshopify_domain = 'myshopify.com'

attr_accessor :url, :token, :name, :extra
attr_reader :api_version

class << self

def setup(params)
params.each { |k,value| public_send("#{k}=", value) }
end

def temp(domain, token, &block)
session = new(domain, token)
def temp(domain, token, api_version = :no_version, &_block)
session = new(domain, token, api_version)
original_site = ShopifyAPI::Base.site.to_s
original_token = ShopifyAPI::Base.headers['X-Shopify-Access-Token']
original_session = new(original_site, original_token)
original_version = ShopifyAPI::Base.api_version
original_session = new(original_site, original_token, original_version)

begin
ShopifyAPI::Base.activate_session(session)
Expand Down Expand Up @@ -65,8 +67,9 @@ def encoded_params_for_signature(params)
end
end

def initialize(url, token = nil, extra = {})
def initialize(url, token = nil, api_version = :no_version, extra = {})
Copy link
Contributor

Choose a reason for hiding this comment

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

Could api_version be a kwarg? Same q for temp.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

compatibility is my biggest concern with that.

Copy link
Contributor

Choose a reason for hiding this comment

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

But couldn't a kwarg just have a default, too?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

could you provide the full signature you are picturing?

Copy link
Contributor

Choose a reason for hiding this comment

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

(See Slack)

Copy link
Contributor

Choose a reason for hiding this comment

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

Converged on "maybe it is better to just go to keyword args", i.e. def initialize(url:, token:, api_version:, ...)

Copy link
Contributor

Choose a reason for hiding this comment

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

The method signature will change in the next PR, in which api_version becomes required.

self.url = self.class.prepare_url(url)
self.api_version = api_version
self.token = token
self.extra = extra
end
Expand Down Expand Up @@ -106,6 +109,10 @@ def site
"https://#{url}"
end

def api_version=(version)
@api_version = ApiVersion.coerce_to_version(version)
end

def valid?
url.present? && token.present?
end
Expand Down
83 changes: 83 additions & 0 deletions test/api_version_test.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
# frozen_string_literal: true
require 'test_helper'

class ApiVersionTest < Test::Unit::TestCase
def teardown
super
ShopifyAPI::ApiVersion.clear_defined_versions
ShopifyAPI::ApiVersion.define_known_versions
end

test "no version creates url that start with /admin/" do
assert_equal(
"/admin/resource_path/id.json",
ShopifyAPI::ApiVersion::NoVersion.new.construct_api_path("resource_path/id.json")
)
end

test "no version creates graphql url that start with /admin/api" do
assert_equal(
"/admin/api/graphql.json",
ShopifyAPI::ApiVersion::NoVersion.new.construct_graphql_path
)
end

test "unstable version creates url that start with /admin/api/unstable/" do
assert_equal(
"/admin/api/unstable/resource_path/id.json",
ShopifyAPI::ApiVersion::Unstable.new.construct_api_path("resource_path/id.json")
)
end

test "unstable version creates graphql url that start with /admin/api/unstable/" do
assert_equal(
"/admin/api/unstable/graphql.json",
ShopifyAPI::ApiVersion::Unstable.new.construct_graphql_path
)
end

test "coerce_to_version returns any version object given" do
version = ShopifyAPI::ApiVersion::Unstable.new
assert_same(version, ShopifyAPI::ApiVersion.coerce_to_version(version))
end

test "coerce_to_version converts a known version into a version object" do
versions = [
ShopifyAPI::ApiVersion::Unstable.new,
ShopifyAPI::ApiVersion::NoVersion.new,
]

assert_equal(versions, [
ShopifyAPI::ApiVersion.coerce_to_version('unstable'),
ShopifyAPI::ApiVersion.coerce_to_version(:no_version),
])
end

test "coerce_to_version raises when coercing a string that doesn't match a known version" do
assert_raises ShopifyAPI::ApiVersion::UnknownVersion do
ShopifyAPI::ApiVersion.coerce_to_version('made up version')
end
end

test "additional defined versions will also be coerced" do
versions = [
TestApiVersion.new('my_name'),
TestApiVersion.new('other_name'),
]

versions.each do |version|
ShopifyAPI::ApiVersion.define_version(version)
end

assert_equal(versions, [
ShopifyAPI::ApiVersion.coerce_to_version('my_name'),
ShopifyAPI::ApiVersion.coerce_to_version('other_name'),
])
end

class TestApiVersion < ShopifyAPI::ApiVersion
def initialize(name)
@version_name = name
end
end
end
28 changes: 28 additions & 0 deletions test/base_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,8 @@ def teardown
end

test "prefix= will forward to resource when the value does not start with admin" do
ShopifyAPI::Base.activate_session(@session1)

TestResource.prefix = 'a/regular/path/'

assert_equal('/admin/a/regular/path/', TestResource.prefix)
Expand Down Expand Up @@ -128,6 +130,32 @@ def teardown
end
end

test "using a different version changes the url" do
no_version = ShopifyAPI::Session.new('shop1.myshopify.com', 'token1', :no_version)
unstable_version = ShopifyAPI::Session.new('shop2.myshopify.com', 'token2', :unstable)

fake(
"shop",
url: "https://shop1.myshopify.com/admin/shop.json",
method: :get,
status: 201,
body: '{ "shop": { "id": 1 } }'
)
fake(
"shop",
url: "https://shop2.myshopify.com/admin/api/unstable/shop.json",
method: :get,
status: 201,
body: '{ "shop": { "id": 2 } }'
)

ShopifyAPI::Base.activate_session(no_version)
assert_equal 1, ShopifyAPI::Shop.current.id

ShopifyAPI::Base.activate_session(unstable_version)
assert_equal 2, ShopifyAPI::Shop.current.id
end

def clear_header(header)
[ActiveResource::Base, ShopifyAPI::Base, ShopifyAPI::Product].each do |klass|
klass.headers.delete(header)
Expand Down
Loading