Elixir Tips and Tricks That You Might Not Know Of
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
enddefmodule 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
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
enddefmodule 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
enddefmodule 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
enddefmodule 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