package config import ( "fmt" "net" "os" "time" "gopkg.in/yaml.v3" ) // rawConfig represents the structure of the YAML configuration file. type rawConfig struct { Routes []rawRoute `yaml:"routes"` Models []rawModel `yaml:"models"` } // rawRoute represents a single route in the YAML configuration. type rawRoute struct { Name string `yaml:"name"` Upstream rawUpstream `yaml:"upstream"` } // rawModel represents a single model entry in the YAML configuration. type rawModel struct { Name string `yaml:"name"` Address string `yaml:"address"` Path string `yaml:"path"` } // rawUpstream represents upstream configuration in YAML. type rawUpstream struct { Address string `yaml:"address"` PathRewrite string `yaml:"pathRewrite"` ConnectTimeout string `yaml:"connectTimeout"` ReadTimeout string `yaml:"readTimeout"` WriteTimeout string `yaml:"writeTimeout"` MaxBodySize int64 `yaml:"maxBodySize"` AuthRequired *bool `yaml:"authRequired"` } // LoadRoutesAndModelsFromFile loads both route and model configuration from a YAML file. func LoadRoutesAndModelsFromFile(path string) (map[string]*Route, map[string]*ModelUpstream, error) { data, err := os.ReadFile(path) if err != nil { return nil, nil, fmt.Errorf("failed to read config file %q: %w", path, err) } var raw rawConfig if err := yaml.Unmarshal(data, &raw); err != nil { return nil, nil, fmt.Errorf("failed to parse config file %q: %w", path, err) } // Load routes routes := make(map[string]*Route) for _, rawRoute := range raw.Routes { if rawRoute.Name == "" { return nil, nil, fmt.Errorf("route has empty name") } if _, exists := routes[rawRoute.Name]; exists { return nil, nil, fmt.Errorf("duplicate route: \"%s\"", rawRoute.Name) } upstream, err := parseUpstream(rawRoute.Name, rawRoute.Upstream) if err != nil { return nil, nil, err } routes[rawRoute.Name] = &Route{ Name: rawRoute.Name, Upstream: upstream, } } // Load models models := make(map[string]*ModelUpstream) for _, rawModel := range raw.Models { // Validate model name is not empty if rawModel.Name == "" { return nil, nil, fmt.Errorf("model has empty name") } // Check for duplicate model names if _, exists := models[rawModel.Name]; exists { return nil, nil, fmt.Errorf("duplicate model: \"%s\"", rawModel.Name) } // Validate address is not empty if rawModel.Address == "" { return nil, nil, fmt.Errorf("model \"%s\": field 'address' is required", rawModel.Name) } // Validate address format (host:port) if _, _, err := net.SplitHostPort(rawModel.Address); err != nil { return nil, nil, fmt.Errorf("model \"%s\": invalid address \"%s\": %w", rawModel.Name, rawModel.Address, err) } models[rawModel.Name] = &ModelUpstream{ Name: rawModel.Name, Address: rawModel.Address, Path: rawModel.Path, } } return routes, models, nil } // LoadRoutesFromFile loads route configuration from a YAML file. // It validates that all required fields are present and have valid values. // Returns an error if the configuration is invalid. // Deprecated: Use LoadRoutesAndModelsFromFile instead. func LoadRoutesFromFile(path string) (map[string]*Route, error) { routes, _, err := LoadRoutesAndModelsFromFile(path) return routes, err } // parseUpstream validates and parses upstream configuration from raw YAML. func parseUpstream(routeName string, raw rawUpstream) (Upstream, error) { // Validate address is not empty if raw.Address == "" { return Upstream{}, fmt.Errorf("route \"%s\": field 'address' is required", routeName) } // Validate address format (host:port) if _, _, err := net.SplitHostPort(raw.Address); err != nil { return Upstream{}, fmt.Errorf("route \"%s\": invalid address \"%s\": %w", routeName, raw.Address, err) } // Validate connectTimeout if raw.ConnectTimeout == "" { return Upstream{}, fmt.Errorf("route \"%s\": field 'connectTimeout' is required", routeName) } connectTimeout, err := time.ParseDuration(raw.ConnectTimeout) if err != nil { return Upstream{}, fmt.Errorf("route \"%s\": invalid connectTimeout \"%s\": %w", routeName, raw.ConnectTimeout, err) } // Validate readTimeout if raw.ReadTimeout == "" { return Upstream{}, fmt.Errorf("route \"%s\": field 'readTimeout' is required", routeName) } readTimeout, err := time.ParseDuration(raw.ReadTimeout) if err != nil { return Upstream{}, fmt.Errorf("route \"%s\": invalid readTimeout \"%s\": %w", routeName, raw.ReadTimeout, err) } // Validate writeTimeout if raw.WriteTimeout == "" { return Upstream{}, fmt.Errorf("route \"%s\": field 'writeTimeout' is required", routeName) } writeTimeout, err := time.ParseDuration(raw.WriteTimeout) if err != nil { return Upstream{}, fmt.Errorf("route \"%s\": invalid writeTimeout \"%s\": %w", routeName, raw.WriteTimeout, err) } // Validate maxBodySize is not zero (it must be explicitly set) if raw.MaxBodySize == 0 { return Upstream{}, fmt.Errorf("route \"%s\": field 'maxBodySize' is required and must be > 0", routeName) } // Validate authRequired is not missing if raw.AuthRequired == nil { return Upstream{}, fmt.Errorf("route \"%s\": field 'authRequired' is required", routeName) } return Upstream{ Address: raw.Address, PathRewrite: raw.PathRewrite, ConnectTimeout: connectTimeout, ReadTimeout: readTimeout, WriteTimeout: writeTimeout, MaxBodySize: raw.MaxBodySize, AuthRequired: *raw.AuthRequired, }, nil }