lib/sdr_client/redesigned_client.rb
# frozen_string_literal: true
require 'digest'
require 'logger'
require 'shellwords'
require 'singleton'
require 'timeout'
module SdrClient
# The SDR client reimagined, built using patterns successfully used in other client gems we maintain
class RedesignedClient
include Singleton
class << self
# rubocop:disable Metrics/MethodLength, Metrics/ParameterLists
def configure(url:, email: nil, password: nil, token_refresher: nil, token: default_token,
request_options: default_request_options, logger: default_logger)
if email.blank? && password.blank? && !token_refresher.respond_to?(:call)
raise ArgumentError, 'email and password cannot be blank without a custom token refresher callable'
end
instance.config = Config.new(
token: token,
url: url,
email: email,
password: password,
request_options: request_options,
logger: logger,
token_refresher: token_refresher
)
instance
end
# rubocop:enable Metrics/MethodLength, Metrics/ParameterLists
# For the initial token, use a dummy value to avoid hitting any APIs
# during configuration, allowing `with_token_refresh_when_unauthorized` to handle
# auto-magic token refreshing. Why not immediately get a valid token? Our apps
# commonly invoke client `.configure` methods in the initializer in all
# application environments, even those that are never expected to
# connect to production APIs, such as local development machines.
#
# NOTE: `nil` and blank string cannot be used as dummy values here as
# they lead to a malformed request to be sent, which triggers an
# exception not rescued by `with_token_refresh_when_unauthorized`
def default_token
'a temporary dummy token to avoid hitting the API before it is needed'
end
def default_logger
Logger.new($stdout)
end
def default_request_options
{
read_timeout: default_timeout,
timeout: default_timeout
}
end
# NOTE: This is the number of seconds it roughly takes for H2 to
# successfully shunt ~10GB files over to SDR API
def default_timeout
900
end
delegate :config, :connection, :deposit_model, :job_status, :find, :update_model, :build_and_deposit,
to: :instance
end
attr_accessor :config
def deposit_model(...)
Deposit.deposit_model(...)
end
def job_status(...)
JobStatus.new(...)
end
def find(...)
Find.run(...)
end
def update_model(...)
UpdateResource.run(...)
end
def build_and_deposit(...)
Metadata.deposit(...)
end
# Send an authenticated GET request
# @param path [String] the path to the SDR API request
def get(path:)
response = with_token_refresh_when_unauthorized do
connection.get(path)
end
UnexpectedResponse.call(response) unless response.success?
return nil if response.body.blank?
JSON.parse(response.body).with_indifferent_access
end
# Send an authenticated POST request
# @param path [String] the path to the SDR API request
# @param body [String] the body of the SDR API request
# @param headers [Hash] extra headers to add to the SDR API request
# @param expected_status [Integer] override if all 2xx statuses aren't success conditions
def post(path:, body:, headers: {}, expected_status: nil) # rubocop:disable Metrics/MethodLength
response = with_token_refresh_when_unauthorized do
connection.post(path) do |request|
request.body = body
request.headers = default_headers.merge(headers)
end
end
if expected_status
UnexpectedResponse.call(response) if response.status != expected_status
elsif !response.success?
UnexpectedResponse.call(response)
end
return nil if response.body.blank?
JSON.parse(response.body).with_indifferent_access
end
# Send an authenticated PUT request
# @param path [String] the path to the SDR API request
# @param body [String] the body of the SDR API request
# @param headers [Hash] extra headers to add to the SDR API request
# @param params [Hash] query parameters to add to the SDR API request
# @param expected_status [Integer] override if all 2xx statuses aren't success conditions
def put(path:, body:, headers: {}, params: {}, expected_status: nil) # rubocop:disable Metrics/MethodLength
response = with_token_refresh_when_unauthorized do
connection.put(path) do |request|
request.body = body
request.headers = default_headers.merge(headers)
request.params = params if params.present?
end
end
if expected_status
UnexpectedResponse.call(response) if response.status != expected_status
elsif !response.success?
UnexpectedResponse.call(response)
end
return nil if response.body.blank?
JSON.parse(response.body).with_indifferent_access
end
private
Config = Struct.new(:url, :email, :password, :token, :logger,
:request_options, :token_refresher, keyword_init: true)
def connection
Faraday.new(
url: SdrClient::RedesignedClient.config.url,
headers: default_headers,
request: SdrClient::RedesignedClient.config.request_options
) do |conn|
conn.adapter :net_http
end
end
def default_headers
{
accept: 'application/json',
content_type: 'application/json',
Authorization: "Bearer #{config.token}"
}
end
def with_token_refresh_when_unauthorized
response = yield
# if unauthorized, token has likely expired. try to get a new token and then retry the same request(s).
if response.status == 401
config.token = config.token_refresher ? config.token_refresher.call : Authenticator.token
response = yield
end
response
end
end
end