From ce094af242775cf9caae25b13ee50cc67d3fd43d Mon Sep 17 00:00:00 2001 From: Kevin Soltysiak Date: Sun, 11 Feb 2024 23:36:08 +0100 Subject: [PATCH] refactor(farady): split up middlewares --- lib/scalingo/api/client.rb | 12 ++- .../{unpack_middleware.rb => extract_meta.rb} | 12 +-- lib/scalingo/faraday/extract_root_value.rb | 18 ++++ lib/scalingo/faraday/response.rb | 3 +- spec/scalingo/faraday/extract_meta_spec.rb | 101 ++++++++++++++++++ .../faraday/extract_root_value_spec.rb | 48 +++++++++ 6 files changed, 180 insertions(+), 14 deletions(-) rename lib/scalingo/faraday/{unpack_middleware.rb => extract_meta.rb} (55%) create mode 100644 lib/scalingo/faraday/extract_root_value.rb create mode 100644 spec/scalingo/faraday/extract_meta_spec.rb create mode 100644 spec/scalingo/faraday/extract_root_value_spec.rb diff --git a/lib/scalingo/api/client.rb b/lib/scalingo/api/client.rb index a4ccb08..a7ba304 100644 --- a/lib/scalingo/api/client.rb +++ b/lib/scalingo/api/client.rb @@ -1,6 +1,7 @@ require "scalingo/token_holder" require "scalingo/faraday/response" -require "scalingo/faraday/unpack_middleware" +require "scalingo/faraday/extract_meta" +require "scalingo/faraday/extract_root_value" require "active_support/core_ext/hash" module Scalingo @@ -87,7 +88,8 @@ def connection(fallback_to_guest: false) def unauthenticated_connection @unauthenticated_conn ||= Faraday.new(connection_options) { |conn| - conn.response :unpack + conn.response :extract_root_value + conn.response :extract_meta conn.response :json, content_type: /\bjson$/, parser_options: {symbolize_names: true} conn.request :json @@ -108,7 +110,8 @@ def authenticated_connection end @connection = Faraday.new(connection_options) { |conn| - conn.response :unpack + conn.response :extract_root_value + conn.response :extract_meta conn.response :json, content_type: /\bjson$/, parser_options: {symbolize_names: true} conn.request :json conn.request :authorization, "Bearer", -> { token_holder.token&.value } @@ -122,7 +125,8 @@ def database_connection(database_id) @database_connections ||= {} @database_connections[database_id] ||= Faraday.new(connection_options) { |conn| - conn.response :unpack + conn.response :extract_root_value + conn.response :extract_meta conn.response :json, content_type: /\bjson$/, parser_options: {symbolize_names: true} conn.request :json conn.request :authorization, "Bearer", -> { token_holder.database_tokens[database_id]&.value } diff --git a/lib/scalingo/faraday/unpack_middleware.rb b/lib/scalingo/faraday/extract_meta.rb similarity index 55% rename from lib/scalingo/faraday/unpack_middleware.rb rename to lib/scalingo/faraday/extract_meta.rb index c7fb3fe..be0304f 100644 --- a/lib/scalingo/faraday/unpack_middleware.rb +++ b/lib/scalingo/faraday/extract_meta.rb @@ -1,12 +1,9 @@ require "faraday" module Scalingo - class UnpackMiddleware < Faraday::Middleware + class ExtractMeta < Faraday::Middleware def on_complete(env) - # We only want to unpack the response for successful responses - return unless env.response.success? - - # Only hash-like objects are relevant to "unpack" + # Only hash-like objects are relevant return unless env.body.is_a?(Hash) # Extract meta from response @@ -23,11 +20,8 @@ def on_complete(env) }.compact } end - - # Dig the root key if it's the only remaining key in the body - env.body = env.body.values.first if env.body.size == 1 end end end -Faraday::Response.register_middleware(unpack: Scalingo::UnpackMiddleware) +Faraday::Response.register_middleware(extract_meta: Scalingo::ExtractMeta) diff --git a/lib/scalingo/faraday/extract_root_value.rb b/lib/scalingo/faraday/extract_root_value.rb new file mode 100644 index 0000000..ac23782 --- /dev/null +++ b/lib/scalingo/faraday/extract_root_value.rb @@ -0,0 +1,18 @@ +require "faraday" + +module Scalingo + class ExtractRootValue < Faraday::Middleware + def on_complete(env) + # We only want to unpack the response for successful responses + return unless env.response.success? + + # Only hash-like objects are relevant to "unpack" + return unless env.body.is_a?(Hash) + + # Dig the root key if it's the only remaining key in the body + env.body = env.body.values.first if env.body.size == 1 + end + end +end + +Faraday::Response.register_middleware(extract_root_value: Scalingo::ExtractRootValue) diff --git a/lib/scalingo/faraday/response.rb b/lib/scalingo/faraday/response.rb index e7c8e7f..078be99 100644 --- a/lib/scalingo/faraday/response.rb +++ b/lib/scalingo/faraday/response.rb @@ -1,7 +1,8 @@ require "faraday" require "faraday/response" -# Some additional methods for Faraday::Response in order to enhance expressiveness +# Note: the file is scalingo/faraday/response but we're reopning Faraday::Response: +# we define additional methods for Faraday::Response in order to enhance expressiveness module Faraday class Response def client_error? diff --git a/spec/scalingo/faraday/extract_meta_spec.rb b/spec/scalingo/faraday/extract_meta_spec.rb new file mode 100644 index 0000000..8f7627e --- /dev/null +++ b/spec/scalingo/faraday/extract_meta_spec.rb @@ -0,0 +1,101 @@ +require "spec_helper" +require "scalingo/faraday/extract_meta" + +RSpec.shared_examples "no meta extraction" do + it "does not extract meta from response" do + expect(subject.meta).to eq(nil) + expect(subject.meta?).to eq(false) + expect(subject.pagination).to eq(nil) + expect(subject.paginated?).to eq(false) + expect(subject.cursor_pagination).to eq(nil) + expect(subject.cursor_paginated?).to eq(false) + end +end + +RSpec.describe Scalingo::ExtractMeta do + let(:headers) { {"Content-Type" => content_type}.compact } + let(:content_type) { "application/json" } + let(:body) { nil } + + let(:client) do + Faraday.new do |b| + b.response :extract_meta + b.response :json, content_type: /\bjson$/, parser_options: {symbolize_names: true} + b.adapter :test do |stub| + stub.get("/") { [nil, headers, body] } + end + end + end + + subject { client.get("/") } + + context "with a non-json response" do + let(:content_type) { "text/html" } + let(:body) { "string".to_json } + + include_examples "no meta extraction" + end + + context "without a body" do + include_examples "no meta extraction" + end + + context "with a non indexable body" do + let(:body) { "string".to_json } + + include_examples "no meta extraction" + end + + context "with a non hash indexable body" do + let(:body) { [1, 2, 3].to_json } + + include_examples "no meta extraction" + end + + context "with a hash" do + context "without a meta key" do + let(:body) { {a: 1, b: 2}.to_json } + + include_examples "no meta extraction" + end + + context "with a generic meta key" do + let(:body) { {meta: {a: 1}}.to_json } + + it "extracts the meta" do + expect(subject.meta).to eq({a: 1}) + expect(subject.meta?).to eq(true) + expect(subject.pagination).to eq(nil) + expect(subject.paginated?).to eq(false) + expect(subject.cursor_pagination).to eq(nil) + expect(subject.cursor_paginated?).to eq(false) + end + end + + context "with a pagination meta key" do + let(:body) { {meta: {pagination: {page: 1}}}.to_json } + + it "extracts the meta" do + expect(subject.meta).to eq({pagination: {page: 1}}) + expect(subject.meta?).to eq(true) + expect(subject.pagination).to eq({page: 1}) + expect(subject.paginated?).to eq(true) + expect(subject.cursor_pagination).to eq(nil) + expect(subject.cursor_paginated?).to eq(false) + end + end + + context "with a cursor pagination" do + let(:body) { {next_cursor: 1, has_more: true}.to_json } + + it "extracts the meta" do + expect(subject.meta).to eq({cursor_pagination: {next_cursor: 1, has_more: true}}) + expect(subject.meta?).to eq(true) + expect(subject.pagination).to eq(nil) + expect(subject.paginated?).to eq(false) + expect(subject.cursor_pagination).to eq({next_cursor: 1, has_more: true}) + expect(subject.cursor_paginated?).to eq(true) + end + end + end +end diff --git a/spec/scalingo/faraday/extract_root_value_spec.rb b/spec/scalingo/faraday/extract_root_value_spec.rb new file mode 100644 index 0000000..30eb2be --- /dev/null +++ b/spec/scalingo/faraday/extract_root_value_spec.rb @@ -0,0 +1,48 @@ +require "spec_helper" +require "scalingo/faraday/extract_root_value" + +RSpec.describe Scalingo::ExtractRootValue do + let(:status) { 200 } + let(:body) { {a: 1} } + + let(:client) do + Faraday.new do |b| + b.response :extract_root_value + b.adapter :test do |stub| + stub.get("/") { [status, {}, body] } + end + end + end + + subject { client.get("/") } + + context "with a non-successful response" do + let(:status) { 300 } + + it "does not change the response body" do + expect(subject.body).to eq({a: 1}) + end + end + + context "with a non-hash response" do + let(:body) { [{a: 1}] } + + it "does not change the response body" do + expect(subject.body).to eq([{a: 1}]) + end + end + + context "with a multi keys hash" do + let(:body) { {a: 1, b: 2} } + + it "does not change the response body" do + expect(subject.body).to eq({a: 1, b: 2}) + end + end + + context "with a single key hash" do + it "extracts the response body" do + expect(subject.body).to eq(1) + end + end +end