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() }