defmodule CloudflareApi do @moduledoc ~S""" Top-level entry point for the Cloudflare API client. `CloudflareApi` is a thin, explicit wrapper around the [Cloudflare v4 API](https://api.cloudflare.com/). It provides convenient functions and Elixir idioms so you don't have to work with raw HTTP requests by hand. ## Status _Note:_ this package is under active development and may be refactored in substantive ways. If you need to get to production quickly, consider pinning to a specific version and watching the changelog. ## Philosophy This library assumes that a well-designed REST API does not need a heavy abstraction layer on top: - Request/response shapes stay close to the official Cloudflare JSON. - Functions generally return `{:ok, result}` / `{:error, reason}` tuples. - Modules and function names mirror Cloudflare's own endpoint groupings. The upside is that you can keep the official Cloudflare documentation open alongside this library instead of learning a separate abstraction. The downside is that if you *want* to be fully shielded from the underlying API details, this probably is not the package for you. ## Getting started The usual way to use the library is: token = System.fetch_env!("CLOUDFLARE_API_TOKEN") client = CloudflareApi.new(token) {:ok, zones} = CloudflareApi.Zones.list(client) or, using the zero-arity client function: client_fun = CloudflareApi.client("api-token") {:ok, records} = CloudflareApi.DnsRecords.list(client_fun, "zone-id", name: "www.example.com") You may also wish to look through the Livebooks in `livebooks/` for additional end-to-end examples. """ use CloudflareApi.Typespecs @typedoc "A configured Tesla client or a zero-arity function that returns one." @type client :: Tesla.Client.t() | (-> Tesla.Client.t()) @typedoc "Common keyword or map options passed to Cloudflare endpoints." @type options :: keyword() | map() | nil @typedoc "Standard error reasons returned by this library when wrapping HTTP calls." @type error_reason :: Tesla.Env.t() | %Tesla.Error{} | term() @typedoc "Helper type for `{:ok, value}` / `{:error, reason}` tuples." @type result(result) :: {:ok, result} | {:error, error_reason()} @typedoc "Generic identifier helper for Cloudflare resource IDs." @type id :: String.t() @doc ~S""" Build a `Tesla.Client.t()` configured for the Cloudflare v4 API. The returned client can be passed to any of the `CloudflareApi.*` endpoint modules. - `cloudflare_api_token` – a Cloudflare API token with the permissions your calls require ## Examples iex> client = CloudflareApi.new("api-token") iex> is_struct(client, Tesla.Client) true You may also enable opt-in rate-limit retries by passing `rate_limit_retry: true` (or a keyword list of retry options) as the second argument. See `CloudflareApi.RateLimitRetry` for available options. ## Examples iex> client = CloudflareApi.new("api-token", rate_limit_retry: true) iex> is_struct(client, Tesla.Client) true """ def new(cloudflare_api_token, opts \\ []) do middlewares = [ {Tesla.Middleware.BaseUrl, "https://api.cloudflare.com/client/v4"}, Tesla.Middleware.JSON, {Tesla.Middleware.BearerAuth, token: cloudflare_api_token} ] |> maybe_add_rate_limit_retry(Keyword.get(opts, :rate_limit_retry)) Tesla.client(middlewares) end @doc ~S""" Build a reusable zero-arity client function. This wraps `new/1` and returns a function that, when called, yields a configured `Tesla.Client.t()`. It is convenient when you want to inject a "current Cloudflare client" into other modules without threading the `%Tesla.Client{}` struct manually. - `cloudflare_api_token` – a Cloudflare API token with the permissions your calls require ## Examples iex> client_fun = CloudflareApi.client("api-token") iex> client1 = client_fun.() iex> client2 = client_fun.() iex> is_struct(client1, Tesla.Client) and client1 == client2 true """ def client(cloudflare_api_token, opts \\ []) do c = CloudflareApi.new(cloudflare_api_token, opts) fn -> c end end @doc false def uri_encode_opts(opts) do URI.encode_query(opts, :rfc3986) end defp maybe_add_rate_limit_retry(middleware, nil), do: middleware defp maybe_add_rate_limit_retry(middleware, false), do: middleware defp maybe_add_rate_limit_retry(middleware, true) do [{CloudflareApi.RateLimitRetry, []} | middleware] end defp maybe_add_rate_limit_retry(middleware, retry_opts) when is_list(retry_opts) do [{CloudflareApi.RateLimitRetry, retry_opts} | middleware] end defp maybe_add_rate_limit_retry(_middleware, other) do raise ArgumentError, "rate_limit_retry must be a boolean or keyword list, got: #{inspect(other)}" end end