DEV Community

loading...
Cover image for Projeto Rockelivery: API para Pedidos em um Restaurante com Elixir e Phoenix (Parte 3)

Projeto Rockelivery: API para Pedidos em um Restaurante com Elixir e Phoenix (Parte 3)

Maiqui Tomé
Junior Backend Developer
・13 min read

Essa é a terceira parte do projeto Rockelivery. Esse projeto faz parte do Bootcamp da Rocketseat, ministrado pelo professor Rafael Camarda.

Acompanhe o repositório no GitHub: https://github.com/maiquitome/rockelivery_api

Conteúdo:

📇 Módulo para Leitura de Usuários
🔎 A Rota de Show dos Usuários
💀 A Rota de Deleção de Usuários
🔴 Centralizando as Mensagens de Erro
📝 Módulo para Update de Usuários
🔄 A Rota Update
🔌 Criando um Plug

📇 Módulo para Leitura de Usuários

Vamos agora fazer um módulo para pesquisar os usuários no banco de dados.

Lembrando que podemos pegar todos os usuários do banco de dados usando a função Rockelivery.Repo.all():

iex> Rockelivery.Repo.all(Rockelivery.User)
[debug] QUERY OK source="users" db=22.5ms decode=3.6ms queue=3.2ms idle=705.1ms
SELECT u0."id", u0."address", u0."age", u0."cep", u0."cpf", u0."email", u0."name", u0."password_hash", u0."inserted_at", u0."updated_at" FROM "users" AS u0 []
[
  %Rockelivery.User{
    __meta__: #Ecto.Schema.Metadata<:loaded, "users">,
    address: "Rua...",
    age: 28,
    cep: "12345678",
    cpf: "12345678910",
    email: "maiqui@teste.com",
    id: "482f95a7-b447-42e9-ae67-aef72954c3f0",
    inserted_at: ~N[2021-06-05 14:45:28],
    name: "Maiqui",
    password: nil,
    password_hash: "$argon2id$v=19$m=131072,t=8,p=4$L3mKYnhn5ooDZRsyd7maaA$92xStD9oCV06HtJYnoYhGY4HBJK69bUvb6HpTv+IlPc",
    updated_at: ~N[2021-06-05 14:45:28]
  },
...
Enter fullscreen mode Exit fullscreen mode

Ou buscar um usuário específico passando o ID desse usuário para a função Rockelivery.Repo.get(). Note que quando o usuário é encontrado, um Schema é retornado:

iex> Rockelivery.Repo.get(Rockelivery.User, "482f95a7-b447-42e9-ae67-aef72954c3f0")
[debug] QUERY OK source="users" db=2.9ms queue=2.4ms idle=1418.4ms
SELECT u0."id", u0."address", u0."age", u0."cep", u0."cpf", u0."email", u0."name", u0."password_hash", u0."inserted_at", u0."updated_at" FROM "users" AS u0 WHERE (u0."id" = $1) [<<72, 47, 149, 167, 180, 71, 66, 233, 174, 103, 174, 247, 41, 84, 195, 240>>]
%Rockelivery.User{
  __meta__: #Ecto.Schema.Metadata<:loaded, "users">,
  address: "Rua...",
  age: 28,
  cep: "12345678",
  cpf: "12345678910",
  email: "maiqui@teste.com",
  id: "482f95a7-b447-42e9-ae67-aef72954c3f0",
  inserted_at: ~N[2021-06-05 14:45:28],
  name: "Maiqui",
  password: nil,
  password_hash: "$argon2id$v=19$m=131072,t=8,p=4$L3mKYnhn5ooDZRsyd7maaA$92xStD9oCV06HtJYnoYhGY4HBJK69bUvb6HpTv+IlPc",
  updated_at: ~N[2021-06-05 14:45:28]
}
Enter fullscreen mode Exit fullscreen mode

Mas quando o usuário não é encontrado, um nil é retornado:
image

Crie o arquivo lib/rockelivery/users/get.ex:

defmodule Rockelivery.Users.Get do
  alias Rockelivery.{Repo, User}

  def by_id(id) do
    case Repo.get(User, id) do
      nil -> {:error, %{status: :not_found, result: "User not found"}}
      user_schema -> {:ok, user_schema}
    end
  end
end
Enter fullscreen mode Exit fullscreen mode

Testando com um ID inválido:

iex> Rockelivery.Users.Get.by_id("482f95a7-b447-42e9-ae67-aef72954c3f1")
[debug] QUERY OK source="users" db=4.6ms queue=0.1ms idle=1023.1ms
SELECT u0."id", u0."address", u0."age", u0."cep", u0."cpf", u0."email", u0."name", u0."password_hash", u0."inserted_at", u0."updated_at" FROM "users" AS u0 WHERE (u0."id" = $1) [<<72, 47, 149, 167, 180, 71, 66, 233, 174, 103, 174, 247, 41, 84, 195, 241>>]
{:error, %{result: "User not found", status: :not_found}}
Enter fullscreen mode Exit fullscreen mode

Testando com um ID válido:

iex> Rockelivery.Users.Get.by_id("482f95a7-b447-42e9-ae67-aef72954c3f0")
[debug] QUERY OK source="users" db=2.0ms queue=0.1ms idle=1622.8ms
SELECT u0."id", u0."address", u0."age", u0."cep", u0."cpf", u0."email", u0."name", u0."password_hash", u0."inserted_at", u0."updated_at" FROM "users" AS u0 WHERE (u0."id" = $1) [<<72, 47, 149, 167, 180, 71, 66, 233, 174, 103, 174, 247, 41, 84, 195, 240>>]
{:ok,
 %Rockelivery.User{
   __meta__: #Ecto.Schema.Metadata<:loaded, "users">,
   address: "Rua...",
   age: 28,
   cep: "12345678",
   cpf: "12345678910",
   email: "maiqui@teste.com",
   id: "482f95a7-b447-42e9-ae67-aef72954c3f0",
   inserted_at: ~N[2021-06-05 14:45:28],
   name: "Maiqui",
   password: nil,
   password_hash: "$argon2id$v=19$m=131072,t=8,p=4$L3mKYnhn5ooDZRsyd7maaA$92xStD9oCV06HtJYnoYhGY4HBJK69bUvb6HpTv+IlPc",
   updated_at: ~N[2021-06-05 14:45:28]
 }}
Enter fullscreen mode Exit fullscreen mode

Se tentarmos usar um ID que não seja no formato UUID, receberemos uma exceção:
image

Então precisamos refatorar o nosso código para verificar se um ID é um UUID. Vamos antes só entender a função Ecto.UUID.cast/1:

iex> Ecto.UUID.cast("123456")
:error

iex> Ecto.UUID.cast("482f95a7-b447-42e9-ae67-aef72954c3f0")
{:ok, "482f95a7-b447-42e9-ae67-aef72954c3f0"}
Enter fullscreen mode Exit fullscreen mode

Em lib/rockelivery/users/get.ex:

defmodule Rockelivery.Users.Get do
  alias Ecto.UUID
  alias Rockelivery.{Repo, User}

  def by_id(id) do
    case UUID.cast(id) do
      :error -> {:error, %{status: :bad_request, result: "Invalid Format!"}}
      {:ok, uuid} -> get(uuid)
    end
  end

  defp get(id) do
    case Repo.get(User, id) do
      nil -> {:error, %{status: :not_found, result: "User not found!"}}
      user_schema -> {:ok, user_schema}
    end
  end
end
Enter fullscreen mode Exit fullscreen mode

🔎 A Rota de Show dos Usuários

A partir de agora vamos construir todo o código para que a rota show funcione. Lembrando que a rota já foi criada quando usamos o código resources:

image

image

Fachada para a função by_id

Vamos construir a nossa fachada para a função Rockelivery.Users.Get.by_id/1, já que precisaremos dela na action show do controller RockeliveryWeb.UsersController.

Em lib/rockelivery.ex:

defmodule Rockelivery do
  alias Rockelivery.Users.Create, as: UserCreate

  # adicione
  alias Rockelivery.Users.Get, as: UserGet

  defdelegate create_user(params), to: UserCreate, as: :call

  # adicione 
  defdelegate get_user_by_id(id), to: UserGet, as: :by_id
end
Enter fullscreen mode Exit fullscreen mode

Construindo a action show

Em lib/rockelivery_web/controllers/user_controller.ex adicione o código abaixo:

def show(conn, %{"id" => id}) do
  with {:ok, %User{} = user} <- Rockelivery.get_user_by_id(id) do
    conn
    |> put_status(:ok)
    |> render("show.json", user: user)
  end
end
Enter fullscreen mode Exit fullscreen mode

Ao contrário da action index, invés de params estamos fazendo Pattern Matching colocando %{"id" => id}.

Adicionando "show.json" na view

Em lib/rockelivery_web/views/users_view.ex adicione o código abaixo:

def render("show.json", %{user: %User{} = user}), do: %{user: user}
Enter fullscreen mode Exit fullscreen mode

Testando a nossa rota show

imageimage

Vamos passar um id com formato inválido para ver a mensagem:
image

Ajustando a mensagem

Em lib/rockelivery_web/views/error_view.ex adicione o código abaixo:

def render("error.json", %{result: %Changeset{} = changeset}) do
  %{message: translate_errors(changeset)}
end
# abaixo do código acima
# adicione
def render("error.json", %{result: error_message}) do
  %{message: error_message}
end
Enter fullscreen mode Exit fullscreen mode

Formato do ID inválido:
image

Formato válido, mas o ID não existe no banco:
image

Refatoração

Lembrando que antes do erro ir para a error_view, ele passa no FallbackController e nosso result que recebia só um changeset agora recebe também uma mensagem, então vamos alterar o nosso código. Podemos alterar o nome da variável changeset para result, ou para changeset_or_message que fica mais claro ao ser lido:
image

💀 A Rota de Deleção de Usuários

Vamos começar com o módulo Delete. Ele vai ser parecido com o módulo Get.

Módulo Delete

Crie um arquivo em lib/rockelivery/users/delete.ex, e o conteúdo deve ter essa aparência:

defmodule Rockelivery.Users.Delete do
  alias Ecto.UUID
  alias Rockelivery.{Repo, User}

  def call(id) do
    case UUID.cast(id) do
      :error -> {:error, %{status: :bad_request, result: "Invalid Format!"}}
      {:ok, uuid} -> delete(uuid)
    end
  end

  defp delete(id) do
    case Repo.get(User, id) do
      nil -> {:error, %{status: :not_found, result: "User not found!"}}
      user_schema -> Repo.delete(user_schema)
    end
  end
end
Enter fullscreen mode Exit fullscreen mode

Vamos agora fazer a fachada para este módulo.

Delete Facade

Em lib/rockelivery.ex adicione o código abaixo:

alias Rockelivery.Users.Delete, as: UserDelete

defdelegate delete_user(id), to: UserDelete, as: :call
Enter fullscreen mode Exit fullscreen mode

Delete Action

Em lib/rockelivery_web/controllers/users_controller.ex adicione o código abaixo:

def delete(conn, %{"id" => id}) do
    with {:ok, %User{}} <- Rockelivery.delete_user(id) do
      conn
      |> put_status(:no_content)
      |> text("")
    end
end
Enter fullscreen mode Exit fullscreen mode

Como estamos colocando um status :no_content, não vamos devolver nenhum body, por isso não vamos usar a função render, apenas devolveremos uma string vazia.

Testando a deleção

image

🔴 Centralizando as Mensagens de Erro

Agora existe uma oportunidade para refarorarmos o nosso código. Podemos observar que começamos a repetir as mensagens de erro e os status code:

image

Vamos então centralizar essas menssagens em um arquivo só.

Criando um arquivo para as mensagens de erro

Crie lib/rockelivery/error.ex e o seu conteúdo deve ter essa aparência:

defmodule Rockelivery.Error do
  @keys [:status, :result]

  @enforce_keys @keys

  defstruct @keys

  def build(status, result) do
    %__MODULE__{
      status: status,
      result: result
    }
  end

  def build_user_not_found, do: build(:not_found, "User not found")
  def build_invalid_id_format, do: build(:bad_request, "Invalid id format")
end
Enter fullscreen mode Exit fullscreen mode

Refatorando

Em lib/rockelivery/users/get.ex:

image

Faça o mesmo em lib/rockelivery/users/delete.ex:
image

E, em lib/rockelivery/users/create.ex use apenas a função Error.build/2:

defp handle_insert({:error, changeset}) do
    # ANTES
    # {:error, %{status: :bad_request, result: changeset}}

    # DEPOIS
    {:error, Error.build(:bad_request, changeset)}
end
Enter fullscreen mode Exit fullscreen mode

Vamos alterar também o FallbackController e colocar agora a nossa struct error. Com ela nosso código fica mais declarativo, ficando mais fácil entender o código.

Em lib/rockelivery_web/controllers/fallback_controller.ex:

image

📝 Módulo para Update de Usuários

Primeiramente vamos verificar na documentação como funciona o callback Ecto.Repo.update: https://hexdocs.pm/ecto/Ecto.Repo-callback-update.html

image

No exemplo da documentação podemos ver que precisaremos de um changeset:
image

Vamos criar o arquivo lib/rockelivery/users/update.ex:

defmodule Rockelivery.Users.Update do
  alias Ecto.UUID
  alias Rockelivery.{Error, Repo, User}

  def call(%{"id" => id} = params) do
    case UUID.cast(id) do
      :error -> {:error, Error.build_invalid_id_format()}
      {:ok, _uuid} -> update(params)
    end
  end

  defp update(%{"id" => id} = params) do
    case Repo.get(User, id) do
      nil -> {:error, Error.build_user_not_found()}
      user_schema -> do_update(user_schema, params)
    end
  end

  defp do_update(%User{} = user, %{} = params) do
    user
    |> User.changeset(params)
    |> Repo.update()
  end
end
Enter fullscreen mode Exit fullscreen mode

Precisamos alterar também lib/rockelivery/user.ex para podermos passar um schema como parâmetro:
image

Testando o Update

Buscando todos os usuários cadastrados com o comando:

iex> Rockelivery.Repo.all Rockelivery.User
Enter fullscreen mode Exit fullscreen mode

image

Pegamos um ID e vamos tentar atualizar o nome:

  • Criando os parâmetros:
iex> params = %{"id" => "482f95a7-b447-42e9-ae67-aef72954c3f0", "name" => "Maiqui Tomé"}
Enter fullscreen mode Exit fullscreen mode
  • Executando a função Update:
iex> Rockelivery.Users.Update.call params
Enter fullscreen mode Exit fullscreen mode
  • Resultado: image

Por que não funcionou?
image

Quando passamos o schema %User{} com os dados puxados do banco para a função User.changeset/2 estamos passando com o campo password nulo, pois o banco de dados não retorna os dados dele já que esse campo é virtual. A função User.changeset/2 faz a validação e diz que esse campo é obrigatório informar. Precisamos refatorar novamente o nosso código.

Em lib/rockelivery/user.ex:

image

Em lib/rockelivery/users/update.ex:

image

Resultado:
image

🔄 A Rota Update

Após o módulo de update criado, vamos partir para a criação de todo o código para a rota update funcionar.

Update Facade

Em lib/rockelivery.ex adicione o código abaixo:

alias Rockelivery.Users.Update, as: UserUpdate

defdelegate update_user(params), to: UserUpdate, as: :call
Enter fullscreen mode Exit fullscreen mode

Update Action

Em lib/rockelivery_web/controllers/user_controller.ex adicione o código abaixo:

def show(conn, %{"id" => id}) do
  with {:ok, %User{} = user} <- Rockelivery.get_user_by_id(id) do
      conn
      |> put_status(:ok)
      # alterando "show.json" para "user.json"
      |> render("user.json", user: user)
  end
end

# adicione
def update(conn, %{} = params) do
  with {:ok, %User{} = user} <- Rockelivery.update_user(params) do
      conn
      |> put_status(:ok)
      |> render("user.json", user: user)
  end
end
Enter fullscreen mode Exit fullscreen mode

Como a action show é praticamente igual a action update vamos usar o template "user.json" para os dois. Vamos alterar agora o código da view.

Em lib/rockelivery_web/views/users_view.ex altere "show.json" para "user.json":
image

Testando a Rota Update

Primeiramente, vamos pesquisar um usuário usando a action show:
image
Para todos os campos aparecerem certifique-se que você acrescentou eles no Jason.Encoder:
image

Agora vamos editar esse usuário usando a rota nova de update. Vou apenas trocar o nome de "Maiqui Tomé" para "Maiqui Pirolli Tomé":
image

Ao pesquisar novamente usando a rota Get:
image

🔌 Criando um Plug

Primeiramente, antes de entendermos melhor o que é um Plug, vamos entender porque vamos precisar de um. Podemos perceber na imagem abaixo, que em vários arquivos estamos repetindo o mesmo trecho de código para verificar se um ID é um UUID.

image

Para melhorar o nosso código precisamos construir um Plug. Vamos antes enteder ele melhor.

Plug é uma convenção para manipular a struct de conexão %Plug.Conn{}. Mesmo não fazendo parte do núcleo de Elixir, Plug é um projeto oficial de Elixir. Você pode conferir a documentação oficial do Plug aqui: https://hexdocs.pm/plug/readme.html

A definição da documentação do Phoenix diz que o Plug é uma especificação para módulos combináveis ​​entre aplicativos da web. É também uma camada de abstração para adaptadores de conexão de diferentes servidores da web. A ideia básica do Plug é unificar o conceito de uma "conexão" na qual operamos. Isso difere de outras camadas de middleware HTTP, como Rack do Ruby on Rails, onde a solicitação e a resposta são separadas na pilha de middleware. Essa definição e mais detalhes você encontra aqui: https://hexdocs.pm/phoenix/plug.html

Function Plugs

Para atuar como um Plug, uma função precisa aceitar uma estrutura de conexão (%Plug.Conn{}) e opções. Ela também precisa retornar uma estrutura de conexão. Qualquer função que atenda a esses critérios servirá.

image

Veja mais detalhes aqui: https://hexdocs.pm/phoenix/plug.html#function-plugs

Module Plugs

Os Module Plugs são outro tipo de Plug que nos permite definir uma transformação de conexão em um módulo. O módulo só precisa implementar duas funções:

  1. init/1 que inicializa quaisquer argumentos ou opções a serem passadas para call/2
  2. call/2 que realiza a transformação da conexão. call/2 é apenas uma Function Plug que vimos anteriormente.

Entendendo o fluxo

Um Plug executa antes da request chegar no controller. Vamos tentar entender melhor esse fluxo no arquivo de rotas.

Vamos pensar que um usuário fez um post. O navegador acessou a barra de endereço http://localhost:4000/api/users, enviou um HTTP Request para a nossa aplicação que estava executando nesse endereço. O HTTP Request foi construído com um par do verbo POST e o caminho /Users, que foi mapeado para um par do controller UsersController e a action create.

Antes de chegar na action create do UsersController:

  1. Primeiramente vai para o scope "/api";
  2. depois para o pipe_throw :api que redireciona para o bloco pipeline :api;
  3. depois para o plug :accepts, ["json"] dizendo que essa rota aceita o formato json,
  4. depois no nosso plug UUIDChecker que criaremos ainda,
  5. e só depois para o UsersController onde os dados serão moldados na action create, nesse caso.

image

O nosso primeiro plug

Crie um arquivo no diretório lib/rockelivery_web/plugs/uuid_checker.ex. O conteúdo deste arquivo deve ter a seguinte aparência:

defmodule RockeliveryWeb.Plugs.UUIDChecker do
  import Plug.Conn

  alias Ecto.UUID
  alias Plug.Conn

  # init/1 inicializa quaisquer argumentos ou opções a serem passadas para call/2
  def init(options), do: options

  # call/2 é uma Function Plug que realiza a transformação da conexão.
  # Toda Function Plug precisa aceitar uma estrutura de conexão %Plug.Conn{} e opções.
  def call(%Conn{params: %{"id" => id}} = conn, _opts) do
    case UUID.cast(id) do
      :error -> render_error(conn)
      # se o ID for um UUID a conexão (conn) pode ir para o controller:
      {:ok, _uuid} -> conn
    end
  end

  # Caso não tenha id nos paramêtros, como na rota create,
  # continua enviando a conexão no fluxo normal
  def call(conn, _opts), do: conn

  defp render_error(conn) do
    body = Jason.encode!(%{message: "Invalid UUID"})

    conn
    |> put_resp_content_type("application/json")
    |> send_resp(:bad_request, body)
    |> halt()
  end
end
Enter fullscreen mode Exit fullscreen mode

Vamos entender algumas funções usadas no código acima:

Em lib/rockelivery_web/router.ex:

image

Refatorando

Em lib/rockelivery/error.view podemos retirar a mensagem de erro do UUID, já que isso está sendo tratado agora no nosso Plug que criamos:
image

Podemos agora retirar a verificação do UUID nos 3 arquivos onde estava sendo usado:
image

Exemplo da refatoração no arquivo de update:
image
Faça essa refatoração nos outros 2 arquivos.

Agora nosso Plug está tratando os IDs inválidos :)
image

E assim terminamos a terceira parte da nossa aplicação :)

Discussion (0)