Skip to content

Commit 34c8511

Browse files
Fixes #1311 by ensuring that the response body is always a Hash, even if ShopifyAPI.Context.response_as_struct is true.
Had to add a Utility (ShopifyAPI::Utils::OstructHashUtils) to handle the conversion since a simple .to_h and even JSON.parse(response.body.to_json) did not work as expected (nested Keys and Array handling failed).
1 parent c1163e0 commit 34c8511

File tree

4 files changed

+100
-4
lines changed

4 files changed

+100
-4
lines changed
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
# typed: strict
2+
# frozen_string_literal: true
3+
4+
require "active_support/core_ext/hash"
5+
6+
module ShopifyAPI
7+
module Utils
8+
module OstructHashUtils
9+
class << self
10+
extend T::Sig
11+
12+
sig { params(object: OpenStruct, hash: T::Hash).returns(T::Hash[String, T.untyped]) }
13+
def open_struct_to_hash(object, hash = {})
14+
object.each_pair do |key, value|
15+
hash[key] = case value
16+
when OpenStruct then open_struct_to_hash(value)
17+
when Array then value.map { |v| v.is_a?(OpenStruct) ? open_struct_to_hash(v) : v }
18+
else value
19+
end
20+
end
21+
hash.deep_stringify_keys
22+
end
23+
24+
sig { params(obj: T.any(Hash, OpenStruct)).returns(T::Hash[String, T.untyped]) }
25+
def ensure_hash(obj)
26+
case obj
27+
when Hash
28+
obj
29+
when OpenStruct
30+
open_struct_to_hash(obj)
31+
else
32+
raise ArgumentError, "Expected Hash or OpenStruct, got #{obj.class}"
33+
end
34+
end
35+
36+
end
37+
end
38+
end
39+
end

lib/shopify_api/webhooks/registry.rb

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -141,7 +141,8 @@ def unregister(topic:, session:)
141141
delete_response = client.query(query: delete_mutation)
142142
raise Errors::WebhookRegistrationError,
143143
"Failed to delete webhook from Shopify" unless delete_response.ok?
144-
result = T.cast(delete_response.body, T::Hash[String, T.untyped])
144+
result = T.cast(ShopifyAPI::Utils::OstructHashUtils.ensure_hash(delete_response.body),
145+
T::Hash[String, T.untyped])
145146
errors = result["errors"] || {}
146147
raise Errors::WebhookRegistrationError,
147148
"Failed to delete webhook from Shopify: #{errors[0]["message"]}" unless errors.empty?
@@ -173,7 +174,8 @@ def get_webhook_id(topic:, client:)
173174
fetch_id_response = client.query(query: fetch_id_query)
174175
raise Errors::WebhookRegistrationError,
175176
"Failed to fetch webhook from Shopify" unless fetch_id_response.ok?
176-
body = T.cast(fetch_id_response.body, T::Hash[String, T.untyped])
177+
body = T.cast(ShopifyAPI::Utils::OstructHashUtils.ensure_hash(fetch_id_response.body),
178+
T::Hash[String, T.untyped])
177179
errors = body["errors"] || {}
178180
raise Errors::WebhookRegistrationError,
179181
"Failed to fetch webhook from Shopify: #{errors[0]["message"]}" unless errors.empty?
@@ -219,7 +221,10 @@ def webhook_registration_needed?(client, registration)
219221
check_response = client.query(query: registration.build_check_query)
220222
raise Errors::WebhookRegistrationError,
221223
"Failed to check if webhook was already registered" unless check_response.ok?
222-
parsed_check_result = registration.parse_check_result(T.cast(check_response.body, T::Hash[String, T.untyped]))
224+
225+
response_body = ShopifyAPI::Utils::OstructHashUtils.ensure_hash(check_response.body)
226+
227+
parsed_check_result = registration.parse_check_result(T.cast(response_body, T::Hash[String, T.untyped]))
223228
must_register = parsed_check_result[:current_address] != registration.callback_address
224229

225230
{ webhook_id: parsed_check_result[:webhook_id], must_register: must_register }
@@ -237,7 +242,8 @@ def send_register_request(client, registration, webhook_id)
237242

238243
raise Errors::WebhookRegistrationError, "Failed to register webhook with Shopify" unless register_response.ok?
239244

240-
T.cast(register_response.body, T::Hash[String, T.untyped])
245+
response_body = ShopifyAPI::Utils::OstructHashUtils.ensure_hash(register_response.body)
246+
T.cast(response_body, T::Hash[String, T.untyped])
241247
end
242248

243249
sig { params(body: T::Hash[String, T.untyped], mutation_name: String).returns(T::Boolean) }
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
# typed: false
2+
# frozen_string_literal: true
3+
4+
require_relative "../test_helper"
5+
module ShopifyAPITest
6+
module Utils
7+
class OstructHashUtilsTest < Test::Unit::TestCase
8+
def setup
9+
@utils = ShopifyAPI::Utils::OstructHashUtils
10+
@ostruct = OpenStruct.new({ key1: "value1", key2: OpenStruct.new({ nested_key: "nested_value" }) })
11+
@hash = { "key1" => "value1", "key2" => { "nested_key" => "nested_value" } }
12+
end
13+
14+
def test_open_struct_to_hash_with_open_struct
15+
assert_equal(@hash, @utils.open_struct_to_hash(@ostruct))
16+
end
17+
18+
def test_ensure_hash_with_hash
19+
assert_equal(@hash, @utils.ensure_hash(@hash))
20+
end
21+
22+
def test_ensure_hash_with_open_struct
23+
assert_equal(@hash, @utils.ensure_hash(@ostruct))
24+
end
25+
26+
end
27+
end
28+
end

test/webhooks/registry_test.rb

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,29 @@ def test_process
5959
assert(handler_called)
6060
end
6161

62+
def test_process_with_response_as_struct
63+
modify_context(response_as_struct: true)
64+
65+
handler_called = false
66+
67+
handler = TestHelpers::FakeWebhookHandler.new(
68+
lambda do |topic, shop, body,|
69+
assert_equal(@topic, topic)
70+
assert_equal(@shop, shop)
71+
assert_equal({}, body)
72+
handler_called = true
73+
end,
74+
)
75+
76+
ShopifyAPI::Webhooks::Registry.add_registration(
77+
topic: @topic, path: "path", delivery_method: :http, handler: handler,
78+
)
79+
80+
ShopifyAPI::Webhooks::Registry.process(@webhook_request)
81+
82+
assert(handler_called)
83+
end
84+
6285
def test_process_new_handler
6386
handler_called = false
6487

0 commit comments

Comments
 (0)