# Copyright 2018 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
defmodule GoogleApi.Gax.Connection do
@moduledoc """
This module helps define and configure server connection.
"""
defmacro __using__(opts) do
quote do
use Tesla
@scopes unquote(Keyword.get(opts, :scopes, []))
# Add any middleware here (authentication)
plug(
Tesla.Middleware.BaseUrl,
Application.get_env(
unquote(Keyword.get(opts, :otp_app)),
:base_url,
unquote(Keyword.get(opts, :base_url))
)
)
plug(Tesla.Middleware.DecompressResponse, [])
plug(Tesla.Middleware.EncodeJson, engine: Poison)
@doc """
Configure a client connection using a provided OAuth2 token as a Bearer token
## Parameters
* `token` (*type:* `String.t`) - Bearer token
## Returns
* `Tesla.Env.client`
"""
@spec new(String.t()) :: Tesla.Client.t()
def new(token) when is_binary(token) do
Tesla.client([
{Tesla.Middleware.Headers, [{"authorization", "Bearer #{token}"}]}
])
end
@doc """
Configure a client connection using a function which yields a Bearer token.
## Parameters
* `token_fetcher` (*type:* `list(String.t()) -> String.t()`) - Callback
which provides an OAuth2 token given a list of scopes
## Returns
* `Tesla.Env.client`
"""
@spec new((list(String.t()) -> String.t())) :: Tesla.Client.t()
def new(token_fetcher) when is_function(token_fetcher) do
token_fetcher.(@scopes)
|> new
end
@doc """
Configure an authless client connection
## Returns
* `Tesla.Env.client`
"""
@spec new() :: Tesla.Client.t()
def new do
Tesla.client([])
end
@doc """
Execute a request on this connection
## Returns
* `{:ok, Tesla.Env.t}` - If the call was successful
* `{:error, reason}` - If the call failed
"""
@spec execute(Tesla.Client.t(), GoogleApi.Gax.Request.t()) :: {:ok, Tesla.Env.t()}
def execute(connection, request) do
request
|> GoogleApi.Gax.Connection.build_request()
|> (&request(connection, &1)).()
end
end
end
@doc """
Converts a GoogleApi.Gax.Request struct into a keyword list to send via
Tesla.
"""
@spec build_request(GoogleApi.Gax.Request.t()) :: keyword()
def build_request(request) do
[url: request.url, method: request.method]
|> build_query(request.query)
|> build_headers(request.header, request.library_version)
|> build_body(request.body, request.file)
end
defp build_query(output, []), do: output
defp build_query(output, query_params) do
Keyword.put(output, :query, query_params)
end
@gax_version Mix.Project.config() |> Keyword.get(:version, "")
defp build_headers(output, header_params, library_version) do
{other_api_client, other_headers} = find_api_client_headers(header_params, [], [])
api_client =
Enum.join(
[
"gl-elixir/#{System.version()}",
"gax/#{@gax_version}",
"gdcl/#{library_version}"
| other_api_client
],
" "
)
headers = [{"x-goog-api-client", api_client} | other_headers]
Keyword.put(output, :headers, headers)
end
defp find_api_client_headers([], found, other_headers) do
{Enum.reverse(found), Enum.reverse(other_headers)}
end
defp find_api_client_headers([{name, value} | remaining], found, other_headers) do
normalized_name = name |> to_string() |> String.downcase()
if normalized_name == "x-goog-api-client" do
find_api_client_headers(remaining, [value | found], other_headers)
else
find_api_client_headers(remaining, found, [{name, value} | other_headers])
end
end
# If no body or file fields and the request is a POST, set an empty body
defp build_body(output, [], []) do
method = Keyword.fetch!(output, :method)
set_default_body(output, method)
end
defp build_body(output, [body: main_body], []) do
Keyword.put(output, :body, main_body)
end
defp build_body(output, [], file_params) do
body =
Enum.reduce(file_params, Tesla.Multipart.new(), fn {file_name, file_path}, b ->
Tesla.Multipart.add_file(b, file_path, name: file_name)
end)
Keyword.put(output, :body, body)
end
defp build_body(output, body_params, file_params) do
body = Tesla.Multipart.new()
{meta, body_params} = extract_metadata(body_params)
body = case meta do
nil -> body
_ -> Tesla.Multipart.add_field(
body,
:metadata,
Poison.encode!(meta),
headers: [{:"Content-Type", "application/json"}]
)
end
body =
Enum.reduce(body_params, body, fn {body_name, data}, b ->
{res, type} = try_encode_multipart_field(data, meta)
Tesla.Multipart.add_field(
b,
body_name,
res,
headers: [{:"Content-Type", type}]
)
end)
body =
Enum.reduce(file_params, body, fn {file_name, file_path}, b ->
Tesla.Multipart.add_file(b, file_path, name: file_name)
end)
Keyword.put(output, :body, body)
end
defp extract_metadata(body_params) do
case Keyword.fetch(body_params, :metadata) do
:error -> {nil, body_params}
{:ok, meta} -> {meta, Keyword.delete(body_params, :metadata)}
end
end
defp try_encode_multipart_field(data, _meta) when is_map(data) do
{Poison.encode!(data), "application/json"}
end
defp try_encode_multipart_field(data, meta) when is_map(meta) do
{data, Map.get(meta, :contentType, "application/octet-stream")}
end
defp try_encode_multipart_field(data, _meta) do
{data, "application/octet-stream"}
end
@required_body_methods [:post, :patch, :put, :delete]
defp set_default_body(output, method) when method in @required_body_methods do
Keyword.put(output, :body, "")
end
defp set_default_body(output, _) do
output
end
end