diff options
| author | Kubernetes Prow Robot <k8s-ci-robot@users.noreply.github.com> | 2019-05-06 07:17:39 -0700 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2019-05-06 07:17:39 -0700 |
| commit | 08281a417f3b964aa63755f4353ec204c94bcef9 (patch) | |
| tree | 306b6f366852dfb791044f2142702447bc92321e /generator/app.go | |
| parent | fa149ea1197de2590560a1c8048c4cec226b9809 (diff) | |
| parent | f2c3064a5bd64339f316e6719094c3782d503102 (diff) | |
Merge pull request #3671 from spiffxp/format-sigs-yaml
Enforce consistent formatting of sigs.yaml
Diffstat (limited to 'generator/app.go')
| -rw-r--r-- | generator/app.go | 241 |
1 files changed, 185 insertions, 56 deletions
diff --git a/generator/app.go b/generator/app.go index 295077b3..e6e302e9 100644 --- a/generator/app.go +++ b/generator/app.go @@ -27,7 +27,7 @@ import ( "strings" "text/template" - "gopkg.in/yaml.v2" + yaml "gopkg.in/yaml.v3" ) const ( @@ -52,11 +52,23 @@ var ( templateDir = "generator" ) +// FoldedString is a string that will be serialized in FoldedStyle by go-yaml +type FoldedString string + +// MarshalYAML customizes how FoldedStrings will be serialized by go-yaml +func (x FoldedString) MarshalYAML() (interface{}, error) { + return &yaml.Node{ + Kind: yaml.ScalarNode, + Style: yaml.FoldedStyle, + Value: string(x), + }, nil +} + // Person represents an individual person holding a role in a group. type Person struct { + GitHub string Name string Company string - GitHub string } // Meeting represents a regular meeting for a group. @@ -66,60 +78,65 @@ type Meeting struct { Time string TZ string Frequency string - URL string - ArchiveURL string `yaml:"archive_url"` - RecordingsURL string `yaml:"recordings_url"` + URL string `yaml:",omitempty"` + ArchiveURL string `yaml:"archive_url,omitempty"` + RecordingsURL string `yaml:"recordings_url,omitempty"` } // Contact represents the various contact points for a group. type Contact struct { - Slack string - MailingList string `yaml:"mailing_list"` - PrivateMailingList string `yaml:"private_mailing_list"` - GithubTeams []GithubTeams `yaml:"teams"` + Slack string `yaml:",omitempty"` + MailingList string `yaml:"mailing_list,omitempty"` + PrivateMailingList string `yaml:"private_mailing_list,omitempty"` + GithubTeams []GithubTeam `yaml:"teams,omitempty"` } -// GithubTeams represents a specific Github Team. -type GithubTeams struct { +// GithubTeam represents a specific Github Team. +type GithubTeam struct { Name string - Description string + Description string `yaml:",omitempty"` } // Subproject represents a specific subproject owned by the group type Subproject struct { Name string - Description string - Contact *Contact + Description string `yaml:",omitempty"` + Contact *Contact `yaml:",omitempty"` Owners []string - Meetings []Meeting + Meetings []Meeting `yaml:",omitempty"` } // LeadershipGroup represents the different groups of leaders within a group type LeadershipGroup struct { Chairs []Person - TechnicalLeads []Person `yaml:"tech_leads"` - EmeritusLeads []Person `yaml:"emeritus_leads"` + TechnicalLeads []Person `yaml:"tech_leads,omitempty"` + EmeritusLeads []Person `yaml:"emeritus_leads,omitempty"` } // Group represents either a Special Interest Group (SIG) or a Working Group (WG) type Group struct { - Name string Dir string - MissionStatement string `yaml:"mission_statement,omitempty"` - CharterLink string `yaml:"charter_link,omitempty"` + Name string + MissionStatement FoldedString `yaml:"mission_statement,omitempty"` + CharterLink string `yaml:"charter_link,omitempty"` + StakeholderSIGs []string `yaml:"stakeholder_sigs,omitempty"` Label string Leadership LeadershipGroup `yaml:"leadership"` Meetings []Meeting Contact Contact - Subprojects []Subproject - StakeholderSIGs []string `yaml:"stakeholder_sigs,omitempty"` + Subprojects []Subproject `yaml:",omitempty"` } // DirName returns the directory that a group's documentation will be // generated into. It is composed of a prefix (sig for SIGs and wg for WGs), // and a formatted version of the group's name (in kebab case). -func (e *Group) DirName(prefix string) string { - return fmt.Sprintf("%s-%s", prefix, strings.ToLower(strings.Replace(e.Name, " ", "-", -1))) +func (g *Group) DirName(prefix string) string { + return fmt.Sprintf("%s-%s", prefix, strings.ToLower(strings.Replace(g.Name, " ", "-", -1))) +} + +// LabelName returns the expected label for a given group +func (g *Group) LabelName(prefix string) string { + return strings.Replace(g.DirName(prefix), fmt.Sprintf("%s-", prefix), "", 1) } // Context is the context for the sigs.yaml file. @@ -130,6 +147,108 @@ type Context struct { Committees []Group } +func index(groups []Group, predicate func(Group) bool) int { + for i, group := range groups { + if predicate(group) { + return i + } + } + return -1 +} + +// PrefixToGroupMap returns a map of prefix to groups, useful for iteration over all groups +func (c *Context) PrefixToGroupMap() map[string][]Group { + return map[string][]Group{ + "sig": c.Sigs, + "wg": c.WorkingGroups, + "ug": c.UserGroups, + "committee": c.Committees, + } +} + +// Sort sorts all lists within the Context struct +func (c *Context) Sort() { + for _, groups := range c.PrefixToGroupMap() { + sort.Slice(groups, func(i, j int) bool { + return groups[i].Dir < groups[j].Dir + }) + for _, group := range groups { + sort.Strings(group.StakeholderSIGs) + for _, people := range [][]Person{ + group.Leadership.Chairs, + group.Leadership.TechnicalLeads, + group.Leadership.EmeritusLeads} { + sort.Slice(people, func(i, j int) bool { + // This ensure OWNERS / OWNERS_ALIAS files are ordered by github + return people[i].GitHub < people[j].GitHub + }) + } + sort.Slice(group.Meetings, func(i, j int) bool { + return group.Meetings[i].Description < group.Meetings[j].Description + }) + sort.Slice(group.Contact.GithubTeams, func(i, j int) bool { + return group.Contact.GithubTeams[i].Name < group.Contact.GithubTeams[j].Name + }) + sort.Slice(group.Subprojects, func(i, j int) bool { + return group.Subprojects[i].Name < group.Subprojects[j].Name + }) + for _, subproject := range group.Subprojects { + if subproject.Contact != nil { + sort.Slice(subproject.Contact.GithubTeams, func(i, j int) bool { + return subproject.Contact.GithubTeams[i].Name < subproject.Contact.GithubTeams[j].Name + }) + } + sort.Strings(subproject.Owners) + sort.Slice(subproject.Meetings, func(i, j int) bool { + return subproject.Meetings[i].Description < subproject.Meetings[j].Description + }) + } + } + } +} + +// Validate returns a list of errors encountered while validating a Context +func (c *Context) Validate() []error { + errors := []error{} + for prefix, groups := range c.PrefixToGroupMap() { + for _, group := range groups { + expectedDir := group.DirName(prefix) + if expectedDir != group.Dir { + errors = append(errors, fmt.Errorf("expected dir: %s, got: %s", expectedDir, group.Dir)) + } + expectedLabel := group.LabelName(prefix) + if expectedLabel != group.Label { + errors = append(errors, fmt.Errorf("%s: expected label: %s, got: %s", group.Dir, expectedLabel, group.Label)) + } + if len(group.StakeholderSIGs) != 0 { + if prefix == "wg" { + for _, name := range group.StakeholderSIGs { + if index(c.Sigs, func(g Group) bool { return g.Name == name }) == -1 { + errors = append(errors, fmt.Errorf("%s: invalid stakeholder sig name %s", group.Dir, name)) + } + } + } else { + errors = append(errors, fmt.Errorf("%s: only WGs may have stakeholder_sigs", group.Dir)) + } + } + if prefix == "sig" { + if group.CharterLink == "" { + errors = append(errors, fmt.Errorf("%s: has no charter", group.Dir)) + } + // TODO(spiffxp): is this required though? + if group.MissionStatement == "" { + errors = append(errors, fmt.Errorf("%s: has no mission statement", group.Dir)) + } + if len(group.Subprojects) == 0 { + errors = append(errors, fmt.Errorf("%s: has no subprojects", group.Dir)) + } + } + + } + } + return errors +} + func pathExists(path string) bool { _, err := os.Stat(path) return err == nil @@ -182,14 +301,14 @@ func getExistingContent(path string, fileFormat string) (string, error) { } var funcMap = template.FuncMap{ - "tzUrlEncode": tzUrlEncode, + "tzUrlEncode": tzURLEncode, "trimSpace": strings.TrimSpace, } // tzUrlEncode returns a url encoded string without the + shortcut. This is // required as the timezone conversion site we are using doesn't recognize + as // a valid url escape character. -func tzUrlEncode(tz string) string { +func tzURLEncode(tz string) string { return strings.Replace(url.QueryEscape(tz), "+", "%20", -1) } @@ -265,7 +384,6 @@ func createGroupReadme(groups []Group, prefix string) error { } for _, group := range groups { - group.Dir = group.DirName(prefix) // skip generation if the user specified only one group if selectedGroupName != nil && strings.HasSuffix(group.Dir, *selectedGroupName) == false { fmt.Printf("Skipping %s/README.md\n", group.Dir) @@ -289,52 +407,63 @@ func createGroupReadme(groups []Group, prefix string) error { return nil } -func main() { - yamlData, err := ioutil.ReadFile(filepath.Join(baseGeneratorDir, sigsYamlFile)) +// readSigsYaml decodes yaml stored in a file at path into the +// specified yaml.Node +func readYaml(path string, data interface{}) error { + file, err := os.Open(path) if err != nil { - log.Fatal(err) + return err } + defer file.Close() + decoder := yaml.NewDecoder(file) + decoder.KnownFields(true) + return decoder.Decode(data) +} - var ctx Context - err = yaml.Unmarshal(yamlData, &ctx) +// writeSigsYaml writes the specified data to a file at path +// indent is set to 2 spaces +func writeYaml(data interface{}, path string) error { + file, err := os.Create(path) if err != nil { - log.Fatal(err) + return err } + defer file.Close() + enc := yaml.NewEncoder(file) + enc.SetIndent(2) + return enc.Encode(data) +} - sort.Slice(ctx.Sigs, func(i, j int) bool { - return strings.ToLower(ctx.Sigs[i].Name) <= strings.ToLower(ctx.Sigs[j].Name) - }) - - sort.Slice(ctx.WorkingGroups, func(i, j int) bool { - return strings.ToLower(ctx.WorkingGroups[i].Name) <= strings.ToLower(ctx.WorkingGroups[j].Name) - }) - - sort.Slice(ctx.UserGroups, func(i, j int) bool { - return strings.ToLower(ctx.UserGroups[i].Name) <= strings.ToLower(ctx.UserGroups[j].Name) - }) - - sort.Slice(ctx.Committees, func(i, j int) bool { - return strings.ToLower(ctx.Committees[i].Name) <= strings.ToLower(ctx.Committees[j].Name) - }) +func main() { + yamlPath := filepath.Join(baseGeneratorDir, sigsYamlFile) + var ctx Context - err = createGroupReadme(ctx.Sigs, "sig") + err := readYaml(yamlPath, &ctx) if err != nil { log.Fatal(err) } - err = createGroupReadme(ctx.WorkingGroups, "wg") - if err != nil { - log.Fatal(err) + ctx.Sort() + + fmt.Printf("Validating %s\n", yamlPath) + errs := ctx.Validate() + if len(errs) != 0 { + for _, err := range errs { + fmt.Printf("NOTICE: %s\n", err.Error()) + } + fmt.Println("NOTICE: validation errors are ignored at present") } - err = createGroupReadme(ctx.UserGroups, "ug") + // Write the Context struct back to yaml to enforce formatting + err = writeYaml(&ctx, yamlPath) if err != nil { log.Fatal(err) } - err = createGroupReadme(ctx.Committees, "committee") - if err != nil { - log.Fatal(err) + for prefix, groups := range ctx.PrefixToGroupMap() { + err = createGroupReadme(groups, prefix) + if err != nil { + log.Fatal(err) + } } fmt.Println("Generating sig-list.md") |
