// Command open5e-import vendors Open5e SRD data (CC-BY-4.0) into the repo and // regenerates the Go data files the plugin compiles against. // // It is a pull-once / use-many tool — the generated files are committed, so // the running bot has no runtime dependency on the Open5e API. // // open5e-import fetch spells # vendor data/open5e/spells.json from the API // open5e-import gen spells # classify → internal/plugin/dnd_spells_srd_data.go // // `bestiary` and `equipment` subcommands are planned; the dispatch below is // structured to grow into them without reshuffling. package main import ( "encoding/json" "fmt" "io" "net/http" "os" "path/filepath" "time" ) const ( spellsAPIBase = "https://api.open5e.com/v1/spells/?document__slug=wotc-srd&limit=50" spellsJSON = "data/open5e/spells.json" spellsGenGo = "internal/plugin/dnd_spells_srd_data.go" ) func main() { if len(os.Args) < 3 { usage() } verb, noun := os.Args[1], os.Args[2] switch verb { case "fetch": switch noun { case "spells": must(fetchSpells()) default: usage() } case "gen": switch noun { case "spells": must(genSpells()) default: usage() } default: usage() } } func usage() { fmt.Fprintln(os.Stderr, "usage: open5e-import (fetch|gen) (spells)") os.Exit(2) } func must(err error) { if err != nil { fmt.Fprintln(os.Stderr, "error:", err) os.Exit(1) } } // open5eSpell is the subset of the Open5e v1 spell schema we consume. Unknown // fields are ignored; the vendored JSON keeps the full payload. type open5eSpell struct { Slug string `json:"slug"` Name string `json:"name"` Desc string `json:"desc"` HigherLevel string `json:"higher_level"` Range string `json:"range"` Material string `json:"material"` CanBeCastAsRitual bool `json:"can_be_cast_as_ritual"` Duration string `json:"duration"` RequiresConcentration bool `json:"requires_concentration"` CastingTime string `json:"casting_time"` LevelInt int `json:"level_int"` School string `json:"school"` // SpellLists is the structured class list, but the SRD dump leaves it // incomplete (no paladin entries at all). DndClass is the free-text // "Druid, Wizard" field and is the more complete source — mapClasses // unions both. Keep both fields vendored. SpellLists []string `json:"spell_lists"` DndClass string `json:"dnd_class"` } type spellsPage struct { Next string `json:"next"` Results []open5eSpell `json:"results"` } // fetchSpells pages through the SRD spell list and writes the full result set // to data/open5e/spells.json, pretty-printed for a reviewable diff. func fetchSpells() error { client := &http.Client{Timeout: 30 * time.Second} var all []open5eSpell url := spellsAPIBase for url != "" { fmt.Fprintln(os.Stderr, "GET", url) page, err := getSpellsPage(client, url) if err != nil { return err } all = append(all, page.Results...) url = page.Next } fmt.Fprintf(os.Stderr, "fetched %d spells\n", len(all)) if err := os.MkdirAll(filepath.Dir(spellsJSON), 0o755); err != nil { return err } out, err := json.MarshalIndent(all, "", " ") if err != nil { return err } out = append(out, '\n') return os.WriteFile(spellsJSON, out, 0o644) } func getSpellsPage(client *http.Client, url string) (*spellsPage, error) { resp, err := client.Get(url) if err != nil { return nil, err } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { return nil, fmt.Errorf("%s: HTTP %d", url, resp.StatusCode) } body, err := io.ReadAll(resp.Body) if err != nil { return nil, err } var page spellsPage if err := json.Unmarshal(body, &page); err != nil { return nil, err } return &page, nil }