From 3cec4288556ffcf0dc8080ffeec5c73f0758302d Mon Sep 17 00:00:00 2001 From: Laura Beatris <48022589+LauraBeatris@users.noreply.github.com> Date: Fri, 20 Oct 2023 20:13:10 -0300 Subject: [PATCH 01/14] Add config modules --- config/config.ex | 3 +++ config/dev.ex | 3 +++ config/prod.ex | 0 config/test.ex | 8 ++++++++ 4 files changed, 14 insertions(+) create mode 100644 config/config.ex create mode 100644 config/dev.ex create mode 100644 config/prod.ex create mode 100644 config/test.ex diff --git a/config/config.ex b/config/config.ex new file mode 100644 index 00000000..871a3d1e --- /dev/null +++ b/config/config.ex @@ -0,0 +1,3 @@ +import Config + +import_config "#{Mix.env()}.exs" diff --git a/config/dev.ex b/config/dev.ex new file mode 100644 index 00000000..890150d2 --- /dev/null +++ b/config/dev.ex @@ -0,0 +1,3 @@ +import Config + +config :workos, WorkOs.Client, client_id: System.get_env("WORKOS_CLIENT_ID"), api_key: System.get_env("WORKOS_API_KEY") diff --git a/config/prod.ex b/config/prod.ex new file mode 100644 index 00000000..e69de29b diff --git a/config/test.ex b/config/test.ex new file mode 100644 index 00000000..ea8bf9d1 --- /dev/null +++ b/config/test.ex @@ -0,0 +1,8 @@ +import Config + +if workos_api_key = System.get_env("WORKOS_API_KEY") do + config :workos, WorkOs.Client, api_key: workos_api_key +else + config :tesla, adapter: Tesla.Mock + config :workos, WorkOs.Client, api_key: "re_123456789" +end From 82699cb6908f1532df20973e8adbd32a21b8459b Mon Sep 17 00:00:00 2001 From: Laura Beatris <48022589+LauraBeatris@users.noreply.github.com> Date: Fri, 20 Oct 2023 20:18:19 -0300 Subject: [PATCH 02/14] Start to build behavior for HTTP client --- config/dev.ex | 4 +++- lib/workos.ex | 4 +++- lib/workos/client.ex | 35 +++++++++++++++++++++++++++++++ lib/workos/client/tesla_client.ex | 6 ++++++ 4 files changed, 47 insertions(+), 2 deletions(-) create mode 100644 lib/workos/client.ex create mode 100644 lib/workos/client/tesla_client.ex diff --git a/config/dev.ex b/config/dev.ex index 890150d2..f8eb6f45 100644 --- a/config/dev.ex +++ b/config/dev.ex @@ -1,3 +1,5 @@ import Config -config :workos, WorkOs.Client, client_id: System.get_env("WORKOS_CLIENT_ID"), api_key: System.get_env("WORKOS_API_KEY") +config :workos, WorkOs.Client, + client_id: System.get_env("WORKOS_CLIENT_ID"), + api_key: System.get_env("WORKOS_API_KEY") diff --git a/lib/workos.ex b/lib/workos.ex index a8e90d7b..d0beabf6 100755 --- a/lib/workos.ex +++ b/lib/workos.ex @@ -1,8 +1,10 @@ defmodule WorkOS do @moduledoc """ - Use the WorkOS module to authenticate your requests to the WorkOS API + Documentation for `WorkOs`. """ + @config_module WorkOs.Client + def host, do: Application.get_env(:workos, :host) def base_url, do: "https://" <> Application.get_env(:workos, :host) def adapter, do: Application.get_env(:workos, :adapter) || Tesla.Adapter.Hackney diff --git a/lib/workos/client.ex b/lib/workos/client.ex new file mode 100644 index 00000000..b3d6b7bc --- /dev/null +++ b/lib/workos/client.ex @@ -0,0 +1,35 @@ +defmodule WorkOs.Client do + @moduledoc """ + WorkOs API client. + """ + + require Logger + + @callback request(t(), Keyword.t()) :: + {:ok, %{body: map(), status: pos_integer()}} | {:error, any()} + + @type response(type) :: {:ok, type} | {:error, WorkOs.Error.t() | :client_error} + + @type t() :: %__MODULE__{ + api_key: String.t(), + base_url: String.t() | nil, + client: module() | nil + } + + @enforce_keys [:api_key, :base_url, :client] + defstruct [:api_key, :base_url, :client] + + @default_opts [ + base_url: "https://api.workos.com", + client: __MODULE__.TeslaClient + ] + + @doc """ + Creates a new WorkOs client struct given a keyword list of config opts. + """ + @spec new(WorkOs.config()) :: t() + def new(config) do + config = Keyword.take(config, [:api_key, :base_url, :client]) + struct!(__MODULE__, Keyword.merge(@default_opts, config)) + end +end diff --git a/lib/workos/client/tesla_client.ex b/lib/workos/client/tesla_client.ex new file mode 100644 index 00000000..ca81338f --- /dev/null +++ b/lib/workos/client/tesla_client.ex @@ -0,0 +1,6 @@ +defmodule WorkOs.Client.TeslaClient do + @moduledoc """ + Tesla client for WorkOs. This is the default HTTP client used. + """ + @behaviour WorkOs.Client +end From 182bec07a564bd8e72a11f178767032987834989 Mon Sep 17 00:00:00 2001 From: Laura Beatris <48022589+LauraBeatris@users.noreply.github.com> Date: Sat, 21 Oct 2023 13:26:19 -0300 Subject: [PATCH 03/14] Add `Castable` module to define behavior for casting or transforming data --- lib/workos/castable.ex | 34 ++++++++++++++++++++++++++++++++++ lib/workos/client.ex | 20 ++++++++++++++++++++ 2 files changed, 54 insertions(+) create mode 100644 lib/workos/castable.ex diff --git a/lib/workos/castable.ex b/lib/workos/castable.ex new file mode 100644 index 00000000..dab9a109 --- /dev/null +++ b/lib/workos/castable.ex @@ -0,0 +1,34 @@ +defmodule WorkOs.Castable do + @moduledoc false + + @type impl :: module() | {module(), module()} | :raw + @type generic_map :: %{String.t() => any()} + + @callback cast(generic_map() | {module(), generic_map()} | nil) :: struct() | nil + + @spec cast(impl(), generic_map() | nil) :: struct() | generic_map() | nil + def cast(_implementation, nil) do + nil + end + + def cast(:raw, generic_map) do + generic_map + end + + def cast({implementation, inner}, generic_map) when is_map(generic_map) do + implementation.cast({inner, generic_map}) + end + + def cast(implementation, generic_map) when is_map(generic_map) do + implementation.cast(generic_map) + end + + @spec cast_list(module(), [generic_map()] | nil) :: [struct()] | nil + def cast_list(_implementation, nil) do + nil + end + + def cast_list(implementation, list_of_generic_maps) when is_list(list_of_generic_maps) do + Enum.map(list_of_generic_maps, &cast(implementation, &1)) + end +end diff --git a/lib/workos/client.ex b/lib/workos/client.ex index b3d6b7bc..9b2adece 100644 --- a/lib/workos/client.ex +++ b/lib/workos/client.ex @@ -5,6 +5,8 @@ defmodule WorkOs.Client do require Logger + alias WorkOs.Castable + @callback request(t(), Keyword.t()) :: {:ok, %{body: map(), status: pos_integer()}} | {:error, any()} @@ -32,4 +34,22 @@ defmodule WorkOs.Client do config = Keyword.take(config, [:api_key, :base_url, :client]) struct!(__MODULE__, Keyword.merge(@default_opts, config)) end + + @spec get(t(), Castable.impl(), String.t()) :: response(any()) + @spec get(t(), Castable.impl(), String.t(), Keyword.t()) :: response(any()) + def get(client, castable_module, path, opts \\ []) do + client_module = client.client || Resend.Client.TeslaClient + + opts = + opts + |> Keyword.put(:method, :get) + |> Keyword.put(:url, path) + + client_module.request(client, opts) + |> handle_response(path, castable_module) + end + + defp handle_response(response, path, castable_module) do + [response, path, castable_module] + end end From dd982e9f0ae003ca9aed9c98dde1210612fbe877 Mon Sep 17 00:00:00 2001 From: Laura Beatris <48022589+LauraBeatris@users.noreply.github.com> Date: Sat, 21 Oct 2023 13:28:56 -0300 Subject: [PATCH 04/14] Handle response --- lib/workos/client.ex | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/lib/workos/client.ex b/lib/workos/client.ex index 9b2adece..55f759c8 100644 --- a/lib/workos/client.ex +++ b/lib/workos/client.ex @@ -38,7 +38,7 @@ defmodule WorkOs.Client do @spec get(t(), Castable.impl(), String.t()) :: response(any()) @spec get(t(), Castable.impl(), String.t(), Keyword.t()) :: response(any()) def get(client, castable_module, path, opts \\ []) do - client_module = client.client || Resend.Client.TeslaClient + client_module = client.client || WorkOs.Client.TeslaClient opts = opts @@ -50,6 +50,24 @@ defmodule WorkOs.Client do end defp handle_response(response, path, castable_module) do - [response, path, castable_module] + case response do + {:ok, %{body: "", status: status}} when status in 200..299 -> + {:ok, Castable.cast(castable_module, %{})} + + {:ok, %{body: body, status: status}} when status in 200..299 -> + {:ok, Castable.cast(castable_module, body)} + + {:ok, %{body: body}} when is_map(body) -> + Logger.error("#{inspect(__MODULE__)} error when calling #{path}: #{inspect(body)}") + {:error, Castable.cast(WorkOs.Error, body)} + + {:ok, %{body: body}} when is_binary(body) -> + Logger.error("#{inspect(__MODULE__)} error when calling #{path}: #{body}") + {:error, body} + + {:error, reason} -> + Logger.error("#{inspect(__MODULE__)} error when calling #{path}: #{inspect(reason)}") + {:error, :client_error} + end end end From 317bc586fa43767d844ba847ea424922a587c090 Mon Sep 17 00:00:00 2001 From: Laura Beatris <48022589+LauraBeatris@users.noreply.github.com> Date: Sat, 21 Oct 2023 13:31:33 -0300 Subject: [PATCH 05/14] Add HTTP methods to client behavior --- lib/workos/client.ex | 48 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/lib/workos/client.ex b/lib/workos/client.ex index 55f759c8..b7d58c85 100644 --- a/lib/workos/client.ex +++ b/lib/workos/client.ex @@ -49,6 +49,54 @@ defmodule WorkOs.Client do |> handle_response(path, castable_module) end + @spec post(t(), Castable.impl(), String.t()) :: response(any()) + @spec post(t(), Castable.impl(), String.t(), map()) :: response(any()) + @spec post(t(), Castable.impl(), String.t(), map(), Keyword.t()) :: response(any()) + def post(client, castable_module, path, body \\ %{}, opts \\ []) do + client_module = client.client || WorkOs.Client.TeslaClient + + opts = + opts + |> Keyword.put(:method, :post) + |> Keyword.put(:url, path) + |> Keyword.put(:body, body) + + client_module.request(client, opts) + |> handle_response(path, castable_module) + end + + @spec put(t(), Castable.impl(), String.t()) :: response(any()) + @spec put(t(), Castable.impl(), String.t(), map()) :: response(any()) + @spec put(t(), Castable.impl(), String.t(), map(), Keyword.t()) :: response(any()) + def put(client, castable_module, path, body \\ %{}, opts \\ []) do + client_module = client.client || WorkOs.Client.TeslaClient + + opts = + opts + |> Keyword.put(:method, :put) + |> Keyword.put(:url, path) + |> Keyword.put(:body, body) + + client_module.request(client, opts) + |> handle_response(path, castable_module) + end + + @spec delete(t(), Castable.impl(), String.t()) :: response(any()) + @spec delete(t(), Castable.impl(), String.t(), map()) :: response(any()) + @spec delete(t(), Castable.impl(), String.t(), map(), Keyword.t()) :: response(any()) + def delete(client, castable_module, path, body \\ %{}, opts \\ []) do + client_module = client.client || WorkOs.Client.TeslaClient + + opts = + opts + |> Keyword.put(:method, :delete) + |> Keyword.put(:url, path) + |> Keyword.put(:body, body) + + client_module.request(client, opts) + |> handle_response(path, castable_module) + end + defp handle_response(response, path, castable_module) do case response do {:ok, %{body: "", status: status}} when status in 200..299 -> From 4576525f7157dec13cbf9329a5816c84a0be4f61 Mon Sep 17 00:00:00 2001 From: Laura Beatris <48022589+LauraBeatris@users.noreply.github.com> Date: Sat, 21 Oct 2023 13:37:22 -0300 Subject: [PATCH 06/14] Add `TeslaClient` implementation --- lib/workos/client/tesla_client.ex | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/lib/workos/client/tesla_client.ex b/lib/workos/client/tesla_client.ex index ca81338f..9ed6d68d 100644 --- a/lib/workos/client/tesla_client.ex +++ b/lib/workos/client/tesla_client.ex @@ -3,4 +3,28 @@ defmodule WorkOs.Client.TeslaClient do Tesla client for WorkOs. This is the default HTTP client used. """ @behaviour WorkOs.Client + + @doc """ + Sends a request to a WorkOs API endpoint, given list of request opts. + """ + @spec request(Resend.Client.t(), Keyword.t()) :: + {:ok, %{body: map(), status: pos_integer()}} | {:error, any()} + def request(client, opts) do + opts = Keyword.take(opts, [:method, :url, :query, :headers, :body, :opts]) + Tesla.request(new(client), opts) + end + + @doc """ + Returns a new `Tesla.Client`, configured for calling the WorkOs API. + """ + @spec new(Resend.Client.t()) :: Tesla.Client.t() + def new(client) do + Tesla.client([ + Tesla.Middleware.Logger, + {Tesla.Middleware.BaseUrl, client.base_url}, + Tesla.Middleware.PathParams, + Tesla.Middleware.JSON, + {Tesla.Middleware.Headers, [{"Authorization", "Bearer #{client.api_key}"}]} + ]) + end end From 9ea43d269ecb7ab047eef283861673547bcea0fa Mon Sep 17 00:00:00 2001 From: Laura Beatris <48022589+LauraBeatris@users.noreply.github.com> Date: Sat, 21 Oct 2023 13:50:31 -0300 Subject: [PATCH 07/14] Add comment to prod.exs to fix `mix format` --- config/prod.ex | 1 + lib/workos/client/tesla_client.ex | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/config/prod.ex b/config/prod.ex index e69de29b..f1fb4ce6 100644 --- a/config/prod.ex +++ b/config/prod.ex @@ -0,0 +1 @@ + # This empty module is for configuration purposes diff --git a/lib/workos/client/tesla_client.ex b/lib/workos/client/tesla_client.ex index 9ed6d68d..889d0ed5 100644 --- a/lib/workos/client/tesla_client.ex +++ b/lib/workos/client/tesla_client.ex @@ -7,7 +7,7 @@ defmodule WorkOs.Client.TeslaClient do @doc """ Sends a request to a WorkOs API endpoint, given list of request opts. """ - @spec request(Resend.Client.t(), Keyword.t()) :: + @spec request(WorkOs.Client.t(), Keyword.t()) :: {:ok, %{body: map(), status: pos_integer()}} | {:error, any()} def request(client, opts) do opts = Keyword.take(opts, [:method, :url, :query, :headers, :body, :opts]) @@ -17,7 +17,7 @@ defmodule WorkOs.Client.TeslaClient do @doc """ Returns a new `Tesla.Client`, configured for calling the WorkOs API. """ - @spec new(Resend.Client.t()) :: Tesla.Client.t() + @spec new(WorkOs.Client.t()) :: Tesla.Client.t() def new(client) do Tesla.client([ Tesla.Middleware.Logger, From 4509003a032b7e7483ed25fcb4aa60df33071b3d Mon Sep 17 00:00:00 2001 From: Laura Beatris <48022589+LauraBeatris@users.noreply.github.com> Date: Sat, 21 Oct 2023 13:53:35 -0300 Subject: [PATCH 08/14] Fix casing for `WorkOS` namespace --- config/dev.ex | 2 +- config/prod.ex | 2 +- config/test.ex | 4 ++-- lib/workos.ex | 4 ++-- lib/workos/castable.ex | 2 +- lib/workos/client.ex | 16 ++++++++-------- lib/workos/client/tesla_client.ex | 4 ++-- 7 files changed, 17 insertions(+), 17 deletions(-) diff --git a/config/dev.ex b/config/dev.ex index f8eb6f45..32741e70 100644 --- a/config/dev.ex +++ b/config/dev.ex @@ -1,5 +1,5 @@ import Config -config :workos, WorkOs.Client, +config :workos, WorkOS.Client, client_id: System.get_env("WORKOS_CLIENT_ID"), api_key: System.get_env("WORKOS_API_KEY") diff --git a/config/prod.ex b/config/prod.ex index f1fb4ce6..d932aaf7 100644 --- a/config/prod.ex +++ b/config/prod.ex @@ -1 +1 @@ - # This empty module is for configuration purposes +# This empty module is for configuration purposes diff --git a/config/test.ex b/config/test.ex index ea8bf9d1..87f7257e 100644 --- a/config/test.ex +++ b/config/test.ex @@ -1,8 +1,8 @@ import Config if workos_api_key = System.get_env("WORKOS_API_KEY") do - config :workos, WorkOs.Client, api_key: workos_api_key + config :workos, WorkOS.Client, api_key: workos_api_key else config :tesla, adapter: Tesla.Mock - config :workos, WorkOs.Client, api_key: "re_123456789" + config :workos, WorkOS.Client, api_key: "re_123456789" end diff --git a/lib/workos.ex b/lib/workos.ex index d0beabf6..c30623b1 100755 --- a/lib/workos.ex +++ b/lib/workos.ex @@ -1,9 +1,9 @@ defmodule WorkOS do @moduledoc """ - Documentation for `WorkOs`. + Documentation for `WorkOS`. """ - @config_module WorkOs.Client + @config_module WorkOS.Client def host, do: Application.get_env(:workos, :host) def base_url, do: "https://" <> Application.get_env(:workos, :host) diff --git a/lib/workos/castable.ex b/lib/workos/castable.ex index dab9a109..65a596c3 100644 --- a/lib/workos/castable.ex +++ b/lib/workos/castable.ex @@ -1,4 +1,4 @@ -defmodule WorkOs.Castable do +defmodule WorkOS.Castable do @moduledoc false @type impl :: module() | {module(), module()} | :raw diff --git a/lib/workos/client.ex b/lib/workos/client.ex index b7d58c85..392c465b 100644 --- a/lib/workos/client.ex +++ b/lib/workos/client.ex @@ -1,6 +1,6 @@ -defmodule WorkOs.Client do +defmodule WorkOS.Client do @moduledoc """ - WorkOs API client. + WorkOS API client. """ require Logger @@ -27,9 +27,9 @@ defmodule WorkOs.Client do ] @doc """ - Creates a new WorkOs client struct given a keyword list of config opts. + Creates a new WorkOS client struct given a keyword list of config opts. """ - @spec new(WorkOs.config()) :: t() + @spec new(WorkOS.config()) :: t() def new(config) do config = Keyword.take(config, [:api_key, :base_url, :client]) struct!(__MODULE__, Keyword.merge(@default_opts, config)) @@ -38,7 +38,7 @@ defmodule WorkOs.Client do @spec get(t(), Castable.impl(), String.t()) :: response(any()) @spec get(t(), Castable.impl(), String.t(), Keyword.t()) :: response(any()) def get(client, castable_module, path, opts \\ []) do - client_module = client.client || WorkOs.Client.TeslaClient + client_module = client.client || WorkOS.Client.TeslaClient opts = opts @@ -53,7 +53,7 @@ defmodule WorkOs.Client do @spec post(t(), Castable.impl(), String.t(), map()) :: response(any()) @spec post(t(), Castable.impl(), String.t(), map(), Keyword.t()) :: response(any()) def post(client, castable_module, path, body \\ %{}, opts \\ []) do - client_module = client.client || WorkOs.Client.TeslaClient + client_module = client.client || WorkOS.Client.TeslaClient opts = opts @@ -69,7 +69,7 @@ defmodule WorkOs.Client do @spec put(t(), Castable.impl(), String.t(), map()) :: response(any()) @spec put(t(), Castable.impl(), String.t(), map(), Keyword.t()) :: response(any()) def put(client, castable_module, path, body \\ %{}, opts \\ []) do - client_module = client.client || WorkOs.Client.TeslaClient + client_module = client.client || WorkOS.Client.TeslaClient opts = opts @@ -85,7 +85,7 @@ defmodule WorkOs.Client do @spec delete(t(), Castable.impl(), String.t(), map()) :: response(any()) @spec delete(t(), Castable.impl(), String.t(), map(), Keyword.t()) :: response(any()) def delete(client, castable_module, path, body \\ %{}, opts \\ []) do - client_module = client.client || WorkOs.Client.TeslaClient + client_module = client.client || WorkOS.Client.TeslaClient opts = opts diff --git a/lib/workos/client/tesla_client.ex b/lib/workos/client/tesla_client.ex index 889d0ed5..df749e9a 100644 --- a/lib/workos/client/tesla_client.ex +++ b/lib/workos/client/tesla_client.ex @@ -1,6 +1,6 @@ -defmodule WorkOs.Client.TeslaClient do +defmodule WorkOS.Client.TeslaClient do @moduledoc """ - Tesla client for WorkOs. This is the default HTTP client used. + Tesla client for WorkOS. This is the default HTTP client used. """ @behaviour WorkOs.Client From 3c199b6779a804961544b27715dab05f8d3722d6 Mon Sep 17 00:00:00 2001 From: Laura Beatris <48022589+LauraBeatris@users.noreply.github.com> Date: Sat, 21 Oct 2023 15:04:46 -0300 Subject: [PATCH 09/14] Add `@deprecated` to `WorkOS.API` --- lib/workos/api.ex | 2 ++ lib/workos/client.ex | 6 +++--- lib/workos/client/tesla_client.ex | 6 +++--- 3 files changed, 8 insertions(+), 6 deletions(-) diff --git a/lib/workos/api.ex b/lib/workos/api.ex index 21f8af87..c203db4d 100644 --- a/lib/workos/api.ex +++ b/lib/workos/api.ex @@ -1,4 +1,6 @@ defmodule WorkOS.API do + @deprecated "This module is deprecated. Please use WorkOS.Client instead." + @moduledoc """ Provides core API communication and data processing functionality. """ diff --git a/lib/workos/client.ex b/lib/workos/client.ex index 392c465b..46c7a653 100644 --- a/lib/workos/client.ex +++ b/lib/workos/client.ex @@ -5,12 +5,12 @@ defmodule WorkOS.Client do require Logger - alias WorkOs.Castable + alias WorkOS.Castable @callback request(t(), Keyword.t()) :: {:ok, %{body: map(), status: pos_integer()}} | {:error, any()} - @type response(type) :: {:ok, type} | {:error, WorkOs.Error.t() | :client_error} + @type response(type) :: {:ok, type} | {:error, WorkOS.Error.t() | :client_error} @type t() :: %__MODULE__{ api_key: String.t(), @@ -107,7 +107,7 @@ defmodule WorkOS.Client do {:ok, %{body: body}} when is_map(body) -> Logger.error("#{inspect(__MODULE__)} error when calling #{path}: #{inspect(body)}") - {:error, Castable.cast(WorkOs.Error, body)} + {:error, Castable.cast(WorkOS.Error, body)} {:ok, %{body: body}} when is_binary(body) -> Logger.error("#{inspect(__MODULE__)} error when calling #{path}: #{body}") diff --git a/lib/workos/client/tesla_client.ex b/lib/workos/client/tesla_client.ex index df749e9a..142fc1ce 100644 --- a/lib/workos/client/tesla_client.ex +++ b/lib/workos/client/tesla_client.ex @@ -2,12 +2,12 @@ defmodule WorkOS.Client.TeslaClient do @moduledoc """ Tesla client for WorkOS. This is the default HTTP client used. """ - @behaviour WorkOs.Client + @behaviour WorkOS.Client @doc """ Sends a request to a WorkOs API endpoint, given list of request opts. """ - @spec request(WorkOs.Client.t(), Keyword.t()) :: + @spec request(WorkOS.Client.t(), Keyword.t()) :: {:ok, %{body: map(), status: pos_integer()}} | {:error, any()} def request(client, opts) do opts = Keyword.take(opts, [:method, :url, :query, :headers, :body, :opts]) @@ -17,7 +17,7 @@ defmodule WorkOS.Client.TeslaClient do @doc """ Returns a new `Tesla.Client`, configured for calling the WorkOs API. """ - @spec new(WorkOs.Client.t()) :: Tesla.Client.t() + @spec new(WorkOS.Client.t()) :: Tesla.Client.t() def new(client) do Tesla.client([ Tesla.Middleware.Logger, From 6ef4330f26e24d79f21ecac82ede193d1da039ad Mon Sep 17 00:00:00 2001 From: Laura Beatris <48022589+LauraBeatris@users.noreply.github.com> Date: Sat, 21 Oct 2023 15:16:06 -0300 Subject: [PATCH 10/14] Fix test config --- config/test.ex | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/config/test.ex b/config/test.ex index 87f7257e..f034e70d 100644 --- a/config/test.ex +++ b/config/test.ex @@ -1,8 +1,9 @@ import Config -if workos_api_key = System.get_env("WORKOS_API_KEY") do - config :workos, WorkOS.Client, api_key: workos_api_key +if workos_api_key = + System.get_env("WORKOS_API_KEY") and workos_client_id = System.get_env("WORKOS_CLIENT_ID") do + config :workos, WorkOS.Client, api_key: workos_api_key, client_id: workos_client_id else config :tesla, adapter: Tesla.Mock - config :workos, WorkOS.Client, api_key: "re_123456789" + config :workos, WorkOS.Client, api_key: "test_123", client_id: "test_123" end From 76c776a255f48ec227620809157c05a7a3ae16a2 Mon Sep 17 00:00:00 2001 From: Laura Beatris <48022589+LauraBeatris@users.noreply.github.com> Date: Sat, 21 Oct 2023 16:50:43 -0300 Subject: [PATCH 11/14] Validate config --- lib/workos.ex | 71 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 71 insertions(+) diff --git a/lib/workos.ex b/lib/workos.ex index c30623b1..8a6cc594 100755 --- a/lib/workos.ex +++ b/lib/workos.ex @@ -5,6 +5,77 @@ defmodule WorkOS do @config_module WorkOS.Client + @type config() :: + list( + {:api_key, String.t()} + | {:client_id, String.t()} + | {:base_url, String.t()} + | {:client, atom()} + ) + + @doc """ + Returns a WorkOS client. + + Accepts a keyword list of config opts, though if omitted then it will attempt to load + them from the application environment. + """ + @spec client() :: WorkOS.Client.t() + @spec client(config()) :: WorkOS.Client.t() + def client(config \\ config()) do + WorkOS.Client.new(config) + end + + @doc """ + Loads config values from the application environment. + + Config options are as follows: + + ```ex + config :workos, WorkOS.Client + api_key: "test_123", + base_url: "https://api.workos.com", + client: WorkOs.Client.TeslaClient + ``` + + The only required config option is `:api_key` and `:client_id`. If you would like to replace the + HTTP client used by WorkOS, configure the `:client` option. By default, this library + uses [Tesla](https://github.com/elixir-tesla/tesla), but changing it is as easy as + defining your own client module. See the `WorkOS.Client` module docs for more info. + """ + @spec config() :: config() + def config() do + config = + Application.get_env(:workos, @config_module) || + raise """ + Missing client configuration for WorkOS. + + Configure your WorkOS API key in one of your config files, for example: + + config :workos, #{inspect(@config_module)}, api_key: "example_123", client_id: "example_123" + """ + + validate_config!(config) + end + + @spec validate_config!(WorkOS.config()) :: WorkOS.config() | no_return() + defp validate_config!(config) do + api_key = + Keyword.get(config, :api_key) || + raise "Missing required config key for #{@config_module}: :api_key" + + String.starts_with?(api_key, "sk_") || + raise "WorkOS API key should start with 'sk_', please check your configuration" + + client_id = + Keyword.get(config, :client_id) || + raise "Missing required config key for #{@config_module}: :client_id" + + String.starts_with?(client_id, "project_") || + raise "WorkOS Client ID should start with 'project_', please check your configuration" + + config + end + def host, do: Application.get_env(:workos, :host) def base_url, do: "https://" <> Application.get_env(:workos, :host) def adapter, do: Application.get_env(:workos, :adapter) || Tesla.Adapter.Hackney From b8a5e6561d949ca49111ffecaebadd624fd2be4c Mon Sep 17 00:00:00 2001 From: Laura Beatris <48022589+LauraBeatris@users.noreply.github.com> Date: Sat, 21 Oct 2023 16:59:53 -0300 Subject: [PATCH 12/14] Fix linter --- lib/workos.ex | 2 +- lib/workos/client/tesla_client.ex | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/workos.ex b/lib/workos.ex index 8a6cc594..badb4e80 100755 --- a/lib/workos.ex +++ b/lib/workos.ex @@ -43,7 +43,7 @@ defmodule WorkOS do defining your own client module. See the `WorkOS.Client` module docs for more info. """ @spec config() :: config() - def config() do + def config do config = Application.get_env(:workos, @config_module) || raise """ diff --git a/lib/workos/client/tesla_client.ex b/lib/workos/client/tesla_client.ex index 142fc1ce..cca946c4 100644 --- a/lib/workos/client/tesla_client.ex +++ b/lib/workos/client/tesla_client.ex @@ -5,7 +5,7 @@ defmodule WorkOS.Client.TeslaClient do @behaviour WorkOS.Client @doc """ - Sends a request to a WorkOs API endpoint, given list of request opts. + Sends a request to a WorkOS API endpoint, given list of request opts. """ @spec request(WorkOS.Client.t(), Keyword.t()) :: {:ok, %{body: map(), status: pos_integer()}} | {:error, any()} From 2761f71a3773849a0d21258449d75766c4e7efef Mon Sep 17 00:00:00 2001 From: Laura Beatris <48022589+LauraBeatris@users.noreply.github.com> Date: Sun, 22 Oct 2023 19:55:23 -0300 Subject: [PATCH 13/14] Add module for structured error response --- lib/workos/error.ex | 39 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 lib/workos/error.ex diff --git a/lib/workos/error.ex b/lib/workos/error.ex new file mode 100644 index 00000000..c6aad658 --- /dev/null +++ b/lib/workos/error.ex @@ -0,0 +1,39 @@ +defmodule WorkOS.Error do + @moduledoc """ + Castable module for returning structured errors from the WorkOS API. + """ + + @behaviour WorkOS.Castable + + @type unprocessable_entity_error() :: %{ + field: String.t(), + code: String.t() + } + + @type t() :: %__MODULE__{ + code: String.t() | nil, + error: String.t() | nil, + errors: [unprocessable_entity_error()] | nil, + message: String.t(), + error_description: String.t() | nil + } + + defstruct [ + :code, + :error, + :errors, + :message, + :error_description + ] + + @impl true + def cast(error) when is_map(error) do + %__MODULE__{ + code: error["code"], + error: error["error"], + errors: error["errors"], + message: error["message"], + error_description: error["error_description"] + } + end +end From c337813bc3b8df3a4fc03024090afac017470039 Mon Sep 17 00:00:00 2001 From: Laura Beatris <48022589+LauraBeatris@users.noreply.github.com> Date: Mon, 23 Oct 2023 20:53:24 -0300 Subject: [PATCH 14/14] Extract env variables to separate variables --- config/test.ex | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/config/test.ex b/config/test.ex index f034e70d..72490659 100644 --- a/config/test.ex +++ b/config/test.ex @@ -1,7 +1,9 @@ import Config -if workos_api_key = - System.get_env("WORKOS_API_KEY") and workos_client_id = System.get_env("WORKOS_CLIENT_ID") do +workos_api_key = System.get_env("WORKOS_API_KEY") +workos_client_id = System.get_env("WORKOS_CLIENT_ID") + +if workos_api_key and workos_client_id do config :workos, WorkOS.Client, api_key: workos_api_key, client_id: workos_client_id else config :tesla, adapter: Tesla.Mock