Skip to content

Instantly share code, notes, and snippets.

@imranismail
Last active April 23, 2017 12:16
Show Gist options
  • Select an option

  • Save imranismail/f8cfa42807f98aa39127f9f8487a009d to your computer and use it in GitHub Desktop.

Select an option

Save imranismail/f8cfa42807f98aa39127f9f8487a009d to your computer and use it in GitHub Desktop.
Tips on Elixir that you might not know of

Elixir Tips and Tricks That You Might Not Know Of

SaaS/Multitenant Migrations with Ecto

defmodule App.Team do
  use App, :schema

  ...

  def changeset(schema, params \\ %{}) do
    schema
    |> cast(params, @allowed)
    |> validate_required(@required)
    |> prepare_changes(fn changeset ->
      case changeset.action do
        :insert ->
          up(changeset.changes.handle)
        :delete ->
          down(changeset.changes.handle)
      end

      changeset
    end)
  end

  defp up(handle) do
    Repo.query!("CREATE SCHEMA \"#{handle}\"")
    Migrator.run(Repo, migration_dir(), :up, all: true, prefix: handle)
  end

  defp down(handle) do
    Migrator.run(Repo, migration_dir(), :down, all: true, prefix: handle)
    Repo.query!("DROP SCHEMA \"#{handle}\" CASCADE")
  end

  defp migration_dir do
    Application.app_dir(:domain, "priv/repo/organization_migrations")
  end
end

defmodule App.Plugs do
  def prefix_team(conn, opts) do
    uri = endpoint_module(conn).struct_url()

    team_uri = struct(URI, [
      host: conn.host,
      scheme: Atom.to_string(conn.scheme),
      port: conn.port
    ])

    team_uri.host
    |> String.trim_trailing(uri.host)
    |> String.trim_trailing(".")
    |> case do
      "" ->
        redirect(conn, to: Keyword.fetch!(opts, :redirect_to))
      handle ->
        team = Repo.get_by!(Team, handle: handle)

        conn
        |> put_private(:team, team)
        |> put_private(:prefix, Phoenix.Param.to_param(team))
    end
  end
end

defmodule App.Team do
  use App, :schema
  @derive {Phoenix.Param, key: :handle}
end

defmodule App.SomeController do
  import App.Plugs

  plug :prefix_team, redirect_to: page_url(App.Endpoint, :sign_in)

  def index(conn, _) do
    Repo.all(Something, prefix: conn.private.prefix)
  end
end

Cross app eventbus with Registry

defmodule App.Notification do
  def start_link() do
    Registry.start_link(:unique, Notification, partitions: System.schedulers_online)
  end

  def subscribe(topic) do
    Registry.register(Notification, topic, nil)
  end

  def notify(topic, message) do
    Registry.dispatch(Notification, topic, fn entries ->
      for {pid, _} <- entries, do: send(pid, message)
    end)
  end
end

defmodule TeamController do
  ...
  def create(conn, params) do
    ...
    {:ok, team} -> Notification.notify(:team, {:created, team})
  end
end

defmodule Worker.TeamCreator do
  use GenServer
  ...

  def init(state) do
    Notification.subscribe(:team)
    super(state)
  end

  def handle_info({:created, team}, state) do
    # do something with the newly created team
  end

Repo composition

defmodule App.ComposableRepo do
  defmacro __using__(opts) do
    quote bind_quoted: binding() do
      @composable_repo Keyword.fetch!(opts, :composing)
      @composable_functions @composable_repo.__info__(:functions)

      @composable_functions
      |> Enum.map(fn
        {function, 0} ->
          {function, []}
        {function, arity} ->
          {function, Enum.map(1..arity, &(Macro.var(:"arg_#{&1}", __MODULE__)))}
      end)
      |> Enum.map(fn {function, arguments} ->
        defdelegate unquote(function)(unquote_splicing(arguments)), to: @composable_repo
      end)

      defoverridable @composable_functions
    end
  end
end

defmodule App.ComposedRepo do
  use App.ComposableRepo, composing: App.Repo
  ...
  def delete(schema, opts \\ []) do
    {force_delete?, opts} = Keyword.pop(opts, :force, false)

    if !force_delete? && soft_deletable?(schema) do
      schema
      |> change(deleted_at: DateTime.utc_now())
      |> update()
    else
      super(schema, opts)
    end
  end
end

Custom Ecto Type

defmodule Domain.Tags do
  @moduledoc """
  Schema:
      defmodule Post do
        use Ecto.Schema
        schema "posts" do
          field :title, :string
          field :body, :string
          field :tags, Domain.TagType
        end
      end
  Migration:
      def change do
        create table(:posts) do
          add :title, :string
          add :body, :string
          add :tags, {:array, :string}
        end
      end
  """
  @behaviour Ecto.Type

  @spec type() :: {atom, atom}
  def type, do: {:array, :string}

  @spec cast(list(String.t) | String.t) :: {atom, list(String.t)}
  def cast(term) when is_list(term), do: {:ok, term}
  def cast(""), do: {:ok, []}
  def cast(term) when is_binary(term) do
    case get_delimiter(term) do
      :error -> :error
      delimeter -> {:ok, String.split(term, delimeter, trim: true)}
    end
  end
  def cast(_), do: :error

  @spec load(list(String.t)) :: {atom, list(String.t)}
  def load(term) when is_list(term), do: {:ok, term}

  @spec dump(list(String.t)) :: {atom, list(String.t)}
  def dump(term) when is_list(term), do: {:ok, term}
  def dump(_), do: :error

  @spec get_delimiter(String.t) :: String.t | atom
  defp get_delimiter(term) do
    cond do
      term =~ ~r/^([^,]+)(,\s)?([^,]+)*?$/ -> ", "
      term =~ ~r/^([^,]+),?([^,]+)*?$/ -> ","
      true -> :error
    end
  end
end

Asynchronous or maybe even parallel assigns

defmodule App.DashboardController do
  use App, :controller

  plug :async_put_stats

  def index(conn, params) do
    render(conn)
  end

  def async_put_stats(conn, _) do
    conn
    |> async_assign(:weekly_sales, fn ->
      Sale
      |> Sale.to_stats(:weekly)
      |> Repo.all()
    end)
    |> async_assign(:trending_products, fn ->
      Product
      |> Product.trending()
      |> Product.to_stats(:weekly)
      |> Repo.all()
    end)
    |> await_assign(:weekly_sales)
    |> await_assign(:trending_products)
  end
end

Why Datastructure > * (macro magic/overbloated function)

defmodule Media do
  defstruct [
    source: %Media.File{},
    sink: %Media.File{},
    transformations: [],
    validations: [],
    store_at: nil,
    store: Application.get_env(:media, :store, Media.LocalStore),
    errors: [],
    saved?: false,
    valid?: false
  ]

  def new(opts \\ []) do
    struct(Media, opts)
  end

  def save(media) do
    media
    |> apply_validations()
    |> case do
      %{valid?: true} = media -> {:ok, apply_transformations!(media)}
      %{valid?: false} = media -> {:error, media}
    end
  end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment