defmodule EPTSDK do
@moduledoc """
EPTSDK is a library for interacting with the Edge Payment Technologies, Inc. HTTP API using their jsonapi.org interface
for all interactions.
"""
@default_query_options %{
filter: %{},
include: [],
sort: [],
page: [],
fields: %{}
}
@enforce_keys [
:authorization,
:user_agent,
:location,
:http_client
]
@default_headers []
defstruct authorization: nil,
user_agent: nil,
location: %URI{scheme: "https", host: "api.tryedge.io"},
http_client: Req.new(),
response: nil,
links: nil,
meta: nil,
fields: %{},
include: []
@type t() :: %__MODULE__{
authorization: String.t(),
user_agent: String.t(),
location: URI.t() | String.t(),
http_client: Req.Request.t(),
fields: map(),
include: list()
}
def client(%{token: token, user_agent: user_agent} = properties) when is_map(properties) do
struct(
__MODULE__,
%{
authorization: "Bearer #{token}",
user_agent: user_agent
}
|> Map.merge(properties)
)
end
@spec sideload(
{:ok, list(struct()) | struct() | nil, list(), EPTSDK.t()},
list(atom())
) ::
{:ok, list(struct()) | struct() | nil, list(), EPTSDK.t()}
| {:error, any()}
| {:error | :unprocessable_content | :decoding_error, any(), Req.Response.t()}
def sideload({:ok, records, included, client}, relationships)
when is_list(records) and is_list(included) and is_list(relationships) do
{:ok,
Enum.map(records, fn record ->
Enum.reduce(relationships, record, &update_record(&1, &2, included, client))
end), included, client}
end
def sideload({:ok, record, included, client}, relationships)
when is_struct(record) and is_list(included) and is_list(relationships) do
{:ok, Enum.reduce(relationships, record, &update_record(&1, &2, included, client)), included,
client}
end
def sideload({:ok, nil, included, client}, _relationships), do: {:ok, nil, included, client}
def sideload({:unprocessable_content, _anything, _response} = exception, _relationships),
do: exception
def sideload({signal, _anything, client} = exception, _relationships)
when is_atom(signal) and is_struct(client, EPTSDK),
do: exception
def sideload({:error, _anything} = exception, _relationships), do: exception
defp update_record(name, record, included, client)
when is_atom(name) and is_list(included) do
Map.merge(record, %{
name =>
record
|> Map.get(name)
|> case do
%EPTSDK.Relationship{has: :many, data: data} ->
Enum.map(data, fn datum ->
find_and_encode_relationship(datum, included, client) || []
end)
%EPTSDK.Relationship{has: :one, data: data} ->
find_and_encode_relationship(data, included, client) || nil
%EPTSDK.RelationshipNotAvailable{} = relationship ->
relationship
nil ->
%EPTSDK.RelationshipNotAvailable{name: name, reason: :undefined}
end
})
end
defp find_and_encode_relationship(relationship, included, client) do
included
|> Enum.find(&compare_relationship_to_included(&1, relationship))
|> case do
nil -> nil
found_record -> EPTSDK.Encoder.to_struct(found_record, %{}, client)
end
end
defp compare_relationship_to_included(
%{"id" => included_id, "type" => included_type},
%{id: id, type: type}
) do
included_id == id and included_type == type
end
def get(%EPTSDK{location: location} = client, path, query \\ [])
when is_binary(path) do
Keyword.validate!(query, [:filter, :sort, :fields, :page, :include])
client.http_client
|> Req.get(
url: encode_uri(location, path, with_query_defaults(query)),
headers:
default_headers(client, [
{"Accept", "application/vnd.api+json"}
])
)
|> response()
end
def delete(%EPTSDK{location: location} = client, path, query \\ [])
when is_binary(path) do
client.http_client
|> Req.delete(
url: encode_uri(location, path, with_query_defaults(query)),
headers: default_headers(client)
)
|> response()
end
def post(%EPTSDK{location: location} = client, path, data, query \\ [])
when is_binary(path) and is_map(data) do
Keyword.validate!(query, [:filter, :sort, :fields, :page, :include])
client.http_client
|> Req.post(
url: encode_uri(location, path, with_query_defaults(query)),
headers:
default_headers(client, [
{"Content-Type", "application/vnd.api+json"},
{"Accept", "application/vnd.api+json"}
]),
json: data
)
|> response()
end
def patch(%EPTSDK{location: location} = client, path, data, query \\ [])
when is_binary(path) and is_map(data) do
client.http_client
|> Req.patch(
url: encode_uri(location, path, with_query_defaults(query)),
headers:
default_headers(client, [
{"Content-Type", "application/vnd.api+json"},
{"Accept", "application/vnd.api+json"}
]),
json: data
)
|> response()
end
def put(%EPTSDK{location: location} = client, path, data, query \\ [])
when is_binary(path) and is_map(data) do
client.http_client
|> Req.put(
url: encode_uri(location, path, with_query_defaults(query)),
headers:
default_headers(client, [
{"Content-Type", "application/vnd.api+json"},
{"Accept", "application/vnd.api+json"}
]),
json: data
)
|> response()
end
defp encode_uri(location, path, nil)
when is_binary(location) and is_binary(path),
do: URI.append_path(%URI{host: location, scheme: "https"}, path)
defp encode_uri(uri, path, nil)
when is_struct(uri, URI) and is_binary(path),
do: URI.append_path(uri, path)
defp encode_uri(location, path, query)
when is_map(query),
do:
URI.append_query(
encode_uri(location, path, nil),
Plug.Conn.Query.encode(query)
)
defp default_headers(
%EPTSDK{user_agent: user_agent, authorization: authorization},
custom_headers \\ []
)
when is_binary(user_agent) and is_list(custom_headers) do
@default_headers
|> Enum.concat([
{"User-Agent", Enum.join(["Edge Payment Client/1.0", user_agent], " ")},
{"Authorization", authorization}
])
|> Enum.concat(custom_headers)
end
defp response({:ok, %Req.Response{status: 422, body: body} = response}),
do: {:unprocessable_content, body, response}
defp response({:ok, %Req.Response{status: 500} = response}),
do: {:internal_server_error, response}
defp response({:ok, %Req.Response{status: status} = response})
when status in 400..499,
do: {:error, response}
defp response({:ok, %Req.Response{status: status} = response})
when status in 500..599,
do: {:error, response}
defp response({:ok, %Req.Response{body: body} = response}), do: {:ok, body, response}
defp response({:error, exception}), do: {:error, exception}
defp with_query_defaults(nil), do: nil
defp with_query_defaults([]), do: nil
defp with_query_defaults(query) when is_list(query) do
%{
filter: query |> Keyword.get(:filter, @default_query_options[:filter]),
fields:
query
|> Keyword.get(:fields, @default_query_options[:fields])
|> encode_fields_query(),
sort: query |> Keyword.get(:sort, @default_query_options[:sort]) |> Enum.join(","),
page: query |> Keyword.get(:page, @default_query_options[:page]),
include: query |> Keyword.get(:include, @default_query_options[:include]) |> Enum.join(",")
}
|> Enum.filter(fn
{_key, ""} -> false
{_key, nil} -> false
{_key, _value} -> true
end)
|> Map.new()
end
defp with_query_defaults(%{}), do: %{}
defp encode_fields_query(fields) when is_map(fields) do
fields
|> Enum.map(fn {resource, fields} when is_list(fields) ->
{resource, Enum.join(fields, ",")}
end)
|> Map.new()
end
def update_client_from_request(
{:ok, payload, %Req.Response{} = response},
client
)
when is_struct(client, EPTSDK) and is_map(payload) do
{:ok, payload,
%__MODULE__{
client
| response: response,
links: payload["links"],
meta: payload["meta"]
}}
end
def update_client_from_request(
{indicator, _exception, _response} = error,
_client
)
when indicator in [:error, :internal_server_error, :decoding_error, :unprocessable_content],
do: error
def update_client_from_request({:error, _exception} = error, _client), do: error
end