Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
158 changes: 156 additions & 2 deletions cmd/docgen/docgen.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,15 +15,19 @@
package docgen

import (
"bytes"
_ "embed"
"fmt"
"io"
"path/filepath"
"slices"
"strings"
"text/template"

"github.com/slackapi/slack-cli/internal/shared"
"github.com/slackapi/slack-cli/internal/slackerror"
"github.com/slackapi/slack-cli/internal/style"
"github.com/spf13/afero"
"github.com/spf13/cobra"
)

Expand Down Expand Up @@ -99,9 +103,9 @@ func runDocGenCommandFunc(clients *shared.ClientFactory, cmd *cobra.Command, arg
return slackerror.New("MkdirAll failed").WithRootCause(err)
}
rootCmd.DisableAutoGenTag = true
err = clients.Cobra.GenMarkdownTree(rootCmd, commandsDirPath)
err = genMarkdownTree(rootCmd, clients.Fs, commandsDirPath)
if err != nil {
return slackerror.New("Cobra.GenMarkdownTree failed").WithRootCause(err)
return slackerror.New(slackerror.ErrDocumentationGenerationFailed).WithRootCause(err)
}

// Generate errors reference
Expand All @@ -124,3 +128,153 @@ func runDocGenCommandFunc(clients *shared.ClientFactory, cmd *cobra.Command, arg
))
return nil
}

// genMarkdownTree creates markdown reference of commands for the docs site.
//
// Reference: https://github.com/spf13/cobra/blob/3f3b81882534a51628f3286e93c6842d9b2e29ea/doc/md_docs.go#L119-L158
func genMarkdownTree(cmd *cobra.Command, fs afero.Fs, dir string) error {
for _, c := range cmd.Commands() {
if !c.IsAvailableCommand() || c.IsAdditionalHelpTopicCommand() {
continue
}
if err := genMarkdownTree(c, fs, dir); err != nil {
return err
}
}
basename := strings.ReplaceAll(cmd.CommandPath(), " ", "_") + ".md"
filename := filepath.Join(dir, basename)
f, err := fs.Create(filename)
if err != nil {
return err
}
defer f.Close()
if err := genMarkdownCommand(cmd, f); err != nil {
return err
}
return nil
}

// genMarkdownCommand creates custom markdown output for a command.
//
// Reference: https://github.com/spf13/cobra/blob/3f3b81882534a51628f3286e93c6842d9b2e29ea/doc/md_docs.go#L56-L117
func genMarkdownCommand(cmd *cobra.Command, w io.Writer) error {
cmd.InitDefaultHelpCmd()
cmd.InitDefaultHelpFlag()

buf := new(bytes.Buffer)
name := cmd.CommandPath()

fmt.Fprintf(buf, "# `%s`\n\n", name)
fmt.Fprintf(buf, "%s\n\n", cmd.Short)
if len(cmd.Long) > 0 {
fmt.Fprintf(buf, "## Description\n\n")
description, err := render(cmd.Long)
if err != nil {
return err
}
fmt.Fprintf(buf, "%s\n\n", description)
}
if cmd.Runnable() {
fmt.Fprintf(buf, "```\n%s\n```\n\n", cmd.UseLine())
}
if len(cmd.Example) > 0 {
fmt.Fprintf(buf, "## Examples\n\n")
fmt.Fprintf(buf, "```\n%s\n```\n\n", cmd.Example)
}
if err := genMarkdownCommandFlags(buf, cmd); err != nil {
return err
}
Comment on lines +180 to +186
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👁️‍🗨️ note: This ordering might be updated in a follow up PR to match --help outputs.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📣 note: This change is made in #195!

if hasSeeAlso(cmd) {
fmt.Fprintf(buf, "## See also\n\n")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ignore-me: While I prefer "Title Case", this looks correct because our docs use "Sentence case". I think it's best for the CLI to stay aligned with our docs. So please ignore me! 🧠

if cmd.HasParent() {
parent := cmd.Parent()
pname := parent.CommandPath()
link := strings.ReplaceAll(pname, " ", "_")
fmt.Fprintf(buf, "* [%s](%s)\t - %s\n", pname, link, parent.Short)
cmd.VisitParents(func(c *cobra.Command) {
if c.DisableAutoGenTag {
cmd.DisableAutoGenTag = c.DisableAutoGenTag
}
})
}
children := cmd.Commands()
slices.SortFunc(children, func(a *cobra.Command, b *cobra.Command) int {
if a.Name() < b.Name() {
return -1
} else {
return 1
}
})
for _, child := range children {
if !child.IsAvailableCommand() || child.IsAdditionalHelpTopicCommand() {
continue
}
cname := name + " " + child.Name()
link := strings.ReplaceAll(cname, " ", "_")
fmt.Fprintf(buf, "* [%s](%s)\t - %s\n", cname, link, child.Short)
}
fmt.Fprintf(buf, "\n")
}
_, err := buf.WriteTo(w)
return err
}

// genMarkdownCommandFlags outputs flag information.
//
// Reference: https://github.com/spf13/cobra/blob/3f3b81882534a51628f3286e93c6842d9b2e29ea/doc/md_docs.go#L32-L49
func genMarkdownCommandFlags(buf *bytes.Buffer, cmd *cobra.Command) error {
flags := cmd.NonInheritedFlags()
flags.SetOutput(buf)
if flags.HasAvailableFlags() {
fmt.Fprintf(buf, "## Flags\n\n```\n")
flags.PrintDefaults()
fmt.Fprintf(buf, "```\n\n")
}
parentFlags := cmd.InheritedFlags()
parentFlags.SetOutput(buf)
if parentFlags.HasAvailableFlags() {
fmt.Fprintf(buf, "## Global flags\n\n```\n")
parentFlags.PrintDefaults()
fmt.Fprintf(buf, "```\n\n")
}
return nil
}

// hasSeeAlso checks for adjancet commands to include in reference.
//
// Reference: https://github.com/spf13/cobra/blob/3f3b81882534a51628f3286e93c6842d9b2e29ea/doc/util.go#L23-L37
func hasSeeAlso(cmd *cobra.Command) bool {
if cmd.HasParent() {
return true
}
for _, c := range cmd.Commands() {
if !c.IsAvailableCommand() || c.IsAdditionalHelpTopicCommand() {
continue
}
return true
}
return false
}

// render formats the templating from a command into markdown.
func render(input string) (string, error) {
tmpl, err := template.New("md").Funcs(template.FuncMap{
"Emoji": func(s string) string {
return ""
},
"LinkText": func(s string) string {
return fmt.Sprintf("[%s](%s)", s, s)
},
"ToBold": func(s string) string {
return fmt.Sprintf("**%s**", s)
},
}).Parse(input)
if err != nil {
return "", err
}
var buf bytes.Buffer
if err := tmpl.Execute(&buf, nil); err != nil {
return "", err
}
return buf.String(), nil
}
31 changes: 0 additions & 31 deletions cmd/docgen/docgen_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,12 +40,6 @@ func TestNewDocsCommand(t *testing.T) {
filepath.Join(slackdeps.MockWorkingDirectory, "docs"),
},
ExpectedAsserts: func(t *testing.T, ctx context.Context, cm *shared.ClientsMock) {
cm.Cobra.AssertCalled(
t,
"GenMarkdownTree",
mock.Anything,
filepath.Join(slackdeps.MockWorkingDirectory, "docs", "commands"),
)
cm.Fs.AssertCalled(
t,
"MkdirAll",
Expand Down Expand Up @@ -84,12 +78,6 @@ func TestNewDocsCommand(t *testing.T) {
filepath.Join(slackdeps.MockWorkingDirectory, "markdown-docs"),
},
ExpectedAsserts: func(t *testing.T, ctx context.Context, cm *shared.ClientsMock) {
cm.Cobra.AssertCalled(
t,
"GenMarkdownTree",
mock.Anything,
filepath.Join(slackdeps.MockWorkingDirectory, "markdown-docs", "commands"),
)
cm.Fs.AssertCalled(
t,
"MkdirAll",
Expand Down Expand Up @@ -126,12 +114,6 @@ func TestNewDocsCommand(t *testing.T) {
CmdArgs: []string{},
ExpectedOutputs: []string{"References saved to: docs"},
ExpectedAsserts: func(t *testing.T, ctx context.Context, cm *shared.ClientsMock) {
cm.Cobra.AssertCalled(
t,
"GenMarkdownTree",
mock.Anything,
filepath.Join("docs", "commands"),
)
cm.Fs.AssertCalled(
t,
"MkdirAll",
Expand Down Expand Up @@ -185,19 +167,6 @@ func TestNewDocsCommand(t *testing.T) {
CmdArgs: []string{},
ExpectedErrorStrings: []string{"no write permission"},
},
"when generating docs fails": {
Setup: func(t *testing.T, ctx context.Context, cm *shared.ClientsMock, cf *shared.ClientFactory) {
cm.Cobra.On(
"GenMarkdownTree",
mock.Anything,
mock.Anything,
).Return(
errors.New("failed to generate docs"),
)
},
CmdArgs: []string{},
ExpectedErrorStrings: []string{"failed to generate docs"},
},
}, func(clients *shared.ClientFactory) *cobra.Command {
return NewCommand(clients)
})
Expand Down
15 changes: 0 additions & 15 deletions internal/shared/clients.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,6 @@ import (
"github.com/slackapi/slack-cli/internal/slackerror"
"github.com/slackapi/slack-cli/internal/tracking"
"github.com/spf13/afero"
"github.com/spf13/cobra"
"github.com/spf13/cobra/doc"
)

// ClientFactory are shared clients and configurations for use across the CLI commands (cmd) and handlers (pkg).
Expand All @@ -66,14 +64,6 @@ type ClientFactory struct {

// CleanupWaitGroup is a group of wait groups shared by all packages and allow functions to cleanup before the process terminates
CleanupWaitGroup sync.WaitGroup

// Cobra are a group of Cobra functions shared by all packages and enables tests & mocking
Cobra struct {
// GenMarkdownTree defaults to `doc.GenMarkdownTree(...)` and can be mocked to test specific use-cases
// TODO - This can be moved to cmd/docs/docs.go when `NewCommand` returns an instance of that can store `GenMarkdownTree` as
// a private member. The current thinking is that `NewCommand` would return a `SlackCommand` instead of `CobraCommand`
GenMarkdownTree func(cmd *cobra.Command, dir string) error
}
Comment on lines -70 to -76
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🪓 🎉

}

const sdkSlackDevDomainFlag = "sdk-slack-dev-domain"
Expand Down Expand Up @@ -102,11 +92,6 @@ func NewClientFactory(options ...func(*ClientFactory)) *ClientFactory {
clients.Auth = clients.defaultAuthFunc
clients.Browser = clients.defaultBrowserFunc

// Command-specific dependencies
// TODO - These are methods that belong to specific commands and should be moved under each command
// when we replace NewCommand with NewSlackCommand that can store member variables.
clients.Cobra.GenMarkdownTree = doc.GenMarkdownTree

// TODO: Temporary hack to get around circular dependency in internal/api/client.go since that imports version
// Follows pattern demonstrated by the GitHub CLI here https://github.com/cli/cli/blob/5a46c1cab601a3394caa8de85adb14f909b811e9/pkg/cmd/factory/default.go#L29
// Used by the APIClient for its userAgent
Expand Down
4 changes: 0 additions & 4 deletions internal/shared/clients_mock.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,6 @@ type ClientsMock struct {
AppClient *app.Client
Browser *slackdeps.BrowserMock
Config *config.Config
Cobra *slackdeps.CobraMock
EventTracker *tracking.EventTrackerMock
Fs *slackdeps.FsMock
IO *iostreams.IOStreamsMock
Expand All @@ -55,7 +54,6 @@ func NewClientsMock() *ClientsMock {
clientsMock.API = &api.APIMock{}
clientsMock.Auth = &auth.AuthMock{}
clientsMock.Browser = slackdeps.NewBrowserMock()
clientsMock.Cobra = slackdeps.NewCobraMock()
clientsMock.EventTracker = &tracking.EventTrackerMock{}
clientsMock.Fs = slackdeps.NewFsMock()
clientsMock.Os = slackdeps.NewOsMock()
Expand All @@ -74,7 +72,6 @@ func (m *ClientsMock) AddDefaultMocks() {
m.API.AddDefaultMocks()
m.Auth.AddDefaultMocks()
m.Browser.AddDefaultMocks()
m.Cobra.AddDefaultMocks()
m.EventTracker.AddDefaultMocks()
m.IO.AddDefaultMocks()
m.Os.AddDefaultMocks()
Expand All @@ -85,7 +82,6 @@ func (m *ClientsMock) AddDefaultMocks() {
func (m *ClientsMock) MockClientFactory() func(c *ClientFactory) {
return func(clients *ClientFactory) {
clients.Browser = func() slackdeps.Browser { return m.Browser }
clients.Cobra.GenMarkdownTree = m.Cobra.GenMarkdownTree
clients.Config = m.Config
clients.EventTracker = m.EventTracker
clients.Os = m.Os
Expand Down
41 changes: 0 additions & 41 deletions internal/slackdeps/cobra_mock.go

This file was deleted.

Loading
Loading