package config import ( "strings" "time" "git.yuntuo.hk/k2safe/go-omni/contract" "git.yuntuo.hk/k2safe/go-omni/cors" apierrors "git.yuntuo.hk/k2safe/go-omni/errors" "git.yuntuo.hk/k2safe/go-omni/httpx" "git.yuntuo.hk/k2safe/go-omni/i18n/httpi18n" "git.yuntuo.hk/k2safe/go-omni/lifecycle" "git.yuntuo.hk/k2safe/go-omni/logx" "git.yuntuo.hk/k2safe/go-omni/response" ) const defaultRequestIDHeader = "X-Request-ID" const ( TransportNetHTTP = "nethttp" TransportNative = "native" TransportHTTP2 = "http2" ) // TLSConfig enables HTTPS when both certificate paths are provided. type TLSConfig struct { CertFile string `json:"certFile"` KeyFile string `json:"keyFile"` } // Config controls the default runtime behavior of an App. type Config struct { CORS cors.Config `json:"cors"` Request contract.RequestPolicy `json:"request"` Address string `json:"address"` Transport string `json:"transport"` TLS TLSConfig `json:"tls"` HTTP HTTPConfig `json:"http"` Endpoints EndpointsConfig `json:"endpoints"` RequestIDHeader string `json:"requestIdHeader"` EnableLogger bool `json:"enableLogger"` SlowRequestThreshold time.Duration `json:"slowRequestThreshold"` TrustedProxies []string `json:"trustedProxies"` ShutdownTimeout time.Duration `json:"shutdownTimeout"` Lifecycle lifecycle.ShutdownConfig `json:"lifecycle"` Log logx.Config `json:"log"` Errors apierrors.Catalog `json:"errors"` Response response.Config `json:"response"` ResponseEncoder response.Encoder `json:"-"` LogInstance *logx.Logger `json:"-"` LanguagePolicy *httpi18n.Policy `json:"-"` ErrorLocalizer apierrors.Localizer `json:"-"` } func Default() Config { return Config{ Address: ":8080", Transport: TransportNetHTTP, RequestIDHeader: defaultRequestIDHeader, ShutdownTimeout: 10 * time.Second, Lifecycle: lifecycle.DefaultShutdownConfig(), Endpoints: DefaultEndpoints(), Log: logx.Default(), } } // Validate checks configuration values before an application starts. func (c Config) Validate() error { if err := c.CORS.Validate(); err != nil { return AtField("cors", err) } if err := c.Lifecycle.Validate(); err != nil { return AtField("lifecycle", err) } if err := c.Request.Validate(); err != nil { return AtField("request", err) } if err := c.Log.Validate(); err != nil { return AtField("log", err) } if err := c.Endpoints.Validate(); err != nil { return err } if err := c.Errors.Validate(); err != nil { return AtField("errors", err) } encoder := c.ResponseEncoder if encoder == nil { f, err := response.New(c.Response) if err != nil { return err } encoder = f } // Check the same composition that App.New installs, including custom // encoders and their existing catalogs, before App construction. if _, err := response.WithErrors(c.Errors, encoder); err != nil { return err } if err := c.Response.Validate(); err != nil { return AtField("response", err) } if c.SlowRequestThreshold < 0 { return &ConfigError{Field: "slowRequestThreshold", Message: "must be nonnegative"} } if strings.TrimSpace(c.Address) == "" { return &ConfigError{Field: "address", Message: "is required"} } if strings.TrimSpace(c.RequestIDHeader) == "" { return &ConfigError{Field: "requestIdHeader", Message: "is required"} } if c.ShutdownTimeout <= 0 { return &ConfigError{Field: "shutdownTimeout", Message: "must be positive"} } if _, err := httpx.NewClientIPResolver(c.TrustedProxies...); err != nil { return &Error{Field: "trustedProxies", Reason: "expected IP addresses or CIDRs", cause: err} } return c.ValidateTransport() } // ValidateTransport checks built-in transport selection without opening files // or sockets. Empty Transport preserves the standard net/http default. func (c Config) ValidateTransport() error { if err := c.HTTP.Validate(); err != nil { return err } switch c.Transport { case "", TransportNetHTTP, TransportNative, TransportHTTP2: default: return &ConfigError{Field: "transport", Message: "must be nethttp, native or http2"} } if c.Transport == TransportHTTP2 && (c.HTTP.ReadHeaderTimeout > 0 || c.HTTP.ReadTimeout > 0 || c.HTTP.WriteTimeout > 0) { return &ConfigError{Field: "http", Message: "readHeaderTimeout, readTimeout and writeTimeout are not supported by the experimental http2 transport"} } if c.HTTP.ErrorLog != nil && c.Transport != "" && c.Transport != TransportNetHTTP { return &ConfigError{Field: "http.errorLog", Message: "is supported only by the nethttp transport"} } cert, key := strings.TrimSpace(c.TLS.CertFile), strings.TrimSpace(c.TLS.KeyFile) if (cert == "") != (key == "") { return &ConfigError{Field: "tls", Message: "requires both certFile and keyFile"} } if c.Transport == TransportHTTP2 && cert == "" { return &ConfigError{Field: "tls", Message: "is required for the experimental http2 transport"} } return nil } // ConfigError describes an invalid application configuration field. type ConfigError struct { Field string Message string } func (e *ConfigError) Error() string { return "config " + e.Field + " " + e.Message } // Option changes an App's configuration. type Option func(*Config) // WithTransport selects a built-in server. HTTP/2 is experimental and requires TLS. func WithTransport(name string) Option { return func(cfg *Config) { cfg.Transport = name } } // WithTLS enables HTTPS. Certificates are loaded before Run opens its listener. func WithTLS(certFile, keyFile string) Option { return func(cfg *Config) { cfg.TLS = TLSConfig{CertFile: certFile, KeyFile: keyFile} } } // WithRequestIDHeader changes the header used to read and return request IDs. func WithRequestIDHeader(header string) Option { return func(cfg *Config) { if value := strings.TrimSpace(header); value != "" { cfg.RequestIDHeader = value } } } // WithLogger enables GoOmni's structured access logger. func WithLogger(enabled bool) Option { return func(cfg *Config) { cfg.EnableLogger = enabled } } // WithSlowRequestThreshold enables access logs and warns after this duration. func WithSlowRequestThreshold(threshold time.Duration) Option { return func(cfg *Config) { cfg.SlowRequestThreshold = threshold; cfg.EnableLogger = true } } // WithTrustedProxies allows only these IPs/CIDRs to supply X-Forwarded-For. // An empty list disables forwarding-header trust. Invalid entries are rejected // by Config.Validate and panic when passed directly to App.New. func WithTrustedProxies(proxies ...string) Option { proxies = append([]string(nil), proxies...) return func(cfg *Config) { cfg.TrustedProxies = append([]string(nil), proxies...) } } // WithConfig applies a complete configuration loaded from a file or // environment. Later options can still override individual fields. func WithConfig(settings Config) Option { settings.CORS = settings.CORS.Clone() settings.Response = settings.Response.Clone() settings.Errors = settings.Errors.Clone() settings.TrustedProxies = append([]string(nil), settings.TrustedProxies...) settings.Log.RedactKeys = append([]string(nil), settings.Log.RedactKeys...) return func(cfg *Config) { *cfg = settings cfg.CORS = settings.CORS.Clone() cfg.Response = settings.Response.Clone() cfg.Errors = settings.Errors.Clone() cfg.TrustedProxies = append([]string(nil), settings.TrustedProxies...) cfg.Log.RedactKeys = append([]string(nil), settings.Log.RedactKeys...) } } // WithCORS installs an app-local CORS policy, before routing and authentication. func WithCORS(settings cors.Config) Option { settings = settings.Clone() return func(cfg *Config) { cfg.CORS = settings.Clone() } } // WithResponse configures this application's default JSON envelope. func WithResponse(settings response.Config) Option { settings = settings.Clone() return func(cfg *Config) { cfg.Response = settings.Clone(); cfg.ResponseEncoder = nil } } // WithResponseEncoder replaces the default encoder with application code. // OpenAPI for custom encoders must describe their actual wire contract explicitly. func WithResponseEncoder(encoder response.Encoder) Option { return func(cfg *Config) { cfg.ResponseEncoder = encoder } } // WithLogConfig configures the logger used by the application runtime. func WithLogConfig(settings logx.Config) Option { settings.RedactKeys = append([]string(nil), settings.RedactKeys...) return func(cfg *Config) { cfg.Log = settings cfg.Log.RedactKeys = append([]string(nil), settings.RedactKeys...) } } // WithShutdownTimeout controls how long RunContext waits for active requests. func WithShutdownTimeout(timeout time.Duration) Option { return func(cfg *Config) { if timeout > 0 { cfg.ShutdownTimeout = timeout } } } // WithErrors installs a project-owned, app-local catalog. func WithErrors(catalog apierrors.Catalog) Option { catalog = catalog.Clone() return func(cfg *Config) { cfg.Errors = catalog.Clone() } } // WithLogInstance borrows a logger for this App without changing process logging. func WithLogInstance(logger *logx.Logger) Option { return func(c *Config) { c.LogInstance = logger } } // WithLanguagePolicy borrows an immutable HTTP language policy. Resource loading // remains explicit in the project; configuration strings are not dictionaries. func WithLanguagePolicy(policy *httpi18n.Policy) Option { return func(c *Config) { c.LanguagePolicy = policy } } func WithErrorLocalizer(localizer apierrors.Localizer) Option { return func(c *Config) { c.ErrorLocalizer = localizer } } func WithRequestPolicy(policy contract.RequestPolicy) Option { return func(c *Config) { c.Request = policy } }