Skip to content

Commit

Permalink
enriched docs gen model
Browse files Browse the repository at this point in the history
Signed-off-by: Phil Prasek <[email protected]>
  • Loading branch information
prasek committed Oct 30, 2024
1 parent 8a133d9 commit 5c14f7b
Show file tree
Hide file tree
Showing 3 changed files with 314 additions and 11 deletions.
252 changes: 252 additions & 0 deletions temporalcli/commandsgen/enrich.go
Original file line number Diff line number Diff line change
@@ -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
}
68 changes: 57 additions & 11 deletions temporalcli/commandsgen/parse.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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.
Expand All @@ -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) {
Expand All @@ -87,6 +132,7 @@ func ParseCommands() (Commands, error) {
return Commands{}, fmt.Errorf("failed parsing command section %q: %w", command.FullName, err)
}
}

return m, nil
}

Expand Down
5 changes: 5 additions & 0 deletions temporalcli/internal/cmd/gen-docs/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down

0 comments on commit 5c14f7b

Please sign in to comment.