From 0f32f5ddc0399a16c85eb491c8c44ce0a4b60b07 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9mi=20Lapeyre?= Date: Sat, 26 Nov 2022 11:03:07 +0100 Subject: [PATCH] Add subcategory support to the generate command This patch adds a --subcategory flag that let users give a mapping that will use to set the `subcategory` field in the documentation: tfplugindocs generate --subcategory consul_acl=ACL --subcategory consul_admin="Admin Partition" The format for the flag is `prefix="Sub Category"` so all resources and datasources starting with `consul_acl` like `consul_acl_policy`, `consul_acl_token`, etc. will have the `subcategory: "ACL"` in the generated documentation. This is not very elegant but should work in most cases as needed. Closes https://github.com/hashicorp/terraform-plugin-docs/issues/156 --- README.md | 14 ++++---- internal/cmd/generate.go | 7 +++- internal/cmd/validate.go | 2 +- internal/provider/generate.go | 66 +++++++++++++++++++++++++++++++---- internal/provider/template.go | 8 +++-- 5 files changed, 79 insertions(+), 18 deletions(-) diff --git a/README.md b/README.md index 63d2c959..7a2e95e2 100644 --- a/README.md +++ b/README.md @@ -29,7 +29,7 @@ Available commands are: the generate command is run by default generate generates a plugin website from code, templates, and examples for the current directory validate validates a plugin website for the current directory - + ``` `generate` command: @@ -39,14 +39,15 @@ $ tfplugindocs generate --help Usage: tfplugindocs generate [] - --examples-dir examples directory (default: "examples") - --ignore-deprecated don't generate documentation for deprecated resources and data-sources (default: "false") - --legacy-sidebar generate the legacy .erb sidebar file (default: "false") + --examples-dir examples directory (default: "examples") + --ignore-deprecated don't generate documentation for deprecated resources and data-sources (default: "false") + --legacy-sidebar generate the legacy .erb sidebar file (default: "false") --provider-name provider name, as used in Terraform configurations --rendered-provider-name provider name, as generated in documentation (ex. page titles, ...) - --rendered-website-dir output directory (default: "docs") + --rendered-website-dir output directory (default: "docs") + --subcategory an optional subcategory mapping to group resources, can be specified multiple time e.g. --subcategory consul_acl=ACL --subcategory consul_admin="Admin Partition" --tf-version terraform binary version to download - --website-source-dir templates directory (default: "templates") + --website-source-dir templates directory (default: "templates") --website-temp-dir temporary directory (used during generation) ``` @@ -150,6 +151,7 @@ using the following data fields and functions: |------------------------:|:------:|-------------------------------------------------------------------------------------------| | `.Name` | string | Name of the resource/data-source (ex. `tls_certificate`) | | `.Type` | string | Either `Resource` or `Data Source` | +| `.SubCategory` | string | The subcategory for this resource or an empty string if unset | | `.Description` | string | Resource / Data Source description | | `.HasExample` | bool | Is there an example file? | | `.ExampleFile` | string | Path to the file with the terraform configuration example | diff --git a/internal/cmd/generate.go b/internal/cmd/generate.go index 54e4d201..6809b342 100644 --- a/internal/cmd/generate.go +++ b/internal/cmd/generate.go @@ -22,6 +22,7 @@ type generateCmd struct { flagWebsiteTmpDir string flagWebsiteSourceDir string tfVersion string + flagSubcategory provider.SubCategories } func (cmd *generateCmd) Synopsis() string { @@ -42,7 +43,7 @@ func (cmd *generateCmd) Help() string { } }) - strBuilder.WriteString(fmt.Sprintf("\nUsage: tfplugindocs generate []\n\n")) + strBuilder.WriteString("\nUsage: tfplugindocs generate []\n\n") cmd.Flags().VisitAll(func(f *flag.Flag) { if f.DefValue != "" { strBuilder.WriteString(fmt.Sprintf(" --%s %s%s%s (default: %q)\n", @@ -67,6 +68,8 @@ func (cmd *generateCmd) Help() string { } func (cmd *generateCmd) Flags() *flag.FlagSet { + cmd.flagSubcategory = provider.SubCategories{} + fs := flag.NewFlagSet("generate", flag.ExitOnError) fs.BoolVar(&cmd.flagLegacySidebar, "legacy-sidebar", false, "generate the legacy .erb sidebar file") fs.StringVar(&cmd.flagProviderName, "provider-name", "", "provider name, as used in Terraform configurations") @@ -77,6 +80,7 @@ func (cmd *generateCmd) Flags() *flag.FlagSet { fs.StringVar(&cmd.flagWebsiteSourceDir, "website-source-dir", "templates", "templates directory") fs.StringVar(&cmd.tfVersion, "tf-version", "", "terraform binary version to download") fs.BoolVar(&cmd.flagIgnoreDeprecated, "ignore-deprecated", false, "don't generate documentation for deprecated resources and data-sources") + fs.Var(&cmd.flagSubcategory, "subcategory", "an optional subcategory mapping to group resources, can be specified multiple time e.g. --subcategory consul_acl=ACL --subcategory consul_admin=\"Admin Partition\"") return fs } @@ -103,6 +107,7 @@ func (cmd *generateCmd) runInternal() error { cmd.flagWebsiteSourceDir, cmd.tfVersion, cmd.flagIgnoreDeprecated, + cmd.flagSubcategory, ) if err != nil { return fmt.Errorf("unable to generate website: %w", err) diff --git a/internal/cmd/validate.go b/internal/cmd/validate.go index 0f54c566..890ef8d1 100644 --- a/internal/cmd/validate.go +++ b/internal/cmd/validate.go @@ -30,7 +30,7 @@ func (cmd *validateCmd) Help() string { } }) - strBuilder.WriteString(fmt.Sprintf("\nUsage: tfplugindocs validate []\n\n")) + strBuilder.WriteString("\nUsage: tfplugindocs validate []\n\n") cmd.Flags().VisitAll(func(f *flag.Flag) { if f.DefValue != "" { strBuilder.WriteString(fmt.Sprintf(" --%s %s%s%s (default: %q)\n", diff --git a/internal/provider/generate.go b/internal/provider/generate.go index ce5a4b13..b1294904 100644 --- a/internal/provider/generate.go +++ b/internal/provider/generate.go @@ -75,9 +75,51 @@ type generator struct { websiteTmpDir string websiteSourceDir string + subcategories SubCategories + ui cli.Ui } +type SubCategories map[string]string + +func (s *SubCategories) String() string { + if s == nil { + return "" + } + + var subcategories []string + for k, v := range *s { + subcategories = append(subcategories, fmt.Sprintf("%s=%q", k, v)) + } + + return strings.Join(subcategories, ", ") +} + +func (s *SubCategories) Set(v string) error { + parts := strings.SplitN(v, "=", 2) + if len(parts) != 2 { + return fmt.Errorf("wrong format for %q, expected prefix=\"SubCategory Name\"", v) + } + + prefix := parts[0] + if sc, found := (*s)[prefix]; found { + return fmt.Errorf("%s is already registered with subcategory %q", prefix, sc) + } + + (*s)[prefix] = parts[1] + return nil +} + +func (s *SubCategories) Get(name string) string { + for k, v := range *s { + if strings.HasPrefix(name, k) { + return v + } + } + + return "" +} + func (g *generator) infof(format string, a ...interface{}) { g.ui.Info(fmt.Sprintf(format, a...)) } @@ -86,7 +128,7 @@ func (g *generator) warnf(format string, a ...interface{}) { g.ui.Warn(fmt.Sprintf(format, a...)) } -func Generate(ui cli.Ui, legacySidebar bool, providerName, renderedProviderName, renderedWebsiteDir, examplesDir, websiteTmpDir, websiteSourceDir, tfVersion string, ignoreDeprecated bool) error { +func Generate(ui cli.Ui, legacySidebar bool, providerName, renderedProviderName, renderedWebsiteDir, examplesDir, websiteTmpDir, websiteSourceDir, tfVersion string, ignoreDeprecated bool, subcategories SubCategories) error { g := &generator{ ignoreDeprecated: ignoreDeprecated, legacySidebar: legacySidebar, @@ -99,6 +141,8 @@ func Generate(ui cli.Ui, legacySidebar bool, providerName, renderedProviderName, websiteTmpDir: websiteTmpDir, websiteSourceDir: websiteSourceDir, + subcategories: subcategories, + ui: ui, } @@ -192,7 +236,7 @@ func (g *generator) Generate(ctx context.Context) error { return nil } -func (g *generator) renderMissingResourceDoc(providerName, name, typeName string, schema *tfjson.Schema, websiteFileTemplate resourceFileTemplate, fallbackWebsiteFileTemplate resourceFileTemplate, websiteStaticCandidateTemplates []resourceFileTemplate, examplesFileTemplate resourceFileTemplate, examplesImportTemplate *resourceFileTemplate) error { +func (g *generator) renderMissingResourceDoc(providerName, name, typeName, subCategory string, schema *tfjson.Schema, websiteFileTemplate resourceFileTemplate, fallbackWebsiteFileTemplate resourceFileTemplate, websiteStaticCandidateTemplates []resourceFileTemplate, examplesFileTemplate resourceFileTemplate, examplesImportTemplate *resourceFileTemplate) error { tmplPath, err := websiteFileTemplate.Render(name, providerName) if err != nil { return fmt.Errorf("unable to render path for resource %q: %w", name, err) @@ -257,7 +301,7 @@ func (g *generator) renderMissingResourceDoc(providerName, name, typeName string } g.infof("generating template for %q", name) - md, err := targetResourceTemplate.Render(name, providerName, g.renderedProviderName, typeName, examplePath, importPath, schema) + md, err := targetResourceTemplate.Render(name, providerName, g.renderedProviderName, typeName, subCategory, examplePath, importPath, schema) if err != nil { return fmt.Errorf("unable to render template for %q: %w", name, err) } @@ -325,7 +369,10 @@ func (g *generator) renderMissingDocs(providerName string, providerSchema *tfjso continue } - err := g.renderMissingResourceDoc(providerName, name, "Resource", schema, + subCategory := g.subcategories.Get(name) + + err := g.renderMissingResourceDoc(providerName, name, "Resource", subCategory, + schema, websiteResourceFileTemplate, websiteResourceFallbackFileTemplate, websiteResourceFileStatic, @@ -342,7 +389,10 @@ func (g *generator) renderMissingDocs(providerName string, providerSchema *tfjso continue } - err := g.renderMissingResourceDoc(providerName, name, "Data Source", schema, + subCategory := g.subcategories.Get(name) + + err := g.renderMissingResourceDoc(providerName, name, "Data Source", subCategory, + schema, websiteDataSourceFileTemplate, websiteDataSourceFallbackFileTemplate, websiteDataSourceFileStatic, @@ -425,10 +475,11 @@ func (g *generator) renderStaticWebsite(providerName string, providerSchema *tfj switch relDir { case "data-sources/": resSchema, resName := resourceSchema(providerSchema.DataSourceSchemas, shortName, relFile) + subCategory := g.subcategories.Get(resName) exampleFilePath := filepath.Join(g.examplesDir, "data-sources", resName, "data-source.tf") if resSchema != nil { tmpl := resourceTemplate(tmplData) - render, err := tmpl.Render(resName, providerName, g.renderedProviderName, "Data Source", exampleFilePath, "", resSchema) + render, err := tmpl.Render(resName, providerName, g.renderedProviderName, "Data Source", subCategory, exampleFilePath, "", resSchema) if err != nil { return fmt.Errorf("unable to render data source template %q: %w", rel, err) } @@ -441,12 +492,13 @@ func (g *generator) renderStaticWebsite(providerName string, providerSchema *tfj g.warnf("data source entitled %q, or %q does not exist", shortName, resName) case "resources/": resSchema, resName := resourceSchema(providerSchema.ResourceSchemas, shortName, relFile) + subCategory := g.subcategories.Get(resName) exampleFilePath := filepath.Join(g.examplesDir, "resources", resName, "resource.tf") importFilePath := filepath.Join(g.examplesDir, "resources", resName, "import.sh") if resSchema != nil { tmpl := resourceTemplate(tmplData) - render, err := tmpl.Render(resName, providerName, g.renderedProviderName, "Resource", exampleFilePath, importFilePath, resSchema) + render, err := tmpl.Render(resName, providerName, g.renderedProviderName, "Resource", subCategory, exampleFilePath, importFilePath, resSchema) if err != nil { return fmt.Errorf("unable to render resource template %q: %w", rel, err) } diff --git a/internal/provider/template.go b/internal/provider/template.go index dcc1e541..ff97ce11 100644 --- a/internal/provider/template.go +++ b/internal/provider/template.go @@ -165,7 +165,7 @@ func (t providerTemplate) Render(providerName, renderedProviderName, exampleFile }) } -func (t resourceTemplate) Render(name, providerName, renderedProviderName, typeName, exampleFile, importFile string, schema *tfjson.Schema) (string, error) { +func (t resourceTemplate) Render(name, providerName, renderedProviderName, typeName, subCategory, exampleFile, importFile string, schema *tfjson.Schema) (string, error) { schemaBuffer := bytes.NewBuffer(nil) err := schemamd.Render(schema, schemaBuffer) if err != nil { @@ -181,6 +181,7 @@ func (t resourceTemplate) Render(name, providerName, renderedProviderName, typeN Type string Name string Description string + SubCategory string HasExample bool ExampleFile string @@ -198,6 +199,7 @@ func (t resourceTemplate) Render(name, providerName, renderedProviderName, typeN Type: typeName, Name: name, Description: schema.Block.Description, + SubCategory: subCategory, HasExample: exampleFile != "" && fileExists(exampleFile), ExampleFile: exampleFile, @@ -217,7 +219,7 @@ func (t resourceTemplate) Render(name, providerName, renderedProviderName, typeN const defaultResourceTemplate resourceTemplate = `--- ` + frontmatterComment + ` page_title: "{{.Name}} {{.Type}} - {{.ProviderName}}" -subcategory: "" +subcategory: "{{.SubCategory}}" description: |- {{ .Description | plainmarkdown | trimspace | prefixlines " " }} --- @@ -246,7 +248,7 @@ Import is supported using the following syntax: const defaultProviderTemplate providerTemplate = `--- ` + frontmatterComment + ` page_title: "{{.ProviderShortName}} Provider" -subcategory: "" +subcategory: "{{.SubCategory}}" description: |- {{ .Description | plainmarkdown | trimspace | prefixlines " " }} ---