Current section
Files
Jump to
Current section
Files
lib/cased.ex
defmodule Cased do
@moduledoc """
Documentation for Cased.
"""
import Norm
defmodule ConfigurationError do
@moduledoc false
defexception message: "invalid configuration", details: nil
@type t :: %__MODULE__{
message: String.t(),
details: nil | any()
}
def message(exc) do
"#{exc.message}\ndetails #{inspect(exc.details)}"
end
end
defmodule RequestError do
@moduledoc false
defexception message: "invalid request configuration", details: nil
@type t :: %__MODULE__{
message: String.t(),
details: nil | any()
}
def message(exc) do
"#{exc.message}\ndetails #{inspect(exc.details)}"
end
end
defmodule ResponseError do
@moduledoc false
defexception message: "invalid response", details: nil, response: nil
@type t :: %__MODULE__{
message: String.t(),
details: nil | any(),
response: nil | Mojito.response()
}
def message(%{response: nil} = exc) do
"#{exc.message}\ndetails #{inspect(exc.details)}\nstatus code: (none)"
end
def message(exc) do
"#{exc.message}\ndetails #{inspect(exc.details)}\nstatus code: #{exc.status_code}"
end
end
@default_publish_opts [
publishers: [Cased.Publisher.HTTP],
handlers: []
]
@typedoc """
Options when publishing.
- `:publishers`, the list of publisher pids (defaults to `#{
inspect(@default_publish_opts[:publishers])
}`).
- `:handlers`, the list of sensitive data handlers (defaults to `#{
inspect(@default_publish_opts[:handlers])
}`);
see `Cased.Sensitive.Handler`.
"""
@type publish_opts :: [publish_opt()]
@type publish_opt ::
{:publishers, [GenServer.server()]}
| {:handlers, [Cased.Sensitive.Handler.t() | Cased.Sensitive.Handler.spec()]}
@doc """
Publish an audit event to Cased.
Note: Uses `GenServer.call/3` to send events to publisher processes.
```
%{
action: "credit_card.charge",
amount: 2000,
currency: "usd",
source: "tok_amex",
description: "My First Test Charge (created for API docs)",
credit_card_id: "card_1dQpXqQwXxsQs9sohN9HrzRAV6y"
}
|> Cased.publish()
```
"""
@spec publish(audit_event :: map(), opts :: publish_opts()) ::
:ok | {:error, Jason.EncodeError.t() | Exception.t()}
def publish(audit_event, opts \\ []) do
opts =
@default_publish_opts
|> Keyword.merge(opts)
audit_event =
audit_event
|> Map.merge(Cased.Context.to_map())
case validate_publish_opts(opts) do
{:ok, %{publishers: publishers, handlers: handlers}} ->
Cased.Sensitive.Processor.process(audit_event, handlers: handlers)
|> do_publish(publishers)
{:error, details} ->
{:error, %ConfigurationError{details: details}}
end
end
@spec do_publish(data :: term(), publishers :: [GenServer.server()]) ::
:ok | {:error, Jason.EncodeError.t() | Exception.t()}
defp do_publish(data, publishers) do
case Jason.encode(data) do
{:ok, json} ->
for publisher <- publishers do
GenServer.call(publisher, {:publish, json})
end
:ok
other ->
other
end
end
@doc """
Publish an audit event to Cased, raising an exception in the event of failure.
"""
@spec publish!(data :: term(), opts :: publish_opts()) :: :ok | no_return()
def publish!(data, opts \\ []) do
case publish(data, opts) do
:ok ->
:ok
{:error, err} ->
raise err
end
end
defp validate_publish_opts(opts) do
opts
|> Map.new()
|> conform(publish_opts_schema())
end
defp publish_opts_schema() do
schema(%{
# Effectively [GenServer.server()]
publishers: coll_of(spec(is_pid() or is_atom() or is_tuple())),
# TODO: Use is_struct() vs is_map(), post-Elixir v1.10
handlers: coll_of(spec(is_tuple() or is_map()))
})
|> selection()
end
end