All checks were successful
Build and Deploy / Build & Push Image (push) Successful in 32s
136 lines
3.8 KiB
Elixir
136 lines
3.8 KiB
Elixir
defmodule CobblemonUi.PokeApi do
|
|
@moduledoc """
|
|
Fetches and caches Pokémon type data from PokeAPI.
|
|
|
|
Uses an ETS table for fast lookups. Data is fetched once per species
|
|
and cached for the lifetime of the process.
|
|
"""
|
|
|
|
use GenServer
|
|
require Logger
|
|
|
|
@table :poke_api_types_cache
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Client API
|
|
# ---------------------------------------------------------------------------
|
|
|
|
def start_link(_opts) do
|
|
GenServer.start_link(__MODULE__, [], name: __MODULE__)
|
|
end
|
|
|
|
@doc """
|
|
Returns a list of type names (lowercase strings) for the given species.
|
|
|
|
Example: `get_types("bulbasaur")` → `["grass", "poison"]`
|
|
"""
|
|
@spec get_types(String.t()) :: [String.t()]
|
|
def get_types(species) when is_binary(species) do
|
|
key = String.downcase(species)
|
|
|
|
case :ets.lookup(@table, key) do
|
|
[{^key, types}] ->
|
|
types
|
|
|
|
[] ->
|
|
GenServer.call(__MODULE__, {:fetch_types, key}, 15_000)
|
|
end
|
|
end
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Server callbacks
|
|
# ---------------------------------------------------------------------------
|
|
|
|
@impl true
|
|
def init(_opts) do
|
|
:ets.new(@table, [:named_table, :set, :public, read_concurrency: true])
|
|
{:ok, %{}}
|
|
end
|
|
|
|
@impl true
|
|
def handle_call({:fetch_types, species}, _from, state) do
|
|
result =
|
|
case :ets.lookup(@table, species) do
|
|
[{^species, types}] ->
|
|
types
|
|
|
|
[] ->
|
|
types = fetch_types(species)
|
|
:ets.insert(@table, {species, types})
|
|
types
|
|
end
|
|
|
|
{:reply, result, state}
|
|
end
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Private helpers
|
|
# ---------------------------------------------------------------------------
|
|
|
|
defp fetch_types(species) do
|
|
api_name = CobblemonUi.PokeApiNames.normalize(species)
|
|
|
|
case fetch_types_direct(api_name) do
|
|
{:ok, types} ->
|
|
types
|
|
|
|
:not_found ->
|
|
# The /pokemon/ endpoint requires the specific form name (e.g.
|
|
# "aegislash-shield"). Fall back through the /pokemon-species/
|
|
# endpoint to resolve the default variety.
|
|
fetch_types_via_species(api_name)
|
|
end
|
|
end
|
|
|
|
defp fetch_types_direct(api_name) do
|
|
url = "https://pokeapi.co/api/v2/pokemon/#{api_name}"
|
|
|
|
case Req.get(url) do
|
|
{:ok, %Req.Response{status: 200, body: %{"types" => types}}} ->
|
|
parsed =
|
|
types
|
|
|> Enum.sort_by(fn t -> t["slot"] end)
|
|
|> Enum.map(fn t -> get_in(t, ["type", "name"]) end)
|
|
|> Enum.reject(&is_nil/1)
|
|
|
|
{:ok, parsed}
|
|
|
|
{:ok, %Req.Response{status: 404}} ->
|
|
:not_found
|
|
|
|
{:ok, %Req.Response{status: status}} ->
|
|
Logger.warning("[PokeApi] Type lookup failed for #{api_name}: HTTP #{status}")
|
|
{:ok, []}
|
|
|
|
{:error, reason} ->
|
|
Logger.warning("[PokeApi] Type lookup failed for #{api_name}: #{inspect(reason)}")
|
|
{:ok, []}
|
|
end
|
|
end
|
|
|
|
defp fetch_types_via_species(api_name) do
|
|
url = "https://pokeapi.co/api/v2/pokemon-species/#{api_name}"
|
|
|
|
with {:ok, %Req.Response{status: 200, body: body}} <- Req.get(url),
|
|
%{"varieties" => varieties} <- body,
|
|
%{"pokemon" => %{"url" => pokemon_url}} <-
|
|
Enum.find(varieties, fn v -> v["is_default"] end) do
|
|
case Req.get(pokemon_url) do
|
|
{:ok, %Req.Response{status: 200, body: %{"types" => types}}} ->
|
|
types
|
|
|> Enum.sort_by(fn t -> t["slot"] end)
|
|
|> Enum.map(fn t -> get_in(t, ["type", "name"]) end)
|
|
|> Enum.reject(&is_nil/1)
|
|
|
|
_ ->
|
|
Logger.warning("[PokeApi] Fallback type lookup failed for #{api_name}")
|
|
[]
|
|
end
|
|
else
|
|
_ ->
|
|
Logger.warning("[PokeApi] Species fallback failed for #{api_name}")
|
|
[]
|
|
end
|
|
end
|
|
end
|