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 | :internal_server_error | :decoding_error | :unprocessable_content, 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}, %__MODULE__{} = client ) when is_map(payload) do {:ok, payload, %{ 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