Skip to content

Commit

Permalink
Improved docs gen (#691)
Browse files Browse the repository at this point in the history
## What was changed
- Improved `Command` model for simpler docs rendering
- Rendered inline options under each command, without cluttering up the
right side nav.
- Acorn rendering fix for JSON examples in `.mdx` 
- Add rendering of experimental options.

## Why?
Nexus docs need to be created and want to use docs gen.

## Checklist
1. How was this tested:
- `go run ./temporalcli/internal/cmd/gen-docs `
- verified via http://localhost:3000/

2. Any docs updates needed?
- will feed into overall docs cleanup

---------

Signed-off-by: Phil Prasek <[email protected]>
  • Loading branch information
prasek authored Nov 1, 2024
1 parent 051ef29 commit 100b8fb
Show file tree
Hide file tree
Showing 3 changed files with 134 additions and 40 deletions.
4 changes: 0 additions & 4 deletions temporalcli/commandsgen/code.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
120 changes: 95 additions & 25 deletions temporalcli/commandsgen/docs.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,23 +3,25 @@ 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,
allCommands: commands.CommandList,
}

// 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)
Expand All @@ -35,33 +37,35 @@ func GenerateDocsFiles(commands Commands) (map[string][]byte, error) {
}

type docWriter struct {
allCommands []Command
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 {
depth := c.depth()
if depth == 1 {
w.writeCommand(c)
} else if commandLength > 2 {
} else if 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")
w.fileMap[fileName].WriteString("title: " + c.FullName + "\n")
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")
Expand All @@ -75,27 +79,75 @@ 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 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)
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(options, func(i, j int) bool {
return options[i].Name < options[j].Name
})

sort.Slice(globalOptions, func(i, j int) bool {
return globalOptions[i].Name < globalOptions[j].Name
})

w.writeOptions("Flags", options, c)
w.writeOptions("Global Flags", globalOptions, c)

}
}

func (w *docWriter) writeOptions(prefix string, options []Option, c *Command) {
if len(options) == 0 {
return
}

// alphabetize options
sort.Slice(allOptions, func(i, j int) bool {
return allOptions[i].Name < allOptions[j].Name
})
fileName := c.fileName()

for _, option := range allOptions {
w.fileMap[fileName].WriteString(fmt.Sprintf("## %s\n\n", option.Name))
w.fileMap[fileName].WriteString(option.Description + "\n\n")
w.fileMap[fileName].WriteString(fmt.Sprintf("**%s:**\n\n", prefix))

for _, o := range options {
// option name and alias
w.fileMap[fileName].WriteString(fmt.Sprintf("**--%s**", o.Name))
if len(o.Short) > 0 {
w.fileMap[fileName].WriteString(fmt.Sprintf(", **-%s**", o.Short))
}
w.fileMap[fileName].WriteString(fmt.Sprintf(" _%s_\n\n", o.Type))

// description
w.fileMap[fileName].WriteString(encodeJSONExample(o.Description))
if o.Required {
w.fileMap[fileName].WriteString(" Required.")
}
if len(o.EnumValues) > 0 {
w.fileMap[fileName].WriteString(fmt.Sprintf(" Accepted values: %s.", strings.Join(o.EnumValues, ", ")))
}
if len(o.Default) > 0 {
w.fileMap[fileName].WriteString(fmt.Sprintf(` (default "%s")`, o.Default))
}
w.fileMap[fileName].WriteString("\n\n")

if o.Experimental {
w.fileMap[fileName].WriteString(":::note" + "\n\n")
w.fileMap[fileName].WriteString("Option is experimental." + "\n\n")
w.fileMap[fileName].WriteString(":::" + "\n\n")
}
}
}

Expand All @@ -115,3 +167,21 @@ 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
// and wrapping in backticks lets it render
re := regexp.MustCompile(`('[a-zA-Z0-9]*={.*}')`)
v = re.ReplaceAllString(v, "`$1`")
return v
}
50 changes: 39 additions & 11 deletions temporalcli/commandsgen/parse.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"fmt"
"regexp"
"slices"
"sort"
"strings"

"gopkg.in/yaml.v3"
Expand All @@ -19,15 +20,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 Down Expand Up @@ -55,8 +57,9 @@ 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.
Expand Down Expand Up @@ -87,6 +90,12 @@ func ParseCommands() (Commands, error) {
return Commands{}, fmt.Errorf("failed parsing command section %q: %w", command.FullName, err)
}
}

// alphabetize commands
sort.Slice(m.CommandList, func(i, j int) bool {
return m.CommandList[i].FullName < m.CommandList[j].FullName
})

return m, nil
}

Expand Down Expand Up @@ -170,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")
Expand Down

0 comments on commit 100b8fb

Please sign in to comment.