Add Go-based PokeAPI fetch tool
Replaces the Python fetch_pokeapi.py script with a Go tool that crawls a local PokeAPI instance and writes seed JSON files. Supports caching and special encounter definitions via JSON config. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
256
tools/fetch-pokeapi/main.go
Normal file
256
tools/fetch-pokeapi/main.go
Normal file
@@ -0,0 +1,256 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"flag"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
// Config structs for version_groups.json
|
||||
type VersionGroupInfo struct {
|
||||
Versions []string `json:"versions"`
|
||||
Generation int `json:"generation"`
|
||||
Region string `json:"region"`
|
||||
RegionID int `json:"region_id"`
|
||||
ExtraRegions []int `json:"extra_regions"`
|
||||
Games map[string]GameInfo `json:"games"`
|
||||
}
|
||||
|
||||
type GameInfo struct {
|
||||
Name string `json:"name"`
|
||||
Slug string `json:"slug"`
|
||||
ReleaseYear int `json:"release_year"`
|
||||
Color *string `json:"color"`
|
||||
}
|
||||
|
||||
// Config structs for route_order.json
|
||||
type RouteOrderFile struct {
|
||||
Routes map[string][]string `json:"routes"`
|
||||
Aliases map[string]string `json:"aliases"`
|
||||
}
|
||||
|
||||
// Config structs for special_encounters.json
|
||||
type SpecialEncountersFile struct {
|
||||
Encounters map[string]map[string][]EncounterOutput `json:"encounters"`
|
||||
Aliases map[string]string `json:"aliases"`
|
||||
}
|
||||
|
||||
func getSpecialEncounters(se *SpecialEncountersFile, vgKey string) map[string][]EncounterOutput {
|
||||
if se == nil {
|
||||
return nil
|
||||
}
|
||||
if data, ok := se.Encounters[vgKey]; ok {
|
||||
return data
|
||||
}
|
||||
if alias, ok := se.Aliases[vgKey]; ok {
|
||||
if data, ok := se.Encounters[alias]; ok {
|
||||
return data
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func main() {
|
||||
clearCache := flag.Bool("clear-cache", false, "Delete cached API responses before fetching")
|
||||
flag.Parse()
|
||||
|
||||
pokeapiURL := os.Getenv("POKEAPI_URL")
|
||||
if pokeapiURL == "" {
|
||||
pokeapiURL = "http://localhost:8000/api/v2"
|
||||
}
|
||||
|
||||
// Resolve paths relative to this tool's location or use the standard layout
|
||||
seedsDir := findSeedsDir()
|
||||
dataDir := filepath.Join(seedsDir, "data")
|
||||
cacheDir := filepath.Join(seedsDir, ".pokeapi_cache")
|
||||
|
||||
client := NewClient(pokeapiURL, cacheDir, 50)
|
||||
|
||||
if *clearCache {
|
||||
if err := client.ClearCache(); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Warning: could not clear cache: %v\n", err)
|
||||
} else {
|
||||
fmt.Println("Cleared API cache.")
|
||||
}
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
// Connectivity check
|
||||
fmt.Printf("Connecting to PokeAPI at %s...\n", pokeapiURL)
|
||||
if _, err := client.Get(ctx, "pokemon-species/1"); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error: Cannot connect to PokeAPI at %s\n %v\nStart the local PokeAPI server or set POKEAPI_URL.\n", pokeapiURL, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// Load configs
|
||||
versionGroups, err := loadJSON[map[string]VersionGroupInfo](filepath.Join(seedsDir, "version_groups.json"))
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error loading version_groups.json: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
routeOrder, err := loadRouteOrder(filepath.Join(seedsDir, "route_order.json"))
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error loading route_order.json: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
specialEnc, err := loadJSON[SpecialEncountersFile](filepath.Join(seedsDir, "special_encounters.json"))
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Warning: could not load special_encounters.json: %v\n", err)
|
||||
// Continue without special encounters
|
||||
}
|
||||
|
||||
if err := os.MkdirAll(dataDir, 0o755); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error creating data dir: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// Build games.json
|
||||
var games []GameOutput
|
||||
for _, vgInfo := range *versionGroups {
|
||||
for _, gameInfo := range vgInfo.Games {
|
||||
games = append(games, GameOutput{
|
||||
Name: gameInfo.Name,
|
||||
Slug: gameInfo.Slug,
|
||||
Generation: vgInfo.Generation,
|
||||
Region: vgInfo.Region,
|
||||
ReleaseYear: gameInfo.ReleaseYear,
|
||||
Color: gameInfo.Color,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
writeJSON(filepath.Join(dataDir, "games.json"), games)
|
||||
fmt.Printf("Wrote %d games to games.json\n", len(games))
|
||||
|
||||
// Process each version
|
||||
pokeIDCollector := NewPokeIDCollector()
|
||||
|
||||
for vgKey, vgInfo := range *versionGroups {
|
||||
for _, verName := range vgInfo.Versions {
|
||||
routes, err := processVersion(ctx, client, verName, vgInfo, vgKey, routeOrder, specialEnc, pokeIDCollector)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error processing %s: %v\n", verName, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
writeJSON(filepath.Join(dataDir, verName+".json"), routes)
|
||||
}
|
||||
}
|
||||
|
||||
// Fetch all species data (reused for pokemon discovery + evolutions)
|
||||
speciesData, err := fetchAllSpecies(ctx, client)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error fetching species: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// Fetch all Pokemon (base + all forms)
|
||||
pokemonList, err := fetchAllPokemon(ctx, client, speciesData, pokeIDCollector.IDs())
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error fetching pokemon: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
writeJSON(filepath.Join(dataDir, "pokemon.json"), pokemonList)
|
||||
fmt.Printf("\nWrote %d Pokemon to pokemon.json\n", len(pokemonList))
|
||||
|
||||
// Build set of all seeded PokeAPI IDs for evolution filtering
|
||||
allSeededDex := make(map[int]bool)
|
||||
for _, p := range pokemonList {
|
||||
allSeededDex[p.PokeAPIID] = true
|
||||
}
|
||||
|
||||
// Fetch evolution chains
|
||||
evolutions, err := fetchEvolutionData(ctx, client, speciesData, allSeededDex)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error fetching evolutions: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
evolutions, err = applyEvolutionOverrides(evolutions, filepath.Join(dataDir, "evolution_overrides.json"))
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error applying evolution overrides: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
writeJSON(filepath.Join(dataDir, "evolutions.json"), evolutions)
|
||||
fmt.Printf("\nWrote %d evolution pairs to evolutions.json\n", len(evolutions))
|
||||
|
||||
fmt.Println("\nDone! JSON files written to seeds/data/")
|
||||
fmt.Println("Review route ordering and curate as needed.")
|
||||
}
|
||||
|
||||
// findSeedsDir locates the backend/src/app/seeds directory.
|
||||
func findSeedsDir() string {
|
||||
// Try relative to CWD (from repo root)
|
||||
candidates := []string{
|
||||
"backend/src/app/seeds",
|
||||
"../../backend/src/app/seeds", // from tools/fetch-pokeapi/
|
||||
}
|
||||
for _, c := range candidates {
|
||||
if _, err := os.Stat(filepath.Join(c, "version_groups.json")); err == nil {
|
||||
abs, _ := filepath.Abs(c)
|
||||
return abs
|
||||
}
|
||||
}
|
||||
// Fallback: try to find from executable location
|
||||
exe, _ := os.Executable()
|
||||
exeDir := filepath.Dir(exe)
|
||||
rel := filepath.Join(exeDir, "../../backend/src/app/seeds")
|
||||
if _, err := os.Stat(filepath.Join(rel, "version_groups.json")); err == nil {
|
||||
abs, _ := filepath.Abs(rel)
|
||||
return abs
|
||||
}
|
||||
// Default
|
||||
abs, _ := filepath.Abs("backend/src/app/seeds")
|
||||
return abs
|
||||
}
|
||||
|
||||
func loadJSON[T any](path string) (*T, error) {
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var result T
|
||||
if err := json.Unmarshal(data, &result); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
func loadRouteOrder(path string) (map[string][]string, error) {
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var rof RouteOrderFile
|
||||
if err := json.Unmarshal(data, &rof); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
routes := make(map[string][]string)
|
||||
for k, v := range rof.Routes {
|
||||
routes[k] = v
|
||||
}
|
||||
for alias, target := range rof.Aliases {
|
||||
routes[alias] = routes[target]
|
||||
}
|
||||
return routes, nil
|
||||
}
|
||||
|
||||
func writeJSON(path string, data interface{}) {
|
||||
content, err := json.MarshalIndent(data, "", " ")
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error marshaling JSON for %s: %v\n", path, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
content = append(content, '\n')
|
||||
if err := os.WriteFile(path, content, 0o644); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error writing %s: %v\n", path, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
fmt.Printf(" -> %s\n", path)
|
||||
}
|
||||
Reference in New Issue
Block a user