class GdsApi::JsonClient

def self.cache(size=DEFAULT_CACHE_SIZE, ttl=DEFAULT_CACHE_TTL)

creates a cache with a size of 1.
LRUCache doesn't respect a cache size of 0, and instead effectively

header if it is included in the response.
Cache TTL will be overridden for a given request/response by the Expires
def self.cache(size=DEFAULT_CACHE_SIZE, ttl=DEFAULT_CACHE_TTL)
  @cache ||= LRUCache.new(max_size: size, ttl: ttl)
end

def self.cache=(c)

def self.cache=(c)
  @cache = c
end

def delete_json!(url, params = nil, additional_headers = {})

def delete_json!(url, params = nil, additional_headers = {})
  do_json_request(:delete, url, params, additional_headers)
end

def do_json_request(method, url, params = nil, additional_headers = {}, &create_response)

from the Net::HTTPResponse
create_response: optional block to instantiate a custom response object
additional_headers: headers to set on the request (in addition to the default ones)
params: the data to send (JSON-serialised) in the request body
url: the request URL
method: the symbolic name of the method to use, e.g. :get, :post
def do_json_request(method, url, params = nil, additional_headers = {}, &create_response)
  begin
    response = do_request_with_cache(method, url, (params.to_json if params), additional_headers)
  rescue RestClient::ResourceNotFound => e
    raise GdsApi::HTTPNotFound.new(e.http_code)
  rescue RestClient::Gone => e
    raise GdsApi::HTTPGone.new(e.http_code)
  rescue RestClient::Exception => e
    # Attempt to parse the body as JSON if possible
    error_details = begin
      e.http_body ? JSON.parse(e.http_body) : nil
    rescue JSON::ParserError
      nil
    end
    raise GdsApi::HTTPErrorResponse.new(e.http_code, error_details), e.http_body
  end
  # If no custom response is given, just instantiate Response
  create_response ||= Proc.new { |r| Response.new(r) }
  create_response.call(response)
end

def do_raw_request(method, url, params = nil)

def do_raw_request(method, url, params = nil)
  response = do_request(method, url, params)
rescue RestClient::ResourceNotFound => e
  raise GdsApi::HTTPNotFound.new(e.http_code)
rescue RestClient::Gone => e
  raise GdsApi::HTTPGone.new(e.http_code)
rescue RestClient::Exception => e
  raise GdsApi::HTTPErrorResponse.new(e.response.code.to_i), e.response.body
end

def do_request(method, url, params = nil, additional_headers = {})

def do_request(method, url, params = nil, additional_headers = {})
  loggable = {request_uri: url, start_time: Time.now.to_f}
  start_logging = loggable.merge(action: 'start')
  logger.debug start_logging.to_json
  method_params = {
    method: method,
    url: url,
    headers: DEFAULT_REQUEST_HEADERS
  }
  method_params[:payload] = params
  method_params = with_auth_options(method_params)
  method_params = with_timeout(method_params)
  method_params = with_headers(method_params, additional_headers)
  if URI.parse(url).is_a? URI::HTTPS
    method_params = with_ssl_options(method_params)
  end
  return ::RestClient::Request.execute(method_params)
rescue Errno::ECONNREFUSED => e
  logger.error loggable.merge(status: 'refused', error_message: e.message, error_class: e.class.name, end_time: Time.now.to_f).to_json
  raise GdsApi::EndpointNotFound.new("Could not connect to #{url}")
rescue RestClient::RequestTimeout => e
  logger.error loggable.merge(status: 'timeout', error_message: e.message, error_class: e.class.name, end_time: Time.now.to_f).to_json
  raise GdsApi::TimedOutException.new
rescue RestClient::MaxRedirectsReached => e
  raise GdsApi::TooManyRedirects
rescue RestClient::Exception => e
  # Log the error here, since we have access to loggable, but raise the
  # exception up to the calling method to deal with
  loggable.merge!(status: e.http_code, end_time: Time.now.to_f, body: e.http_body)
  logger.warn loggable.to_json
  raise
rescue Errno::ECONNRESET => e
  logger.error loggable.merge(status: 'connection_reset', error_message: e.message, error_class: e.class.name, end_time: Time.now.to_f).to_json
  raise GdsApi::TimedOutException.new
end

def do_request_with_cache(method, url, params = nil, additional_headers = {})

def do_request_with_cache(method, url, params = nil, additional_headers = {})
  # Only read GET requests from the cache: any other request methods should
  # always be passed through. Note that this means HEAD requests won't get
  # cached, but that would involve separating the cache by method and URL.
  # Also, we don't generally make HEAD requests.
  use_cache = (method == :get)
  if use_cache
    cached_response = @cache[url]
    return cached_response if cached_response
  end
  response = do_request(method, url, params, additional_headers)
  if use_cache
    cache_time = response_cache_time(response)
    # If cache_time is nil, this will fall back on @cache's default
    @cache.store(url, response, cache_time)
  end
  response
end

def get_json!(url, additional_headers = {}, &create_response)

def get_json!(url, additional_headers = {}, &create_response)
  do_json_request(:get, url, nil, additional_headers, &create_response)
end

def get_raw(url)

def get_raw(url)
  ignoring_missing do
    get_raw!(url)
  end
end

def get_raw!(url)

def get_raw!(url)
  do_raw_request(:get, url)
end

def initialize(options = {})

def initialize(options = {})
  if options[:disable_timeout] or options[:timeout].to_i < 0
    raise "It is no longer possible to disable the timeout."
  end
  @logger = options[:logger] || GdsApi::Base.logger
  if options[:disable_cache] || (options[:cache_size] == 0)
    @cache = NullCache.new
  else
    cache_size = options[:cache_size] || DEFAULT_CACHE_SIZE
    cache_ttl = options[:cache_ttl] || DEFAULT_CACHE_TTL
    @cache = JsonClient.cache(cache_size, cache_ttl)
  end
  @options = options
end

def post_json!(url, params, additional_headers = {})

def post_json!(url, params, additional_headers = {})
  do_json_request(:post, url, params, additional_headers)
end

def post_multipart(url, params)

def post_multipart(url, params)
  r = do_raw_request(:post, url, params.merge({
    :multipart => true
  }))
  Response.new(r)
end

def put_json!(url, params, additional_headers = {})

def put_json!(url, params, additional_headers = {})
  do_json_request(:put, url, params, additional_headers)
end

def put_multipart(url, params)

def put_multipart(url, params)
  r = do_raw_request(:put, url, params.merge({
    :multipart => true
  }))
  Response.new(r)
end

def response_cache_time(response)

or nil if no cache information is provided
Return either a Time object representing the expiry time of the response
def response_cache_time(response)
  if response.headers[:cache_control]
    # The Cache-control header is composed of a comma-separated string
    # so split this apart before we look for particular values
    cache_parts = response.headers[:cache_control].split(',').map(&:strip)
    # If no-cache is present, this takes precedent over any other value
    # in this header
    return Time.now.utc if cache_parts.include?("no-cache")
    # Otherwise, look for a 'max-age=123' value, which is the number of
    # seconds for which to cache the response.
    max_age = cache_parts.map {|x| x.match(/max-age=(\d+)/) }.compact.first
    if max_age
      return Time.now.utc + max_age[1].to_i
    end
  end
  if response.headers[:expires]
    Time.httpdate response.headers[:expires]
  end
end

def with_auth_options(method_params)

parameters with authentication information included
Take a hash of parameters for Request#execute; return a hash of
def with_auth_options(method_params)
  if @options[:bearer_token]
    headers = method_params[:headers] || {}
    method_params.merge(headers: headers.merge(
      {"Authorization" => "Bearer #{@options[:bearer_token]}"}
    ))
  elsif @options[:basic_auth]
    method_params.merge(
      user: @options[:basic_auth][:user],
      password: @options[:basic_auth][:password]
    )
  else
    method_params
  end
end

def with_headers(method_params, headers)

def with_headers(method_params, headers)
  headers = headers.merge(govuk_request_id: GdsApi::GovukRequestId.value) if GdsApi::GovukRequestId.set?
  method_params.merge(
    headers: method_params[:headers].merge(headers)
  )
end

def with_ssl_options(method_params)

def with_ssl_options(method_params)
  method_params.merge(
    # This is the default value anyway, but we should probably be explicit
    verify_ssl: OpenSSL::SSL::VERIFY_NONE
  )
end

def with_timeout(method_params)

parameters with timeouts included
Take a hash of parameters for Request#execute; return a hash of
def with_timeout(method_params)
  method_params.merge(
    timeout: options[:timeout] || DEFAULT_TIMEOUT_IN_SECONDS
  )
end