sb logoToday I Learned

5 posts by herminiotorres @herminiotorres

How to take leverage from on_mount to reduce code

Phoenix LiveView has implemented some cool features, and one of them is the on_mount/1 callback function.

This callback function will run before the mount/3 function in your LiveView.

There are two ways to set the on_mount callback function:

  1. In router using live_session/3.
  2. In your LiveView modules with on_mount macro.

If you need to do something before the mount/3 in all your LiveViews, live_session/3 is likely the best fit. However, if it isonly for a few them, the on_mount macro will be better for your needs.

on_mount is helpful for reducing repetitive code in your LiveViews. Let’s look at an example.

defmodule ExampleWeb.UserHook do
  import Phoenix.LiveView

  def on_mount(:default, _params, %{"current_user" => current_user} = _session, socket) do
    if authorized?(current_user) do
      {:cont, socket}
    else
      {:halt, socket}
    end
  end
  
  def on_mount(:admin, _params, %{"current_user" => current_user} = _session, socket) do
    if admin?(current_user) do
      {:cont, socket}
    else
      {:halt, socket}
    end
  end
end

The live_session/3 on Router:

live_session :default, on_mount: ExampleWeb.UserHook do
  scope "/", ExampleWeb do
    pipe_through [:browser, :auth]

    live "/", HomeLive, :page
  end
end

The on_mount macro:

defmodule ExampleWeb.HomeLive do
  use ExampleWeb, :live_view
  
  on_mount {ExampleWeb.UserHook, :admin}

  def mount(_params, _session, socket) do
    # ...
  end
end

How to import CSV file to the Database

Today I learned how to import CSV data file to the database, and populate the table.

Imagine you have this migration in your application with the following columns:

create table(:users) do
  add(:first_name, :string, null: false)
  add(:last_name, :string, null: false)
  add(:username, :string, null: false)
  add(:email, :string, null: false)
end

And this would be the CSV file:

First Name,Last Name,Username,Email
John,Doe,john_doe,john@doe.com
Jane,Doe,jane_doe,jane@doe.com

And how can I import my CSV file to the users’ table on the database?

~$ psql -U user -d database <<USERS
COPY users(first_name, last_name, username, email) FROM '/path/to/users.csv' DELIMITER ',' CSV HEADER;
USERS

After finishing, you will receive and output with COPY 2 the quantity of copies into your table.

Idempotence in Distributed Systems

Sooner or later, you will come across the term “idempotence” in the context of distributed systems. What is the relationship between these terms?

Let’s consider writing an REST API with a POST request. When you try to create a resource and call it multiple times, the system should only create this resource once, or update it, for a given unique entity.

A more specific example of this in a distributed system could be a payment system. A payment operation will be considered idempotent if we attempt to apply the same charge or payment multiple times, but it only gets processed once.

Creating idempotent operations in a distributed system can be challenging, especially if implemented in the application layer. If possible, you can push this responsibility to your database and ensure idempotence with features like unique indices.

This is what I’ve been learning and I’m excited to learn more!

Using the Keyword module for options

You should consider using Keyword.fetch!/2 and Keyword.get/3 for options to APIs.

Without options

defmodule MyApp do
  def config(name, author \\ "Herminio Torres", description \\ "Description") do
    %{
      name: name,
      author: author,
      description: description
    }
  end
end
iex> MyApp.config
config/1    config/2    config/3
iex> MyApp.config("my_app")
%{
  author: "Herminio Torres",
  description: "Description",
  name: "my_app"
}
iex> MyApp.config("my_app", "Change")
%{
  author: "Change",
  description: "Description",
  name: "my_app"
}
  • Creates a config function with many arities
  • You are forced to pass all paramaters when you intend to change just the last default argument.

With Options

defmodule MyApp do
  def config(opts) do
    name = Keyword.fetch!(opts, :name)
    author = Keyword.get(opts, :author, "Herminio Torres")
    description = Keyword.get(opts, :description, "Description")
    
    %{
      name: name,
      author: author,
      description: description
    }
  end
end
iex> MyApp.config([])
** (KeyError) key :name not found in: []
    (elixir 1.12.3) lib/keyword.ex:420: Keyword.fetch!/2
    iex:3: MyApp.config/1
iex> MyApp.config([name: "my_app"])
%{
  author: "Herminio Torres",
  description: "Description",
  name: "my_app"
}
iex> MyApp.config([name: "my_app", description: "Change"])
%{
  author: "Herminio Torres",
  description: "Change",
  name: "my_app"
}
  • The raised error leads you to which options are required
  • Keyword lists make the arguments named
  • Only one function arity is exposed

Awesome!

Taming data with Ecto.Enum and Ecto.Type

A coworker and I discussed about taking advantage of Ecto.Enum and Ecto.Type instead of having one more dependency.

The schema:

defmodule Blog.Category do
  use Blog.Schema

  schema "categories" do
    field(:name, Ecto.Enum, [:til, :elixir, :ecto])
  end
end

Divide & Conquer with Reflections and Ecto.Type.load/3:

iex> type = Blog.Category.__schema__(:type, :name)
{:parameterized, Ecto.Enum,
 %{
   mappings: [til: "til", elixir: "elixir", ecto: "ecto"],
   on_cast: %{"til" => :til, "elixir" => :elixir, "ecto" => :ecto},
   on_dump: %{til: "til", elixir: "elixir", ecto: "ecto"},
   on_load: %{"til" => :til, "elixir" => :elixir, "ecto" => :ecto},
   type: :string
 }}
iex> Ecto.Type.load(type, "unknown")
:error
iex> Ecto.Type.load(type, "ecto")
{:ok, :ecto}
iex> Ecto.Type.load(type, :ecto)
:error

In the meantime:

iex> Ecto.Enum.values(Blog.Category, :name)
[:til, :elixir, :ecto]
iex> Ecto.Enum.dump_values(Blog.Category, :name)
["til", "elixir", "ecto"]
iex> Ecto.Enum.mappings(Blog.Category, :name)
[til: "til", elixir: "elixir", ecto: "ecto"]

Also, now we have the same API ecto_enum:

iex> valid? = fn list, value -> Enum.any?(list, fn item -> item == value end) end
#Function<43.40011524/2 in :erl_eval.expr/5>
iex> categories = Ecto.Enum.dump_values(Blog.Category, :name)
["til", "elixir", "ecto"]
iex> category = "unknown"
"unknown"
iex> if valid?.(categories, category), do: {:ok, String.to_existing_atom(category)}, else: :error
:error
iex> category = "ecto"
"ecto"
iex> if valid?.(categories, category), do: {:ok, String.to_existing_atom(category)}, else: :error
{:ok, :ecto}

Cool!