// Package config defines the application configuration shape and validation. package config import ( "net/url" "time" "go.bigb.es/auxilia/culpa" "sourcecraft.dev/bigbes/huntsman/internal/domain/search" ) // Config is the top-level configuration for huntsman. type Config struct { Server ServerConfig `mapstructure:"server"` Log LogConfig `mapstructure:"log"` Search SearchConfig `mapstructure:"search"` } // ServerConfig configures the HTTP server. type ServerConfig struct { Host string `mapstructure:"host"` Port int `mapstructure:"port"` ReadTimeout time.Duration `mapstructure:"read_timeout"` WriteTimeout time.Duration `mapstructure:"write_timeout"` // PublicURL is the externally-visible base URL of this service. // It's embedded in OpenSearch description XML so browsers know where to // fetch the search template. Example: "https://search.example.com". PublicURL string `mapstructure:"public_url"` } // LogConfig configures structured logging. type LogConfig struct { Level string `mapstructure:"level"` Format string `mapstructure:"format"` } // SearchConfig controls search-routing behavior. type SearchConfig struct { // DefaultProvider is the provider used when no prefix matches. // Must be one of the registered provider IDs (see search.AllProviders). DefaultProvider string `mapstructure:"default_provider"` } // Validate runs hand-written checks against the loaded config. Failures // carry an "INVALID_CONFIG" code so call sites can distinguish them. func (c *Config) Validate() error { if c.Server.Host == "" { return invalid("server.host is required") } if c.Server.Port < 1 || c.Server.Port > 65535 { return invalid("server.port must be between 1 and 65535") } if c.Server.PublicURL == "" { return invalid("server.public_url is required") } if u, err := url.Parse(c.Server.PublicURL); err != nil || u.Scheme == "" || u.Host == "" { return invalid("server.public_url must be an absolute URL") } switch c.Log.Level { case "debug", "info", "warn", "error": default: return invalid("log.level must be one of debug|info|warn|error") } switch c.Log.Format { case "human", "json": default: return invalid("log.format must be one of human|json") } if !knownProvider(c.Search.DefaultProvider) { return invalid("search.default_provider must be a known provider id") } return nil } func invalid(msg string) error { return culpa.WithCode(culpa.New(msg), "INVALID_CONFIG") } func knownProvider(id string) bool { for _, p := range search.AllProviders() { if p.ID == id { return true } } return false }