show evolutions as well
All checks were successful
Build and Deploy / Build & Push Image (push) Successful in 33s

This commit is contained in:
2026-03-18 20:28:06 -06:00
parent a8e3c9b1cc
commit d48f23b022
7 changed files with 4431 additions and 106 deletions

View File

@@ -0,0 +1,141 @@
defmodule CobblemonUi.EvolutionApi do
@moduledoc """
Fetches and caches Pokémon evolution data from PokeAPI.
Provides `get_evolutions/1` which returns the list of species that
a given Pokémon can evolve into (direct next-stage evolutions).
Results are cached in an ETS table to avoid repeated API calls.
"""
use GenServer
require Logger
@table :evolution_cache
# ---------------------------------------------------------------------------
# Client API
# ---------------------------------------------------------------------------
def start_link(_opts) do
GenServer.start_link(__MODULE__, [], name: __MODULE__)
end
@doc """
Returns a list of species names (lowercase strings) that the given species
evolves into. Returns an empty list if there are no evolutions or the lookup
fails.
Results are cached after the first successful fetch.
"""
@spec get_evolutions(String.t()) :: [String.t()]
def get_evolutions(species) when is_binary(species) do
key = String.downcase(species)
case :ets.lookup(@table, key) do
[{^key, evolutions}] ->
evolutions
[] ->
GenServer.call(__MODULE__, {:fetch, 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, species}, _from, state) do
# Double-check cache (another caller may have populated it)
result =
case :ets.lookup(@table, species) do
[{^species, evolutions}] ->
evolutions
[] ->
evolutions = fetch_evolutions(species)
:ets.insert(@table, {species, evolutions})
evolutions
end
{:reply, result, state}
end
# ---------------------------------------------------------------------------
# Private helpers
# ---------------------------------------------------------------------------
defp fetch_evolutions(species) do
with {:ok, chain_url} <- fetch_species_chain_url(species),
{:ok, chain} <- fetch_chain(chain_url) do
find_next_evolutions(chain, species)
else
_ -> []
end
end
defp fetch_species_chain_url(species) do
url = "https://pokeapi.co/api/v2/pokemon-species/#{species}"
case Req.get(url) do
{:ok, %Req.Response{status: 200, body: %{"evolution_chain" => %{"url" => url}}}} ->
{:ok, url}
{:ok, %Req.Response{status: status}} ->
Logger.warning("[EvolutionApi] Species lookup failed for #{species}: HTTP #{status}")
{:error, :not_found}
{:error, reason} ->
Logger.warning("[EvolutionApi] Species lookup failed for #{species}: #{inspect(reason)}")
{:error, reason}
end
end
defp fetch_chain(url) do
case Req.get(url) do
{:ok, %Req.Response{status: 200, body: %{"chain" => chain}}} ->
{:ok, chain}
{:ok, %Req.Response{status: status}} ->
Logger.warning("[EvolutionApi] Chain fetch failed: HTTP #{status}")
{:error, :not_found}
{:error, reason} ->
Logger.warning("[EvolutionApi] Chain fetch failed: #{inspect(reason)}")
{:error, reason}
end
end
@doc false
# Walks the evolution chain tree and returns the species names of
# the direct next-stage evolutions for the given species.
def find_next_evolutions(chain, target_species) do
target = String.downcase(target_species)
do_find(chain, target)
end
defp do_find(%{"species" => %{"name" => name}, "evolves_to" => evolves_to}, target) do
if String.downcase(name) == target do
# Found the target — return names of its direct evolutions
Enum.map(evolves_to, fn evo ->
get_in(evo, ["species", "name"]) |> String.downcase()
end)
else
# Recurse into each branch
Enum.find_value(evolves_to, [], fn evo ->
case do_find(evo, target) do
[] -> nil
result -> result
end
end)
end
end
defp do_find(_, _target), do: []
end