From 4d621987403cff839c062fae2002573402c99dc7 Mon Sep 17 00:00:00 2001 From: Robert Kaussow Date: Thu, 21 Dec 2023 09:13:00 +0100 Subject: [PATCH] feat: add helper to auto-generate cli docs (#40) --- docs/docs.go | 99 ++++++++++++++++++++++++++++++ docs/docs_test.go | 67 ++++++++++++++++++++ docs/templates/markdown.md.tmpl | 15 +++++ docs/testdata/expected-doc-full.md | 13 ++++ 4 files changed, 194 insertions(+) create mode 100644 docs/docs.go create mode 100644 docs/docs_test.go create mode 100644 docs/templates/markdown.md.tmpl create mode 100644 docs/testdata/expected-doc-full.md diff --git a/docs/docs.go b/docs/docs.go new file mode 100644 index 0000000..9dfef19 --- /dev/null +++ b/docs/docs.go @@ -0,0 +1,99 @@ +package docs + +import ( + "bytes" + "embed" + "html/template" + "sort" + "strings" + + "github.com/urfave/cli/v2" +) + +type PluginArg struct { + Name string + EnvVars []string + Description string + Default string +} + +type CliTemplate struct { + Name string + Version string + Description string + Usage string + UsageText string + GlobalArgs []*PluginArg +} + +//go:embed templates +var templateFs embed.FS + +// ToMarkdown creates a markdown string for the `*App` +// The function errors if either parsing or writing of the string fails. +func ToMarkdown(app *cli.App) (string, error) { + var w bytes.Buffer + + tpls, err := template.New("cli").ParseFS(templateFs, "**/*.tmpl") + if err != nil { + return "", err + } + + if err := tpls.ExecuteTemplate(&w, "markdown.md.tmpl", GetTemplateData(app)); err != nil { + return "", err + } + + return w.String(), nil +} + +func GetTemplateData(app *cli.App) *CliTemplate { + return &CliTemplate{ + Name: app.Name, + Version: app.Version, + Description: prepareMultilineString(app.Description), + Usage: prepareMultilineString(app.Usage), + UsageText: prepareMultilineString(app.UsageText), + GlobalArgs: prepareArgsWithValues(app.VisibleFlags()), + } +} + +func prepareMultilineString(s string) string { + return strings.TrimRight( + strings.TrimSpace( + strings.ReplaceAll(s, "\n", " "), + ), + ".\r\n\t", + ) +} + +func prepareArgsWithValues(flags []cli.Flag) []*PluginArg { + return parseFlags(flags) +} + +func parseFlags(flags []cli.Flag) []*PluginArg { + args := make([]*PluginArg, 0) + + for _, f := range flags { + flag, ok := f.(cli.DocGenerationFlag) + if !ok { + continue + } + + modArg := &PluginArg{} + + name := flag.GetEnvVars()[0] + name = strings.TrimPrefix(name, "PLUGIN_") + modArg.Name = strings.ToLower(strings.TrimSpace(name)) + + modArg.Description = flag.GetUsage() + modArg.Default = flag.GetDefaultText() + + args = append(args, modArg) + } + + sort.SliceStable(args, func(i, j int) bool { + return args[i].Name < args[j].Name + }) + + return args +} diff --git a/docs/docs_test.go b/docs/docs_test.go new file mode 100644 index 0000000..af861ac --- /dev/null +++ b/docs/docs_test.go @@ -0,0 +1,67 @@ +package docs + +import ( + "bytes" + "os" + "testing" + + "github.com/urfave/cli/v2" +) + +func testApp() *cli.App { + app := &cli.App{ + Name: "test", + Description: "test description", + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "dummy-flag", + Usage: "dummy flag desc", + EnvVars: []string{"PLUGIN_DUMMY_FLAG"}, + Value: "test", + }, + &cli.StringFlag{ + Name: "simpe-flag", + EnvVars: []string{"PLUGIN_X_SIMPLE_FLAG"}, + }, + &cli.StringFlag{ + Name: "other.flag", + Usage: "other flag with desc", + EnvVars: []string{"PLUGIN_Z_OTHER_FLAG"}, + }, + }, + } + + return app +} + +func testFileContent(t *testing.T, file string) string { + t.Helper() + + data, err := os.ReadFile(file) + if err != nil { + t.Error(err) + } + + data = bytes.ReplaceAll(data, []byte("\r\n"), []byte("\n")) + + return string(data) +} + +func TestToMarkdownFull(t *testing.T) { + tests := []struct { + name string + app *cli.App + want string + }{ + {"normal branch", testApp(), "testdata/expected-doc-full.md"}, + } + + for _, tt := range tests { + want := testFileContent(t, tt.want) + t.Run(tt.name, func(t *testing.T) { + if got, _ := ToMarkdown(tt.app); got != want { + t.Errorf("got = %v, want %v", got, want) + } + }) + } +} diff --git a/docs/templates/markdown.md.tmpl b/docs/templates/markdown.md.tmpl new file mode 100644 index 0000000..2356ed4 --- /dev/null +++ b/docs/templates/markdown.md.tmpl @@ -0,0 +1,15 @@ +{{ with .Name }}# {{ . }}{{ end }} +{{- with .Description }} + +{{ . }} +{{- end }} +{{- if .GlobalArgs }} + +## Parameters +{{ range $v := .GlobalArgs }} +**_{{ $v.Name }}_**{{ with $v.Default }} (defaut: {{ . }}){{ end }}{{ if $v.Description }}{{ "\\" }}{{ end }} +{{- with $v.Description }} + {{ . }} +{{- end }} +{{ end -}} +{{ end -}} diff --git a/docs/testdata/expected-doc-full.md b/docs/testdata/expected-doc-full.md new file mode 100644 index 0000000..4daab5b --- /dev/null +++ b/docs/testdata/expected-doc-full.md @@ -0,0 +1,13 @@ +# test + +test description + +## Parameters + +**_dummy_flag_** (defaut: "test")\ + dummy flag desc + +**_x_simple_flag_** + +**_z_other_flag_**\ + other flag with desc