Files
cobblemon-ui/lib/cobblemon_ui/poke_api.ex
Alex Mickelson 991d284f69
All checks were successful
Build and Deploy / Build & Push Image (push) Successful in 32s
updates
2026-03-25 21:36:51 -06:00

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