defmodule Plausible.Audit.EncoderError do defexception [:message] end defprotocol Plausible.Audit.Encoder do def encode(x) def encode(x, opts) end defimpl Plausible.Audit.Encoder, for: Ecto.Changeset do def encode(changeset, opts \\ []) do changes = Enum.reduce(changeset.changes, %{}, fn {k, v}, acc -> Map.put(acc, k, Plausible.Audit.Encoder.encode(v, opts)) end) data = Plausible.Audit.Encoder.encode(changeset.data, opts) case {map_size(data), map_size(changes)} do {n, 0} when n > 0 -> data {0, n} when n > 0 -> changes {0, 0} -> %{} _ -> %{before: data, after: changes} end end end defimpl Plausible.Audit.Encoder, for: Map do def encode(x, opts \\ []) do {allow_not_loaded, data} = Map.pop(x, :__allow_not_loaded__) raise_on_not_loaded? = Keyword.get(opts, :raise_on_not_loaded?, true) Enum.reduce(data, %{}, fn {k, %Ecto.Association.NotLoaded{}}, acc -> if k in allow_not_loaded or not raise_on_not_loaded? do acc else raise Plausible.Audit.EncoderError, message: "#{k} association not loaded. Either preload, exclude or mark it as :allow_not_loaded in #{__MODULE__} options" end {k, v}, acc -> Map.put(acc, k, Plausible.Audit.Encoder.encode(v, opts)) end) end end defimpl Plausible.Audit.Encoder, for: [Integer, BitString, Float] do def encode(x, _opts \\ []), do: x end defimpl Plausible.Audit.Encoder, for: [DateTime, Date, NaiveDateTime, Time] do def encode(x, _opts \\ []), do: to_string(x) end defimpl Plausible.Audit.Encoder, for: Atom do def encode(x, opts \\ []) def encode(nil, _opts), do: nil def encode(true, _opts), do: true def encode(false, _opts), do: false def encode(x, _opts), do: Atom.to_string(x) end defimpl Plausible.Audit.Encoder, for: List do def encode(x, opts \\ []) do Enum.map(x, &Plausible.Audit.Encoder.encode(&1, opts)) end end defimpl Plausible.Audit.Encoder, for: Any do defmacro __deriving__(module, struct, options) do deriving(module, struct, options) end def deriving(module, _struct, options) do only = options[:only] except = options[:except] allow_not_loaded = options[:allow_not_loaded] || [] extractor = cond do only -> quote( do: struct |> Map.take(unquote(only)) |> Map.put(:__allow_not_loaded__, unquote(allow_not_loaded)) ) except -> except = [:__struct__ | except] quote( do: struct |> Map.drop( unquote(except) |> Map.put(:__allow_not_loaded__, unquote(allow_not_loaded)) ) ) true -> quote( do: struct |> Map.delete(:__struct__) |> Map.put(:__allow_not_loaded__, unquote(allow_not_loaded)) ) end quote do defimpl Plausible.Audit.Encoder, for: unquote(module) do def encode(struct, opts \\ []) do Plausible.Audit.Encoder.encode(unquote(extractor), opts) end end end end def encode(_), do: raise("Implement me") def encode(_, _), do: raise("Implement me") end