lib/cf_sync/entry.ex

defmodule CFSync.Entry do
  @moduledoc """
  The `Entry` struct holds standard data from Contentful entries. It's provided as
  is and is not configurable, so if you need more fields than those currently mapped,
  feel free to send a PR.
  """

  require Logger

  alias CFSync.Entry.Fields

  @enforce_keys [
    :store,
    :space_id,
    :id,
    :locale,
    :revision,
    :content_type,
    :fields
  ]

  defstruct [
    :store,
    :space_id,
    :id,
    :locale,
    :revision,
    :content_type,
    :fields
  ]

  @type t :: %__MODULE__{
          store: CFSync.store(),
          space_id: binary(),
          id: binary(),
          locale: atom(),
          revision: integer(),
          content_type: atom(),
          fields: Fields.t()
        }

  @doc false
  # locale is the "CFSync" locale: it is an atom, used as a key in ETS tables.
  # cf_locale is the Contentful locale: it is a binary, used as a key in the Contentful API.
  @spec new(
          data :: map(),
          content_types :: map(),
          locales :: map(),
          store :: CFSync.store(),
          locale :: atom()
        ) :: t()
  def new(
        %{
          "sys" => %{
            "id" => id,
            "type" => "Entry",
            "revision" => revision,
            "contentType" => %{
              "sys" => %{
                "id" => content_type
              }
            },
            "space" => %{
              "sys" => %{
                "id" => space_id
              }
            }
          },
          "fields" => fields
        },
        content_types,
        locales,
        store,
        locale
      ) do
    case get_config_for_content_type(content_types, content_type) do
      {:ok,
       %{
         content_type: content_type,
         fields_module: fields_module
       }} ->
        %__MODULE__{
          store: store,
          id: id,
          revision: revision,
          space_id: space_id,
          content_type: content_type,
          locale: locale,
          fields:
            fields_module.new(%{
              fields: fields,
              locales: locales,
              store: store,
              locale: locale
            })
        }

      :error ->
        %__MODULE__{
          store: store,
          id: id,
          revision: revision,
          space_id: space_id,
          content_type: :unknown,
          locale: locale,
          fields: nil
        }
    end
  end

  defp get_config_for_content_type(content_types, content_type) when is_binary(content_type) do
    with {:ok, config} <- fetch_config_for_content_type(content_types, content_type),
         :ok <- validate_config(config) do
      {:ok, config}
    else
      {:error, :no_config_for_content_type} ->
        error(content_type, "No mapping provided for this content type.")

      {:error, :invalid_config} ->
        error(content_type, "Invalid mapping.")

      {:error, :undefined_fields_module, mod} ->
        error(content_type, "Undefined fields module: #{inspect(mod)}.")
    end
  end

  defp error(content_type, msg) do
    Logger.error("CFSync mapping error for content type \"#{content_type}\":")
    Logger.error(msg)
    :error
  end

  defp fetch_config_for_content_type(content_types, content_type) do
    case Map.fetch(content_types, content_type) do
      {:ok, config} -> {:ok, config}
      _ -> {:error, :no_config_for_content_type}
    end
  end

  defp validate_config(%{
         content_type: content_type,
         fields_module: fields_module
       })
       when is_atom(content_type) and is_atom(fields_module) do
    case Code.ensure_loaded(fields_module) do
      {:module, ^fields_module} -> :ok
      _ -> {:error, :undefined_fields_module, fields_module}
    end
  end

  defp validate_config(_invalid) do
    {:error, :invalid_config}
  end
end