lib/dryhard.ex

defmodule Dryhard do
  @moduledoc """
  Documentation for `Dryhard`.
  """

  defmacro __using__(_) do
    quote do
      require Dryhard
      import Ecto.Query, warn: false, only: [from: 2]
    end
  end

  defmacro list(config) do
    quote bind_quoted: [config: config] do
      def unquote(:"list_#{config.plural_name}")(queries \\ & &1) do
        unquote(config.schema)
        |> queries.()
        |> unquote(config.repo).all()
      end
    end
  end

  defmacro base(config) do
    quote bind_quoted: [config: config] do
      @doc """
      Initial query for #{config.plural_name}. Like: `from(u in User)`
      """
      def unquote(:"base_#{config.plural_name}")(queries \\ & &1) do
        unquote(config.schema).__schema__(:query)
        |> queries.()
      end
    end
  end

  defmacro paginate(config) do
    quote bind_quoted: [config: config] do
      @doc """
      Pagination for #{config.plural_name}.

      Params:
        - page: integer
        - per_page: integer
        - queries: function that accepts an Ecto.Query for further filtering
      """
      def unquote(:"paginate_#{config.plural_name}")(queries \\ & &1, page \\ 1, page_size \\ 25) do
        unquote(config.schema)
        |> queries.()
        |> Dryhard.Pager.page(unquote(config.repo), page, page_size)
      end
    end
  end

  defmacro get!(config) do
    quote bind_quoted: [config: config] do
      @doc """
      Hard get for a single record of #{config.plural_name}.

      Params:
        - id: integer / uuid / etc
        - queries: function that accepts an Ecto.Query for further filtering
      """
      def unquote(:"get_#{config.singular_name}!")(id, queries \\ & &1) do
        unquote(config.schema)
        |> queries.()
        |> unquote(config.repo).get!(id)
      end
    end
  end

  defmacro get(config) do
    quote bind_quoted: [config: config] do
      @doc """
      Soft get for a single record of #{config.plural_name}.

      Params:
        - id: integer / uuid / etc
        - queries: function that accepts an Ecto.Query for further filtering
      """
      def unquote(:"get_#{config.singular_name}")(id, queries \\ & &1) do
        unquote(config.schema)
        |> queries.()
        |> unquote(config.repo).get(id)
      end
    end
  end

  defmacro get_by_attr!(config, attr_name) do
    quote bind_quoted: [config: config, attr_name: attr_name] do
      @doc """
      Hard get a single record of #{config.plural_name} by #{attr_name}
      Params:
        - value: value for #{attr_name}
        - queries: function that accepts an Ecto.Query for further filtering
      """
      def unquote(:"get_#{config.singular_name}_by_#{attr_name}!")(value, queries \\ & &1) do
        unquote(config.schema)
        |> queries.()
        |> unquote(config.repo).get_by!([{unquote(attr_name), value}])
      end
    end
  end

  defmacro get_by_attr(config, attr_name) do
    quote bind_quoted: [config: config, attr_name: attr_name] do
      @doc """
      Soft get a single record of #{config.plural_name} by #{attr_name}
      Params:
        - value: value for #{attr_name}
        - queries: function that accepts an Ecto.Query for further filtering
      """
      def unquote(:"get_#{config.singular_name}_by_#{attr_name}")(value, queries \\ & &1) do
        unquote(config.schema)
        |> queries.()
        |> unquote(config.repo).get_by([{unquote(attr_name), value}])
      end
    end
  end

  defmacro get_for!(config, assoc_name) do
    quote bind_quoted: [config: config, assoc_name: assoc_name] do
      @doc """
      Hard get a single record of #{config.plural_name} for #{assoc_name}
      Params:
        - assoc_record: instance of #{assoc_name}
        - queries: function that accepts an Ecto.Query for further filtering
      """
      def unquote(:"get_#{config.singular_name}_for_#{assoc_name}!")(
            assoc_record,
            queries \\ & &1
          ) do
        foreign_key =
          unquote(config.schema).__schema__(:association, unquote(assoc_name)).owner_key

        from(resource in unquote(config.schema),
          where: field(resource, ^foreign_key) == ^assoc_record.id
        )
        |> queries.()
        |> unquote(config.repo).one!()
      end
    end
  end

  defmacro get_for(config, assoc_name) do
    quote bind_quoted: [config: config, assoc_name: assoc_name] do
      @doc """
      Soft get a single record of #{config.plural_name} for #{assoc_name}

      Params:
        - assoc_record: instance of #{assoc_name}
        - queries: function that accepts an Ecto.Query for further filtering
      """
      def unquote(:"get_#{config.singular_name}_for_#{assoc_name}")(assoc_record, queries \\ & &1) do
        foreign_key =
          unquote(config.schema).__schema__(:association, unquote(assoc_name)).owner_key

        from(resource in unquote(config.schema),
          where: field(resource, ^foreign_key) == ^assoc_record.id
        )
        |> queries.()
        |> unquote(config.repo).one()
      end
    end
  end

  defmacro new(config) do
    quote bind_quoted: [config: config] do
      @doc """
      Returns a new struct of #{config.plural_name}
      """
      def unquote(:"new_#{config.singular_name}")() do
        unquote(config.schema) |> struct()
      end
    end
  end

  defmacro create(config, changeset_fn) do
    quote bind_quoted: [config: config, changeset_fn: changeset_fn] do
      @doc """
      Creates a single #{config.plural_name} record

      Params:
        - attrs: map to pass into the changeset function
      """
      def unquote(:"create_#{config.singular_name}")(attrs) do
        unquote(config.schema)
        |> struct()
        |> unquote(changeset_fn).(attrs)
        |> unquote(config.repo).insert()
      end
    end
  end

  defmacro create(config, association_name, changeset_fn) do
    quote bind_quoted: [
            config: config,
            association_name: association_name,
            changeset_fn: changeset_fn
          ] do
      association_schema = config.schema.__schema__(:association, association_name).queryable

      def unquote(:"create_#{config.singular_name}")(association_struct, attrs) do
        association_foreign_key =
          unquote(config.schema).__schema__(:association, unquote(association_name)).owner_key
          |> Atom.to_string()

        attrs = Map.merge(attrs, %{association_foreign_key => association_struct.id})

        unquote(config.schema)
        |> struct()
        |> unquote(changeset_fn).(attrs)
        |> unquote(config.repo).insert()
      end
    end
  end

  defmacro change(config, changeset_fn) do
    quote bind_quoted: [config: config, changeset_fn: changeset_fn] do
      def unquote(:"change_#{config.singular_name}")(resource, attrs) do
        unquote(changeset_fn).(resource, attrs)
      end
    end
  end

  defmacro update(config, changeset_fn) do
    quote bind_quoted: [config: config, changeset_fn: changeset_fn] do
      def unquote(:"update_#{config.singular_name}")(resource, attrs) do
        resource
        |> unquote(changeset_fn).(attrs)
        |> unquote(config.repo).update()
      end
    end
  end

  defmacro delete(config) do
    quote bind_quoted: [config: config] do
      def unquote(:"delete_#{config.singular_name}")(resource) do
        unquote(config.repo).delete(resource)
      end
    end
  end

  defmacro preload(config, preload_assoc_name) do
    quote bind_quoted: [config: config, preload_assoc_name: preload_assoc_name] do
      def unquote(:"preload_#{config.singular_name}_#{preload_assoc_name}")(query) do
        from(query, preload: unquote(preload_assoc_name))
      end
    end
  end

  defmacro join(config, join_assoc_name) do
    quote bind_quoted: [config: config, join_assoc_name: join_assoc_name] do
      def unquote(:"join_#{config.singular_name}_#{join_assoc_name}")(query) do
        from(resource in query,
          join: assoc_resource in assoc(resource, unquote(join_assoc_name)),
          preload: [{unquote(join_assoc_name), assoc_resource}]
        )
      end
    end
  end

  defmacro order_by(config, attr_name, direction) do
    quote bind_quoted: [config: config, attr_name: attr_name, direction: direction] do
      def unquote(:"order_#{config.plural_name}_by_#{attr_name}")(query) do
        from(query, order_by: [{unquote(direction), unquote(attr_name)}])
      end
    end
  end

  defmacro filter_by_one(config, assoc_name) do
    quote bind_quoted: [config: config, assoc_name: assoc_name] do
      def unquote(:"filter_#{config.plural_name}_by_#{assoc_name}")(query, assoc_record) do
        foreign_key =
          unquote(config.schema).__schema__(:association, unquote(assoc_name)).owner_key

        from(resource in query, where: field(resource, ^foreign_key) == ^assoc_record.id)
      end
    end
  end

  defmacro filter_by_many(config, assoc_name) do
    quote bind_quoted: [config: config, assoc_name: assoc_name] do
      def unquote(:"filter_#{config.plural_name}_by_#{assoc_name}")(query, assoc_records) do
        foreign_key =
          unquote(config.schema).__schema__(:association, unquote(assoc_name)).owner_key

        assoc_ids = Enum.map(assoc_records, & &1.id)
        from(resource in query, where: field(resource, ^foreign_key) in ^assoc_ids)
      end
    end
  end

  defmacro filter_by_one_or_many(config, assoc_name) do
    quote bind_quoted: [config: config, assoc_name: assoc_name] do
      def unquote(:"filter_#{config.plural_name}_by_#{assoc_name}")(
            query,
            assoc_record_or_records
          ) do
        foreign_key =
          unquote(config.schema).__schema__(:association, unquote(assoc_name)).owner_key

        assoc_ids =
          assoc_record_or_records
          |> List.wrap()
          |> Enum.map(& &1.id)

        from(resource in query, where: field(resource, ^foreign_key) in ^assoc_ids)
      end
    end
  end

  def config(schema, repo, plural) do
    resource_name = Dryhard.Naming.resource_name(schema)

    %{
      singular_name: resource_name,
      plural_name: plural,
      schema: schema,
      repo: repo
    }
  end
end