From 5748ac38f4bafc8baa4b91467c1a9f0faae0dd54 Mon Sep 17 00:00:00 2001 From: Hakan Ensari Date: Mon, 7 Oct 2024 13:54:51 +0200 Subject: [PATCH] Allow custom parser Paves the way for returning what's in Amazon's Open API models --- CHANGELOG.md | 1 + lib/peddler/api.rb | 17 ++- lib/peddler/response_decorator.rb | 46 ++++++++ test/peddler/custom_parser_test.rb | 42 ++++++++ test/peddler/response_decorator_test.rb | 70 ++++++++++++ .../test_custom_parser_on_class.yml | 101 ++++++++++++++++++ .../test_custom_parser_on_instance.yml | 101 ++++++++++++++++++ 7 files changed, 377 insertions(+), 1 deletion(-) create mode 100644 lib/peddler/response_decorator.rb create mode 100644 test/peddler/custom_parser_test.rb create mode 100644 test/peddler/response_decorator_test.rb create mode 100644 test/vcr_cassettes/Peddler/CustomParserTest/test_custom_parser_on_class.yml create mode 100644 test/vcr_cassettes/Peddler/CustomParserTest/test_custom_parser_on_instance.yml diff --git a/CHANGELOG.md b/CHANGELOG.md index 60da11e2..95942fdc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ This project adheres to [Semantic Versioning](http://semver.org/). ### Added +- Allow custom parser - Marketplace.id and Marketplace.ids shorthands ## [3.0.0] - 2024-10-04 diff --git a/lib/peddler/api.rb b/lib/peddler/api.rb index 05b10c26..8c4b5644 100644 --- a/lib/peddler/api.rb +++ b/lib/peddler/api.rb @@ -5,6 +5,7 @@ require "peddler/endpoint" require "peddler/error" require "peddler/marketplace" +require "peddler/response_decorator" require "peddler/version" module Peddler @@ -13,6 +14,11 @@ class API class CannotSandbox < StandardError; end class MustSandbox < StandardError; end + class << self + # @return [#call] + attr_accessor :parser + end + # @return [Peddler::Endpoint] attr_reader :endpoint @@ -122,10 +128,19 @@ def meter(rate_limit) raise error if error end - response + ResponseDecorator.decorate(response, parser:) end end + # @param [#call] + attr_writer :parser + + # @!attribute [r] + # @return [#call] + def parser + @parser || self.class.parser + end + private def user_agent diff --git a/lib/peddler/response_decorator.rb b/lib/peddler/response_decorator.rb new file mode 100644 index 00000000..59d8849f --- /dev/null +++ b/lib/peddler/response_decorator.rb @@ -0,0 +1,46 @@ +# frozen_string_literal: true + +require "delegate" +require "forwardable" + +module Peddler + # Wraps HTTP::Response to allow custom parsing + class ResponseDecorator < SimpleDelegator + extend Forwardable + + # @!method dig(*key) + # Delegates to the Hash returned by {Response#to_h} to extract a nested + # value specified by the sequence of keys + # + # @param [String] key one or more keys + # @see https://ruby-doc.org/core/Hash.html#method-i-dig + def_delegator :to_h, :dig + + class << self + # Decorates an HTTP::Response + # + # @param [HTTP::Response] response + # @param [nil, #call] parser (if any) + # @return [ResponseDecorator] + def decorate(response, parser: nil) + new(response).tap do |decorator| + decorator.parser = parser + end + end + end + + # @return [#call] + attr_accessor :parser + + def parse + parser ? parser.call(__getobj__) : __getobj__.parse + end + + # Converts the response body to a Hash + # + # @return [Hash] + def to_h + (parser && parser.respond_to?(:to_h) ? parser : parse).to_h + end + end +end diff --git a/test/peddler/custom_parser_test.rb b/test/peddler/custom_parser_test.rb new file mode 100644 index 00000000..c32a2ad1 --- /dev/null +++ b/test/peddler/custom_parser_test.rb @@ -0,0 +1,42 @@ +# frozen_string_literal: true + +require "helper" + +require "peddler/api/reports_2021_06_30" + +module Peddler + class CustomParserTest < Minitest::Test + include FeatureHelpers + + def test_custom_parser_on_instance + api.parser = custom_parser + res = api.get_report("1234567") + + assert_nil(api_class.parser) + assert(api.parser) + assert(res.dig(:reportId)) + end + + def test_custom_parser_on_class + klass = Class.new(api_class) + klass.parser = custom_parser + access_token = request_access_token(grantless: false) + api = klass.new(aws_region, access_token) + res = api.get_report("1234567") + + assert_nil(api_class.parser) + assert(klass.parser) + assert(res.dig(:reportId)) + end + + private + + def custom_parser + ->(response) { JSON.parse(response, symbolize_names: true) } + end + + def api_class + API::Reports20210630 + end + end +end diff --git a/test/peddler/response_decorator_test.rb b/test/peddler/response_decorator_test.rb new file mode 100644 index 00000000..e455b972 --- /dev/null +++ b/test/peddler/response_decorator_test.rb @@ -0,0 +1,70 @@ +# frozen_string_literal: true + +require "helper" +require "peddler/response_decorator" + +module Peddler + class ResponseDecoratorTest < Minitest::Test + def test_parses + decorator = ResponseDecorator.decorate(response) + + assert_equal(payload, decorator.parse) + end + + def test_to_h + decorator = ResponseDecorator.decorate(response) + + assert_equal(payload, decorator.to_h) + end + + def test_dig + decorator = ResponseDecorator.decorate(response) + + assert(decorator.dig("foo")) + end + + def test_parses_with_custom_parser + decorator = ResponseDecorator.decorate( + response, parser: ->(response) { JSON.parse(response, symbolize_names: true) } + ) + + assert_equal(payload_with_symbolized_keys, decorator.parse) + end + + def test_to_h_with_custom_parser + decorator = ResponseDecorator.decorate( + response, parser: ->(response) { JSON.parse(response, symbolize_names: true) } + ) + + assert_equal(payload_with_symbolized_keys, decorator.to_h) + end + + def test_dig_with_custom_parser + decorator = ResponseDecorator.decorate( + response, parser: ->(response) { JSON.parse(response, symbolize_names: true) } + ) + + assert(decorator.dig(:foo)) + end + + private + + def response + HTTP::Response.new( + body: JSON.dump(payload), + headers: { "Content-Type" => "application/json" }, + status: nil, + version: nil, + request: nil, + ) + end + + def payload + { "foo" => "bar" } + end + + def payload_with_symbolized_keys + payload.transform_keys(&:to_sym) + end + end +end diff --git a/test/vcr_cassettes/Peddler/CustomParserTest/test_custom_parser_on_class.yml b/test/vcr_cassettes/Peddler/CustomParserTest/test_custom_parser_on_class.yml new file mode 100644 index 00000000..f1c5987f --- /dev/null +++ b/test/vcr_cassettes/Peddler/CustomParserTest/test_custom_parser_on_class.yml @@ -0,0 +1,101 @@ +--- +http_interactions: + - request: + method: post + uri: https://api.amazon.com/auth/o2/token + body: + encoding: ASCII-8BIT + string: grant_type=refresh_token&refresh_token=FILTERED&client_id=FILTERED&client_secret=FILTERED + headers: + Connection: + - close + Content-Type: + - application/x-www-form-urlencoded + Host: + - api.amazon.com + User-Agent: + - http.rb/5.2.0 + response: + status: + code: 200 + message: OK + headers: + Server: + - Server + Date: + - Thu, 12 Sep 2024 19:57:33 GMT + Content-Type: + - application/json;charset=UTF-8 + Content-Length: + - "806" + Connection: + - close + X-Amz-Rid: + - RRDKVWGM5DDFAA5RDXV3 + X-Amzn-Requestid: + - 8e73f2b7-037d-4732-90e1-c21a7046ad97 + X-Amz-Date: + - Thu, 12 Sep 2024 19:57:33 GMT + Cache-Control: + - no-cache, no-store, must-revalidate + Pragma: + - no-cache + Vary: + - Content-Type,Accept-Encoding,User-Agent + Strict-Transport-Security: + - max-age=47474747; includeSubDomains; preload + body: + encoding: UTF-8 + string: '{"access_token":"FILTERED","refresh_token":"FILTERED","token_type":"bearer","expires_in":3600}' + recorded_at: Thu, 12 Sep 2024 19:57:33 GMT + - request: + method: get + uri: https://sellingpartnerapi-eu.amazon.com/reports/2021-06-30/reports/1234567 + body: + encoding: ASCII-8BIT + string: "" + headers: + Host: + - sellingpartnerapi-eu.amazon.com + User-Agent: + - Peddler/3.0.0.pre (Language=Ruby; Hakans-MacBook-Pro.local) + X-Amz-Access-Token: + - FILTERED + X-Amz-Date: + - 20240912T195733Z + Connection: + - close + response: + status: + code: 200 + message: OK + headers: + Server: + - Server + Date: + - Thu, 12 Sep 2024 19:57:33 GMT + Content-Type: + - application/json + Content-Length: + - "460" + Connection: + - close + X-Amz-Rid: + - FVNFTSVVK3KES8YZ4ZJM + X-Amzn-Ratelimit-Limit: + - "2.0" + X-Amzn-Requestid: + - 838973d3-c682-4ab4-b88c-afbd19d32bb5 + X-Amz-Apigw-Id: + - OPF838973d3c682 + X-Amzn-Trace-Id: + - Root=1-66e347ad-838973d3c6824ab4 + Vary: + - Content-Type,Accept-Encoding,User-Agent + Strict-Transport-Security: + - max-age=47474747; includeSubDomains; preload + body: + encoding: UTF-8 + string: '{"reportType":"GET_MERCHANTS_LISTINGS_FYP_REPORT","processingEndTime":"2024-09-12T19:41:13+00:00","processingStatus":"DONE","marketplaceIds":["A1F83G8C2ARO7P"],"reportDocumentId":"amzn1.spdoc.1.4.eu.00051127-bd5b-48ff-ab6a-6d76c9061260.TXVRX1WISIZWM.5900","reportId":"292248019978","dataEndTime":"2024-09-12T19:40:59+00:00","createdTime":"2024-09-12T19:40:59+00:00","processingStartTime":"2024-09-12T19:41:02+00:00","dataStartTime":"2024-09-12T19:40:59+00:00"}' + recorded_at: Thu, 12 Sep 2024 19:57:33 GMT +recorded_with: VCR 6.3.1 diff --git a/test/vcr_cassettes/Peddler/CustomParserTest/test_custom_parser_on_instance.yml b/test/vcr_cassettes/Peddler/CustomParserTest/test_custom_parser_on_instance.yml new file mode 100644 index 00000000..f1c5987f --- /dev/null +++ b/test/vcr_cassettes/Peddler/CustomParserTest/test_custom_parser_on_instance.yml @@ -0,0 +1,101 @@ +--- +http_interactions: + - request: + method: post + uri: https://api.amazon.com/auth/o2/token + body: + encoding: ASCII-8BIT + string: grant_type=refresh_token&refresh_token=FILTERED&client_id=FILTERED&client_secret=FILTERED + headers: + Connection: + - close + Content-Type: + - application/x-www-form-urlencoded + Host: + - api.amazon.com + User-Agent: + - http.rb/5.2.0 + response: + status: + code: 200 + message: OK + headers: + Server: + - Server + Date: + - Thu, 12 Sep 2024 19:57:33 GMT + Content-Type: + - application/json;charset=UTF-8 + Content-Length: + - "806" + Connection: + - close + X-Amz-Rid: + - RRDKVWGM5DDFAA5RDXV3 + X-Amzn-Requestid: + - 8e73f2b7-037d-4732-90e1-c21a7046ad97 + X-Amz-Date: + - Thu, 12 Sep 2024 19:57:33 GMT + Cache-Control: + - no-cache, no-store, must-revalidate + Pragma: + - no-cache + Vary: + - Content-Type,Accept-Encoding,User-Agent + Strict-Transport-Security: + - max-age=47474747; includeSubDomains; preload + body: + encoding: UTF-8 + string: '{"access_token":"FILTERED","refresh_token":"FILTERED","token_type":"bearer","expires_in":3600}' + recorded_at: Thu, 12 Sep 2024 19:57:33 GMT + - request: + method: get + uri: https://sellingpartnerapi-eu.amazon.com/reports/2021-06-30/reports/1234567 + body: + encoding: ASCII-8BIT + string: "" + headers: + Host: + - sellingpartnerapi-eu.amazon.com + User-Agent: + - Peddler/3.0.0.pre (Language=Ruby; Hakans-MacBook-Pro.local) + X-Amz-Access-Token: + - FILTERED + X-Amz-Date: + - 20240912T195733Z + Connection: + - close + response: + status: + code: 200 + message: OK + headers: + Server: + - Server + Date: + - Thu, 12 Sep 2024 19:57:33 GMT + Content-Type: + - application/json + Content-Length: + - "460" + Connection: + - close + X-Amz-Rid: + - FVNFTSVVK3KES8YZ4ZJM + X-Amzn-Ratelimit-Limit: + - "2.0" + X-Amzn-Requestid: + - 838973d3-c682-4ab4-b88c-afbd19d32bb5 + X-Amz-Apigw-Id: + - OPF838973d3c682 + X-Amzn-Trace-Id: + - Root=1-66e347ad-838973d3c6824ab4 + Vary: + - Content-Type,Accept-Encoding,User-Agent + Strict-Transport-Security: + - max-age=47474747; includeSubDomains; preload + body: + encoding: UTF-8 + string: '{"reportType":"GET_MERCHANTS_LISTINGS_FYP_REPORT","processingEndTime":"2024-09-12T19:41:13+00:00","processingStatus":"DONE","marketplaceIds":["A1F83G8C2ARO7P"],"reportDocumentId":"amzn1.spdoc.1.4.eu.00051127-bd5b-48ff-ab6a-6d76c9061260.TXVRX1WISIZWM.5900","reportId":"292248019978","dataEndTime":"2024-09-12T19:40:59+00:00","createdTime":"2024-09-12T19:40:59+00:00","processingStartTime":"2024-09-12T19:41:02+00:00","dataStartTime":"2024-09-12T19:40:59+00:00"}' + recorded_at: Thu, 12 Sep 2024 19:57:33 GMT +recorded_with: VCR 6.3.1