show evolutions as well
All checks were successful
Build and Deploy / Build & Push Image (push) Successful in 33s
All checks were successful
Build and Deploy / Build & Push Image (push) Successful in 33s
This commit is contained in:
141
lib/cobblemon_ui/evolution_api.ex
Normal file
141
lib/cobblemon_ui/evolution_api.ex
Normal 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
|
||||
Reference in New Issue
Block a user