Files
rb-search/config.go
2026-05-26 13:29:52 +05:00

153 lines
3.5 KiB
Go

package main
import (
"encoding/json"
"fmt"
"net/url"
"os"
"path/filepath"
"strings"
)
type ServerConfig struct {
URL string `json:"url"`
}
type Config struct {
Default string `json:"default"`
Servers map[string]ServerConfig `json:"servers"`
path string
fromEnv bool
}
const envServerName = "env"
func defaultConfigPath() string {
if p := os.Getenv("RB_SEARCH_CONFIG"); p != "" {
return p
}
if x := os.Getenv("XDG_CONFIG_HOME"); x != "" {
return filepath.Join(x, "rb-search", "config.json")
}
if h, err := os.UserHomeDir(); err == nil {
return filepath.Join(h, ".config", "rb-search", "config.json")
}
return "config.json"
}
func loadConfig(path string) (*Config, error) {
explicit := path != ""
if path == "" {
path = defaultConfigPath()
}
data, err := os.ReadFile(path)
if err != nil {
if !os.IsNotExist(err) {
return nil, err
}
if envCfg := configFromEnv(); envCfg != nil {
envCfg.path = path
return envCfg, nil
}
hint := "EVENTBUS_AMQP_HOST/USER/PASS env vars not set either"
if explicit {
return nil, fmt.Errorf("config not found at %s (%s)", path, hint)
}
return nil, fmt.Errorf("config not found at %s (%s; run `rb-search init` to create one)", path, hint)
}
var c Config
if err := json.Unmarshal(data, &c); err != nil {
return nil, fmt.Errorf("parse %s: %w", path, err)
}
if c.Servers == nil {
c.Servers = map[string]ServerConfig{}
}
c.path = path
return &c, nil
}
// configFromEnv builds a synthetic in-memory config from EVENTBUS_AMQP_*.
// Returns nil when EVENTBUS_AMQP_HOST is unset — host is the only required field.
func configFromEnv() *Config {
host := strings.TrimSpace(os.Getenv("EVENTBUS_AMQP_HOST"))
if host == "" {
return nil
}
port := os.Getenv("EVENTBUS_AMQP_PORT")
if port == "" {
port = "5672"
}
vhost := os.Getenv("EVENTBUS_AMQP_VHOST")
user := os.Getenv("EVENTBUS_AMQP_USER")
pass := os.Getenv("EVENTBUS_AMQP_PASS")
u := &url.URL{
Scheme: "amqp",
Host: host + ":" + port,
Path: "/" + strings.TrimPrefix(vhost, "/"),
}
if user != "" {
u.User = url.UserPassword(user, pass)
}
return &Config{
Default: envServerName,
Servers: map[string]ServerConfig{
envServerName: {URL: u.String()},
},
fromEnv: true,
}
}
func (c *Config) save() error {
if c.fromEnv {
return fmt.Errorf("config was synthesized from EVENTBUS_AMQP_* env vars; run `rb-search init` to materialize a file first")
}
if err := os.MkdirAll(filepath.Dir(c.path), 0o755); err != nil {
return err
}
data, err := json.MarshalIndent(c, "", " ")
if err != nil {
return err
}
tmp := c.path + ".tmp"
if err := os.WriteFile(tmp, append(data, '\n'), 0o600); err != nil {
return err
}
return os.Rename(tmp, c.path)
}
func (c *Config) resolveServer(name string) (string, ServerConfig, error) {
if name == "" {
name = c.Default
}
if name == "" {
return "", ServerConfig{}, fmt.Errorf("no server specified and no default set")
}
srv, ok := c.Servers[name]
if !ok {
return "", ServerConfig{}, fmt.Errorf("unknown server %q", name)
}
if srv.URL == "" {
return "", ServerConfig{}, fmt.Errorf("server %q has empty url", name)
}
return name, srv, nil
}
func writeSampleConfig(path string) error {
if path == "" {
path = defaultConfigPath()
}
if _, err := os.Stat(path); err == nil {
return fmt.Errorf("%s already exists", path)
}
sample := Config{
Default: "local",
Servers: map[string]ServerConfig{
"local": {URL: "amqp://guest:guest@localhost:5672/"},
},
path: path,
}
return sample.save()
}