feat(inventory-go): spell name -> ID matchers for search spell filters

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Erik 2026-07-14 23:37:28 +02:00
parent bd9c1ba556
commit b0a6b494bf
2 changed files with 112 additions and 0 deletions

View file

@ -0,0 +1,60 @@
package main
import (
"sort"
"strings"
)
// Spell-filter support for /search/items: implements the has_spell /
// spell_contains / legendary_cantrips params the Go port had ignored
// (design doc 2026-07-14; legacy inventory-service/main.py:3345-3452).
// Names are matched against the in-memory enum-DB spell map; the resulting
// IDs are filtered in SQL against the item_spells table.
// spellIDsContaining returns the IDs of all spells whose name contains q,
// case-insensitively, in ascending order (deterministic SQL args).
func spellIDsContaining(spells map[int]map[string]any, q string) []int {
q = strings.ToLower(strings.TrimSpace(q))
var ids []int
for id, sp := range spells {
name, _ := sp["name"].(string)
if name != "" && strings.Contains(strings.ToLower(name), q) {
ids = append(ids, id)
}
}
sort.Ints(ids)
return ids
}
// spellIDsForCantrip matches one cantrip display name using the legacy
// flexible rule: spell name contains the label OR the label contains the
// spell name (both case-insensitive).
func spellIDsForCantrip(spells map[int]map[string]any, cantrip string) []int {
c := strings.ToLower(strings.TrimSpace(cantrip))
var ids []int
for id, sp := range spells {
name, _ := sp["name"].(string)
n := strings.ToLower(name)
if n == "" {
continue
}
if strings.Contains(n, c) || strings.Contains(c, n) {
ids = append(ids, id)
}
}
sort.Ints(ids)
return ids
}
// exactSpellID returns the lowest ID whose name equals q case-insensitively.
func exactSpellID(spells map[int]map[string]any, q string) (int, bool) {
q = strings.ToLower(strings.TrimSpace(q))
best := -1
for id, sp := range spells {
name, _ := sp["name"].(string)
if strings.ToLower(name) == q && (best == -1 || id < best) {
best = id
}
}
return best, best != -1
}

View file

@ -0,0 +1,52 @@
package main
import (
"reflect"
"testing"
)
// Minimal stand-in for the enum-DB spell map (Server.spells).
var testSpells = map[int]map[string]any{
1: {"name": "Legendary Invulnerability"},
2: {"name": "Epic Invulnerability"},
3: {"name": "Legendary Summoning Prowess"},
4: {"name": "Strength Other VI"},
5: {"name": "Summoning"}, // shorter name CONTAINED IN a cantrip label
6: {"name": ""}, // malformed entry must never match
}
func TestSpellIDsContaining(t *testing.T) {
if got := spellIDsContaining(testSpells, "invulnerability"); !reflect.DeepEqual(got, []int{1, 2}) {
t.Errorf("substring match = %v, want [1 2]", got)
}
if got := spellIDsContaining(testSpells, "INVULN"); !reflect.DeepEqual(got, []int{1, 2}) {
t.Errorf("case-insensitive match = %v, want [1 2]", got)
}
if got := spellIDsContaining(testSpells, "frostbite"); got != nil {
t.Errorf("no-match = %v, want nil", got)
}
}
func TestSpellIDsForCantrip(t *testing.T) {
// Forward direction: spell name contains the cantrip label.
if got := spellIDsForCantrip(testSpells, "Invulnerability"); !reflect.DeepEqual(got, []int{1, 2}) {
t.Errorf("forward contains = %v, want [1 2]", got)
}
// Both directions (legacy flexible rule): "Legendary Summoning Prowess"
// matches spell 3 (equal) AND spell 5 ("Summoning" is contained in the label).
if got := spellIDsForCantrip(testSpells, "Legendary Summoning Prowess"); !reflect.DeepEqual(got, []int{3, 5}) {
t.Errorf("both-direction = %v, want [3 5]", got)
}
if got := spellIDsForCantrip(testSpells, "Piercing Bane"); got != nil {
t.Errorf("no-match = %v, want nil", got)
}
}
func TestExactSpellID(t *testing.T) {
if id, ok := exactSpellID(testSpells, "legendary invulnerability"); !ok || id != 1 {
t.Errorf("exact case-insensitive = (%d,%v), want (1,true)", id, ok)
}
if _, ok := exactSpellID(testSpells, "Invulnerability"); ok {
t.Error("partial name must not exact-match")
}
}