From 7011cd9cc4bad0f840c8e74224406323a8b38b0a Mon Sep 17 00:00:00 2001 From: Phil Prasek Date: Tue, 15 Oct 2024 12:10:27 -0700 Subject: [PATCH 1/8] enriched docs gen model Signed-off-by: Phil Prasek --- temporalcli/commandsgen/enrich.go | 252 ++++++++++++++++++++++ temporalcli/commandsgen/parse.go | 68 +++++- temporalcli/internal/cmd/gen-docs/main.go | 5 + 3 files changed, 314 insertions(+), 11 deletions(-) create mode 100644 temporalcli/commandsgen/enrich.go diff --git a/temporalcli/commandsgen/enrich.go b/temporalcli/commandsgen/enrich.go new file mode 100644 index 000000000..afa56d4fe --- /dev/null +++ b/temporalcli/commandsgen/enrich.go @@ -0,0 +1,252 @@ +// Package commandsgen is built to read the YAML format described in +// temporalcli/commandsgen/commands.yml and generate code from it. +package commandsgen + +import ( + _ "embed" + "sort" + "strings" +) + +func EnrichCommands(m Commands) (Commands, error) { + commandLookup := make(map[string]*Command) + + for i, command := range m.CommandList { + m.CommandList[i].Index = i + commandLookup[command.FullName] = &m.CommandList[i] + } + + var rootCommand *Command + + //populate parent and basic meta + for i, c := range m.CommandList { + commandLength := len(strings.Split(c.FullName, " ")) + if commandLength == 1 { + rootCommand = &m.CommandList[i] + continue + } + parentName := strings.Join(strings.Split(c.FullName, " ")[:commandLength-1], " ") + parent, ok := commandLookup[parentName] + if ok { + m.CommandList[i].Parent = &m.CommandList[parent.Index] + m.CommandList[i].Depth = len(strings.Split(c.FullName, " ")) - 1 + m.CommandList[i].FileName = strings.Split(c.FullName, " ")[1] + m.CommandList[i].LeafName = strings.Join(strings.Split(c.FullName, " ")[m.CommandList[i].Depth:], "") + } + } + + //populate children and base command + for _, c := range m.CommandList { + if c.Parent == nil { + continue + } + + //fmt.Printf("add child: %s\n", m.CommandList[c.Index].FullName) + m.CommandList[c.Parent.Index].Children = append(m.CommandList[c.Parent.Index].Children, &m.CommandList[c.Index]) + + base := &c + for base.Depth > 1 { + base = base.Parent + } + m.CommandList[c.Index].Base = &m.CommandList[base.Index] + } + + setMaxChildDepthVisitor(*rootCommand, &m) + + for i, c := range m.CommandList { + if c.Parent == nil { + continue + } + + subCommandStartDepth := 1 + if c.Base.MaxChildDepth > 2 { + subCommandStartDepth = 2 + } + + subCommandName := "" + if c.Depth >= subCommandStartDepth { + subCommandName = strings.Join(strings.Split(c.FullName, " ")[subCommandStartDepth:], " ") + } + + if len(subCommandName) == 0 && c.Depth == 1 { + // for operator base command to show up in tags, keywords, etc. + subCommandName = c.LeafName + } + + m.CommandList[i].SubCommandName = subCommandName + } + + // sorted ascending by full name of command (activity complete, batch list, etc) + sortChildrenVisitor(rootCommand) + + // pull flat list in same order as sorted children + m.CommandList = make([]Command, 0) + collectCommandVisitor(*rootCommand, &m) + + // option usages + optionUsages := getAllOptionUsages(m) + optionUsagesByOptionDescription := getOptionUsagesByOptionDescription(optionUsages) + m.Usages = Usages{ + OptionUsages: optionUsages, + OptionUsagesByOptionDescription: optionUsagesByOptionDescription, + } + + return m, nil +} + +func collectCommandVisitor(c Command, m *Commands) { + + m.CommandList = append(m.CommandList, c) + + for _, child := range c.Children { + collectCommandVisitor(*child, m) + } +} + +func sortChildrenVisitor(c *Command) { + sort.Slice(c.Children, func(i, j int) bool { + //option to put nested commands at end of the list + /* + if c.Children[i].MaxChildDepth != c.Children[j].MaxChildDepth { + return c.Children[i].MaxChildDepth < c.Children[j].MaxChildDepth + } + */ + + return c.Children[i].FullName < c.Children[j].FullName + }) + for _, command := range c.Children { + sortChildrenVisitor(command) + } +} + +func setMaxChildDepthVisitor(c Command, commands *Commands) int { + maxChildDepth := 0 + children := commands.CommandList[c.Index].Children + if len(children) > 0 { + for _, child := range children { + depth := setMaxChildDepthVisitor(*child, commands) + if depth > maxChildDepth { + maxChildDepth = depth + } + } + } + + commands.CommandList[c.Index].MaxChildDepth = maxChildDepth + return maxChildDepth + 1 +} + +func getAllOptionUsages(commands Commands) []OptionUsages { + // map[optionName]map[usageSite]OptionUsageSite + var optionUsageSitesMap = make(map[string]map[string]OptionUsageSite) + + // option sets + for i, optionSet := range commands.OptionSets { + usage := optionSet.Description + if len(usage) == 0 { + usage = optionSet.Name + } + + for j, option := range optionSet.Options { + _, found := optionUsageSitesMap[option.Name] + if !found { + optionUsageSitesMap[option.Name] = make(map[string]OptionUsageSite) + } + optionUsageSitesMap[option.Name][optionSet.Name] = OptionUsageSite{ + Option: commands.OptionSets[i].Options[j], + UsageSiteDescription: usage, + UsageSiteType: UsageTypeOptionSet, + } + } + } + + //command options + for i, cmd := range commands.CommandList { + usage := cmd.FullName + if len(usage) == 0 { + usage = cmd.FullName + } + + for j, option := range cmd.Options { + _, found := optionUsageSitesMap[option.Name] + if !found { + optionUsageSitesMap[option.Name] = make(map[string]OptionUsageSite) + } + optionUsageSitesMap[option.Name][cmd.FullName] = OptionUsageSite{ + Option: commands.CommandList[i].Options[j], + UsageSiteDescription: usage, + UsageSiteType: UsageTypeOptionSet, + } + } + } + + // all options + var allOptionUsages = make([]OptionUsages, 0) + + for optionName, usages := range optionUsageSitesMap { + option := OptionUsages{ + OptionName: optionName, + UsageSites: make([]OptionUsageSite, 0), + } + for _, usage := range usages { + option.UsageSites = append(option.UsageSites, usage) + } + allOptionUsages = append(allOptionUsages, option) + } + + sort.Slice(allOptionUsages, func(i, j int) bool { + return allOptionUsages[i].OptionName < allOptionUsages[j].OptionName + }) + + for u := range allOptionUsages { + sort.Slice(allOptionUsages[u].UsageSites, func(i, j int) bool { + return allOptionUsages[u].UsageSites[i].UsageSiteDescription < allOptionUsages[u].UsageSites[j].UsageSiteDescription + }) + } + + return allOptionUsages +} + +func getOptionUsagesByOptionDescription(allOptionUsages []OptionUsages) []OptionUsagesByOptionDescription { + out := make([]OptionUsagesByOptionDescription, len(allOptionUsages)) + + for i, optionUsages := range allOptionUsages { + out[i].OptionName = optionUsages.OptionName + + if len(optionUsages.UsageSites) == 1 { + usage := allOptionUsages[i].UsageSites[0] + out[i].Usages = make([]OptionUsageByOptionDescription, 1) + out[i].Usages[0].OptionDescription = usage.Option.Description + out[i].Usages[0].UsageSites = []OptionUsageSite{usage} + + continue + } + + // map[optionDescription]OptionUsageByOptionDescription + optionUsageByOptionDescriptionMap := make(map[string]OptionUsageByOptionDescription) + + // collate on option description in each usage site + for j, usage := range optionUsages.UsageSites { + _, found := optionUsageByOptionDescriptionMap[usage.Option.Description] + if !found { + optionUsageByOptionDescriptionMap[usage.Option.Description] = OptionUsageByOptionDescription{ + OptionDescription: usage.Option.Description, + UsageSites: make([]OptionUsageSite, 0), + } + } + u := optionUsageByOptionDescriptionMap[usage.Option.Description] + u.UsageSites = append(u.UsageSites, allOptionUsages[i].UsageSites[j]) + + // put all distinct option descriptions withing the option usages + optionUsageByOptionDescriptionMap[u.OptionDescription] = u + } + + out[i].Usages = make([]OptionUsageByOptionDescription, len(optionUsageByOptionDescriptionMap)) + j := 0 + for _, v := range optionUsageByOptionDescriptionMap { + out[i].Usages[j] = v + j++ + } + } + + return out +} diff --git a/temporalcli/commandsgen/parse.go b/temporalcli/commandsgen/parse.go index d4aea8764..ec0f39753 100644 --- a/temporalcli/commandsgen/parse.go +++ b/temporalcli/commandsgen/parse.go @@ -19,15 +19,16 @@ var CommandsYAML []byte type ( // Option represents the structure of an option within option sets. Option struct { - Name string `yaml:"name"` - Type string `yaml:"type"` - Description string `yaml:"description"` - Short string `yaml:"short,omitempty"` - Default string `yaml:"default,omitempty"` - Env string `yaml:"env,omitempty"` - Required bool `yaml:"required,omitempty"` - Aliases []string `yaml:"aliases,omitempty"` - EnumValues []string `yaml:"enum-values,omitempty"` + Name string `yaml:"name"` + Type string `yaml:"type"` + Description string `yaml:"description"` + Short string `yaml:"short,omitempty"` + Default string `yaml:"default,omitempty"` + Env string `yaml:"env,omitempty"` + Required bool `yaml:"required,omitempty"` + Aliases []string `yaml:"aliases,omitempty"` + EnumValues []string `yaml:"enum-values,omitempty"` + Experimental bool `yaml:"experimental,omitempty"` } // Command represents the structure of each command in the commands map. @@ -45,6 +46,15 @@ type ( Options []Option `yaml:"options"` OptionSets []string `yaml:"option-sets"` Docs Docs `yaml:"docs"` + Index int + Base *Command + Parent *Command + Children []*Command + Depth int + FileName string + SubCommandName string + LeafName string + MaxChildDepth int } // Docs represents docs-only information that is not used in CLI generation. @@ -55,15 +65,50 @@ type ( // OptionSets represents the structure of option sets. OptionSets struct { - Name string `yaml:"name"` - Options []Option `yaml:"options"` + Name string `yaml:"name"` + Description string `yaml:"description"` + Options []Option `yaml:"options"` } // Commands represents the top-level structure holding commands and option sets. Commands struct { CommandList []Command `yaml:"commands"` OptionSets []OptionSets `yaml:"option-sets"` + Usages Usages + } + + Usages struct { + OptionUsages []OptionUsages + OptionUsagesByOptionDescription []OptionUsagesByOptionDescription + } + + OptionUsages struct { + OptionName string + UsageSites []OptionUsageSite + } + + OptionUsageSite struct { + Option Option + UsageSiteDescription string + UsageSiteType UsageSiteType } + + UsageSiteType string + + OptionUsagesByOptionDescription struct { + OptionName string + Usages []OptionUsageByOptionDescription + } + + OptionUsageByOptionDescription struct { + OptionDescription string + UsageSites []OptionUsageSite + } +) + +const ( + UsageTypeCommand UsageSiteType = "command" + UsageTypeOptionSet UsageSiteType = "optionset" ) func ParseCommands() (Commands, error) { @@ -87,6 +132,7 @@ func ParseCommands() (Commands, error) { return Commands{}, fmt.Errorf("failed parsing command section %q: %w", command.FullName, err) } } + return m, nil } diff --git a/temporalcli/internal/cmd/gen-docs/main.go b/temporalcli/internal/cmd/gen-docs/main.go index 94ff20ba4..26b77c79f 100644 --- a/temporalcli/internal/cmd/gen-docs/main.go +++ b/temporalcli/internal/cmd/gen-docs/main.go @@ -32,6 +32,11 @@ func run() error { return fmt.Errorf("failed parsing markdown: %w", err) } + cmds, err = commandsgen.EnrichCommands(cmds) + if err != nil { + return fmt.Errorf("failed enriching commands: %w", err) + } + // Generate docs b, err := commandsgen.GenerateDocsFiles(cmds) if err != nil { From 0ea8ba2755517f5ab8e7cb290b0f09a2cf00822f Mon Sep 17 00:00:00 2001 From: Phil Prasek Date: Wed, 16 Oct 2024 17:29:42 -0700 Subject: [PATCH 2/8] address feedback Signed-off-by: Phil Prasek --- temporalcli/commandsgen/enrich.go | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) diff --git a/temporalcli/commandsgen/enrich.go b/temporalcli/commandsgen/enrich.go index afa56d4fe..40e84192f 100644 --- a/temporalcli/commandsgen/enrich.go +++ b/temporalcli/commandsgen/enrich.go @@ -8,6 +8,9 @@ import ( "strings" ) +// EnrichCommands populates additional fields on Commands +// beyond those read from commands.yml to make it easier +// for docs.go to generate docs func EnrichCommands(m Commands) (Commands, error) { commandLookup := make(map[string]*Command) @@ -41,7 +44,6 @@ func EnrichCommands(m Commands) (Commands, error) { continue } - //fmt.Printf("add child: %s\n", m.CommandList[c.Index].FullName) m.CommandList[c.Parent.Index].Children = append(m.CommandList[c.Parent.Index].Children, &m.CommandList[c.Index]) base := &c @@ -105,13 +107,6 @@ func collectCommandVisitor(c Command, m *Commands) { func sortChildrenVisitor(c *Command) { sort.Slice(c.Children, func(i, j int) bool { - //option to put nested commands at end of the list - /* - if c.Children[i].MaxChildDepth != c.Children[j].MaxChildDepth { - return c.Children[i].MaxChildDepth < c.Children[j].MaxChildDepth - } - */ - return c.Children[i].FullName < c.Children[j].FullName }) for _, command := range c.Children { @@ -136,7 +131,6 @@ func setMaxChildDepthVisitor(c Command, commands *Commands) int { } func getAllOptionUsages(commands Commands) []OptionUsages { - // map[optionName]map[usageSite]OptionUsageSite var optionUsageSitesMap = make(map[string]map[string]OptionUsageSite) // option sets @@ -221,7 +215,6 @@ func getOptionUsagesByOptionDescription(allOptionUsages []OptionUsages) []Option continue } - // map[optionDescription]OptionUsageByOptionDescription optionUsageByOptionDescriptionMap := make(map[string]OptionUsageByOptionDescription) // collate on option description in each usage site From af506a3b73687c0c3ce57deed539f6b359164f5a Mon Sep 17 00:00:00 2001 From: Phil Prasek Date: Tue, 29 Oct 2024 19:43:16 -0700 Subject: [PATCH 3/8] move command enrichments to parse.go Signed-off-by: Phil Prasek --- temporalcli/commandsgen/enrich.go | 245 ---------------------- temporalcli/commandsgen/parse.go | 239 ++++++++++++++++++++- temporalcli/internal/cmd/gen-docs/main.go | 5 - 3 files changed, 238 insertions(+), 251 deletions(-) delete mode 100644 temporalcli/commandsgen/enrich.go diff --git a/temporalcli/commandsgen/enrich.go b/temporalcli/commandsgen/enrich.go deleted file mode 100644 index 40e84192f..000000000 --- a/temporalcli/commandsgen/enrich.go +++ /dev/null @@ -1,245 +0,0 @@ -// Package commandsgen is built to read the YAML format described in -// temporalcli/commandsgen/commands.yml and generate code from it. -package commandsgen - -import ( - _ "embed" - "sort" - "strings" -) - -// EnrichCommands populates additional fields on Commands -// beyond those read from commands.yml to make it easier -// for docs.go to generate docs -func EnrichCommands(m Commands) (Commands, error) { - commandLookup := make(map[string]*Command) - - for i, command := range m.CommandList { - m.CommandList[i].Index = i - commandLookup[command.FullName] = &m.CommandList[i] - } - - var rootCommand *Command - - //populate parent and basic meta - for i, c := range m.CommandList { - commandLength := len(strings.Split(c.FullName, " ")) - if commandLength == 1 { - rootCommand = &m.CommandList[i] - continue - } - parentName := strings.Join(strings.Split(c.FullName, " ")[:commandLength-1], " ") - parent, ok := commandLookup[parentName] - if ok { - m.CommandList[i].Parent = &m.CommandList[parent.Index] - m.CommandList[i].Depth = len(strings.Split(c.FullName, " ")) - 1 - m.CommandList[i].FileName = strings.Split(c.FullName, " ")[1] - m.CommandList[i].LeafName = strings.Join(strings.Split(c.FullName, " ")[m.CommandList[i].Depth:], "") - } - } - - //populate children and base command - for _, c := range m.CommandList { - if c.Parent == nil { - continue - } - - m.CommandList[c.Parent.Index].Children = append(m.CommandList[c.Parent.Index].Children, &m.CommandList[c.Index]) - - base := &c - for base.Depth > 1 { - base = base.Parent - } - m.CommandList[c.Index].Base = &m.CommandList[base.Index] - } - - setMaxChildDepthVisitor(*rootCommand, &m) - - for i, c := range m.CommandList { - if c.Parent == nil { - continue - } - - subCommandStartDepth := 1 - if c.Base.MaxChildDepth > 2 { - subCommandStartDepth = 2 - } - - subCommandName := "" - if c.Depth >= subCommandStartDepth { - subCommandName = strings.Join(strings.Split(c.FullName, " ")[subCommandStartDepth:], " ") - } - - if len(subCommandName) == 0 && c.Depth == 1 { - // for operator base command to show up in tags, keywords, etc. - subCommandName = c.LeafName - } - - m.CommandList[i].SubCommandName = subCommandName - } - - // sorted ascending by full name of command (activity complete, batch list, etc) - sortChildrenVisitor(rootCommand) - - // pull flat list in same order as sorted children - m.CommandList = make([]Command, 0) - collectCommandVisitor(*rootCommand, &m) - - // option usages - optionUsages := getAllOptionUsages(m) - optionUsagesByOptionDescription := getOptionUsagesByOptionDescription(optionUsages) - m.Usages = Usages{ - OptionUsages: optionUsages, - OptionUsagesByOptionDescription: optionUsagesByOptionDescription, - } - - return m, nil -} - -func collectCommandVisitor(c Command, m *Commands) { - - m.CommandList = append(m.CommandList, c) - - for _, child := range c.Children { - collectCommandVisitor(*child, m) - } -} - -func sortChildrenVisitor(c *Command) { - sort.Slice(c.Children, func(i, j int) bool { - return c.Children[i].FullName < c.Children[j].FullName - }) - for _, command := range c.Children { - sortChildrenVisitor(command) - } -} - -func setMaxChildDepthVisitor(c Command, commands *Commands) int { - maxChildDepth := 0 - children := commands.CommandList[c.Index].Children - if len(children) > 0 { - for _, child := range children { - depth := setMaxChildDepthVisitor(*child, commands) - if depth > maxChildDepth { - maxChildDepth = depth - } - } - } - - commands.CommandList[c.Index].MaxChildDepth = maxChildDepth - return maxChildDepth + 1 -} - -func getAllOptionUsages(commands Commands) []OptionUsages { - var optionUsageSitesMap = make(map[string]map[string]OptionUsageSite) - - // option sets - for i, optionSet := range commands.OptionSets { - usage := optionSet.Description - if len(usage) == 0 { - usage = optionSet.Name - } - - for j, option := range optionSet.Options { - _, found := optionUsageSitesMap[option.Name] - if !found { - optionUsageSitesMap[option.Name] = make(map[string]OptionUsageSite) - } - optionUsageSitesMap[option.Name][optionSet.Name] = OptionUsageSite{ - Option: commands.OptionSets[i].Options[j], - UsageSiteDescription: usage, - UsageSiteType: UsageTypeOptionSet, - } - } - } - - //command options - for i, cmd := range commands.CommandList { - usage := cmd.FullName - if len(usage) == 0 { - usage = cmd.FullName - } - - for j, option := range cmd.Options { - _, found := optionUsageSitesMap[option.Name] - if !found { - optionUsageSitesMap[option.Name] = make(map[string]OptionUsageSite) - } - optionUsageSitesMap[option.Name][cmd.FullName] = OptionUsageSite{ - Option: commands.CommandList[i].Options[j], - UsageSiteDescription: usage, - UsageSiteType: UsageTypeOptionSet, - } - } - } - - // all options - var allOptionUsages = make([]OptionUsages, 0) - - for optionName, usages := range optionUsageSitesMap { - option := OptionUsages{ - OptionName: optionName, - UsageSites: make([]OptionUsageSite, 0), - } - for _, usage := range usages { - option.UsageSites = append(option.UsageSites, usage) - } - allOptionUsages = append(allOptionUsages, option) - } - - sort.Slice(allOptionUsages, func(i, j int) bool { - return allOptionUsages[i].OptionName < allOptionUsages[j].OptionName - }) - - for u := range allOptionUsages { - sort.Slice(allOptionUsages[u].UsageSites, func(i, j int) bool { - return allOptionUsages[u].UsageSites[i].UsageSiteDescription < allOptionUsages[u].UsageSites[j].UsageSiteDescription - }) - } - - return allOptionUsages -} - -func getOptionUsagesByOptionDescription(allOptionUsages []OptionUsages) []OptionUsagesByOptionDescription { - out := make([]OptionUsagesByOptionDescription, len(allOptionUsages)) - - for i, optionUsages := range allOptionUsages { - out[i].OptionName = optionUsages.OptionName - - if len(optionUsages.UsageSites) == 1 { - usage := allOptionUsages[i].UsageSites[0] - out[i].Usages = make([]OptionUsageByOptionDescription, 1) - out[i].Usages[0].OptionDescription = usage.Option.Description - out[i].Usages[0].UsageSites = []OptionUsageSite{usage} - - continue - } - - optionUsageByOptionDescriptionMap := make(map[string]OptionUsageByOptionDescription) - - // collate on option description in each usage site - for j, usage := range optionUsages.UsageSites { - _, found := optionUsageByOptionDescriptionMap[usage.Option.Description] - if !found { - optionUsageByOptionDescriptionMap[usage.Option.Description] = OptionUsageByOptionDescription{ - OptionDescription: usage.Option.Description, - UsageSites: make([]OptionUsageSite, 0), - } - } - u := optionUsageByOptionDescriptionMap[usage.Option.Description] - u.UsageSites = append(u.UsageSites, allOptionUsages[i].UsageSites[j]) - - // put all distinct option descriptions withing the option usages - optionUsageByOptionDescriptionMap[u.OptionDescription] = u - } - - out[i].Usages = make([]OptionUsageByOptionDescription, len(optionUsageByOptionDescriptionMap)) - j := 0 - for _, v := range optionUsageByOptionDescriptionMap { - out[i].Usages[j] = v - j++ - } - } - - return out -} diff --git a/temporalcli/commandsgen/parse.go b/temporalcli/commandsgen/parse.go index ec0f39753..68bb3397c 100644 --- a/temporalcli/commandsgen/parse.go +++ b/temporalcli/commandsgen/parse.go @@ -8,6 +8,7 @@ import ( "fmt" "regexp" "slices" + "sort" "strings" "gopkg.in/yaml.v3" @@ -133,7 +134,7 @@ func ParseCommands() (Commands, error) { } } - return m, nil + return enrichCommands(m) } var markdownLinkPattern = regexp.MustCompile(`\[(.*?)\]\((.*?)\)`) @@ -252,3 +253,239 @@ func (o *Option) processSection() error { } return nil } + +// EnrichCommands populates additional fields on Commands +// beyond those read from commands.yml to make it easier +// for docs.go to generate docs +func enrichCommands(m Commands) (Commands, error) { + commandLookup := make(map[string]*Command) + + for i, command := range m.CommandList { + m.CommandList[i].Index = i + commandLookup[command.FullName] = &m.CommandList[i] + } + + var rootCommand *Command + + //populate parent and basic meta + for i, c := range m.CommandList { + commandLength := len(strings.Split(c.FullName, " ")) + if commandLength == 1 { + rootCommand = &m.CommandList[i] + continue + } + parentName := strings.Join(strings.Split(c.FullName, " ")[:commandLength-1], " ") + parent, ok := commandLookup[parentName] + if ok { + m.CommandList[i].Parent = &m.CommandList[parent.Index] + m.CommandList[i].Depth = len(strings.Split(c.FullName, " ")) - 1 + m.CommandList[i].FileName = strings.Split(c.FullName, " ")[1] + m.CommandList[i].LeafName = strings.Join(strings.Split(c.FullName, " ")[m.CommandList[i].Depth:], "") + } + } + + //populate children and base command + for _, c := range m.CommandList { + if c.Parent == nil { + continue + } + + m.CommandList[c.Parent.Index].Children = append(m.CommandList[c.Parent.Index].Children, &m.CommandList[c.Index]) + + base := &c + for base.Depth > 1 { + base = base.Parent + } + m.CommandList[c.Index].Base = &m.CommandList[base.Index] + } + + setMaxChildDepthVisitor(*rootCommand, &m) + + for i, c := range m.CommandList { + if c.Parent == nil { + continue + } + + subCommandStartDepth := 1 + if c.Base.MaxChildDepth > 2 { + subCommandStartDepth = 2 + } + + subCommandName := "" + if c.Depth >= subCommandStartDepth { + subCommandName = strings.Join(strings.Split(c.FullName, " ")[subCommandStartDepth:], " ") + } + + if len(subCommandName) == 0 && c.Depth == 1 { + // for operator base command to show up in tags, keywords, etc. + subCommandName = c.LeafName + } + + m.CommandList[i].SubCommandName = subCommandName + } + + // sorted ascending by full name of command (activity complete, batch list, etc) + sortChildrenVisitor(rootCommand) + + // pull flat list in same order as sorted children + m.CommandList = make([]Command, 0) + collectCommandVisitor(*rootCommand, &m) + + // option usages + optionUsages := getAllOptionUsages(m) + optionUsagesByOptionDescription := getOptionUsagesByOptionDescription(optionUsages) + m.Usages = Usages{ + OptionUsages: optionUsages, + OptionUsagesByOptionDescription: optionUsagesByOptionDescription, + } + + return m, nil +} + +func collectCommandVisitor(c Command, m *Commands) { + + m.CommandList = append(m.CommandList, c) + + for _, child := range c.Children { + collectCommandVisitor(*child, m) + } +} + +func sortChildrenVisitor(c *Command) { + sort.Slice(c.Children, func(i, j int) bool { + return c.Children[i].FullName < c.Children[j].FullName + }) + for _, command := range c.Children { + sortChildrenVisitor(command) + } +} + +func setMaxChildDepthVisitor(c Command, commands *Commands) int { + maxChildDepth := 0 + children := commands.CommandList[c.Index].Children + if len(children) > 0 { + for _, child := range children { + depth := setMaxChildDepthVisitor(*child, commands) + if depth > maxChildDepth { + maxChildDepth = depth + } + } + } + + commands.CommandList[c.Index].MaxChildDepth = maxChildDepth + return maxChildDepth + 1 +} + +func getAllOptionUsages(commands Commands) []OptionUsages { + var optionUsageSitesMap = make(map[string]map[string]OptionUsageSite) + + // option sets + for i, optionSet := range commands.OptionSets { + usage := optionSet.Description + if len(usage) == 0 { + usage = optionSet.Name + } + + for j, option := range optionSet.Options { + _, found := optionUsageSitesMap[option.Name] + if !found { + optionUsageSitesMap[option.Name] = make(map[string]OptionUsageSite) + } + optionUsageSitesMap[option.Name][optionSet.Name] = OptionUsageSite{ + Option: commands.OptionSets[i].Options[j], + UsageSiteDescription: usage, + UsageSiteType: UsageTypeOptionSet, + } + } + } + + //command options + for i, cmd := range commands.CommandList { + usage := cmd.FullName + if len(usage) == 0 { + usage = cmd.FullName + } + + for j, option := range cmd.Options { + _, found := optionUsageSitesMap[option.Name] + if !found { + optionUsageSitesMap[option.Name] = make(map[string]OptionUsageSite) + } + optionUsageSitesMap[option.Name][cmd.FullName] = OptionUsageSite{ + Option: commands.CommandList[i].Options[j], + UsageSiteDescription: usage, + UsageSiteType: UsageTypeOptionSet, + } + } + } + + // all options + var allOptionUsages = make([]OptionUsages, 0) + + for optionName, usages := range optionUsageSitesMap { + option := OptionUsages{ + OptionName: optionName, + UsageSites: make([]OptionUsageSite, 0), + } + for _, usage := range usages { + option.UsageSites = append(option.UsageSites, usage) + } + allOptionUsages = append(allOptionUsages, option) + } + + sort.Slice(allOptionUsages, func(i, j int) bool { + return allOptionUsages[i].OptionName < allOptionUsages[j].OptionName + }) + + for u := range allOptionUsages { + sort.Slice(allOptionUsages[u].UsageSites, func(i, j int) bool { + return allOptionUsages[u].UsageSites[i].UsageSiteDescription < allOptionUsages[u].UsageSites[j].UsageSiteDescription + }) + } + + return allOptionUsages +} + +func getOptionUsagesByOptionDescription(allOptionUsages []OptionUsages) []OptionUsagesByOptionDescription { + out := make([]OptionUsagesByOptionDescription, len(allOptionUsages)) + + for i, optionUsages := range allOptionUsages { + out[i].OptionName = optionUsages.OptionName + + if len(optionUsages.UsageSites) == 1 { + usage := allOptionUsages[i].UsageSites[0] + out[i].Usages = make([]OptionUsageByOptionDescription, 1) + out[i].Usages[0].OptionDescription = usage.Option.Description + out[i].Usages[0].UsageSites = []OptionUsageSite{usage} + + continue + } + + optionUsageByOptionDescriptionMap := make(map[string]OptionUsageByOptionDescription) + + // collate on option description in each usage site + for j, usage := range optionUsages.UsageSites { + _, found := optionUsageByOptionDescriptionMap[usage.Option.Description] + if !found { + optionUsageByOptionDescriptionMap[usage.Option.Description] = OptionUsageByOptionDescription{ + OptionDescription: usage.Option.Description, + UsageSites: make([]OptionUsageSite, 0), + } + } + u := optionUsageByOptionDescriptionMap[usage.Option.Description] + u.UsageSites = append(u.UsageSites, allOptionUsages[i].UsageSites[j]) + + // put all distinct option descriptions withing the option usages + optionUsageByOptionDescriptionMap[u.OptionDescription] = u + } + + out[i].Usages = make([]OptionUsageByOptionDescription, len(optionUsageByOptionDescriptionMap)) + j := 0 + for _, v := range optionUsageByOptionDescriptionMap { + out[i].Usages[j] = v + j++ + } + } + + return out +} diff --git a/temporalcli/internal/cmd/gen-docs/main.go b/temporalcli/internal/cmd/gen-docs/main.go index 26b77c79f..94ff20ba4 100644 --- a/temporalcli/internal/cmd/gen-docs/main.go +++ b/temporalcli/internal/cmd/gen-docs/main.go @@ -32,11 +32,6 @@ func run() error { return fmt.Errorf("failed parsing markdown: %w", err) } - cmds, err = commandsgen.EnrichCommands(cmds) - if err != nil { - return fmt.Errorf("failed enriching commands: %w", err) - } - // Generate docs b, err := commandsgen.GenerateDocsFiles(cmds) if err != nil { From 039ccce887135684e801af399a6f33987a5e89f6 Mon Sep 17 00:00:00 2001 From: Phil Prasek Date: Tue, 29 Oct 2024 21:07:12 -0700 Subject: [PATCH 4/8] remove `cmd-options.mdx` enhancements and render cleaner inline options Signed-off-by: Phil Prasek --- temporalcli/commandsgen/docs.go | 69 ++++++++----- temporalcli/commandsgen/parse.go | 164 ++----------------------------- 2 files changed, 49 insertions(+), 184 deletions(-) diff --git a/temporalcli/commandsgen/docs.go b/temporalcli/commandsgen/docs.go index e7fef0cec..f71f7ac61 100644 --- a/temporalcli/commandsgen/docs.go +++ b/temporalcli/commandsgen/docs.go @@ -3,23 +3,24 @@ package commandsgen import ( "bytes" "fmt" + "regexp" "sort" "strings" ) -type DocsFile struct { - FileName string -} - func GenerateDocsFiles(commands Commands) (map[string][]byte, error) { + optionSetMap := make(map[string]OptionSets) for i, optionSet := range commands.OptionSets { optionSetMap[optionSet.Name] = commands.OptionSets[i] } - w := &docWriter{fileMap: make(map[string]*bytes.Buffer), optionSetMap: optionSetMap} + w := &docWriter{ + fileMap: make(map[string]*bytes.Buffer), + optionSetMap: optionSetMap, + } - // sort by parent command (activity, batch, etc) + // sorted ascending by full name of command (activity complete, batch list, etc) for _, cmd := range commands.CommandList { if err := cmd.writeDoc(w); err != nil { return nil, fmt.Errorf("failed writing docs for command %s: %w", cmd.FullName, err) @@ -41,20 +42,19 @@ type docWriter struct { } func (c *Command) writeDoc(w *docWriter) error { - commandLength := len(strings.Split(c.FullName, " ")) w.processOptions(c) // If this is a root command, write a new file - if commandLength == 2 { + if c.Depth == 1 { w.writeCommand(c) - } else if commandLength > 2 { + } else if c.Depth > 1 { w.writeSubcommand(c) } return nil } func (w *docWriter) writeCommand(c *Command) { - fileName := strings.Split(c.FullName, " ")[1] + fileName := c.FileName w.fileMap[fileName] = &bytes.Buffer{} w.fileMap[fileName].WriteString("---\n") w.fileMap[fileName].WriteString("id: " + fileName + "\n") @@ -62,6 +62,7 @@ func (w *docWriter) writeCommand(c *Command) { w.fileMap[fileName].WriteString("sidebar_label: " + c.FullName + "\n") w.fileMap[fileName].WriteString("description: " + c.Docs.DescriptionHeader + "\n") w.fileMap[fileName].WriteString("toc_max_heading_level: 4\n") + w.fileMap[fileName].WriteString("keywords:\n") for _, keyword := range c.Docs.Keywords { w.fileMap[fileName].WriteString(" - " + keyword + "\n") @@ -75,27 +76,32 @@ func (w *docWriter) writeCommand(c *Command) { } func (w *docWriter) writeSubcommand(c *Command) { - fileName := strings.Split(c.FullName, " ")[1] - subCommand := strings.Join(strings.Split(c.FullName, " ")[2:], "") - w.fileMap[fileName].WriteString("## " + subCommand + "\n\n") + fileName := c.FileName + prefix := strings.Repeat("#", c.Depth) + w.fileMap[fileName].WriteString(prefix + " " + c.LeafName + "\n\n") w.fileMap[fileName].WriteString(c.Description + "\n\n") - w.fileMap[fileName].WriteString("Use the following options to change the behavior of this command.\n\n") - // gather options from command and all options aviailable from parent commands - var allOptions = make([]Option, 0) - for _, options := range w.optionsStack { - allOptions = append(allOptions, options...) - } + if c.IsLeafCommand { + w.fileMap[fileName].WriteString("Use the following options to change the behavior of this command.\n\n") - // alphabetize options - sort.Slice(allOptions, func(i, j int) bool { - return allOptions[i].Name < allOptions[j].Name - }) - - for _, option := range allOptions { - w.fileMap[fileName].WriteString(fmt.Sprintf("## %s\n\n", option.Name)) - w.fileMap[fileName].WriteString(option.Description + "\n\n") + // gather options from command and all options aviailable from parent commands + var allOptions = make([]Option, 0) + for _, options := range w.optionsStack { + allOptions = append(allOptions, options...) + } + // alphabetize options + sort.Slice(allOptions, func(i, j int) bool { + return allOptions[i].Name < allOptions[j].Name + }) + + for _, option := range allOptions { + w.fileMap[c.FileName].WriteString(fmt.Sprintf("**--%s**\n\n", option.Name)) + w.fileMap[c.FileName].WriteString(encodeJSONExample(option.Description) + "\n\n") + if len(option.Short) > 0 { + w.fileMap[c.FileName].WriteString("Alias: `" + option.Short + "`\n\n") + } + } } } @@ -115,3 +121,12 @@ func (w *docWriter) processOptions(c *Command) { w.optionsStack = append(w.optionsStack, options) } + +func encodeJSONExample(v string) string { + // example: 'YourKey={"your": "value"}' + // results in an mdx acorn rendering error + // and wrapping in backticks lets it render + re := regexp.MustCompile(`('[a-zA-Z0-9]*={.*}')`) + v = re.ReplaceAllString(v, "`$1`") + return v +} diff --git a/temporalcli/commandsgen/parse.go b/temporalcli/commandsgen/parse.go index 68bb3397c..93fcd5f48 100644 --- a/temporalcli/commandsgen/parse.go +++ b/temporalcli/commandsgen/parse.go @@ -54,6 +54,7 @@ type ( Depth int FileName string SubCommandName string + IsLeafCommand bool LeafName string MaxChildDepth int } @@ -75,43 +76,9 @@ type ( Commands struct { CommandList []Command `yaml:"commands"` OptionSets []OptionSets `yaml:"option-sets"` - Usages Usages - } - - Usages struct { - OptionUsages []OptionUsages - OptionUsagesByOptionDescription []OptionUsagesByOptionDescription - } - - OptionUsages struct { - OptionName string - UsageSites []OptionUsageSite - } - - OptionUsageSite struct { - Option Option - UsageSiteDescription string - UsageSiteType UsageSiteType - } - - UsageSiteType string - - OptionUsagesByOptionDescription struct { - OptionName string - Usages []OptionUsageByOptionDescription - } - - OptionUsageByOptionDescription struct { - OptionDescription string - UsageSites []OptionUsageSite } ) -const ( - UsageTypeCommand UsageSiteType = "command" - UsageTypeOptionSet UsageSiteType = "optionset" -) - func ParseCommands() (Commands, error) { // Fix CRLF md := bytes.ReplaceAll(CommandsYAML, []byte("\r\n"), []byte("\n")) @@ -254,7 +221,7 @@ func (o *Option) processSection() error { return nil } -// EnrichCommands populates additional fields on Commands +// enrichCommands populates additional fields on Commands // beyond those read from commands.yml to make it easier // for docs.go to generate docs func enrichCommands(m Commands) (Commands, error) { @@ -322,6 +289,11 @@ func enrichCommands(m Commands) (Commands, error) { } m.CommandList[i].SubCommandName = subCommandName + + if len(c.Children) == 0 { + m.CommandList[i].IsLeafCommand = true + } + } // sorted ascending by full name of command (activity complete, batch list, etc) @@ -331,14 +303,6 @@ func enrichCommands(m Commands) (Commands, error) { m.CommandList = make([]Command, 0) collectCommandVisitor(*rootCommand, &m) - // option usages - optionUsages := getAllOptionUsages(m) - optionUsagesByOptionDescription := getOptionUsagesByOptionDescription(optionUsages) - m.Usages = Usages{ - OptionUsages: optionUsages, - OptionUsagesByOptionDescription: optionUsagesByOptionDescription, - } - return m, nil } @@ -375,117 +339,3 @@ func setMaxChildDepthVisitor(c Command, commands *Commands) int { commands.CommandList[c.Index].MaxChildDepth = maxChildDepth return maxChildDepth + 1 } - -func getAllOptionUsages(commands Commands) []OptionUsages { - var optionUsageSitesMap = make(map[string]map[string]OptionUsageSite) - - // option sets - for i, optionSet := range commands.OptionSets { - usage := optionSet.Description - if len(usage) == 0 { - usage = optionSet.Name - } - - for j, option := range optionSet.Options { - _, found := optionUsageSitesMap[option.Name] - if !found { - optionUsageSitesMap[option.Name] = make(map[string]OptionUsageSite) - } - optionUsageSitesMap[option.Name][optionSet.Name] = OptionUsageSite{ - Option: commands.OptionSets[i].Options[j], - UsageSiteDescription: usage, - UsageSiteType: UsageTypeOptionSet, - } - } - } - - //command options - for i, cmd := range commands.CommandList { - usage := cmd.FullName - if len(usage) == 0 { - usage = cmd.FullName - } - - for j, option := range cmd.Options { - _, found := optionUsageSitesMap[option.Name] - if !found { - optionUsageSitesMap[option.Name] = make(map[string]OptionUsageSite) - } - optionUsageSitesMap[option.Name][cmd.FullName] = OptionUsageSite{ - Option: commands.CommandList[i].Options[j], - UsageSiteDescription: usage, - UsageSiteType: UsageTypeOptionSet, - } - } - } - - // all options - var allOptionUsages = make([]OptionUsages, 0) - - for optionName, usages := range optionUsageSitesMap { - option := OptionUsages{ - OptionName: optionName, - UsageSites: make([]OptionUsageSite, 0), - } - for _, usage := range usages { - option.UsageSites = append(option.UsageSites, usage) - } - allOptionUsages = append(allOptionUsages, option) - } - - sort.Slice(allOptionUsages, func(i, j int) bool { - return allOptionUsages[i].OptionName < allOptionUsages[j].OptionName - }) - - for u := range allOptionUsages { - sort.Slice(allOptionUsages[u].UsageSites, func(i, j int) bool { - return allOptionUsages[u].UsageSites[i].UsageSiteDescription < allOptionUsages[u].UsageSites[j].UsageSiteDescription - }) - } - - return allOptionUsages -} - -func getOptionUsagesByOptionDescription(allOptionUsages []OptionUsages) []OptionUsagesByOptionDescription { - out := make([]OptionUsagesByOptionDescription, len(allOptionUsages)) - - for i, optionUsages := range allOptionUsages { - out[i].OptionName = optionUsages.OptionName - - if len(optionUsages.UsageSites) == 1 { - usage := allOptionUsages[i].UsageSites[0] - out[i].Usages = make([]OptionUsageByOptionDescription, 1) - out[i].Usages[0].OptionDescription = usage.Option.Description - out[i].Usages[0].UsageSites = []OptionUsageSite{usage} - - continue - } - - optionUsageByOptionDescriptionMap := make(map[string]OptionUsageByOptionDescription) - - // collate on option description in each usage site - for j, usage := range optionUsages.UsageSites { - _, found := optionUsageByOptionDescriptionMap[usage.Option.Description] - if !found { - optionUsageByOptionDescriptionMap[usage.Option.Description] = OptionUsageByOptionDescription{ - OptionDescription: usage.Option.Description, - UsageSites: make([]OptionUsageSite, 0), - } - } - u := optionUsageByOptionDescriptionMap[usage.Option.Description] - u.UsageSites = append(u.UsageSites, allOptionUsages[i].UsageSites[j]) - - // put all distinct option descriptions withing the option usages - optionUsageByOptionDescriptionMap[u.OptionDescription] = u - } - - out[i].Usages = make([]OptionUsageByOptionDescription, len(optionUsageByOptionDescriptionMap)) - j := 0 - for _, v := range optionUsageByOptionDescriptionMap { - out[i].Usages[j] = v - j++ - } - } - - return out -} From 2eae5996cb21f0553b10149534d79cff71da5fc1 Mon Sep 17 00:00:00 2001 From: Phil Prasek Date: Wed, 30 Oct 2024 11:35:31 -0700 Subject: [PATCH 5/8] address feedback and add experimental option rendering Signed-off-by: Phil Prasek --- temporalcli/commandsgen/docs.go | 12 ++++++- temporalcli/commandsgen/parse.go | 56 -------------------------------- 2 files changed, 11 insertions(+), 57 deletions(-) diff --git a/temporalcli/commandsgen/docs.go b/temporalcli/commandsgen/docs.go index f71f7ac61..8742d7c65 100644 --- a/temporalcli/commandsgen/docs.go +++ b/temporalcli/commandsgen/docs.go @@ -81,7 +81,7 @@ func (w *docWriter) writeSubcommand(c *Command) { w.fileMap[fileName].WriteString(prefix + " " + c.LeafName + "\n\n") w.fileMap[fileName].WriteString(c.Description + "\n\n") - if c.IsLeafCommand { + if isLeafCommand(c) { w.fileMap[fileName].WriteString("Use the following options to change the behavior of this command.\n\n") // gather options from command and all options aviailable from parent commands @@ -101,6 +101,12 @@ func (w *docWriter) writeSubcommand(c *Command) { if len(option.Short) > 0 { w.fileMap[c.FileName].WriteString("Alias: `" + option.Short + "`\n\n") } + + if option.Experimental { + w.fileMap[fileName].WriteString(":::note" + "\n\n") + w.fileMap[fileName].WriteString("Option is experimental." + "\n\n") + w.fileMap[fileName].WriteString(":::" + "\n\n") + } } } } @@ -130,3 +136,7 @@ func encodeJSONExample(v string) string { v = re.ReplaceAllString(v, "`$1`") return v } + +func isLeafCommand(c *Command) bool { + return len(c.Children) == 0 +} diff --git a/temporalcli/commandsgen/parse.go b/temporalcli/commandsgen/parse.go index 93fcd5f48..96cea1399 100644 --- a/temporalcli/commandsgen/parse.go +++ b/temporalcli/commandsgen/parse.go @@ -48,15 +48,11 @@ type ( OptionSets []string `yaml:"option-sets"` Docs Docs `yaml:"docs"` Index int - Base *Command Parent *Command Children []*Command Depth int FileName string - SubCommandName string - IsLeafCommand bool LeafName string - MaxChildDepth int } // Docs represents docs-only information that is not used in CLI generation. @@ -258,42 +254,6 @@ func enrichCommands(m Commands) (Commands, error) { } m.CommandList[c.Parent.Index].Children = append(m.CommandList[c.Parent.Index].Children, &m.CommandList[c.Index]) - - base := &c - for base.Depth > 1 { - base = base.Parent - } - m.CommandList[c.Index].Base = &m.CommandList[base.Index] - } - - setMaxChildDepthVisitor(*rootCommand, &m) - - for i, c := range m.CommandList { - if c.Parent == nil { - continue - } - - subCommandStartDepth := 1 - if c.Base.MaxChildDepth > 2 { - subCommandStartDepth = 2 - } - - subCommandName := "" - if c.Depth >= subCommandStartDepth { - subCommandName = strings.Join(strings.Split(c.FullName, " ")[subCommandStartDepth:], " ") - } - - if len(subCommandName) == 0 && c.Depth == 1 { - // for operator base command to show up in tags, keywords, etc. - subCommandName = c.LeafName - } - - m.CommandList[i].SubCommandName = subCommandName - - if len(c.Children) == 0 { - m.CommandList[i].IsLeafCommand = true - } - } // sorted ascending by full name of command (activity complete, batch list, etc) @@ -323,19 +283,3 @@ func sortChildrenVisitor(c *Command) { sortChildrenVisitor(command) } } - -func setMaxChildDepthVisitor(c Command, commands *Commands) int { - maxChildDepth := 0 - children := commands.CommandList[c.Index].Children - if len(children) > 0 { - for _, child := range children { - depth := setMaxChildDepthVisitor(*child, commands) - if depth > maxChildDepth { - maxChildDepth = depth - } - } - } - - commands.CommandList[c.Index].MaxChildDepth = maxChildDepth - return maxChildDepth + 1 -} From ff043de572f630d9bf4627025d265b69d1ff1532 Mon Sep 17 00:00:00 2001 From: Phil Prasek Date: Wed, 30 Oct 2024 18:53:21 -0700 Subject: [PATCH 6/8] address feedback on options rendering Signed-off-by: Phil Prasek --- temporalcli/commandsgen/docs.go | 71 +++++++++++++++++++++++---------- 1 file changed, 51 insertions(+), 20 deletions(-) diff --git a/temporalcli/commandsgen/docs.go b/temporalcli/commandsgen/docs.go index 8742d7c65..8a73a33e4 100644 --- a/temporalcli/commandsgen/docs.go +++ b/temporalcli/commandsgen/docs.go @@ -76,37 +76,68 @@ func (w *docWriter) writeCommand(c *Command) { } func (w *docWriter) writeSubcommand(c *Command) { - fileName := c.FileName prefix := strings.Repeat("#", c.Depth) - w.fileMap[fileName].WriteString(prefix + " " + c.LeafName + "\n\n") - w.fileMap[fileName].WriteString(c.Description + "\n\n") + w.fileMap[c.FileName].WriteString(prefix + " " + c.LeafName + "\n\n") + w.fileMap[c.FileName].WriteString(c.Description + "\n\n") if isLeafCommand(c) { - w.fileMap[fileName].WriteString("Use the following options to change the behavior of this command.\n\n") + w.fileMap[c.FileName].WriteString("Use the following options to change the behavior of this command.\n\n") // gather options from command and all options aviailable from parent commands - var allOptions = make([]Option, 0) - for _, options := range w.optionsStack { - allOptions = append(allOptions, options...) + var options = make([]Option, 0) + var globalOptions = make([]Option, 0) + for i, o := range w.optionsStack { + if i == len(w.optionsStack)-1 { + options = append(options, o...) + } else { + globalOptions = append(globalOptions, o...) + } } // alphabetize options - sort.Slice(allOptions, func(i, j int) bool { - return allOptions[i].Name < allOptions[j].Name + sort.Slice(options, func(i, j int) bool { + return options[i].Name < options[j].Name }) - for _, option := range allOptions { - w.fileMap[c.FileName].WriteString(fmt.Sprintf("**--%s**\n\n", option.Name)) - w.fileMap[c.FileName].WriteString(encodeJSONExample(option.Description) + "\n\n") - if len(option.Short) > 0 { - w.fileMap[c.FileName].WriteString("Alias: `" + option.Short + "`\n\n") - } + sort.Slice(globalOptions, func(i, j int) bool { + return globalOptions[i].Name < globalOptions[j].Name + }) - if option.Experimental { - w.fileMap[fileName].WriteString(":::note" + "\n\n") - w.fileMap[fileName].WriteString("Option is experimental." + "\n\n") - w.fileMap[fileName].WriteString(":::" + "\n\n") - } + w.writeOptions("Flags", options, c) + w.writeOptions("Global Flags", globalOptions, c) + + } +} + +func (w *docWriter) writeOptions(prefix string, options []Option, c *Command) { + + w.fileMap[c.FileName].WriteString(fmt.Sprintf("**%s:**\n\n", prefix)) + + for _, o := range options { + // option name and alias + w.fileMap[c.FileName].WriteString(fmt.Sprintf("**--%s** _%s_", o.Name, o.Type)) + if len(o.Short) > 0 { + w.fileMap[c.FileName].WriteString(fmt.Sprintf(", **-%s** _%s_", o.Short, o.Type)) + } + w.fileMap[c.FileName].WriteString("\n\n") + + // description + w.fileMap[c.FileName].WriteString(encodeJSONExample(o.Description)) + if o.Required { + w.fileMap[c.FileName].WriteString(" Required.") + } + if len(o.EnumValues) > 0 { + w.fileMap[c.FileName].WriteString(fmt.Sprintf(" Accepted values: %s.", strings.Join(o.EnumValues, ", "))) + } + if len(o.Default) > 0 { + w.fileMap[c.FileName].WriteString(fmt.Sprintf(` (default "%s")`, o.Default)) + } + w.fileMap[c.FileName].WriteString("\n\n") + + if o.Experimental { + w.fileMap[c.FileName].WriteString(":::note" + "\n\n") + w.fileMap[c.FileName].WriteString("Option is experimental." + "\n\n") + w.fileMap[c.FileName].WriteString(":::" + "\n\n") } } } From 6000ed753c50694aee142281be857878eb0abd65 Mon Sep 17 00:00:00 2001 From: Phil Prasek Date: Wed, 30 Oct 2024 20:36:01 -0700 Subject: [PATCH 7/8] address feedback to use derieved state to keep the model small Signed-off-by: Phil Prasek --- temporalcli/commandsgen/code.go | 4 -- temporalcli/commandsgen/docs.go | 62 ++++++++++++-------- temporalcli/commandsgen/parse.go | 99 ++++++++------------------------ 3 files changed, 63 insertions(+), 102 deletions(-) diff --git a/temporalcli/commandsgen/code.go b/temporalcli/commandsgen/code.go index 60230e2f9..604609d10 100644 --- a/temporalcli/commandsgen/code.go +++ b/temporalcli/commandsgen/code.go @@ -104,10 +104,6 @@ func (c *codeWriter) importIsatty() string { return c.importPkg("github.com/matt func (c *Command) structName() string { return namify(c.FullName, true) + "Command" } -func (c *Command) isSubCommand(maybeParent *Command) bool { - return len(c.NamePath) == len(maybeParent.NamePath)+1 && strings.HasPrefix(c.FullName, maybeParent.FullName+" ") -} - func (o *OptionSets) writeCode(w *codeWriter) error { if o.Name == "" { return fmt.Errorf("missing option set name") diff --git a/temporalcli/commandsgen/docs.go b/temporalcli/commandsgen/docs.go index 8a73a33e4..f67d99f2b 100644 --- a/temporalcli/commandsgen/docs.go +++ b/temporalcli/commandsgen/docs.go @@ -18,6 +18,7 @@ func GenerateDocsFiles(commands Commands) (map[string][]byte, error) { w := &docWriter{ fileMap: make(map[string]*bytes.Buffer), optionSetMap: optionSetMap, + allCommands: commands.CommandList, } // sorted ascending by full name of command (activity complete, batch list, etc) @@ -36,6 +37,7 @@ func GenerateDocsFiles(commands Commands) (map[string][]byte, error) { } type docWriter struct { + allCommands []Command fileMap map[string]*bytes.Buffer optionSetMap map[string]OptionSets optionsStack [][]Option @@ -45,16 +47,17 @@ func (c *Command) writeDoc(w *docWriter) error { w.processOptions(c) // If this is a root command, write a new file - if c.Depth == 1 { + depth := c.depth() + if depth == 1 { w.writeCommand(c) - } else if c.Depth > 1 { + } else if depth > 1 { w.writeSubcommand(c) } return nil } func (w *docWriter) writeCommand(c *Command) { - fileName := c.FileName + fileName := c.fileName() w.fileMap[fileName] = &bytes.Buffer{} w.fileMap[fileName].WriteString("---\n") w.fileMap[fileName].WriteString("id: " + fileName + "\n") @@ -76,12 +79,13 @@ func (w *docWriter) writeCommand(c *Command) { } func (w *docWriter) writeSubcommand(c *Command) { - prefix := strings.Repeat("#", c.Depth) - w.fileMap[c.FileName].WriteString(prefix + " " + c.LeafName + "\n\n") - w.fileMap[c.FileName].WriteString(c.Description + "\n\n") + fileName := c.fileName() + prefix := strings.Repeat("#", c.depth()) + w.fileMap[fileName].WriteString(prefix + " " + c.leafName() + "\n\n") + w.fileMap[fileName].WriteString(c.Description + "\n\n") - if isLeafCommand(c) { - w.fileMap[c.FileName].WriteString("Use the following options to change the behavior of this command.\n\n") + if w.isLeafCommand(c) { + w.fileMap[fileName].WriteString("Use the following options to change the behavior of this command.\n\n") // gather options from command and all options aviailable from parent commands var options = make([]Option, 0) @@ -110,34 +114,39 @@ func (w *docWriter) writeSubcommand(c *Command) { } func (w *docWriter) writeOptions(prefix string, options []Option, c *Command) { + if len(options) == 0 { + return + } + + fileName := c.fileName() - w.fileMap[c.FileName].WriteString(fmt.Sprintf("**%s:**\n\n", prefix)) + w.fileMap[fileName].WriteString(fmt.Sprintf("**%s:**\n\n", prefix)) for _, o := range options { // option name and alias - w.fileMap[c.FileName].WriteString(fmt.Sprintf("**--%s** _%s_", o.Name, o.Type)) + w.fileMap[fileName].WriteString(fmt.Sprintf("**--%s** _%s_", o.Name, o.Type)) if len(o.Short) > 0 { - w.fileMap[c.FileName].WriteString(fmt.Sprintf(", **-%s** _%s_", o.Short, o.Type)) + w.fileMap[fileName].WriteString(fmt.Sprintf(", **-%s** _%s_", o.Short, o.Type)) } - w.fileMap[c.FileName].WriteString("\n\n") + w.fileMap[fileName].WriteString("\n\n") // description - w.fileMap[c.FileName].WriteString(encodeJSONExample(o.Description)) + w.fileMap[fileName].WriteString(encodeJSONExample(o.Description)) if o.Required { - w.fileMap[c.FileName].WriteString(" Required.") + w.fileMap[fileName].WriteString(" Required.") } if len(o.EnumValues) > 0 { - w.fileMap[c.FileName].WriteString(fmt.Sprintf(" Accepted values: %s.", strings.Join(o.EnumValues, ", "))) + w.fileMap[fileName].WriteString(fmt.Sprintf(" Accepted values: %s.", strings.Join(o.EnumValues, ", "))) } if len(o.Default) > 0 { - w.fileMap[c.FileName].WriteString(fmt.Sprintf(` (default "%s")`, o.Default)) + w.fileMap[fileName].WriteString(fmt.Sprintf(` (default "%s")`, o.Default)) } - w.fileMap[c.FileName].WriteString("\n\n") + w.fileMap[fileName].WriteString("\n\n") if o.Experimental { - w.fileMap[c.FileName].WriteString(":::note" + "\n\n") - w.fileMap[c.FileName].WriteString("Option is experimental." + "\n\n") - w.fileMap[c.FileName].WriteString(":::" + "\n\n") + w.fileMap[fileName].WriteString(":::note" + "\n\n") + w.fileMap[fileName].WriteString("Option is experimental." + "\n\n") + w.fileMap[fileName].WriteString(":::" + "\n\n") } } } @@ -159,6 +168,15 @@ func (w *docWriter) processOptions(c *Command) { w.optionsStack = append(w.optionsStack, options) } +func (w *docWriter) isLeafCommand(c *Command) bool { + for _, maybeSubCmd := range w.allCommands { + if maybeSubCmd.isSubCommand(c) { + return false + } + } + return true +} + func encodeJSONExample(v string) string { // example: 'YourKey={"your": "value"}' // results in an mdx acorn rendering error @@ -167,7 +185,3 @@ func encodeJSONExample(v string) string { v = re.ReplaceAllString(v, "`$1`") return v } - -func isLeafCommand(c *Command) bool { - return len(c.Children) == 0 -} diff --git a/temporalcli/commandsgen/parse.go b/temporalcli/commandsgen/parse.go index 96cea1399..f4d4657fb 100644 --- a/temporalcli/commandsgen/parse.go +++ b/temporalcli/commandsgen/parse.go @@ -47,12 +47,6 @@ type ( Options []Option `yaml:"options"` OptionSets []string `yaml:"option-sets"` Docs Docs `yaml:"docs"` - Index int - Parent *Command - Children []*Command - Depth int - FileName string - LeafName string } // Docs represents docs-only information that is not used in CLI generation. @@ -97,7 +91,12 @@ func ParseCommands() (Commands, error) { } } - return enrichCommands(m) + // alphabetize commands + sort.Slice(m.CommandList, func(i, j int) bool { + return m.CommandList[i].FullName < m.CommandList[j].FullName + }) + + return m, nil } var markdownLinkPattern = regexp.MustCompile(`\[(.*?)\]\((.*?)\)`) @@ -180,6 +179,25 @@ func (c *Command) processSection() error { return nil } +func (c *Command) isSubCommand(maybeParent *Command) bool { + return len(c.NamePath) == len(maybeParent.NamePath)+1 && strings.HasPrefix(c.FullName, maybeParent.FullName+" ") +} + +func (c *Command) leafName() string { + return strings.Join(strings.Split(c.FullName, " ")[c.depth():], "") +} + +func (c *Command) fileName() string { + if c.depth() <= 0 { + return "" + } + return strings.Split(c.FullName, " ")[1] +} + +func (c *Command) depth() int { + return len(strings.Split(c.FullName, " ")) - 1 +} + func (o *Option) processSection() error { if o.Name == "" { return fmt.Errorf("missing option name") @@ -216,70 +234,3 @@ func (o *Option) processSection() error { } return nil } - -// enrichCommands populates additional fields on Commands -// beyond those read from commands.yml to make it easier -// for docs.go to generate docs -func enrichCommands(m Commands) (Commands, error) { - commandLookup := make(map[string]*Command) - - for i, command := range m.CommandList { - m.CommandList[i].Index = i - commandLookup[command.FullName] = &m.CommandList[i] - } - - var rootCommand *Command - - //populate parent and basic meta - for i, c := range m.CommandList { - commandLength := len(strings.Split(c.FullName, " ")) - if commandLength == 1 { - rootCommand = &m.CommandList[i] - continue - } - parentName := strings.Join(strings.Split(c.FullName, " ")[:commandLength-1], " ") - parent, ok := commandLookup[parentName] - if ok { - m.CommandList[i].Parent = &m.CommandList[parent.Index] - m.CommandList[i].Depth = len(strings.Split(c.FullName, " ")) - 1 - m.CommandList[i].FileName = strings.Split(c.FullName, " ")[1] - m.CommandList[i].LeafName = strings.Join(strings.Split(c.FullName, " ")[m.CommandList[i].Depth:], "") - } - } - - //populate children and base command - for _, c := range m.CommandList { - if c.Parent == nil { - continue - } - - m.CommandList[c.Parent.Index].Children = append(m.CommandList[c.Parent.Index].Children, &m.CommandList[c.Index]) - } - - // sorted ascending by full name of command (activity complete, batch list, etc) - sortChildrenVisitor(rootCommand) - - // pull flat list in same order as sorted children - m.CommandList = make([]Command, 0) - collectCommandVisitor(*rootCommand, &m) - - return m, nil -} - -func collectCommandVisitor(c Command, m *Commands) { - - m.CommandList = append(m.CommandList, c) - - for _, child := range c.Children { - collectCommandVisitor(*child, m) - } -} - -func sortChildrenVisitor(c *Command) { - sort.Slice(c.Children, func(i, j int) bool { - return c.Children[i].FullName < c.Children[j].FullName - }) - for _, command := range c.Children { - sortChildrenVisitor(command) - } -} From 1cab984a81ad5f350dc372fd8e0959d62fdb3476 Mon Sep 17 00:00:00 2001 From: Phil Prasek Date: Fri, 1 Nov 2024 10:13:41 -0700 Subject: [PATCH 8/8] emit type once for all forms of an option Signed-off-by: Phil Prasek --- temporalcli/commandsgen/docs.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/temporalcli/commandsgen/docs.go b/temporalcli/commandsgen/docs.go index f67d99f2b..fd00f2475 100644 --- a/temporalcli/commandsgen/docs.go +++ b/temporalcli/commandsgen/docs.go @@ -124,11 +124,11 @@ func (w *docWriter) writeOptions(prefix string, options []Option, c *Command) { for _, o := range options { // option name and alias - w.fileMap[fileName].WriteString(fmt.Sprintf("**--%s** _%s_", o.Name, o.Type)) + w.fileMap[fileName].WriteString(fmt.Sprintf("**--%s**", o.Name)) if len(o.Short) > 0 { - w.fileMap[fileName].WriteString(fmt.Sprintf(", **-%s** _%s_", o.Short, o.Type)) + w.fileMap[fileName].WriteString(fmt.Sprintf(", **-%s**", o.Short)) } - w.fileMap[fileName].WriteString("\n\n") + w.fileMap[fileName].WriteString(fmt.Sprintf(" _%s_\n\n", o.Type)) // description w.fileMap[fileName].WriteString(encodeJSONExample(o.Description))