Current section
Files
Jump to
Current section
Files
lib/cloud_kit/config.ex
defmodule CloudKit.Config do
@moduledoc false
@default_base_url "https://api.apple-cloudkit.com"
@default_environment "development"
@default_ttl 10 * 60
defstruct [
:container_id,
:environment,
:server_key_id,
:private_key,
:private_key_path,
:base_url,
:token_ttl_seconds,
:req_options
]
@type t :: %__MODULE__{
container_id: String.t() | nil,
environment: String.t(),
server_key_id: String.t() | nil,
private_key: String.t() | nil,
private_key_path: String.t() | nil,
base_url: String.t(),
token_ttl_seconds: pos_integer(),
req_options: keyword()
}
@doc """
Build a config struct by merging `Application.get_all_env/1` with per-call `opts`.
Per-call options win.
"""
@spec load(keyword()) :: t()
def load(opts \\ []) do
env = Application.get_all_env(:cloud_kit)
merged = Keyword.merge(env, opts)
%__MODULE__{
container_id: fetch(merged, :container_id),
environment: Keyword.get(merged, :environment, @default_environment),
server_key_id: fetch(merged, :server_key_id),
private_key: fetch(merged, :private_key),
private_key_path: fetch(merged, :private_key_path),
base_url: Keyword.get(merged, :base_url, @default_base_url),
token_ttl_seconds: Keyword.get(merged, :token_ttl_seconds, @default_ttl),
req_options: Keyword.get(merged, :req_options, [])
}
end
@doc """
Build the API URL path for a given endpoint.
Raises `ArgumentError` if `:container_id` is missing or contains characters
outside CloudKit container identifiers (letters, digits, `.`, `-`, `_`), or
if `:environment` is not `"development"` or `"production"`.
"""
@spec api_path(t(), String.t()) :: String.t()
def api_path(%__MODULE__{} = config, endpoint) do
container_id = validate_container_id!(config.container_id)
environment = validate_environment!(config.environment)
"/database/1/#{container_id}/#{environment}#{endpoint}"
end
defp validate_container_id!(container_id)
when is_binary(container_id) and container_id != "" do
if container_id =~ ~r/^[A-Za-z0-9._-]+$/ do
container_id
else
raise ArgumentError, "invalid :container_id format: #{inspect(container_id)}"
end
end
defp validate_container_id!(other) do
raise ArgumentError, "missing or invalid :container_id: #{inspect(other)}"
end
defp validate_environment!(env) when env in ["development", "production"], do: env
defp validate_environment!(other) do
raise ArgumentError,
"invalid :environment #{inspect(other)} — must be \"development\" or \"production\""
end
@doc "Return the PEM string, reading from `:private_key_path` if needed."
@spec private_key_pem!(t()) :: String.t()
def private_key_pem!(%__MODULE__{private_key: pem}) when is_binary(pem) and pem != "", do: pem
def private_key_pem!(%__MODULE__{private_key_path: path}) when is_binary(path) and path != "" do
File.read!(path)
end
def private_key_pem!(_), do: raise(ArgumentError, "missing :private_key or :private_key_path")
defp fetch(kw, key) do
case Keyword.get(kw, key) do
"" -> nil
value -> value
end
end
end