cmds/contest-generator/config.go (148 lines of code) (raw):

// Copyright (c) Facebook, Inc. and its affiliates. // // This source code is licensed under the MIT license found in the // LICENSE file in the root directory of this source tree. package main import ( "fmt" "io/ioutil" "log" "os" "path" "regexp" "sort" "strings" "gopkg.in/yaml.v2" ) // I don't know what a valid charset for alias imports is, so here I am // allowing case-insensitive alphanumeric strings that start with a letter // and can contain underscores. var aliasRegexp = regexp.MustCompile("^(?i)[a-z][0-9a-z_]+$") // Config is a configuration object mapped to the plugin configuration file. type Config struct { ConfigFile string TargetManagers ConfigEntries TestFetchers ConfigEntries TestSteps ConfigEntries Reporters ConfigEntries } // ConfigEntry maps a plugin config entry as a couple of import path, and import // alias type ConfigEntry struct { Path string Alias string } // Validate validates the ConfigEntry object. func (ce ConfigEntry) Validate() error { if ce.Path == "" { return fmt.Errorf("path cannot be empty") } // validate that if Alias is present, it's a valid alias string. if ce.Alias != "" && !aliasRegexp.MatchString(ce.Alias) { return fmt.Errorf("invalid alias '%s', not matching regexp '%s'", ce.Alias, aliasRegexp) } return nil } // ToAlias returns the import alias for the plugin. For example, if the import // path is github.com/facebookincubator/plugins/teststeps/cmd , it will return // the string "cmd". If an explicit alias is specified in the `Alias` attribute, // that string will be returned instead. func (ce ConfigEntry) ToAlias() string { if ce.Alias != "" { return ce.Alias } return path.Base(ce.Path) } func (ce ConfigEntry) String() string { ret := ce.Path if ce.Alias != "" { ret += " => " + ce.Alias } return ret } // ConfigEntries is a list of ConfigEntry objects. type ConfigEntries []ConfigEntry // Validate validates the ConfigEntries object. func (ces *ConfigEntries) Validate() error { if len(*ces) < 1 { return fmt.Errorf("no config entry found") } for _, ce := range *ces { if err := ce.Validate(); err != nil { return err } } return nil } func (c *Config) String() string { pp := func(ee ConfigEntries) string { var ret string for _, e := range ee { ret += " " + e.String() + "\n" } return ret } ret := "TargetManagers\n" + pp(c.TargetManagers) ret += "TestFetchers\n" + pp(c.TestFetchers) ret += "TestSteps\n" + pp(c.TestSteps) ret += "Reporters\n" + pp(c.Reporters) return ret } // Validate validates the Config object. func (c *Config) Validate() error { if err := c.TargetManagers.Validate(); err != nil { return fmt.Errorf("target managers validation failed: %w", err) } if err := c.TestFetchers.Validate(); err != nil { return fmt.Errorf("test fetchers validation failed: %w", err) } if err := c.TestSteps.Validate(); err != nil { return fmt.Errorf("test steps validation failed: %w", err) } if err := c.Reporters.Validate(); err != nil { return fmt.Errorf("reporters validation failed: %w", err) } // ensure that there are no duplicate package names // map of alias => plugintypes plugins := make(map[string][]string) for _, e := range c.TargetManagers { plugins[e.ToAlias()] = append(plugins[e.ToAlias()], "targetmanagers") } for _, e := range c.TestFetchers { plugins[e.ToAlias()] = append(plugins[e.ToAlias()], "testfetchers") } for _, e := range c.TestSteps { plugins[e.ToAlias()] = append(plugins[e.ToAlias()], "teststeps") } for _, e := range c.Reporters { plugins[e.ToAlias()] = append(plugins[e.ToAlias()], "reporters") } var duplicates []string for name, ptypes := range plugins { if len(ptypes) > 1 { duplicates = append(duplicates, name) } } if len(duplicates) > 0 { return fmt.Errorf("found %d duplicate plugin(s): %v", len(duplicates), duplicates) } return nil } func readConfig(filename string) (*Config, error) { r, err := os.Open(filename) if err != nil { return nil, fmt.Errorf("failed to read file '%s': %v", filename, err) } defer func() { if err := r.Close(); err != nil { log.Printf("Error closing file '%s': %v", filename, err) } }() data, err := ioutil.ReadAll(r) if err != nil { return nil, fmt.Errorf("failed to read file: %w", err) } return parseConfig(filename, data) } func parseConfig(filename string, data []byte) (*Config, error) { var cfg Config cfg.ConfigFile = filename if err := yaml.Unmarshal(data, &cfg); err != nil { return nil, fmt.Errorf("failed to unmarshal YAML: %w", err) } // sort package entries sort.Slice(cfg.TargetManagers, func(i, j int) bool { return strings.Compare(cfg.TargetManagers[i].Path, cfg.TargetManagers[j].Path) < 0 }) sort.Slice(cfg.TestFetchers, func(i, j int) bool { return strings.Compare(cfg.TestFetchers[i].Path, cfg.TestFetchers[j].Path) < 0 }) sort.Slice(cfg.TestSteps, func(i, j int) bool { return strings.Compare(cfg.TestSteps[i].Path, cfg.TestSteps[j].Path) < 0 }) sort.Slice(cfg.Reporters, func(i, j int) bool { return strings.Compare(cfg.Reporters[i].Path, cfg.Reporters[j].Path) < 0 }) // validate configuration if err := cfg.Validate(); err != nil { return nil, fmt.Errorf("invalid configuration: %w", err) } return &cfg, nil }