Advertisement
Guest User

Untitled

a guest
Mar 23rd, 2017
59
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
text 6.13 KB | None | 0 0
  1. Elixir Tips and Tricks That You Might Not Know Of
  2.  
  3. # SaaS/Multitenant Migrations with Ecto
  4.  
  5. ```elixir
  6. defmodule App.Team do
  7. use App, :schema
  8.  
  9. ...
  10.  
  11. def changeset(schema, params \\ %{}) do
  12. schema
  13. |> cast(params, @allowed)
  14. |> validate_required(@required)
  15. |> prepare_changes(fn changeset ->
  16. case changeset.action do
  17. :insert ->
  18. up(changeset.changes.handle)
  19. :delete ->
  20. down(changeset.changes.handle)
  21. end
  22.  
  23. changeset
  24. end)
  25. end
  26.  
  27. defp up(handle) do
  28. Repo.query!("CREATE SCHEMA \"#{handle}\"")
  29. Migrator.run(Repo, migration_dir(), :up, all: true, prefix: handle)
  30. end
  31.  
  32. defp down(handle) do
  33. Migrator.run(Repo, migration_dir(), :down, all: true, prefix: handle)
  34. Repo.query!("DROP SCHEMA \"#{handle}\" CASCADE")
  35. end
  36.  
  37. defp migration_dir do
  38. Application.app_dir(:domain, "priv/repo/organization_migrations")
  39. end
  40. end
  41.  
  42. defmodule App.Plugs do
  43. def prefix_team(conn, opts) do
  44. uri = endpoint_module(conn).struct_url()
  45.  
  46. team_uri = struct(URI, [
  47. host: conn.host,
  48. scheme: Atom.to_string(conn.scheme),
  49. port: conn.port
  50. ])
  51.  
  52. team_uri.host
  53. |> String.trim_trailing(uri.host)
  54. |> String.trim_trailing(".")
  55. |> case do
  56. "" ->
  57. redirect(conn, to: Keyword.fetch!(opts, :redirect_to))
  58. handle ->
  59. team = Repo.get_by!(Team, handle: handle)
  60.  
  61. conn
  62. |> put_private(:team, team)
  63. |> put_private(:prefix, Phoenix.Param.to_param(team))
  64. end
  65. end
  66. end
  67.  
  68. defmodule App.Team do
  69. use App, :schema
  70. @derive {Phoenix.Param, key: :handle}
  71. end
  72.  
  73. defmodule App.SomeController do
  74. import App.Plugs
  75.  
  76. plug :prefix_team, redirect_to: page_url(App.Endpoint, :sign_in)
  77.  
  78. def index(conn, _) do
  79. Repo.all(Something, prefix: conn.private.prefix)
  80. end
  81. end
  82. ```
  83.  
  84. # Cross app eventbus with Registry
  85.  
  86. ```elixir
  87. defmodule App.Notification do
  88. def start_link() do
  89. Registry.start_link(:unique, Notification, partitions: System.schedulers_online)
  90. end
  91.  
  92. def subscribe(topic) do
  93. Registry.register(Notification, topic, nil)
  94. end
  95.  
  96. def notify(topic, message) do
  97. Registry.dispatch(Notification, topic, fn entries ->
  98. for {pid, _} <- entries, do: send(pid, message)
  99. end)
  100. end
  101. end
  102.  
  103. defmodule TeamController do
  104. ...
  105. def create(conn, params) do
  106. ...
  107. {:ok, team} -> Notification.notify(:team, {:created, team})
  108. end
  109. end
  110.  
  111. defmodule Worker.TeamCreator do
  112. use GenServer
  113. ...
  114.  
  115. def init(state) do
  116. Notification.subscribe(:team)
  117. super(state)
  118. end
  119.  
  120. def handle_info({:created, team}, state) do
  121. # do something with the newly created team
  122. end
  123. ```
  124.  
  125. # Repo composition
  126.  
  127. ```elixir
  128. defmodule App.ComposableRepo do
  129. defmacro __using__(opts) do
  130. quote bind_quoted: binding() do
  131. @composable_repo Keyword.fetch!(opts, :composing)
  132. @composable_functions @composable_repo.__info__(:functions)
  133.  
  134. @composable_functions
  135. |> Enum.map(fn
  136. {function, 0} ->
  137. {function, []}
  138. {function, arity} ->
  139. {function, Enum.map(1..arity, &(Macro.var(:"arg_#{&1}", __MODULE__)))}
  140. end)
  141. |> Enum.map(fn {function, arguments} ->
  142. defdelegate unquote(function)(unquote_splicing(arguments)), to: @composable_repo
  143. end)
  144.  
  145. defoverridable @composable_functions
  146. end
  147. end
  148. end
  149.  
  150. defmodule App.ComposedRepo do
  151. use App.ComposableRepo, composing: App.Repo
  152. ...
  153. def delete(schema, opts \\ []) do
  154. {force_delete?, opts} = Keyword.pop(opts, :force, false)
  155.  
  156. if !force_delete? && soft_deletable?(schema) do
  157. schema
  158. |> change(deleted_at: DateTime.utc_now())
  159. |> update()
  160. else
  161. super(schema, opts)
  162. end
  163. end
  164. end
  165. ```
  166.  
  167. # Custom Ecto Type
  168.  
  169. ```elixir
  170. defmodule Domain.Tags do
  171. @moduledoc """
  172. Schema:
  173. defmodule Post do
  174. use Ecto.Schema
  175. schema "posts" do
  176. field :title, :string
  177. field :body, :string
  178. field :tags, Domain.TagType
  179. end
  180. end
  181. Migration:
  182. def change do
  183. create table(:posts) do
  184. add :title, :string
  185. add :body, :string
  186. add :tags, {:array, :string}
  187. end
  188. end
  189. """
  190. @behaviour Ecto.Type
  191.  
  192. @spec type() :: {atom, atom}
  193. def type, do: {:array, :string}
  194.  
  195. @spec cast(list(String.t) | String.t) :: {atom, list(String.t)}
  196. def cast(term) when is_list(term), do: {:ok, term}
  197. def cast(""), do: {:ok, []}
  198. def cast(term) when is_binary(term) do
  199. case get_delimiter(term) do
  200. :error -> :error
  201. delimeter -> {:ok, String.split(term, delimeter, trim: true)}
  202. end
  203. end
  204. def cast(_), do: :error
  205.  
  206. @spec load(list(String.t)) :: {atom, list(String.t)}
  207. def load(term) when is_list(term), do: {:ok, term}
  208.  
  209. @spec dump(list(String.t)) :: {atom, list(String.t)}
  210. def dump(term) when is_list(term), do: {:ok, term}
  211. def dump(_), do: :error
  212.  
  213. @spec get_delimiter(String.t) :: String.t | atom
  214. defp get_delimiter(term) do
  215. cond do
  216. term =~ ~r/^([^,]+)(,\s)?([^,]+)*?$/ -> ", "
  217. term =~ ~r/^([^,]+),?([^,]+)*?$/ -> ","
  218. true -> :error
  219. end
  220. end
  221. end
  222. ```
  223.  
  224. # Asynchronous or maybe even parallel assigns
  225.  
  226. ```elixir
  227. defmodule App.DashboardController do
  228. use App, :controller
  229.  
  230. plug :async_put_stats
  231.  
  232. def index(conn, params) do
  233. render(conn)
  234. end
  235.  
  236. def async_put_stats(conn, _) do
  237. conn
  238. |> async_assign(:weekly_sales, fn ->
  239. Sale
  240. |> Sale.to_stats(:weekly)
  241. |> Repo.all()
  242. end)
  243. |> async_assign(:trending_products, fn ->
  244. Product
  245. |> Product.trending()
  246. |> Product.to_stats(:weekly)
  247. |> Repo.all()
  248. end)
  249. |> await_assign(:weekly_sales)
  250. |> await_assign(:trending_products)
  251. end
  252. end
  253. ```
  254.  
  255. # Why Datastructure > * (macro magic/overbloated function)
  256.  
  257. ```elixir
  258. defmodule Media do
  259. defstruct [
  260. source: %Media.File{},
  261. sink: %Media.File{},
  262. transformations: [],
  263. validations: [],
  264. store_at: nil,
  265. store: Application.get_env(:media, :store, Media.LocalStore),
  266. errors: [],
  267. saved?: false,
  268. valid?: false
  269. ]
  270.  
  271. def new(opts \\ []) do
  272. struct(Media, opts)
  273. end
  274.  
  275. def save(media) do
  276. media
  277. |> apply_validations()
  278. |> case do
  279. %{valid?: true} = media -> {:ok, apply_transformations!(media)}
  280. %{valid?: false} = media -> {:error, media}
  281. end
  282. end
  283. end
  284. ```
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement