Skip to content

Commit

Permalink
Allow adding custom headers in REST Resource HTTP calls (#1275)
Browse files Browse the repository at this point in the history
* Add a headers class attribute to REST Base base_rest_resource

This attribute can be used to set headers that will be sent
with every request made by the resource through methods like `all`,
`delete`, etc...
* Add CHANGELOG
* Add documentation for custom headers
* Adds additional tests for non-find methods

---------

Co-authored-by: Si Le <omnisyle@gmail.com>
  • Loading branch information
matteodepalo and sle-c authored Feb 26, 2024
1 parent 11cbfd3 commit 95271c5
Show file tree
Hide file tree
Showing 4 changed files with 79 additions and 5 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ Note: For changes to the API, see https://shopify.dev/changelog?filter=api
- [#1254](https://github.com/Shopify/shopify-api-ruby/pull/1254) Introduce token exchange API for fetching access tokens. This feature is currently unstable and cannot be used yet.
- [#1268](https://github.com/Shopify/shopify-api-ruby/pull/1268) Add [new webhook handler interface](https://github.com/Shopify/shopify-api-ruby/blob/main/docs/usage/webhooks.md#create-a-webhook-handler) to provide `webhook_id ` and `api_version` information to webhook handlers.
- [#1274](https://github.com/Shopify/shopify-api-ruby/pull/1274) Update sorbet and rbi dependencies. Remove support for ruby 2.7.
- [#1275](https://github.com/Shopify/shopify-api-ruby/pull/1275) Allow adding custom headers in REST Resource HTTP calls.

## 13.4.0
- [#1210](https://github.com/Shopify/shopify-api-ruby/pull/1246) Add context option `response_as_struct` to allow GraphQL API responses to be accessed via dot notation.
Expand Down
10 changes: 10 additions & 0 deletions docs/usage/rest.md
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,16 @@ draft_order.save!
When updating a resource, only the modified attributes, the resource's primary key, and required parameters are sent to the API. The primary key is usually the `id` attribute of the resource, but it can vary if the `primary_key` method is overwritten in the resource's class. The required parameters are identified using the path parameters of the `PUT` endpoint of the resource.
### Headers
You can add custom headers to the HTTP calls made by methods like `find`, `delete`, `all`, `count`
by setting the `headers` attribute on the `ShopifyAPI::Rest::Base` class in an initializer, like so:
```ruby
ShopifyAPI::Rest::Base.headers = { "X-Custom-Header" => "Custom Value" }
# `find` will call the API endpoint with the custom header
ShopifyAPI::Customer.find(id: customer_id)
```
### Usage Examples
⚠️ The [API reference documentation](https://shopify.dev/docs/api/admin-rest) contains more examples on how to use each REST Resources.

Expand Down
25 changes: 20 additions & 5 deletions lib/shopify_api/rest/base.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ class Base
extend T::Helpers
abstract!

@headers = T.let(nil, T.nilable(T::Hash[T.any(Symbol, String), String]))
@has_one = T.let({}, T::Hash[Symbol, T::Class[T.anything]])
@has_many = T.let({}, T::Hash[Symbol, T::Class[T.anything]])
@paths = T.let([], T::Array[T::Hash[Symbol, T.any(T::Array[Symbol], String, Symbol)]])
Expand Down Expand Up @@ -60,6 +61,18 @@ class << self
sig { returns(T::Hash[Symbol, T::Class[T.anything]]) }
attr_reader :has_one

sig { returns(T.nilable(T::Hash[T.any(Symbol, String), String])) }
attr_accessor :headers

sig { params(subclass: T::Class[T.anything]).returns(T.untyped) }
def inherited(subclass)
super

subclass.define_singleton_method(:headers) do
ShopifyAPI::Rest::Base.headers
end
end

sig do
params(
session: T.nilable(Auth::Session),
Expand All @@ -73,7 +86,7 @@ def base_find(session: nil, ids: {}, params: {})
client = ShopifyAPI::Clients::Rest::Admin.new(session: session)

path = T.must(get_path(http_method: :get, operation: :get, ids: ids))
response = client.get(path: path, query: params.compact)
response = client.get(path: path, query: params.compact, headers: headers)

instance_variable_get(:"@prev_page_info").value = response.prev_page_info
instance_variable_get(:"@next_page_info").value = response.next_page_info
Expand Down Expand Up @@ -219,13 +232,13 @@ def request(http_method:, operation:, session:, ids: {}, params: {}, body: nil,

case http_method
when :get
client.get(path: T.must(path), query: params.compact)
client.get(path: T.must(path), query: params.compact, headers: headers)
when :post
client.post(path: T.must(path), query: params.compact, body: body || {})
client.post(path: T.must(path), query: params.compact, body: body || {}, headers: headers)
when :put
client.put(path: T.must(path), query: params.compact, body: body || {})
client.put(path: T.must(path), query: params.compact, body: body || {}, headers: headers)
when :delete
client.delete(path: T.must(path), query: params.compact)
client.delete(path: T.must(path), query: params.compact, headers: headers)
else
raise Errors::InvalidHttpRequestError, "Invalid HTTP method: #{http_method}"
end
Expand Down Expand Up @@ -355,6 +368,7 @@ def delete(params: {})
@client.delete(
path: T.must(self.class.get_path(http_method: :delete, operation: :delete, entity: self)),
query: params.compact,
headers: self.class.headers,
)
rescue ShopifyAPI::Errors::HttpResponseError => e
@errors.errors << e
Expand All @@ -378,6 +392,7 @@ def save(update_object: false)
method,
body: body,
path: deduce_write_path(method),
headers: self.class.headers,
)

if update_object
Expand Down
48 changes: 48 additions & 0 deletions test/clients/base_rest_resource_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,51 @@ def test_finds_all_resources
assert_equal([2, "attribute2"], [got[1].id, got[1].attribute])
end

def test_finds_all_resources_with_headers
ShopifyAPI::Rest::Base.stubs(:headers).returns({ "X-Shopify-Test" => "test" })

stub_request(:get, "#{@prefix}/fake_resources.json")
.with(headers: { "X-Shopify-Test" => "test" })

TestHelpers::FakeResource.all(session: @session)
end

def test_update_resource_with_headers
ShopifyAPI::Rest::Base.stubs(:headers).returns({ "X-Shopify-Test" => "test" })

stub_request(:put, "#{@prefix}/fake_resources/1.json")
.with(headers: { "X-Shopify-Test" => "test" })

fake_resource = TestHelpers::FakeResource.new(session: @session)
fake_resource.id = 1
fake_resource.attribute = "updated"

assert(fake_resource.save)
end

def test_create_resource_with_headers
ShopifyAPI::Rest::Base.stubs(:headers).returns({ "X-Shopify-Test" => "test" })

stub_request(:post, "#{@prefix}/fake_resources.json")
.with(headers: { "X-Shopify-Test" => "test" })

fake_resource = TestHelpers::FakeResource.new(session: @session)
fake_resource.attribute = "create"

assert(fake_resource.save)
end

def test_delete_resource_with_headers
ShopifyAPI::Rest::Base.stubs(:headers).returns({ "X-Shopify-Test" => "test" })

stub_request(:delete, "#{@prefix}/fake_resources/1.json")
.with(headers: { "X-Shopify-Test" => "test" })

fake_resource = TestHelpers::FakeResource.new(session: @session)
fake_resource.id = 1
assert(fake_resource.delete)
end

def test_saves
request_body = { fake_resource: { attribute: "attribute" } }.to_json
response_body = { fake_resource: { id: 1, attribute: "attribute" } }.to_json
Expand Down Expand Up @@ -428,6 +473,7 @@ def test_put_requests_only_modify_changed_attributes
body: { "product" => { "metafields" => [{ "key" => "new", "value" => "newvalue", "type" => "single_line_text_field",
"namespace" => "global", }], "id" => 632910392, } },
path: "products/632910392.json",
headers: nil,
)
product.metafields = [
{
Expand All @@ -452,6 +498,7 @@ def test_put_request_for_has_one_association_works
customer.client.expects(:put).with(
body: { "customer" => { "tags" => "New Customer, Repeat Customer", "id" => 207119551 } },
path: "customers/207119551.json",
headers: nil,
)
customer.tags = "New Customer, Repeat Customer"

Expand Down Expand Up @@ -500,6 +547,7 @@ def test_put_requests_for_resource_with_read_only_attributes
variant.client.expects(:put).with(
body: { "variant" => { "barcode" => "1234", "id" => 169 } },
path: "variants/169.json",
headers: nil,
)
variant.barcode = "1234"
variant.save
Expand Down

0 comments on commit 95271c5

Please sign in to comment.