// 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
}