diff --git a/go-services/inventory-go/spell_filter.go b/go-services/inventory-go/spell_filter.go new file mode 100644 index 00000000..b85056e5 --- /dev/null +++ b/go-services/inventory-go/spell_filter.go @@ -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 +} diff --git a/go-services/inventory-go/spell_filter_test.go b/go-services/inventory-go/spell_filter_test.go new file mode 100644 index 00000000..f556dee7 --- /dev/null +++ b/go-services/inventory-go/spell_filter_test.go @@ -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") + } +}