207 lines
6.9 KiB
Go
207 lines
6.9 KiB
Go
package config
|
|
|
|
import (
|
|
"fmt"
|
|
"net/url"
|
|
"strings"
|
|
|
|
"gitea.maximumdirect.net/eric/weatherreporter/internal/timeutil"
|
|
)
|
|
|
|
func Validate(cfg Config) error {
|
|
if err := validateReportModules(cfg); err != nil {
|
|
return err
|
|
}
|
|
if err := validateReportDistributorPathOverrides(cfg); err != nil {
|
|
return err
|
|
}
|
|
if cfg.WeatherAPI.BaseURL != "" {
|
|
parsed, err := url.Parse(cfg.WeatherAPI.BaseURL)
|
|
if err != nil || parsed.Scheme == "" || parsed.Host == "" {
|
|
return fmt.Errorf("weather_api.base_url must be an absolute URL")
|
|
}
|
|
}
|
|
if cfg.WeatherAPI.Timeout <= 0 {
|
|
return fmt.Errorf("weather_api.timeout must be greater than zero")
|
|
}
|
|
if cfg.WeatherAPI.Precision < 0 {
|
|
return fmt.Errorf("weather_api.precision must be zero or greater")
|
|
}
|
|
if cfg.WeatherAPI.Units == "" {
|
|
return fmt.Errorf("weather_api.units is required")
|
|
}
|
|
if cfg.WeatherAPI.Timezone == "" {
|
|
return fmt.Errorf("weather_api.timezone is required")
|
|
}
|
|
if _, err := timeutil.LoadLocation(cfg.WeatherAPI.Timezone); err != nil {
|
|
return fmt.Errorf("weather_api.timezone %q is invalid: %w", cfg.WeatherAPI.Timezone, err)
|
|
}
|
|
if cfg.WeatherAPI.Format == "" {
|
|
return fmt.Errorf("weather_api.format is required")
|
|
}
|
|
if cfg.WeatherAPI.Format != "json" {
|
|
return fmt.Errorf("weather_api.format must be json")
|
|
}
|
|
|
|
if err := validatePolicy("missing_source.default", cfg.MissingSource.Default); err != nil {
|
|
return err
|
|
}
|
|
for source, policy := range cfg.MissingSource.Sources {
|
|
if strings.TrimSpace(source) == "" {
|
|
return fmt.Errorf("missing_source.sources contains an empty source name")
|
|
}
|
|
if err := validatePolicy("missing_source.sources."+source, policy); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
if err := validateDistributorNotify(cfg.Notify.Distributor); err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := validatePromptkit(cfg.Promptkit); err != nil {
|
|
return err
|
|
}
|
|
if len(cfg.Dayparts) == 0 {
|
|
return fmt.Errorf("dayparts must contain at least one entry")
|
|
}
|
|
for i, daypart := range cfg.Dayparts {
|
|
if strings.TrimSpace(daypart.Name) == "" {
|
|
return fmt.Errorf("dayparts[%d].name is required", i)
|
|
}
|
|
if _, err := timeutil.ParseClock(daypart.Start); err != nil {
|
|
return fmt.Errorf("dayparts[%d].start is invalid: %w", i, err)
|
|
}
|
|
if _, err := timeutil.ParseClock(daypart.End); err != nil {
|
|
return fmt.Errorf("dayparts[%d].end is invalid: %w", i, err)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func validatePromptkit(cfg PromptkitConfig) error {
|
|
if cfg.ProfileFile != "" && cfg.ProfileDir != "" {
|
|
return fmt.Errorf("promptkit.profile_file and promptkit.profile_dir cannot both be configured")
|
|
}
|
|
if cfg.Timeout <= 0 {
|
|
return fmt.Errorf("promptkit.timeout must be greater than zero")
|
|
}
|
|
if cfg.Local.Endpoint != "" {
|
|
parsed, err := url.Parse(cfg.Local.Endpoint)
|
|
if err != nil || parsed.Scheme == "" || parsed.Host == "" {
|
|
return fmt.Errorf("promptkit.local.endpoint must be an absolute URL when configured")
|
|
}
|
|
}
|
|
if cfg.Local.ConcurrencyLimit < 0 {
|
|
return fmt.Errorf("promptkit.local.concurrency_limit must be zero or greater")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func validateDistributorNotify(cfg DistributorNotifyConfig) error {
|
|
if !cfg.Enabled {
|
|
return nil
|
|
}
|
|
|
|
parsed, err := url.Parse(cfg.Endpoint)
|
|
if err != nil || parsed.Scheme == "" || parsed.Host == "" {
|
|
return fmt.Errorf("notify.distributor.endpoint must be an absolute URL when enabled")
|
|
}
|
|
if cfg.TokenEnv == "" {
|
|
return fmt.Errorf("notify.distributor.token_env is required when enabled")
|
|
}
|
|
if !secretNamePattern.MatchString(cfg.TokenEnv) {
|
|
return fmt.Errorf("notify.distributor.token_env must be a valid environment variable name")
|
|
}
|
|
if cfg.Timeout <= 0 {
|
|
return fmt.Errorf("notify.distributor.timeout must be greater than zero when enabled")
|
|
}
|
|
if cfg.FailurePolicy != NotifyFailureError {
|
|
return fmt.Errorf("notify.distributor.failure_policy must be error when enabled")
|
|
}
|
|
if cfg.PipelineIDTemplate == "" {
|
|
return fmt.Errorf("notify.distributor.pipeline_id_template is required when enabled")
|
|
}
|
|
if err := validateDistributorTemplate("notify.distributor.pipeline_id_template", cfg.PipelineIDTemplate, distributorPipelineTemplateVariables); err != nil {
|
|
return err
|
|
}
|
|
if cfg.BundleIDTemplate == "" {
|
|
return fmt.Errorf("notify.distributor.bundle_id_template is required when enabled")
|
|
}
|
|
if err := validateDistributorTemplate("notify.distributor.bundle_id_template", cfg.BundleIDTemplate, distributorTemplateVariables); err != nil {
|
|
return err
|
|
}
|
|
if cfg.IdempotencyKeyTemplate == "" {
|
|
return fmt.Errorf("notify.distributor.idempotency_key_template is required when enabled")
|
|
}
|
|
if err := validateDistributorTemplate("notify.distributor.idempotency_key_template", cfg.IdempotencyKeyTemplate, distributorIdempotencyTemplateVariables); err != nil {
|
|
return err
|
|
}
|
|
values := sampleDistributorTemplateValues()
|
|
bundleID, err := RenderDistributorBundleID(cfg.BundleIDTemplate, values)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
values.BundleID = bundleID
|
|
if _, err := RenderDistributorPipelineID(cfg.PipelineIDTemplate, values); err != nil {
|
|
return err
|
|
}
|
|
if err := validateDistributorBatchNotify(cfg.Batch); err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func validateDistributorBatchNotify(cfg DistributorBatchNotifyConfig) error {
|
|
if !cfg.Enabled {
|
|
return nil
|
|
}
|
|
if cfg.PipelineIDTemplate == "" {
|
|
return fmt.Errorf("notify.distributor.batch.pipeline_id_template is required when enabled")
|
|
}
|
|
if err := validateDistributorBatchTemplate("notify.distributor.batch.pipeline_id_template", cfg.PipelineIDTemplate, distributorBatchPipelineTemplateVariables); err != nil {
|
|
return err
|
|
}
|
|
if cfg.BundleIDTemplate == "" {
|
|
return fmt.Errorf("notify.distributor.batch.bundle_id_template is required when enabled")
|
|
}
|
|
if err := validateDistributorBatchTemplate("notify.distributor.batch.bundle_id_template", cfg.BundleIDTemplate, distributorBatchTemplateVariables); err != nil {
|
|
return err
|
|
}
|
|
if cfg.IdempotencyKeyTemplate == "" {
|
|
return fmt.Errorf("notify.distributor.batch.idempotency_key_template is required when enabled")
|
|
}
|
|
if err := validateDistributorBatchTemplate("notify.distributor.batch.idempotency_key_template", cfg.IdempotencyKeyTemplate, distributorBatchIdempotencyTemplateVariables); err != nil {
|
|
return err
|
|
}
|
|
|
|
values := DistributorBatchTemplateValues{
|
|
LocationID: "location",
|
|
Batch: "morning",
|
|
BatchRunID: "20260529T100000.000000000Z_morning",
|
|
BatchStartedDate: "2026-05-29",
|
|
}
|
|
bundleID, err := RenderDistributorBatchBundleID(cfg.BundleIDTemplate, values)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
values.BundleID = bundleID
|
|
if _, err := RenderDistributorBatchPipelineID(cfg.PipelineIDTemplate, values); err != nil {
|
|
return err
|
|
}
|
|
if _, err := RenderDistributorBatchIdempotencyKey(cfg.IdempotencyKeyTemplate, values); err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func validatePolicy(name string, policy MissingSourcePolicy) error {
|
|
switch policy {
|
|
case MissingSourceError, MissingSourceWarn, MissingSourceNone:
|
|
return nil
|
|
default:
|
|
return fmt.Errorf("%s must be one of error, warn, or none", name)
|
|
}
|
|
}
|