Essa é a terceira parte do projeto Rockelivery. Esse projeto faz parte do Bootcamp da Rocketseat, ministrado pelo professor Rafael Camarda.
Caso queira adquirir os cursos da Rocketseat com o meu cupom de desconto Acesse esse link
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]
  },
...
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]
}
Mas quando o usuário não é encontrado, um nil é retornado:

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
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}}
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]
 }}
Se tentarmos usar um ID que não seja no formato UUID, receberemos uma exceção:

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"}
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
🔎 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:
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
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
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}
Testando a nossa rota show
Vamos passar um id com formato inválido para ver a mensagem:
 
 
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
Formato válido, mas o ID não existe no banco:

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:

💀 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
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
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
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
🔴 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:
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
Refatorando
Em lib/rockelivery/users/get.ex:
Faça o mesmo em lib/rockelivery/users/delete.ex:
 
 
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
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:
📝 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
No exemplo da documentação podemos ver que precisaremos de um changeset:
 
 
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
Precisamos alterar também lib/rockelivery/user.ex para podermos passar um schema como parâmetro:

Testando o Update
Buscando  todos os usuários cadastrados com o comando:
iex> Rockelivery.Repo.all Rockelivery.User
Pegamos um ID e vamos tentar atualizar o nome:
- Criando os parâmetros:
iex> params = %{"id" => "482f95a7-b447-42e9-ae67-aef72954c3f0", "name" => "Maiqui Tomé"}
- Executando a função Update:
iex> Rockelivery.Users.Update.call params
- Resultado:
  
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:
Em lib/rockelivery/users/update.ex:
🔄 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
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
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":

Testando a Rota Update
Primeiramente, vamos pesquisar um usuário usando a action show:

Para todos os campos aparecerem certifique-se que você acrescentou eles no Jason.Encoder:

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

Ao pesquisar novamente usando a rota Get:

🔌 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.
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á. 
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:
- 
init/1que inicializa quaisquer argumentos ou opções a serem passadas paracall/2
- 
call/2que 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:
- Primeiramente vai para o scope "/api";
- depois para o pipe_throw :apique redireciona para o blocopipeline :api;
- depois para o plug :accepts, ["json"]dizendo que essa rota aceita o formatojson,
- depois no nosso plug UUIDCheckerque criaremos ainda,
- e só depois para o UsersControlleronde os dados serão moldados na action create, nesse caso.
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
Vamos entender algumas funções usadas no código acima:
- Plug.Conn.put_resp_content_type(conn, content_type)informa que será devolvido uma resposta no formato json. Veja na documentação: https://hexdocs.pm/plug/Plug.Conn.html#put_resp_content_type/3
- Plug.Conn.send_resp(conn, status, body)envia uma resposta com o status e o corpo fornecidos. Veja na documentação: https://hexdocs.pm/plug/Plug.Conn.html#send_resp/3
- Plug.Conn.halt(conn)interrompe o pipeline de Plug, evitando que mais plugs downstream sejam invocados. Veja na documentação: https://hexdocs.pm/plug/Plug.Conn.html#halt/1
Em lib/rockelivery_web/router.ex:
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:

Podemos agora retirar a verificação do UUID nos 3 arquivos onde estava sendo usado:
 
 
Exemplo da refatoração no arquivo de update:

Faça essa refatoração nos outros 2 arquivos.
Agora nosso Plug está tratando os IDs inválidos :)

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




















 
    
Top comments (0)