// Package main implements the fetch_ml configuration linter package main import ( "bytes" "encoding/json" "flag" "fmt" "log" "os" "path/filepath" "strings" "github.com/BurntSushi/toml" "github.com/jfraeys/fetch_ml/internal/fileutil" "github.com/xeipuuv/gojsonschema" "gopkg.in/yaml.v3" ) func main() { var ( schemaPath string failFast bool ) flag.StringVar(&schemaPath, "schema", "configs/schema.yaml", "Path to JSON schema in YAML format") flag.BoolVar(&failFast, "fail-fast", false, "Stop on first error") flag.Parse() if flag.NArg() == 0 { log.Fatalf("usage: configlint [--schema path] [--fail-fast] ") } schemaLoader, err := loadSchema(schemaPath) if err != nil { log.Fatalf("failed to load schema: %v", err) } var hadError bool for _, configPath := range flag.Args() { if err := validateConfig(schemaLoader, configPath); err != nil { hadError = true fmt.Fprintf(os.Stderr, "configlint: %s: %v\n", configPath, err) if failFast { os.Exit(1) } } } if hadError { os.Exit(1) } fmt.Println("All configuration files are valid.") } func loadSchema(schemaPath string) (gojsonschema.JSONLoader, error) { data, err := fileutil.SecureFileRead(schemaPath) if err != nil { return nil, err } var schemaYAML any if err := yaml.Unmarshal(data, &schemaYAML); err != nil { return nil, err } schemaJSON, err := json.Marshal(schemaYAML) if err != nil { return nil, err } return gojsonschema.NewBytesLoader(schemaJSON), nil } func validateConfig(schemaLoader gojsonschema.JSONLoader, configPath string) error { data, err := fileutil.SecureFileRead(configPath) if err != nil { return err } ext := strings.ToLower(filepath.Ext(configPath)) var decoded any switch ext { case ".toml": var configTOML map[string]any if _, err := toml.Decode(string(data), &configTOML); err != nil { return fmt.Errorf("failed to parse TOML: %w", err) } decoded = configTOML default: // YAML (default) var configYAML any dec := yaml.NewDecoder(bytes.NewReader(data)) dec.KnownFields(false) if err := dec.Decode(&configYAML); err != nil { return fmt.Errorf("failed to parse YAML: %w", err) } decoded = configYAML } configJSON, err := json.Marshal(decoded) if err != nil { return err } result, err := gojsonschema.Validate(schemaLoader, gojsonschema.NewBytesLoader(configJSON)) if err != nil { return err } if result.Valid() { fmt.Printf("%s: valid\n", configPath) return nil } var builder strings.Builder for _, issue := range result.Errors() { builder.WriteString("- ") builder.WriteString(issue.String()) builder.WriteByte('\n') } return fmt.Errorf("validation failed:\n%s", builder.String()) }