~bigbes/huntsman

huntsman/internal/domain/search/providers.go -rw-r--r-- 4.1 KiB
766fa805 — Eugene Blikh Add BSD 2-Clause license 6 days ago
                                                                                
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
// Package search implements the multi-provider search router.
//
// Three search providers are supported out of the box:
//
//   - ud    → Urban Dictionary
//   - gh    → GitHub
//   - steam → Steam Store
//
// Each provider has a stable ID, a display name, and a URL template
// where {q} is substituted with the URL-encoded query.
package search

import (
	"net/url"
	"strings"

	"go.bigb.es/auxilia/culpa"
)

// Provider describes a single upstream search engine.
type Provider struct {
	ID          string // short prefix, e.g. "ud"
	Name        string // human-readable name, e.g. "Urban Dictionary"
	Description string // shown in OpenSearch description XML
	SearchURL   string // template containing the literal "{q}" placeholder
	HomeURL     string // upstream homepage, used as fallback
	IconURL     string // 16x16 favicon, optional
}

// SearchURLFor returns the upstream search URL for the given raw query.
//
// The query is trimmed and percent-encoded; an empty query returns the
// provider's HomeURL so a stray /search?q= still lands somewhere useful.
func (p Provider) SearchURLFor(q string) string {
	q = strings.TrimSpace(q)
	if q == "" {
		return p.HomeURL
	}
	return strings.ReplaceAll(p.SearchURL, "{q}", url.QueryEscape(q))
}

// AllProviders returns the registered providers in stable order.
//
// The order is also the order they appear in /providers and OpenSearch
// listings, so it's intentional rather than a map iteration.
func AllProviders() []Provider {
	return []Provider{
		{
			ID:          "ud",
			Name:        "Urban Dictionary",
			Description: "Search slang definitions on Urban Dictionary",
			SearchURL:   "https://www.urbandictionary.com/define.php?term={q}",
			HomeURL:     "https://www.urbandictionary.com/",
			IconURL:     "https://www.urbandictionary.com/favicon.ico",
		},
		{
			ID:          "gh",
			Name:        "GitHub",
			Description: "Search repositories, code, and issues on GitHub",
			SearchURL:   "https://github.com/search?q={q}",
			HomeURL:     "https://github.com/",
			IconURL:     "https://github.com/favicon.ico",
		},
		{
			ID:          "steam",
			Name:        "Steam Store",
			Description: "Search games on the Steam store",
			SearchURL:   "https://store.steampowered.com/search/?term={q}",
			HomeURL:     "https://store.steampowered.com/",
			IconURL:     "https://store.steampowered.com/favicon.ico",
		},
	}
}

// providerByID indexes AllProviders by ID for O(1) lookup.
func providerByID() map[string]Provider {
	all := AllProviders()
	m := make(map[string]Provider, len(all))
	for _, p := range all {
		m[p.ID] = p
	}
	return m
}

// Lookup returns the provider with the given ID, or false if none matches.
func Lookup(id string) (Provider, bool) {
	p, ok := providerByID()[id]
	return p, ok
}

// Route inspects a raw user query and decides which provider should handle it.
//
// The first whitespace-delimited token is treated as a possible prefix.
// Both "ud foo" and "ud:foo" route to the urbandictionary provider with
// the query "foo". If no token matches a provider ID, the default provider
// receives the original query unchanged.
//
// Returns the matched provider and the cleaned query to forward.
func Route(raw string, providers map[string]Provider, defaultID string) (Provider, string, error) {
	def, ok := providers[defaultID]
	if !ok {
		return Provider{}, "", culpa.WithCode(
			culpa.Errorf("default provider %q is not registered", defaultID),
			"UNKNOWN_PROVIDER",
		)
	}

	trimmed := strings.TrimSpace(raw)
	if trimmed == "" {
		return def, "", nil
	}

	// "ud:foo bar" → prefix "ud", rest "foo bar"
	if i := strings.IndexByte(trimmed, ':'); i > 0 {
		prefix := strings.ToLower(trimmed[:i])
		if p, ok := providers[prefix]; ok {
			return p, strings.TrimSpace(trimmed[i+1:]), nil
		}
	}

	// "ud foo bar" → prefix "ud", rest "foo bar"
	if i := strings.IndexByte(trimmed, ' '); i > 0 {
		prefix := strings.ToLower(trimmed[:i])
		if p, ok := providers[prefix]; ok {
			return p, strings.TrimSpace(trimmed[i+1:]), nil
		}
	}

	// Bare prefix like "ud" with no query — go to that provider's homepage.
	if p, ok := providers[strings.ToLower(trimmed)]; ok {
		return p, "", nil
	}

	return def, trimmed, nil
}