summaryrefslogtreecommitdiff
path: root/generator/app.go
diff options
context:
space:
mode:
authorKubernetes Prow Robot <k8s-ci-robot@users.noreply.github.com>2019-05-06 07:17:39 -0700
committerGitHub <noreply@github.com>2019-05-06 07:17:39 -0700
commit08281a417f3b964aa63755f4353ec204c94bcef9 (patch)
tree306b6f366852dfb791044f2142702447bc92321e /generator/app.go
parentfa149ea1197de2590560a1c8048c4cec226b9809 (diff)
parentf2c3064a5bd64339f316e6719094c3782d503102 (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.go241
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")