Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,6 @@ workos-*.tar

# Temporary files for e.g. tests
/tmp

# Editor
.DS_Store
16 changes: 10 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
# WorkOS Elixir Library

> **Note:** this an experimental SDK and breaking changes may occur. We don't recommend using this in production since we can't guarantee its stability.

The WorkOS library for Elixir provides convenient access to the WorkOS API from applications written in Elixir.

## Documentation
Expand All @@ -10,14 +8,20 @@ See the [API Reference](https://workos.com/docs/reference/client-libraries) for

## Installation

Add this package to the list of dependencies in your `mix.exs` file:
The hex package can be found here: https://hex.pm/packages/workos

To use WorkOS SDK with your projects, edit your mix.exs file and add it as a dependency. WorkOS SDK does not install a JSON library nor HTTP client by itself. It will default to trying to use Jason for JSON operations and Hackney for HTTP requests, but can be configured to use other ones. To use the default ones, do:

```ex
def deps do
[{:workos, "~> 0.2.0"}]
defp deps do
[
# ...
{:workos, "~> 1.0.0"},
{:jason, "~> 1.1"},
{:hackney, "~> 1.8"},
]
end
```
The hex package can be found here: https://hex.pm/packages/workos

## Configuration

Expand Down
43 changes: 15 additions & 28 deletions lib/workos/api.ex
Original file line number Diff line number Diff line change
Expand Up @@ -4,57 +4,44 @@ defmodule WorkOS.API do
"""

@doc """
Generates the Tesla client used to make requests to WorkOS
Generates the HTTP client used to make requests to WorkOS
"""
def client(opts \\ []) do
def client(http_client, opts \\ []) do
auth = opts |> Keyword.get(:access_token, WorkOS.api_key(opts))

middleware = [
{Tesla.Middleware.BaseUrl, WorkOS.base_url()},
Tesla.Middleware.JSON,
{Tesla.Middleware.Headers,
[
{"Authorization", "Bearer " <> auth}
]}
]

Tesla.client(middleware, WorkOS.adapter())
http_client.new(opts: opts, auth: auth)
end

@doc """
Performs a GET request
"""
def get(path, query \\ [], opts \\ []) do
client(opts)
|> Tesla.get(path, query: query)
|> handle_response
def get(http_client, url, query \\ [], opts \\ []) do
http_client.get(url, query, opts)
|> handle_response()
end

@doc """
Performs a POST request
"""
def post(path, params \\ "", opts \\ []) do
client(opts)
|> Tesla.post(path, params)
|> handle_response
def post(http_client, url, body, opts \\ []) do
http_client.post(url, body, opts)
|> handle_response()
end

@doc """
Performs a DELETE request
"""
def delete(path, params \\ "", opts \\ []) do
client(opts)
|> Tesla.delete(path, query: params)
|> handle_response
def delete(http_client, url, query \\ [], opts \\ []) do
http_client.delete(url, query, opts)
|> handle_response()
end

@doc """
Performs a PUT request
"""
def put(path, params \\ "", opts \\ []) do
client(opts)
|> Tesla.put(path, params)
|> handle_response
def put(http_client, url, body, opts \\ []) do
http_client.put(url, body, opts)
|> handle_response()
end

@doc """
Expand Down
56 changes: 56 additions & 0 deletions lib/workos/application.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
defmodule WorkOS.Application do
@moduledoc false

use Application

alias WorkOS.Config

@impl true
def start(_type, _opts) do
http_client = Config.client()

if http_client == WorkOS.HackneyClient do
unless Code.ensure_loaded?(:hackney) do
raise """
cannot start the :workos application because the HTTP client is set to \
WorkOS.HackneyClient (which is the default), but the Hackney library is not loaded. \
Add :hackney to your dependencies to fix this.
"""
end

case Application.ensure_all_started(:hackney) do
{:ok, _apps} -> :ok
{:error, reason} -> raise "failed to start the :hackney application: #{inspect(reason)}"
end
end

validate_json_config!()
end

defp validate_json_config!() do
case Config.json_library() do
nil ->
raise ArgumentError.exception("nil is not a valid :json_library configuration")

library ->
try do
with {:ok, %{}} <- library.decode("{}"),
{:ok, "{}"} <- library.encode(%{}) do
:ok
else
_ ->
raise ArgumentError.exception(
"configured :json_library #{inspect(library)} does not implement decode/1 and encode/1"
)
end
rescue
UndefinedFunctionError ->
reraise ArgumentError.exception("""
configured :json_library #{inspect(library)} is not available or does not implement decode/1 and encode/1.
Do you need to add #{inspect(library)} to your mix.exs?
"""),
__STACKTRACE__
end
end
end
end
9 changes: 9 additions & 0 deletions lib/workos/config.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
defmodule WorkOS.Config do
@moduledoc false

def client, do: Application.get_env(:workos, :client, WorkOS.HackneyClient)

def hackney_opts, do: Application.get_env(:workos, :hackney_opts, [])

def json_library, do: Application.get_env(:workos, :json_library, Jason)
end
34 changes: 34 additions & 0 deletions lib/workos/hackney_client.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
defmodule WorkOS.HackneyClient do
@behaviour WorkOS.HttpClient
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reference regarding how behaviors work in Elixir: https://elixirschool.com/en/lessons/advanced/behaviours


@moduledoc """
The built-in HTTP client.

This client implements the `WorkOS.HTTPClient` behaviour.

It's based on the [hackney](https://github.com/benoitc/hackney) Erlang HTTP client,
which is an *optional dependency* of this library. If you wish to use another
HTTP client, you'll have to implement your own `WorkOS.HTTPClient`. See the
documentation for `WorkOS.HTTPClient` for more information.

WorkOS SDK starts its own hackney pool called `:workos_pool`. If you need to set other
[hackney configuration options](https://github.com/benoitc/hackney/blob/master/doc/hackney.md#request5)
for things such as proxies, using your own pool, or response timeouts, the `:hackney_opts`
configuration is passed directly to hackney for each request. See the configuration
documentation in the `WorkOS` module.
"""

@hackney_pool_name :workos_pool

@impl true
def post(url, headers, body) do
hackney_opts =
WorkOS.Config.hackney_opts()
|> Keyword.put_new(:pool, @hackney_pool_name)

case :hackney.request(:post, url, headers, body, [:with_body] ++ hackney_opts) do
{:ok, _status, _headers, _body} = result -> result
{:error, _reason} = error -> error
end
end
end
92 changes: 92 additions & 0 deletions lib/workos/http_client.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
defmodule WorkOS.HttpClient do
@moduledoc """
A behaviour for HTTP clients that WorkOS can use.

The default HTTP client is `WorkOS.HackneyClient`.

To configure a different HTTP client, implement the `WorkOS.HTTPClient` behaviour and
change the `:client` configuration:

config :workos,
client: MyHTTPClient

## Alternative Clients

Let's look at an example of using an alternative HTTP client. In this example, we'll
use [Finch](https://github.com/sneako/finch), a lightweight HTTP client for Elixir.

First, we need to add Finch to our dependencies:

# In mix.exs
defp deps do
[
# ...
{:finch, "~> 0.16"}
]
end

Then, we need to define a module that implements the `WorkOS.HTTPClient` behaviour:

defmodule MyApp.WorkOSFinchHTTPClient do
@behaviour WorkOS.HTTPClient

@impl true
def child_spec do
Supervisor.child_spec({Finch, name: __MODULE__}, id: __MODULE__)
end

@impl true
def post(url, headers, body) do
request = Finch.build(:post, url, headers, body)

case Finch.request(request, __MODULE__) do
{:ok, %Finch.Response{status: status, headers: headers, body: body}} ->
{:ok, status, headers, body}

{:error, error} ->
{:error, error}
end
end
end

Last, we need to configure WorkOS to use our new HTTP client:

config :workos,
client: MyApp.WorkOSFinchHTTPClient
"""

@typedoc """
The response status for an HTTP request.
"""
@typedoc since: "1.0.0"
@type status :: 100..599

@typedoc """
HTTP request or response headers.
"""
@type headers :: [{String.t(), String.t()}]

@typedoc """
HTTP request or response body.
"""
@typedoc since: "1.0.0"
@type body :: binary()

@doc """
Should return a **child specification** to start the HTTP client.

For example, this can start a pool of HTTP connections dedicated to WorkOS SDK.
If not provided, WorkOS SDK won't do anything to start your HTTP client. See
[the module documentation](#module-child-spec) for more info.
"""
@callback child_spec() :: :supervisor.child_spec()

@doc """
Should make an HTTP `POST` request to `url` with the given `headers` and `body`.
"""
@callback post(url :: String.t(), request_headers :: headers(), request_body :: body()) ::
{:ok, status(), response_headers :: headers(), response_body :: body()}
| {:error, term()}

@optional_callbacks [child_spec: 0]
end
5 changes: 3 additions & 2 deletions mix.exs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ defmodule WorkOS.MixProject do

def application do
[
mod: {WorkOS.Application, []},
extra_applications: [:logger],
env: env()
]
Expand All @@ -32,8 +33,8 @@ defmodule WorkOS.MixProject do
defp deps do
[
{:tesla, "~> 1.4"},
{:hackney, "~> 1.18.0"},
{:jason, ">= 1.0.0"},
{:hackney, "~> 1.18.0", optional: true},
{:jason, ">= 1.0.0", optional: true},
{:plug_crypto, "~> 1.0"},
{:ex_doc, "~> 0.23", only: :dev, runtime: false},
{:credo, "~> 1.6", only: [:dev, :test], runtime: false}
Expand Down