lib/bubble_lib/dsl_struct.ex

defmodule BubbleLib.DslStruct do
  @moduledoc """
  A "DSL Struct" is a struct which can be exposed in Bubblescript.

  Most notable are intent, message, attachment, location, event.
  """

  defmacro __using__(struct) do
    str_fields = struct |> Keyword.keys() |> Enum.map(&to_string/1)

    quote do
      defstruct unquote(struct)

      @str_fields unquote(str_fields)

      @behaviour Access

      @impl Access
      def fetch(term, key) when key in unquote(str_fields) do
        fetch(term, String.to_atom(key))
      end

      def fetch(term, key) do
        Map.fetch(term, key)
      end

      defoverridable fetch: 2

      @impl Access
      def get_and_update(term, key, fun) when key in unquote(str_fields) do
        get_and_update(term, String.to_atom(key), fun)
      end

      def get_and_update(term, key, fun), do: Map.get_and_update(term, key, fun)

      defoverridable get_and_update: 3

      @impl Access
      def pop(term, key) when key in unquote(str_fields) do
        pop(term, String.to_atom(key))
      end

      def pop(term, key), do: Map.pop(term, key)

      defoverridable pop: 2

      def __jason_encode__(struct, opts, only) do
        struct
        |> Map.keys()
        |> Enum.reject(&(&1 != "__struct__" && (is_list(only) && &1 in only)))
        |> Enum.map(fn k -> {to_string(k), Map.get(struct, k)} end)
        |> Map.new()
        |> Jason.Encode.map(opts)
      end
    end
  end

  defmacro jason_derive(mod, only \\ nil) do
    quote do
      defimpl Jason.Encoder, for: unquote(mod) do
        def encode(struct, opts) do
          unquote(mod).__jason_encode__(struct, opts, unquote(only))
        end
      end
    end
  end

  def instantiate_structs(%{"__struct__" => mod} = struct) do
    struct =
      struct
      |> Enum.map(fn {k, v} ->
        {k, instantiate_structs(v)}
      end)
      |> Map.new()

    mod = String.to_atom(mod)
    orig = apply(mod, :__struct__, [])

    Map.keys(orig)
    |> Enum.map(fn k -> {k, Map.get(struct, to_string(k)) || Map.get(orig, k)} end)
    |> Map.new()
    |> Map.put(:__struct__, mod)
  end

  def instantiate_structs(%{__struct__: _} = struct) do
    struct
  end

  def instantiate_structs(%{} = map) do
    map
    |> Enum.map(fn {k, v} ->
      {k, instantiate_structs(v)}
    end)
    |> Map.new()
  end

  def instantiate_structs(list) when is_list(list) do
    Enum.map(list, &instantiate_structs/1)
  end

  def instantiate_structs(value), do: value

  def struct_from_map(struct, input) do
    Enum.reduce(Map.to_list(struct), struct, fn {k, _}, acc ->
      case Map.fetch(input, Atom.to_string(k)) do
        {:ok, v} -> %{acc | k => v}
        :error -> acc
      end
    end)
  end
end