From dcc2437cdda5ab3efbb48b6c04a36ae9c798989c Mon Sep 17 00:00:00 2001 From: Tobias Brumhard Date: Fri, 29 Apr 2022 16:39:31 +0200 Subject: [PATCH] docs(template): add autogenerated docs for options (#98) Signed-off-by: Tobias Brumhard --- .githooks/pre-commit | 16 ++++++++ .golangci.yml | 4 +- Makefile | 3 +- README.md | 2 +- cmd/dotembed/main.go | 16 ++++---- cmd/options2md/main.go | 79 ++++++++++++++++++++++++++++++++++++++ docs/options.md | 41 ++++++++++++++++++++ embed.go | 1 - embed_gen.go | 3 +- pkg/gotemplate/new_test.go | 46 +++++++++++----------- pkg/gotemplate/options.go | 40 +++++++++---------- pkg/gotemplate/print.go | 2 +- 12 files changed, 196 insertions(+), 57 deletions(-) create mode 100755 .githooks/pre-commit create mode 100644 cmd/options2md/main.go create mode 100644 docs/options.md diff --git a/.githooks/pre-commit b/.githooks/pre-commit new file mode 100755 index 00000000..5024e1dc --- /dev/null +++ b/.githooks/pre-commit @@ -0,0 +1,16 @@ +#!/bin/bash + +## compare amount of changed file before and after generate +CHANGE_STATS=$(git diff --shortstat) + +make generate &>/dev/null + +CHANGE_STATS_NEW=$(git diff --shortstat) + +## we can check to see if this is empty +if [[ "$CHANGE_STATS" != "$CHANGE_STATS_NEW" ]]; then + echo -e "Files have been generated. Pls treat them also." + exit 1 +fi + +echo "No changes happened due to make generate, ready to proceed" diff --git a/.golangci.yml b/.golangci.yml index c4200ed1..57516918 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -129,7 +129,9 @@ issues: - goerr113 - funlen - goconst - - path: dotembed/main\.go + # only small tools that use globals for templating + # and errors for CLI error output + - path: (dotembed|options2md)\/main\.go linters: - gochecknoglobals - goerr113 diff --git a/Makefile b/Makefile index df383aa5..5a9acef1 100644 --- a/Makefile +++ b/Makefile @@ -32,7 +32,8 @@ run: fmt ## Run a controller from your host @go run ./main.go generate: ## Generates files - @go generate ./... + @go run cmd/dotembed/main.go -target _template -o embed_gen.go -pkg gotemplate -var FS + @go run cmd/options2md/main.go -o docs/options.md GOLANGCI_LINT = bin/golangci-lint-$(GOLANGCI_VERSION) $(GOLANGCI_LINT): diff --git a/README.md b/README.md index 2e98a1f2..aa2bc6db 100644 --- a/README.md +++ b/README.md @@ -71,7 +71,7 @@ make all ## Options -To get an overview of all options that can be set for the template you can take a look at the [options definition file](pkg/gotemplate/options.go), run the CLI or check out the [testing example values file](pkg/gotemplate/testdata/values.yml). +To get an overview of all options that can be set for the template you can take a look at the [options docs](docs/options.md), run the CLI or check out the [testing example values file](pkg/gotemplate/testdata/values.yml). ## Maintainers diff --git a/cmd/dotembed/main.go b/cmd/dotembed/main.go index 6f02831b..94ace2bf 100644 --- a/cmd/dotembed/main.go +++ b/cmd/dotembed/main.go @@ -89,18 +89,18 @@ func sortStrings(slice []string) []string { } var ( - tmplString = `package {{ .Package }} + tmplString = `// Code generated by dotembed. DO NOT EDIT. +package {{ .Package }} import "embed" //go:embed {{ join (sort (.EmbedPaths | keys)) " " }} var {{ .VariableName }} embed.FS ` - tmpl = template.Must( - template.New("").Funcs(template.FuncMap{ - "join": strings.Join, - "keys": keys, - "sort": sortStrings, - }).Parse(tmplString), - ) + funcMap = template.FuncMap{ + "join": strings.Join, + "keys": keys, + "sort": sortStrings, + } + tmpl = template.Must(template.New("").Funcs(funcMap).Parse(tmplString)) ) diff --git a/cmd/options2md/main.go b/cmd/options2md/main.go new file mode 100644 index 00000000..2c2faa30 --- /dev/null +++ b/cmd/options2md/main.go @@ -0,0 +1,79 @@ +package main + +import ( + "errors" + "flag" + "fmt" + "os" + "strings" + + "text/template" + + "github.com/schwarzit/go-template/pkg/gotemplate" +) + +// options2md tranlates all options available in go/template to a markdown file +// defined by the -o flag. This can be used for documentation. +func main() { + if err := run(os.Args[1:]); err != nil { + fmt.Fprintf(os.Stderr, "an error occurred: %s\n", err) + os.Exit(1) + } +} + +func run(args []string) error { + var outputFile string + flag.StringVar(&outputFile, "o", "./options.md", "The file to write") + flag.CommandLine.Parse(args) + + if outputFile == "" { + return errors.New("`o` is a required parameter") + } + + file, err := os.Create(outputFile) + if err != nil { + return err + } + + return tmpl.Execute(file, gotemplate.NewOptions(nil)) +} + +var ( + tmplString = ` +# Options + +The following sections describe all options that are currently available for go/template for templating. +The options are divided into base options and extension options. +Base options are needed for the minimal base template and are mandatory in any case. +The extension options on the other hand enable optional features in the template such as gRPC support or open source lincenses. + +## Base + +| Name | Description | +| :--- | :---------- | +{{- range $index, $option := .Base}} +| {{ $option.Name | code }} | {{ $option.Description | replace "\n" "
" }} | +{{- end}} + +## Extensions +{{- range $index, $category := .Extensions}} + +### {{ $category.Name | code }} + +| Name | Description | +| :--- | :---------- | +{{- range $index, $option := $category.Options}} +| {{ $option.Name | code }} | {{ $option.Description | replace "\n" "
" }} | +{{- end}} +{{- end}} +` + funcMap = template.FuncMap{ + "replace": func(old, new, src string) string { + return strings.ReplaceAll(src, old, new) + }, + "code": func(s string) string { + return fmt.Sprintf("`%s`", s) + }, + } + tmpl = template.Must(template.New("").Funcs(funcMap).Parse(tmplString)) +) diff --git a/docs/options.md b/docs/options.md new file mode 100644 index 00000000..2be6ccfe --- /dev/null +++ b/docs/options.md @@ -0,0 +1,41 @@ + +# Options + +The following sections describe all options that are currently available for go/template for templating. +The options are divided into base options and extension options. +Base options are needed for the minimal base template and are mandatory in any case. +The extension options on the other hand enable optional features in the template such as gRPC support or open source lincenses. + +## Base + +| Name | Description | +| :--- | :---------- | +| `projectName` | Name of the project | +| `projectSlug` | Technical name of the project for folders and names. This will also be used as output directory. | +| `projectDescription` | Description of the project used in the README. | +| `appName` | The name of the binary that you want to create.
Could be the same as your "projectSlug" but since Go supports multiple apps in one repo it could also be sth. else.
For example if your project is for some API there could be one app for the server and one CLI client. | +| `moduleName` | The name of the Go module defined in the "go.mod" file.
This is used if you want to "go get" the module.
Please be aware that this depends on your version control system.
The default points to "github.com" but for devops for example it would look sth. like this "dev.azure.com/org/project/repo.git" | +| `golangciVersion` | Golangci-lint version to use. | + +## Extensions + +### `openSource` + +| Name | Description | +| :--- | :---------- | +| `license` | Set an OpenSource license.
Unsure which to pick? Checkout Github's https://choosealicense.com/
Options:
0: Add no license
1: MIT License
2: Apache License 2.0
3: GNU AGPLv3
4: GNU GPLv3
5: GNU LGPLv3
6: Mozilla Public License 2.0
7: Boost Software License 1.0
8: The Unlicense | +| `author` | License author | +| `codeowner` | Set the codeowner of the project | + +### `ci` + +| Name | Description | +| :--- | :---------- | +| `provider` | Set an CI pipeline provider integration
Options:
0: No CI
1: Github
2: Gitlab
3: Azure DevOps | + +### `grpc` + +| Name | Description | +| :--- | :---------- | +| `base` | Base configuration for gRPC | +| `grpcGateway` | Extend gRPC configuration with grpc-gateway | diff --git a/embed.go b/embed.go index 86e21fef..16fd76fc 100644 --- a/embed.go +++ b/embed.go @@ -1,4 +1,3 @@ package gotemplate -//go:generate go run cmd/dotembed/main.go -target _template -o embed_gen.go -pkg gotemplate -var FS const Key = "_template" diff --git a/embed_gen.go b/embed_gen.go index d7a3b02e..b54b879d 100644 --- a/embed_gen.go +++ b/embed_gen.go @@ -1,6 +1,7 @@ +// Code generated by dotembed. DO NOT EDIT. package gotemplate import "embed" -//go:embed _template _template/.github _template/.azure-pipelines.yml _template/.gitlab-ci.yml _template/.dockerignore _template/.editorconfig _template/.githooks _template/.gitignore _template/.golangci.yml _template/assets/.gitkeep _template/configs/.gitkeep _template/deployments/.gitkeep _template/internal/.gitkeep _template/pkg/.gitkeep +//go:embed _template _template/.azure-pipelines.yml _template/.dockerignore _template/.editorconfig _template/.githooks _template/.github _template/.gitignore _template/.gitlab-ci.yml _template/.golangci.yml _template/assets/.gitkeep _template/configs/.gitkeep _template/deployments/.gitkeep _template/internal/.gitkeep _template/pkg/.gitkeep var FS embed.FS diff --git a/pkg/gotemplate/new_test.go b/pkg/gotemplate/new_test.go index cfe43e7e..fbc911d8 100644 --- a/pkg/gotemplate/new_test.go +++ b/pkg/gotemplate/new_test.go @@ -50,7 +50,7 @@ func TestGT_LoadConfigValuesFromFile(t *testing.T) { gt := gotemplate.GT{ Options: &gotemplate.Options{ Base: []gotemplate.Option{ - gotemplate.NewOption(optionName, gotemplate.StringValue("description"), gotemplate.StaticValue("theDefault")), + gotemplate.NewOption(optionName, "description", gotemplate.StaticValue("theDefault")), }, }, } @@ -66,7 +66,7 @@ func TestGT_LoadConfigValuesFromFile(t *testing.T) { Options: []gotemplate.Option{ gotemplate.NewOption( categoryOptionName, - gotemplate.StringValue("desc"), + "description", gotemplate.StaticValue(false), ), }, @@ -106,7 +106,7 @@ base: t.Run("validates validator if set", func(t *testing.T) { gt.Options.Base[0] = gotemplate.NewOption( optionName, - gotemplate.StringValue("description"), + "description", gotemplate.StaticValue("theDefault"), gotemplate.WithValidator(gotemplate.RegexValidator( `[a-z1-9]+(-[a-z1-9]+)*$`, @@ -129,7 +129,7 @@ base: Options: []gotemplate.Option{ gotemplate.NewOption( "string", - gotemplate.StringValue("desc"), + "description", gotemplate.StaticValue("default"), ), }, @@ -153,17 +153,17 @@ base: Base: []gotemplate.Option{ gotemplate.NewOption( "int", - gotemplate.StringValue("desc"), + "description", gotemplate.StaticValue(2), ), gotemplate.NewOption( "string", - gotemplate.StringValue("desc"), + "description", gotemplate.StaticValue("string"), ), gotemplate.NewOption( "bool", - gotemplate.StringValue("desc"), + "description", gotemplate.StaticValue(false), ), }, @@ -189,7 +189,7 @@ base: t.Run("error on type mismatch", func(t *testing.T) { gt.Options.Base[0] = gotemplate.NewOption( optionName, - gotemplate.StringValue("description"), + "description", gotemplate.StaticValue(false), ) @@ -209,7 +209,7 @@ base: Options: []gotemplate.Option{ gotemplate.NewOption( "option", - gotemplate.StringValue("description"), + "description", gotemplate.StaticValue(false), gotemplate.WithShouldDisplay(gotemplate.BoolValue(false)), ), @@ -234,7 +234,7 @@ extensions: Options: []gotemplate.Option{ gotemplate.NewOption( "option", - gotemplate.StringValue("description"), + "description", gotemplate.StaticValue(true), gotemplate.WithShouldDisplay(gotemplate.BoolValue(false)), ), @@ -279,7 +279,7 @@ func TestGT_LoadConfigValuesInteractively(t *testing.T) { gt.Options.Base = []gotemplate.Option{ gotemplate.NewOption( optionName, - gotemplate.StringValue("desc"), + "description", gotemplate.StaticValue("theDefault"), ), } @@ -289,7 +289,7 @@ func TestGT_LoadConfigValuesInteractively(t *testing.T) { Options: []gotemplate.Option{ gotemplate.NewOption( categoryOptionName, - gotemplate.StringValue("desc"), + "description", gotemplate.StaticValue(false), ), }, @@ -321,7 +321,7 @@ func TestGT_LoadConfigValuesInteractively(t *testing.T) { gt.Options.Base = []gotemplate.Option{ gotemplate.NewOption( optionName, - gotemplate.StringValue("desc"), + "description", gotemplate.StaticValue("DOES_NOT_MATCH"), gotemplate.WithValidator(gotemplate.RegexValidator( `[a-z1-9]+(-[a-z1-9]+)*$`, @@ -345,7 +345,7 @@ func TestGT_LoadConfigValuesInteractively(t *testing.T) { gt.Options.Base = []gotemplate.Option{ gotemplate.NewOption( optionName, - gotemplate.StringValue("desc"), + "description", gotemplate.StaticValue("DOES_NOT_MATCH"), gotemplate.WithValidator(gotemplate.RegexValidator( `[a-z1-9]+(-[a-z1-9]+)*$`, @@ -365,7 +365,7 @@ func TestGT_LoadConfigValuesInteractively(t *testing.T) { gt.Err = out gt.InScanner = bufio.NewScanner(strings.NewReader(optionValue + "not a bool\ntrue\n")) gt.Options.Base = []gotemplate.Option{ - gotemplate.NewOption(optionName, gotemplate.StringValue("desc"), gotemplate.StaticValue(false)), + gotemplate.NewOption(optionName, "description", gotemplate.StaticValue(false)), } optionValues, err := gt.LoadConfigValuesInteractively() @@ -381,12 +381,12 @@ func TestGT_LoadConfigValuesInteractively(t *testing.T) { gt.Options.Base = []gotemplate.Option{ gotemplate.NewOption( optionName, - gotemplate.StringValue("desc"), + "description", gotemplate.StaticValue("theDefault"), ), gotemplate.NewOption( templateOptionName, - gotemplate.StringValue("desc"), + "description", gotemplate.DynamicValue(func(vals *gotemplate.OptionValues) interface{} { return vals.Base[optionName].(string) + "-templated" }), @@ -416,7 +416,7 @@ func TestGT_LoadConfigValuesInteractively(t *testing.T) { gt.Options.Base = []gotemplate.Option{ gotemplate.NewOption( dependentOptionName, - gotemplate.StringValue("desc"), + "description", gotemplate.StaticValue(false), gotemplate.WithShouldDisplay(gotemplate.BoolValue(false)), ), @@ -439,12 +439,12 @@ func TestGT_LoadConfigValuesInteractively(t *testing.T) { gt.Options.Base = []gotemplate.Option{ gotemplate.NewOption( optionName, - gotemplate.StringValue("desc"), + "description", gotemplate.StaticValue(true), ), gotemplate.NewOption( intOptionName, - gotemplate.StringValue("desc"), + "description", gotemplate.StaticValue(3), ), } @@ -464,7 +464,7 @@ func TestGT_LoadConfigValuesInteractively(t *testing.T) { gt.Options.Base = []gotemplate.Option{ gotemplate.NewOption( optionName, - gotemplate.StringValue("desc"), + "description", // currently float is not supported gotemplate.StaticValue(2.0), ), @@ -587,7 +587,7 @@ func TestGT_InitNewProject(t *testing.T) { postHookTriggered := false gt.Options.Base = append(gt.Options.Base, gotemplate.NewOption( "testOption", - gotemplate.StringValue("desc"), + "description", gotemplate.StaticValue(true), gotemplate.WithPosthook(func(value interface{}, optionValues *gotemplate.OptionValues, targetDir string) error { postHookTriggered = true @@ -608,7 +608,7 @@ func TestGT_InitNewProject(t *testing.T) { postHookTriggered := false gt.Options.Base = append(gt.Options.Base, gotemplate.NewOption( "testOption", - gotemplate.StringValue("desc"), + "description", gotemplate.StaticValue(false), gotemplate.WithPosthook(func(value interface{}, optionValues *gotemplate.OptionValues, targetDir string) error { postHookTriggered = true diff --git a/pkg/gotemplate/options.go b/pkg/gotemplate/options.go index b7b535b4..2b8577cc 100644 --- a/pkg/gotemplate/options.go +++ b/pkg/gotemplate/options.go @@ -57,7 +57,7 @@ type Option struct { name string // description is the description of the option that should be shown. // It's a StringValuer since it could depend on some earlier input. - description StringValuer + description string // defaultValue is the default value of the option. // It's a Valuer since it could be depend on earlier inputs or some http call. defaultValue Valuer @@ -77,7 +77,7 @@ type Option struct { type PostHookFunc func(value interface{}, optionValues *OptionValues, targetDir string) error -func NewOption(name string, description StringValuer, defaultValue Valuer, opts ...NewOptionOption) Option { +func NewOption(name, description string, defaultValue Valuer, opts ...NewOptionOption) Option { option := Option{ name: name, description: description, @@ -115,8 +115,8 @@ func (s *Option) Name() string { return s.name } -func (s *Option) Description(currentValues *OptionValues) string { - return s.description.Value(currentValues) +func (s *Option) Description() string { + return s.description } // Default either returns the default value (possibly calculated with currentValues). @@ -194,7 +194,7 @@ func NewOptions(githubTagLister repos.GithubTagLister) *Options { // nolint: fun { name: "projectName", defaultValue: StaticValue("Awesome Project"), - description: StringValue("Name of the project"), + description: "Name of the project", }, { name: "projectSlug", @@ -202,22 +202,22 @@ func NewOptions(githubTagLister repos.GithubTagLister) *Options { // nolint: fun projectName := ov.Base["projectName"].(string) return strings.ReplaceAll(strings.ToLower(projectName), " ", "-") }), - description: StringValue("Technical name of the project for folders and names. This will also be used as output directory."), + description: "Technical name of the project for folders and names. This will also be used as output directory.", validator: RegexValidator(`^[a-z1-9]+(-[a-z1-9]+)*$`, "only lowercase letters, numbers and dashes"), }, { name: "projectDescription", defaultValue: StaticValue("The awesome project provides awesome features to awesome people."), - description: StringValue("Description of the project used in the README."), + description: "Description of the project used in the README.", }, { name: "appName", defaultValue: DynamicValue(func(ov *OptionValues) interface{} { return ov.Base["projectSlug"].(string) }), - description: StringValue(`The name of the binary that you want to create. + description: `The name of the binary that you want to create. Could be the same as your "projectSlug" but since Go supports multiple apps in one repo it could also be sth. else. -For example if your project is for some API there could be one app for the server and one CLI client.`), +For example if your project is for some API there could be one app for the server and one CLI client.`, validator: RegexValidator(`^[a-z1-9]+(-[a-z1-9]+)*$`, "only lowercase letters, numbers and dashes"), }, { @@ -226,10 +226,10 @@ For example if your project is for some API there could be one app for the serve projectSlug := vals.Base["projectSlug"].(string) return fmt.Sprintf("github.com/user/%s", projectSlug) }), - description: StringValue(`The name of the Go module defined in the "go.mod" file. + description: `The name of the Go module defined in the "go.mod" file. This is used if you want to "go get" the module. Please be aware that this depends on your version control system. -The default points to "github.com" but for devops for example it would look sth. like this "dev.azure.com/org/project/repo.git"`), +The default points to "github.com" but for devops for example it would look sth. like this "dev.azure.com/org/project/repo.git"`, validator: RegexValidator(`^[\S]+$`, "no whitespaces"), }, { @@ -242,7 +242,7 @@ The default points to "github.com" but for devops for example it would look sth. return latestTag.String() }), - description: StringValue("Golangci-lint version to use."), + description: "Golangci-lint version to use.", validator: RegexValidator( semverRegex, "valid semver version string", @@ -256,7 +256,7 @@ The default points to "github.com" but for devops for example it would look sth. { name: "license", defaultValue: StaticValue(1), - description: StringValue(`Set an OpenSource license. + description: `Set an OpenSource license. Unsure which to pick? Checkout Github's https://choosealicense.com/ Options: 0: Add no license @@ -267,7 +267,7 @@ Options: 5: GNU LGPLv3 6: Mozilla Public License 2.0 7: Boost Software License 1.0 - 8: The Unlicense`), + 8: The Unlicense`, postHook: func(v interface{}, _ *OptionValues, targetDir string) error { if v.(int) == 0 { return os.RemoveAll(path.Join(targetDir, "LICENSE")) @@ -286,7 +286,7 @@ Options: } return strings.TrimSpace(buffer.String()) }), - description: StringValue(`License author`), + description: `License author`, shouldDisplay: DynamicBoolValue(func(vals *OptionValues) bool { switch vals.Extensions["openSource"]["license"].(int) { case 1, 2: @@ -306,7 +306,7 @@ Options: } return strings.TrimSpace(buffer.String()) }), - description: StringValue("Set the codeowner of the project"), + description: "Set the codeowner of the project", }, }, }, @@ -316,12 +316,12 @@ Options: { name: "provider", defaultValue: StaticValue(1), - description: StringValue(`Set an CI pipeline provider integration + description: `Set an CI pipeline provider integration Options: 0: No CI 1: Github 2: Gitlab - 3: Azure DevOps`), + 3: Azure DevOps`, postHook: func(v interface{}, _ *OptionValues, targetDir string) error { ciFiles := map[int][]string{ 0: {}, @@ -351,7 +351,7 @@ Options: { name: "base", defaultValue: StaticValue(false), - description: StringValue("Base configuration for gRPC"), + description: "Base configuration for gRPC", postHook: func(v interface{}, _ *OptionValues, targetDir string) error { set := v.(bool) files := []string{"api/proto", "tools.go", "buf.gen.yaml", "buf.yaml", "api/openapi.v1.yml"} @@ -365,7 +365,7 @@ Options: { name: "grpcGateway", defaultValue: StaticValue(false), - description: StringValue("Extend gRPC configuration with grpc-gateway"), + description: "Extend gRPC configuration with grpc-gateway", shouldDisplay: DynamicBoolValue(func(vals *OptionValues) bool { return vals.Extensions["grpc"]["base"].(bool) }), diff --git a/pkg/gotemplate/print.go b/pkg/gotemplate/print.go index d8c28541..6291e5bd 100644 --- a/pkg/gotemplate/print.go +++ b/pkg/gotemplate/print.go @@ -28,7 +28,7 @@ func (gt *GT) printWarningf(format string, a ...interface{}) { func (gt *GT) printOption(opts *Option, optionValues *OptionValues) { highlight := color.New(color.FgCyan).SprintFunc() underline := color.New(color.FgHiYellow, color.Underline).SprintFunc() - gt.printf("%s\n", underline(opts.Description(optionValues))) + gt.printf("%s\n", underline(opts.Description())) gt.printf("%s: (%v) ", highlight(opts.Name()), opts.Default(optionValues)) }