Your Web News in One Place

Help Webnuz

Referal links:

Sign up for GreenGeeks web hosting
May 16, 2022 12:21 am GMT

Elixir: Consumindo dados de uma API externa

Neste post vamos aprender a consumir dados de uma API externa. No momento da criao do usurio, vamos realizar uma chamada na API do ViaCep para validar o CEP dele, buscando e gravando as informaes de cidade e UF desse usurio. Vamos aprender tambm como evitar fazer chamadas desnecessrias.

  • Criando o projeto
    • Tabela users
    • Funo para criar um usurio
  • Consumindo dados de uma API externa
    • Instalando o Tesla
    • Instalando o HTTPoison
    • Behaviour para a funo get_cep_info()
    • HTTP Client com Tesla
    • HTTP Client com HTTPoison
    • Validando o CEP na criao do usurio
    • Evitando chamadas desnecessarias para API Externa (apply_action)
    • User-Agent header
  • Concluso

Criando o projeto

$ mix phx.new learning_external_api --app my_app

Link do projeto pronto: https://github.com/maiquitome/learning_external_api

Tabela users

Criando a migration e o schema:

$ mix phx.gen.schema User users first_name last_name email cep city uf

Alterando o schema do user:

Remova city e uf apenas do validate_required.

Image description

Criando o banco de dados e rodando as migrations:

$ mix ecto.setup

Funo para criar um usurio

Precisaremos de uma funo para criar um usurio, pois vamos usar a API do ViaCEP para validar o CEP dele. Vamos adicionar essa validao posteriormente.

Em lib/my_app/users/create.ex:

defmodule MyApp.Users.Create do  alias MyApp.{Repo, User}  @type user_params :: %{          first_name: String.t(),          last_name: integer,          cep: String.t(),          email: String.t()        }  @doc """  Inserts a user into the database.  ## Examples      iex> alias MyApp.{User, Users}      ...>      ...> user_params = %{      ...>  first_name: "Mike",      ...>  last_name: "Wazowski",      ...>  cep: "95270000",      ...>  email: "mike_wazowski@monstros_sa.com"      ...> }      ...>      ...> {:ok, %User{}} = Users.Create.call user_params      ...>      iex> {:error, %Ecto.Changeset{}} = Users.Create.call %{}  """  @spec call(user_params()) :: {:error, Ecto.Changeset.t()} | {:ok, Ecto.Schema.t()}  def call(params) do    %User{}    |> User.changeset(params)    |> Repo.insert()  endend

Consumindo dados de uma API externa

Para conseguir consumir dados de uma API externa precisamos de um HTTP client. Podemos usar o Tesla ou o HTTPoison. A vantagem do Tesla que ele possui middlewares prontos para usarmos. Neste post vamos usar os dois clients para compar-los.

Instalando o Tesla

Para instalar o Tesla, adicione ao mix.exs:

defp deps do  [    {:tesla, "~> 1.4"},    # opcional, mas recomendado    {:hackney, "~> 1.17"}    # esse no precisa pois j vem com o phoenix    {:jason, ">= 1.0.0"}  ]end

Voc pode encontrar a verso mais recente do Tesla em: https://hex.pm/packages/tesla

Instalando as dependncias:

$ mix deps.get

Testando o Tesla:

$ iex -S mix
iex> Tesla.get "https://viacep.com.br/ws/01001000/json/"{:ok, %Tesla.Env{}}iex> Tesla.get ""                                       {:error, {:no_scheme}}iex> Tesla.get "https://exemplo.com"                     {:error, :econnrefused}

Instalando o HTTPoison

Para instalar o HTTPoison, adicione ao mix.exs:

defp deps do  [    {:httpoison, "~> 1.8"}  ]end

Voc pode encontrar a verso mais recente do HTTPoison em: https://hex.pm/packages/httpoison

Instalando as dependncias:

$ mix deps.get

Testando o HTTPoison:

$ iex -S mix
iex> HTTPoison.get "https://viacep.com.br/ws/01001000/json/"{:ok, %HTTPoison.Response{}}iex> HTTPoison.get ""                                   ** (CaseClauseError) no case clause matching: []iex> HTTPoison.get "https://exemplo.com" {:error, %HTTPoison.Error{id: nil, reason: :closed}}

Behaviour para a funo get_cep_info()

Vamos definir um contrato para a funo get_cep_info(). Quando essa funo for implementada ela obrigatoriamente deve receber uma String como parmetro e retornar sempre uma tupla de {:ok, map()} ou {:error, map()}.

Em lib/my_app/via_cep/behaviour.ex:

defmodule MyApp.ViaCep.Behaviour do  @callback get_cep_info(String.t()) :: {:ok, map()} | {:error, map()}end

Se tentarmos retornar algo diferente para esta funo, receberemos um aviso:

Image description

HTTP Client com Tesla

Vamos colocar o nome do arquivo de tesla_client.ex, pois faremos outro chamado httpoison_client.ex e, desta forma, conseguiremos comparar os HTTP clients de um jeito melhor.

Em lib/my_app/via_cep/tesla_client.ex:

defmodule MyApp.ViaCep.TeslaClient do  # Ao invs de Tesla.get(), vc vai usar apenas get()  use Tesla  alias Tesla.Env  @behaviour MyApp.ViaCep.Behaviour  @base_url "https://viacep.com.br/ws/"  # codifica (encode) os parametros para json  # e descodifica (decode) a resposta para json automaticamente.  plug Tesla.Middleware.JSON  def get_cep_info(url \\ @base_url, cep) do    "#{url}#{cep}/json/"    |> get()    |> handle_get()  end  # casos abaixo de sucesso e erro  defp handle_get({:ok, %Env{status: 200, body: %{"erro" => "true"}}}) do    {:error, %{status: :not_found, result: "CEP not found!"}}  end  defp handle_get({:ok, %Env{status: 200, body: body}}) do    {:ok, body}  end  defp handle_get({:ok, %Env{status: 400, body: _body}}) do    {:error, %{status: :bad_request, result: "Invalid CEP!"}}  end  defp handle_get({:error, reason}) do    {:error, %{status: :bad_request, result: reason}}  endend

Vamos fazer o teste:

iex(1)> MyApp.ViaCep.TeslaClient.get_cep_info "95270000"{:ok, %{   "bairro" => "",   "cep" => "95270-000",   "complemento" => "",   "ddd" => "54",   "gia" => "",   "ibge" => "4308201",   "localidade" => "Flores da Cunha",   "logradouro" => "",   "siafi" => "8661",   "uf" => "RS" }}iex(2)> MyApp.ViaCep.TeslaClient.get_cep_info "95270001"{:error, %{result: "CEP not found!", status: :not_found}}iex(3)> MyApp.ViaCep.TeslaClient.get_cep_info ""        {:error, %{result: "Invalid CEP!", status: :bad_request}}

HTTP Client com HTTPoison

No HTTPoison no temos um middleware pronto para transformar o json em map, ento vamos precisar usar o Jason.decode() que j vem instalado no Phoenix.

Em lib/my_app/via_cep/httpoison_client.ex:

defmodule MyApp.ViaCep.HttpoisonClient do  alias HTTPoison.{Error, Response}  @behaviour MyApp.ViaCep.Behaviour  @base_url "https://viacep.com.br/ws/"  def get_cep_info(url \\ @base_url, cep) do    "#{url}#{cep}/json/"    |> HTTPoison.get()    |> json_to_map()    |> handle_get()  end  defp json_to_map({:ok, %Response{body: body} = response}) do    {_ok_or_error, body} = Jason.decode(body)    {:ok, Map.put(response, :body, body)}  end  defp json_to_map({:error, %Error{}} = error), do: error  defp handle_get({:ok, %Response{status_code: 200, body: %{"erro" => "true"}}}) do    {:error, %{status: :not_found, result: "CEP not found!"}}  end  defp handle_get({:ok, %Response{status_code: 200, body: body}}) do    {:ok, body}  end  defp handle_get({:ok, %Response{status_code: 400, body: _body}}) do    {:error, %{status: :bad_request, result: "Invalid CEP!"}}  end  defp handle_get({:error, reason}) do    {:error, %{status: :bad_request, result: reason}}  endend

Vamos fazer o teste:

iex(1)> MyApp.ViaCep.HttpoisonClient.get_cep_info "95270000"{:ok, %{   "bairro" => "",   "cep" => "95270-000",   "complemento" => "",   "ddd" => "54",   "gia" => "",   "ibge" => "4308201",   "localidade" => "Flores da Cunha",   "logradouro" => "",   "siafi" => "8661",   "uf" => "RS" }}iex(2)> MyApp.ViaCep.HttpoisonClient.get_cep_info "95270001"{:error, %{result: "CEP not found!", status: :not_found}}iex(3)> MyApp.ViaCep.HttpoisonClient.get_cep_info ""        {:error, %{result: "Invalid CEP!", status: :bad_request}}

Validando o CEP na criao do usurio

Em lib/my_app/users/create.ex, vamos alterar a funo para passar a validar o CEP do usurio e, tambm, vamos pegar as informaes da cidade e UF. Esta funo apresenta um problema mas, vamos melhorar ela depois.

alias MyApp.ViaCep.HttpoisonClient, as: Client...@spec call(user_params()) :: {:error, Ecto.Changeset.t() | map()} | {:ok, Ecto.Schema.t()}  def call(params) do    cep = Map.get(params, :cep)    with {:ok, %{"localidade" => city, "uf" => uf}} <- Client.get_cep_info(cep),         params <- Map.merge(params, %{city: city, uf: uf}),         changeset <- User.changeset(%User{}, params),         {:ok, %User{}} = user <- Repo.insert(changeset) do      user    end  end

Criando um usurio:

iex(1)> user_params = %{                   ...(1)>     first_name: "Mike",...(1)>     last_name: "Wazowski",...(1)>     cep: "95270000",...(1)>     email: "mike_wazowski@monstros_sa.com"...(1)>    }%{  cep: "95270000",  email: "mike_wazowski@monstros_sa.com",  first_name: "Mike",  last_name: "Wazowski"}iex(2)> MyApp.Users.Create.call user_params       CEP: "95270000"{:ok, %MyApp.User{   __meta__: #Ecto.Schema.Metadata<:loaded, "users">,   cep: "95270000",   city: "Flores da Cunha",   email: "mike_wazowski@monstros_sa.com",   first_name: "Mike",   id: 1,   inserted_at: ~N[2022-05-15 21:38:14],   last_name: "Wazowski",   uf: "RS",   updated_at: ~N[2022-05-15 21:38:14] }}

CEP Invlido

iex(1)> user_params = %{                   ...(1)>     first_name: "Mike",...(1)>     last_name: "Wazowski",...(1)>     cep: "123",...(1)>     email: "mike_wazowski@monstros_sa.com"...(1)>    }%{  cep: "123",  email: "mike_wazowski@monstros_sa.com",  first_name: "Mike",  last_name: "Wazowski"}iex(2)> MyApp.Users.Create.call user_params       CEP: "123"{:error, %{result: "Invalid CEP!", status: :bad_request}}

Perceba que foi mostrada a mensagem Invalid CEP! ao invs de mostrar a validao do changeset, alertando que o CEP devia possuir 8 caracteres, desta forma, teriamos a validao sem precisar fazer uma chamada desnecessria para a API externa.

Evitando chamadas desnecessarias para API Externa (apply_action)

Vamos agora validar todos os dados do changeset antes de fazer a chamada para a API do ViaCep.

Testando a validao do changeset sem usar o Repo.insert():

iex(1)> import Ecto.Changesetiex(2)> user_params = %{...(2)>     first_name: "Mike",...(2)>     last_name: "Wazowski",...(2)>     cep: "123",...(2)>     email: "mike_wazowski@monstros_sa.com"...(2)>    }%{  cep: "123",  email: "mike_wazowski@monstros_sa.com",  first_name: "Mike",  last_name: "Wazowski"}iex(3)> MyApp.User.changeset(%MyApp.User{}, user_params) |> apply_action(:create){:error, #Ecto.Changeset<   action: :create,   changes: %{     cep: "123",     email: "mike_wazowski@monstros_sa.com",     first_name: "Mike",     last_name: "Wazowski"   },   errors: [     cep: {"should be %{count} character(s)",      [count: 8, validation: :length, kind: :is, type: :string]}   ],   data: #MyApp.User<>,   valid?: false >}

Vamos alterar o arquivo lib/my_app/user.ex, adicionando a funo validate_before_insert:

def validate_before_insert(changeset), do: apply_action(changeset, :insert)

Image description

Vamos agora alterar o arquivo lib/my_app/users/create.ex:

@spec call(user_params()) :: {:error, Ecto.Changeset.t() | map()} | {:ok, Ecto.Schema.t()}  def call(params) do    cep = Map.get(params, :cep)    changeset = User.changeset(%User{}, params)    with {:ok, %User{}} <- User.validate_before_insert(changeset),         {:ok, %{"localidade" => city, "uf" => uf}} <- Client.get_cep_info(cep),         params <- Map.merge(params, %{city: city, uf: uf}),         changeset <- User.changeset(%User{}, params),         {:ok, %User{}} = user <- Repo.insert(changeset) do      user    end  end

Agora antes de fazer a chamada para o ViaCep estamos checando todas as validaes do do changeset antes:

Image description

User-Agent header

Algumas API's externas podem solicitar algo a mais para realizar as chamadas, muitas podem pedir um token, no qual voc consegue na documentao, ou no caso da API do github, um User-Agent header:

Image description

Com o Tesla facilmente voc pode usar o plug Tesla.Middleware.Headers:

Image description

No Httpoison:

iex> HTTPoison.get "https://api.github.com/users/maiquitome/repos", [{"User-Agent", "foobar"}]

Leia na documentao do HTTPoison a parte de options.

Ento, fique atento a documentao da API na qual voc est realizando a chamada.

Concluso

Em algum momento voc vai precisar consumir dados de uma API externa; o que nos dias de hoje bem normal. Ler a documentao da API externa o primeiro passo para o sucesso. Em Elixir temos timas ferramentas de HTTP Client, a documentao dessas ferramentas tambm merece ser lida. Precisamos cuidar para no fazer chamadas desnecessrias em API's externas para no comprometer a performance da nossa aplicao. O prximo passo agora saber como realizar testes automatizados nessas chamadas externas; assunto para um prximo post.


Original Link: https://dev.to/maiquitome/elixir-consumindo-dados-de-uma-api-externa-3g78

Share this article:    Share on Facebook
View Full Article

Dev To

An online community for sharing and discovering great ideas, having debates, and making friends

More About this Source Visit Dev To