diff --git a/.rubocop.yml b/.rubocop.yml index e7e2d436c..8bd331f1b 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -57,6 +57,8 @@ Layout/EmptyLineAfterGuardClause: Enabled: true Style/GlobalStdStream: Enabled: true +Style/OpenStructUse: + Enabled: false Layout/LineLength: Exclude: - "test/clients/*" diff --git a/CHANGELOG.md b/CHANGELOG.md index bc71e2c1b..56f7995a1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ Note: For changes to the API, see https://shopify.dev/changelog?filter=api ## Unreleased +[#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. ## 13.3.1 diff --git a/lib/shopify_api/clients/graphql/client.rb b/lib/shopify_api/clients/graphql/client.rb index 958cc2166..35db8a0ec 100644 --- a/lib/shopify_api/clients/graphql/client.rb +++ b/lib/shopify_api/clients/graphql/client.rb @@ -42,6 +42,7 @@ def query(query:, variables: nil, headers: nil, tries: 1) body_type: "application/json", tries: tries, ), + response_as_struct: Context.response_as_struct || false, ) end end diff --git a/lib/shopify_api/clients/http_client.rb b/lib/shopify_api/clients/http_client.rb index 260ade137..30480f443 100644 --- a/lib/shopify_api/clients/http_client.rb +++ b/lib/shopify_api/clients/http_client.rb @@ -32,8 +32,8 @@ def initialize(base_path:, session: nil) end end - sig { params(request: HttpRequest).returns(HttpResponse) } - def request(request) + sig { params(request: HttpRequest, response_as_struct: T::Boolean).returns(HttpResponse) } + def request(request, response_as_struct: false) request.verify headers = @headers @@ -60,6 +60,11 @@ def request(request) body = res.body end + if response_as_struct && body.is_a?(Hash) + json_body = body.to_json + body = JSON.parse(json_body, object_class: OpenStruct) + end + response = HttpResponse.new(code: res.code.to_i, headers: res.headers.to_h, body: body) if response.headers["x-shopify-api-deprecated-reason"] diff --git a/lib/shopify_api/clients/http_response.rb b/lib/shopify_api/clients/http_response.rb index 02c203b82..0196a6e3f 100644 --- a/lib/shopify_api/clients/http_response.rb +++ b/lib/shopify_api/clients/http_response.rb @@ -12,7 +12,7 @@ class HttpResponse sig { returns(T::Hash[String, T::Array[String]]) } attr_reader :headers - sig { returns(T.any(T::Hash[String, T.untyped], String)) } + sig { returns(T.any(T::Hash[String, T.untyped], String, OpenStruct)) } attr_reader :body sig { returns(T.nilable(String)) } @@ -22,7 +22,7 @@ class HttpResponse params( code: Integer, headers: T::Hash[String, T::Array[String]], - body: T.any(T::Hash[String, T.untyped], String), + body: T.any(T::Hash[String, T.untyped], String, OpenStruct), ).void end def initialize(code:, headers:, body:) diff --git a/lib/shopify_api/context.rb b/lib/shopify_api/context.rb index 2fbc430b3..6edc077c3 100644 --- a/lib/shopify_api/context.rb +++ b/lib/shopify_api/context.rb @@ -21,6 +21,7 @@ class Context @active_session = T.let(Concurrent::ThreadLocalVar.new { nil }, T.nilable(Concurrent::ThreadLocalVar)) @user_agent_prefix = T.let(nil, T.nilable(String)) @old_api_secret_key = T.let(nil, T.nilable(String)) + @response_as_struct = T.let(false, T.nilable(T::Boolean)) @rest_resource_loader = T.let(nil, T.nilable(Zeitwerk::Loader)) @@ -43,6 +44,7 @@ class << self user_agent_prefix: T.nilable(String), old_api_secret_key: T.nilable(String), api_host: T.nilable(String), + response_as_struct: T.nilable(T::Boolean), ).void end def setup( @@ -59,7 +61,8 @@ def setup( private_shop: nil, user_agent_prefix: nil, old_api_secret_key: nil, - api_host: nil + api_host: nil, + response_as_struct: false ) unless ShopifyAPI::AdminVersions::SUPPORTED_ADMIN_VERSIONS.include?(api_version) raise Errors::UnsupportedVersionError, @@ -78,6 +81,7 @@ def setup( @private_shop = private_shop @user_agent_prefix = user_agent_prefix @old_api_secret_key = old_api_secret_key + @response_as_struct = response_as_struct @log_level = if valid_log_level?(log_level) log_level.to_sym else @@ -128,6 +132,9 @@ def load_rest_resources(api_version:) sig { returns(Symbol) } attr_reader :log_level + sig { returns T.nilable(T::Boolean) } + attr_reader :response_as_struct + sig { returns(T::Boolean) } def private? @is_private diff --git a/test/clients/http_client_test.rb b/test/clients/http_client_test.rb index 38c674013..fce62c21c 100644 --- a/test/clients/http_client_test.rb +++ b/test/clients/http_client_test.rb @@ -270,6 +270,16 @@ def test_json_parser_error assert_equal(502, error.code) end + def test_response_as_struct + stub_request(@request.http_method, "https://#{@shop}#{@base_path}/#{@request.path}") + .with(body: @request.body.to_json, query: @request.query, headers: @expected_headers) + .to_return(body: { "key" => { "nested_key" => "nested_value" } }.to_json, headers: @response_headers) + + response = @client.request(@request, response_as_struct: true) + assert_kind_of(OpenStruct, response.body) + assert_equal("nested_value", response.body.key.nested_key) + end + private def simple_http_test(http_method) diff --git a/test/test_helper.rb b/test/test_helper.rb index ff3bb81f2..d389c9d08 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -51,6 +51,7 @@ def setup private_shop: T.nilable(String), user_agent_prefix: T.nilable(String), old_api_secret_key: T.nilable(String), + response_as_struct: T.nilable(T::Boolean), ).void end def modify_context( @@ -64,7 +65,8 @@ def modify_context( logger: nil, private_shop: "do-not-set", user_agent_prefix: nil, - old_api_secret_key: nil + old_api_secret_key: nil, + response_as_struct: nil ) ShopifyAPI::Context.setup( api_key: api_key ? api_key : ShopifyAPI::Context.api_key, @@ -79,6 +81,7 @@ def modify_context( user_agent_prefix: user_agent_prefix ? user_agent_prefix : ShopifyAPI::Context.user_agent_prefix, old_api_secret_key: old_api_secret_key ? old_api_secret_key : ShopifyAPI::Context.old_api_secret_key, log_level: :off, + response_as_struct: response_as_struct || ShopifyAPI::Context.response_as_struct, ) end end