From 5c14f7bbfac08dc1b05f110478ef5a8fc179cae7 Mon Sep 17 00:00:00 2001 From: Phil Prasek Date: Tue, 15 Oct 2024 12:10:27 -0700 Subject: [PATCH 1/5] 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 bbeee0dd9e8b229ec0a93674fd7c402c82e3fb21 Mon Sep 17 00:00:00 2001 From: Phil Prasek Date: Wed, 16 Oct 2024 17:29:42 -0700 Subject: [PATCH 2/5] 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 309bf35049af96fcb6e3081d23f1444c2954136b Mon Sep 17 00:00:00 2001 From: Phil Prasek Date: Tue, 29 Oct 2024 19:43:16 -0700 Subject: [PATCH 3/5] 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 54079335d234224dc82ef5b94944bc8513e57674 Mon Sep 17 00:00:00 2001 From: Phil Prasek Date: Tue, 15 Oct 2024 12:18:22 -0700 Subject: [PATCH 4/5] enhanced docs gen to more closely match existing docs Signed-off-by: Phil Prasek --- temporalcli/commandsgen/docs.go | 186 ++++++++++++++++++++++++-------- 1 file changed, 142 insertions(+), 44 deletions(-) diff --git a/temporalcli/commandsgen/docs.go b/temporalcli/commandsgen/docs.go index e7fef0cec..7e9480f72 100644 --- a/temporalcli/commandsgen/docs.go +++ b/temporalcli/commandsgen/docs.go @@ -7,19 +7,23 @@ import ( "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, + usages: commands.Usages, + } + + // cmd-options.mdx + w.writeCommandOptions() - // 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) @@ -34,27 +38,44 @@ func GenerateDocsFiles(commands Commands) (map[string][]byte, error) { return finalMap, nil } -type docWriter struct { - fileMap map[string]*bytes.Buffer - optionSetMap map[string]OptionSets - optionsStack [][]Option -} - 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 } +type docWriter struct { + fileMap map[string]*bytes.Buffer + optionSetMap map[string]OptionSets + optionsStack [][]Option + usages Usages +} + +func (w *docWriter) processOptions(c *Command) { + // Pop options from stack if we are moving up a level + if len(w.optionsStack) >= len(strings.Split(c.FullName, " ")) { + w.optionsStack = w.optionsStack[:len(w.optionsStack)-1] + } + var options []Option + options = append(options, c.Options...) + + // Maintain stack of options available from parent commands + for _, set := range c.OptionSets { + optionSetOptions := w.optionSetMap[set].Options + options = append(options, optionSetOptions...) + } + + w.optionsStack = append(w.optionsStack, options) +} + 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 +83,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,43 +97,119 @@ 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 len(c.Children) == 0 { + 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 - }) + // 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...) + } - for _, option := range allOptions { - w.fileMap[fileName].WriteString(fmt.Sprintf("## %s\n\n", option.Name)) - w.fileMap[fileName].WriteString(option.Description + "\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](/cli/cmd-options#%s)\n\n", option.Name, option.Name)) + } } } -func (w *docWriter) processOptions(c *Command) { - // Pop options from stack if we are moving up a level - if len(w.optionsStack) >= len(strings.Split(c.FullName, " ")) { - w.optionsStack = w.optionsStack[:len(w.optionsStack)-1] - } - var options []Option - options = append(options, c.Options...) +func (w *docWriter) writeCommandOptions() { + fileName := "cmd-options" + w.fileMap[fileName] = &bytes.Buffer{} + w.fileMap[fileName].WriteString("---\n") + w.fileMap[fileName].WriteString("id: " + fileName + "\n") + w.fileMap[fileName].WriteString("title: Temporal CLI command options reference\n") + w.fileMap[fileName].WriteString("sidebar_label: cmd options\n") + w.fileMap[fileName].WriteString("description: Discover how to manage Temporal Workflows, from Activity Execution to Workflow Ids, using clusters, cron schedules, dynamic configurations, and logging. Perfect for developers.\n") + w.fileMap[fileName].WriteString("toc_max_heading_level: 4\n") - // Maintain stack of options available from parent commands - for _, set := range c.OptionSets { - optionSetOptions := w.optionSetMap[set].Options - options = append(options, optionSetOptions...) + w.fileMap[fileName].WriteString("keywords:\n") + w.fileMap[fileName].WriteString(" - " + "cli reference" + "\n") + w.fileMap[fileName].WriteString(" - " + "command line interface cli" + "\n") + w.fileMap[fileName].WriteString(" - " + "temporal cli" + "\n") + + w.fileMap[fileName].WriteString("tags:\n") + w.fileMap[fileName].WriteString(" - " + "cli-reference" + "\n") + w.fileMap[fileName].WriteString(" - " + "command-line-interface-cli" + "\n") + w.fileMap[fileName].WriteString(" - " + "temporal-cli" + "\n") + + w.fileMap[fileName].WriteString("---\n\n") + + /////// option a + for _, option := range w.usages.OptionUsagesByOptionDescription { + w.fileMap[fileName].WriteString(fmt.Sprintf("## %s\n\n", option.OptionName)) + + if len(option.Usages) == 1 { + usageDescription := option.Usages[0] + usage := usageDescription.UsageSites[0] + w.fileMap[fileName].WriteString(usage.Option.Description + "\n\n") + + if usage.Option.Experimental { + w.fileMap[fileName].WriteString(":::note" + "\n\n") + w.fileMap[fileName].WriteString("Option is experimental." + "\n\n") + w.fileMap[fileName].WriteString(":::" + "\n\n") + } + } else { + for i, usageDescription := range option.Usages { + if i > 0 { + w.fileMap[fileName].WriteString("\n") + + } + w.fileMap[fileName].WriteString(usageDescription.OptionDescription + "\n\n") + + for _, usage := range usageDescription.UsageSites { + experimentalDescr := "" + if usage.Option.Experimental { + experimentalDescr = " (option usage is EXPERIMENTAL)" + } + if usage.UsageSiteType == UsageTypeCommand { + w.fileMap[fileName].WriteString("- `" + usage.UsageSiteDescription + "`" + experimentalDescr + "\n") + } else { + w.fileMap[fileName].WriteString("- " + usage.UsageSiteDescription + experimentalDescr + "\n") + } + } + + } + } } - w.optionsStack = append(w.optionsStack, options) + /////// option b + + /* + + for _, option := range w.usages.OptionUsages { + w.fileMap[fileName].WriteString(fmt.Sprintf("## %s\n\n", option.OptionName)) + + if len(option.Usages) == 1 { + usage := option.Usages[0] + w.fileMap[fileName].WriteString(usage.Option.Description + "\n\n") + + if usage.Option.Experimental { + w.fileMap[fileName].WriteString(":::note" + "\n\n") + w.fileMap[fileName].WriteString("Option is experimental and may be removed at a future date." + "\n\n") + w.fileMap[fileName].WriteString(":::" + "\n\n") + } + } else { + for _, usage := range option.Usages { + w.fileMap[fileName].WriteString("**" + usage.UsageDescription + "**\n") + w.fileMap[fileName].WriteString(usage.Option.Description + "\n\n") + + if usage.Option.Experimental { + w.fileMap[fileName].WriteString(":::note" + "\n\n") + w.fileMap[fileName].WriteString("Option is experimental and may be removed at a future date." + "\n\n") + w.fileMap[fileName].WriteString(":::" + "\n\n") + } + } + } + } + */ } From b45bcbae1247b84acec3fdb7610384ca6e8b605e Mon Sep 17 00:00:00 2001 From: Phil Prasek Date: Wed, 16 Oct 2024 16:59:31 -0700 Subject: [PATCH 5/5] encode JSON examples to avoid mdx acorn rendering error Signed-off-by: Phil Prasek --- temporalcli/commandsgen/docs.go | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/temporalcli/commandsgen/docs.go b/temporalcli/commandsgen/docs.go index 7e9480f72..c0ac28bc9 100644 --- a/temporalcli/commandsgen/docs.go +++ b/temporalcli/commandsgen/docs.go @@ -3,6 +3,7 @@ package commandsgen import ( "bytes" "fmt" + "regexp" "sort" "strings" ) @@ -151,7 +152,7 @@ func (w *docWriter) writeCommandOptions() { if len(option.Usages) == 1 { usageDescription := option.Usages[0] usage := usageDescription.UsageSites[0] - w.fileMap[fileName].WriteString(usage.Option.Description + "\n\n") + w.fileMap[fileName].WriteString(encodeJSONExample(usage.Option.Description) + "\n\n") if usage.Option.Experimental { w.fileMap[fileName].WriteString(":::note" + "\n\n") @@ -164,7 +165,7 @@ func (w *docWriter) writeCommandOptions() { w.fileMap[fileName].WriteString("\n") } - w.fileMap[fileName].WriteString(usageDescription.OptionDescription + "\n\n") + w.fileMap[fileName].WriteString(encodeJSONExample(usageDescription.OptionDescription) + "\n\n") for _, usage := range usageDescription.UsageSites { experimentalDescr := "" @@ -213,3 +214,10 @@ func (w *docWriter) writeCommandOptions() { } */ } + +func encodeJSONExample(v string) string { + // example: `'YourKey={"your": "value"}'` + re := regexp.MustCompile(`('[a-zA-Z0-9]*={.*}')`) + v = re.ReplaceAllString(v, "`$1`") + return v +}