From f5bebdc2a6318a8968618eb20355f30c41f45c80 Mon Sep 17 00:00:00 2001 From: Brian Flad Date: Tue, 19 Aug 2025 13:39:17 -0400 Subject: [PATCH 1/3] feat(generate): Support multiple configuration example files in default templates Reference: https://github.com/hashicorp/terraform-plugin-docs/issues/508 This change updates the generate command to search for multiple configuration example files using a wildcard and render all found files with the default templates (e.g. `resource*.tf` instead of just `resource.tf`). Compatibility with the existing singular `HasExample` and `ExampleFile` fields for templating is preserved, while new plural `HasExamples` and `ExampleFiles` fields were added into the template data. The testing for this feature was added by duplicating the existing "no templates" txtar, then adding second configuration example files: ```diff 4c4 < # Successful run of tfplugindocs on a Framework provider with examples but no templates or pre-exiting docs. --- > # Successful run of tfplugindocs on a Framework provider with multiple configuration examples but no templates or pre-exiting docs. 57c57 < configurable_attribute = "some-value" --- > configurable_attribute = "some-value1" 60a61,66 > ```terraform > data "scaffolding_example" "example" { > configurable_attribute = "some-value2" > } > ``` > 91a98,103 > ```terraform > output "test" { > value = provider::scaffolding::example("testvalue3", "testvalue4") > } > ``` > 122a135,140 > } > ``` > > ```terraform > output "test" { > value = provider::scaffolding::no-variadic("testvalue2") 153c171 < # example configuration here --- > # first example configuration here 156a175,180 > ```terraform > provider "scaffolding" { > # second example configuration here > } > ``` > 180c204 < configurable_attribute = "some-value" --- > configurable_attribute = "some-value1" 183a208,213 > ```terraform > resource "scaffolding_example" "example" { > configurable_attribute = "some-value2" > } > ``` > 258c288 < configurable_attribute = "some-value" --- > configurable_attribute = "some-value1" 261a292,297 > ```terraform > resource "scaffolding_example" "example" { > configurable_attribute = "some-value2" > } > ``` > 285c321 < configurable_attribute = "some-value" --- > configurable_attribute = "some-value1" 286a323,326 > -- examples/data-sources/scaffolding_example/data-source2.tf -- > data "scaffolding_example" "example" { > configurable_attribute = "some-value2" > } 290a331,334 > -- examples/functions/example/function2.tf -- > output "test" { > value = provider::scaffolding::example("testvalue3", "testvalue4") > } 294a339,342 > -- examples/functions/no-variadic/function2.tf -- > output "test" { > value = provider::scaffolding::no-variadic("testvalue2") > } 297c345 < # example configuration here --- > # first example configuration here 298a347,350 > -- examples/provider/provider2.tf -- > provider "scaffolding" { > # second example configuration here > } 323c375 < configurable_attribute = "some-value" --- > configurable_attribute = "some-value1" 324a377,380 > -- examples/resources/scaffolding_example/resource2.tf -- > resource "scaffolding_example" "example" { > configurable_attribute = "some-value2" > } 327c383 < configurable_attribute = "some-value" --- > configurable_attribute = "some-value1" 329c385,388 < --- > -- examples/ephemeral-resources/scaffolding_example/ephemeral-resource2.tf -- > resource "scaffolding_example" "example" { > configurable_attribute = "some-value2" > } ``` --- .../unreleased/FEATURES-20250819-133400.yaml | 5 + README.md | 40 +- ...ccess_no_templates_multiple_examples.txtar | 547 ++++++++++++++++++ ...success_no_templates_single_example.txtar} | 0 internal/provider/generate.go | 69 ++- internal/provider/template.go | 63 +- internal/provider/template_test.go | 4 +- 7 files changed, 676 insertions(+), 52 deletions(-) create mode 100644 .changes/unreleased/FEATURES-20250819-133400.yaml create mode 100644 cmd/tfplugindocs/testdata/scripts/schema-json/generate/framework_provider_success_no_templates_multiple_examples.txtar rename cmd/tfplugindocs/testdata/scripts/schema-json/generate/{framework_provider_success_no_templates.txtar => framework_provider_success_no_templates_single_example.txtar} (100%) diff --git a/.changes/unreleased/FEATURES-20250819-133400.yaml b/.changes/unreleased/FEATURES-20250819-133400.yaml new file mode 100644 index 00000000..0896a503 --- /dev/null +++ b/.changes/unreleased/FEATURES-20250819-133400.yaml @@ -0,0 +1,5 @@ +kind: FEATURES +body: 'generate: Support multiple configuration example files in default templates' +time: 2025-08-19T13:34:00.429551-04:00 +custom: + Issue: "508" diff --git a/README.md b/README.md index c72c187f..4976dec0 100644 --- a/README.md +++ b/README.md @@ -223,17 +223,17 @@ For examples: > **NOTE:** In the following conventional paths for examples, `` and `` include the provider prefix as well, but the provider prefix is **NOT** included in``. > For example, the data source [`caller_identity`](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/caller_identity) in the `aws` provider would have an "example" conventional path of: `examples/data-sources/aws_caller_identity/data-source.tf` -| Path | Description | -|---------------------------------------------------------------------------|--------------------------------------------| -| `examples/` | Root of examples | -| `examples/provider/provider.tf` | Provider example config | -| `examples/data-sources//data-source.tf` | Data source example config | -| `examples/ephemeral-resources//ephemeral-resource.tf` | Ephemeral resource example config | -| `examples/functions//function.tf` | Function example config | -| `examples/resources//resource.tf` | Resource example config | -| `examples/resources//import.sh` | Resource example import command | -| `examples/resources//import-by-string-id.tf` | Resource example import by id config | -| `examples/resources//import-by-identity.tf` | Resource example import by identity config | +| Path | Description | +|------------------------------------------------------------------------------|--------------------------------------------| +| `examples/` | Root of examples | +| `examples/provider/provider<*>.tf` | Provider example config(s) | +| `examples/data-sources//data-source<*>.tf` | Data source example config(s) | +| `examples/ephemeral-resources//ephemeral-resource<*>.tf` | Ephemeral resource example config(s) | +| `examples/functions//function<*>.tf` | Function example config(s) | +| `examples/resources//resource<*>.tf` | Resource example config(s) | +| `examples/resources//import.sh` | Resource example import command | +| `examples/resources//import-by-string-id.tf` | Resource example import by id config | +| `examples/resources//import-by-identity.tf` | Resource example import by identity config | #### Migration @@ -285,8 +285,10 @@ using the following data fields and functions: | Field | Type | Description | |-------------------------|--------|-------------------------------------------------------------------------------------------| | `.Description` | string | Provider description | -| `.HasExample` | bool | Is there an example file? | -| `.ExampleFile` | string | Path to the file with the terraform configuration example | +| `.HasExample` | bool | (Legacy) Is there an example file? | +| `.HasExamples` | bool | Are there example files? Always true if HasExample is true. | +| `.ExampleFile` | string | (Legacy) Path to the file with the Terraform configuration example. | +| `.ExampleFiles` | string | Paths to the files with the Terraform configuration example. Includes ExampleFile. | | `.ProviderName` | string | Canonical provider name (ex. `terraform-provider-random`) | | `.ProviderShortName` | string | Short version of the rendered provider name (ex. `random`) | | `.RenderedProviderName` | string | Value provided via argument `--rendered-provider-name`, otherwise same as `.ProviderName` | @@ -299,8 +301,10 @@ 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` | | `.Description` | string | Resource / Data Source description | -| `.HasExample` | bool | Is there an example file? | -| `.ExampleFile` | string | Path to the file with the terraform configuration example | +| `.HasExample` | bool | (Legacy) Is there an example file? | +| `.HasExamples` | bool | Are there example files? Always true if HasExample is true. | +| `.ExampleFile` | string | (Legacy) Path to the file with the Terraform configuration example. | +| `.ExampleFiles` | string | Paths to the files with Terraform configuration examples. Includes ExampleFile. | | `.HasImport` | bool | Is there an import shell file? (`terraform import` shell example) | | `.ImportFile` | string | Path to the file with the command for importing the resource | | `.HasImportIDConfig` | bool | Is there an import terraform config file? (`import` block example with `id`) | @@ -321,8 +325,10 @@ using the following data fields and functions: | `.Type` | string | Returns `Function` | | `.Description` | string | Function description | | `.Summary` | string | Function summary | -| `.HasExample` | bool | Is there an example file? | -| `.ExampleFile` | string | Path to the file with the terraform configuration example | +| `.HasExample` | bool | (Legacy) Is there an example file? | +| `.HasExamples` | bool | Are there example files? Always true if HasExample is true. | +| `.ExampleFile` | string | (Legacy) Path to the file with the Terraform configuration example | +| `.ExampleFiles` | string | Paths to the files with Terraform configuration examples. Includes ExampleFile. | | `.ProviderName` | string | Canonical provider name (ex. `terraform-provider-random`) | | `.ProviderShortName` | string | Short version of the rendered provider name (ex. `random`) | | `.RenderedProviderName` | string | Value provided via argument `--rendered-provider-name`, otherwise same as `.ProviderName` | diff --git a/cmd/tfplugindocs/testdata/scripts/schema-json/generate/framework_provider_success_no_templates_multiple_examples.txtar b/cmd/tfplugindocs/testdata/scripts/schema-json/generate/framework_provider_success_no_templates_multiple_examples.txtar new file mode 100644 index 00000000..713c0918 --- /dev/null +++ b/cmd/tfplugindocs/testdata/scripts/schema-json/generate/framework_provider_success_no_templates_multiple_examples.txtar @@ -0,0 +1,547 @@ +# Copyright (c) HashiCorp, Inc. +# SPDX-License-Identifier: MPL-2.0 + +# Successful run of tfplugindocs on a Framework provider with multiple configuration examples but no templates or pre-exiting docs. +[!unix] skip +exec tfplugindocs --provider-name=terraform-provider-scaffolding --providers-schema=schema.json --rendered-provider-name=Scaffolding +cmp stdout expected-output.txt +cmp docs/index.md expected-index.md +cmp docs/data-sources/example.md expected-datasource.md +cmp docs/resources/example.md expected-resource.md +cmp docs/functions/example.md expected-function.md +cmp docs/functions/no-variadic.md expected-no-variadic-function.md +cmp docs/ephemeral-resources/example.md expected-ephemeral-resource.md + +-- expected-output.txt -- +rendering website for provider "terraform-provider-scaffolding" (as "Scaffolding") +exporting schema from JSON file +getting provider schema +generating missing templates +generating missing resource content +generating new template for "scaffolding_example" +generating missing data source content +generating new template for data-source "scaffolding_example" +generating missing function content +generating new template for function "example" +generating new template for function "no-variadic" +generating missing ephemeral resource content +generating new template for "scaffolding_example" +generating missing provider content +generating new template for "terraform-provider-scaffolding" +rendering static website +cleaning rendered website dir +rendering templated website to static markdown +rendering "data-sources/example.md.tmpl" +rendering "ephemeral-resources/example.md.tmpl" +rendering "functions/example.md.tmpl" +rendering "functions/no-variadic.md.tmpl" +rendering "index.md.tmpl" +rendering "resources/example.md.tmpl" +-- expected-datasource.md -- +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "scaffolding_example Data Source - Scaffolding" +subcategory: "" +description: |- + Example data source +--- + +# scaffolding_example (Data Source) + +Example data source + +## Example Usage + +```terraform +data "scaffolding_example" "example" { + configurable_attribute = "some-value1" +} +``` + +```terraform +data "scaffolding_example" "example" { + configurable_attribute = "some-value2" +} +``` + + +## Schema + +### Optional + +- `configurable_attribute` (String) Example configurable attribute + +### Read-Only + +- `id` (String) Example identifier +-- expected-function.md -- +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "example function - Scaffolding" +subcategory: "" +description: |- + Echo a string +--- + +# function: example + +Given a string value, returns the same value. + +## Example Usage + +```terraform +output "test" { + value = provider::scaffolding::example("testvalue1", "testvalue2") +} +``` + +```terraform +output "test" { + value = provider::scaffolding::example("testvalue3", "testvalue4") +} +``` + +## Signature + + +```text +example(input string, variadicInput string...) string +``` + +## Arguments + + +1. `input` (String) Value to echo. + +1. `variadicInput` (Variadic, String) Variadic input to echo. +-- expected-no-variadic-function.md -- +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "no-variadic function - Scaffolding" +subcategory: "" +description: |- + Echo a string +--- + +# function: no-variadic + +Given a string value, returns the same value. + +## Example Usage + +```terraform +output "test" { + value = provider::scaffolding::no-variadic("testvalue1") +} +``` + +```terraform +output "test" { + value = provider::scaffolding::no-variadic("testvalue2") +} +``` + +## Signature + + +```text +no-variadic(input string) string +``` + +## Arguments + + +1. `input` (String) Value to echo. +-- expected-index.md -- +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "Scaffolding Provider" +description: |- + Example provider +--- + +# Scaffolding Provider + +Example provider + +## Example Usage + +```terraform +provider "scaffolding" { + # first example configuration here +} +``` + +```terraform +provider "scaffolding" { + # second example configuration here +} +``` + + +## Schema + +### Optional + +- `endpoint` (String) Example provider attribute +-- expected-resource.md -- +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "scaffolding_example Resource - Scaffolding" +subcategory: "" +description: |- + Example resource +--- + +# scaffolding_example (Resource) + +Example resource + +## Example Usage + +```terraform +resource "scaffolding_example" "example" { + configurable_attribute = "some-value1" +} +``` + +```terraform +resource "scaffolding_example" "example" { + configurable_attribute = "some-value2" +} +``` + + +## Schema + +### Optional + +- `configurable_attribute` (String) Example configurable attribute +- `defaulted` (String) Example configurable attribute with default value + +### Read-Only + +- `id` (String) Example identifier + +## Import + +Import is supported using the following syntax: + +In Terraform v1.12.0 and later, the [`import` block](https://developer.hashicorp.com/terraform/language/import) can be used with the `identity` attribute, for example: + +```terraform +import { + to = scaffolding_example.example + identity = { + name = "resource-abc" + project = "test-project-123" # can also be read via SCAFFOLDING_PROJECT environment variable + region = "us-east-2" # can also be read via SCAFFOLDING_REGION environment variable + } +} + +resource "scaffolding_example" "example" { + configurable_attribute = "some-value" +} +``` + + +### Identity Schema + +#### Required + +- `name` (String) Name of resource. + +#### Optional + +- `project` (String) Project of resource, can also be sourced via `SCAFFOLDING_PROJECT` environment variable. +- `region` (String) Region of resource, can also be sourced via `SCAFFOLDING_REGION` environment variable. + +In Terraform v1.5.0 and later, the [`import` block](https://developer.hashicorp.com/terraform/language/import) can be used with the `id` attribute, for example: + +```terraform +import { + to = scaffolding_example.example + id = "id-123" +} + +resource "scaffolding_example" "example" { + configurable_attribute = "some-value" +} +``` +-- expected-ephemeral-resource.md -- +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "scaffolding_example Ephemeral Resource - Scaffolding" +subcategory: "" +description: |- + Example ephemeral resource +--- + +# scaffolding_example (Ephemeral Resource) + +Example ephemeral resource + +## Example Usage + +```terraform +resource "scaffolding_example" "example" { + configurable_attribute = "some-value1" +} +``` + +```terraform +resource "scaffolding_example" "example" { + configurable_attribute = "some-value2" +} +``` + + +## Schema + +### Optional + +- `configurable_attribute` (String) Example configurable attribute +- `defaulted` (String) Example configurable attribute with default value + +### Read-Only + +- `id` (String) Example identifier +-- examples/README.md -- +# Examples + +This directory contains examples that are mostly used for documentation, but can also be run/tested manually via the Terraform CLI. + +The document generation tool looks for files in the following locations by default. All other *.tf files besides the ones mentioned below are ignored by the documentation tool. This is useful for creating examples that can run and/or ar testable even if some parts are not relevant for the documentation. + +* **provider/provider.tf** example file for the provider index page +* **data-sources/`full data source name`/data-source.tf** example file for the named data source page +* **resources/`full resource name`/resource.tf** example file for the named data source page +-- examples/data-sources/scaffolding_example/data-source.tf -- +data "scaffolding_example" "example" { + configurable_attribute = "some-value1" +} +-- examples/data-sources/scaffolding_example/data-source2.tf -- +data "scaffolding_example" "example" { + configurable_attribute = "some-value2" +} +-- examples/functions/example/function.tf -- +output "test" { + value = provider::scaffolding::example("testvalue1", "testvalue2") +} +-- examples/functions/example/function2.tf -- +output "test" { + value = provider::scaffolding::example("testvalue3", "testvalue4") +} +-- examples/functions/no-variadic/function.tf -- +output "test" { + value = provider::scaffolding::no-variadic("testvalue1") +} +-- examples/functions/no-variadic/function2.tf -- +output "test" { + value = provider::scaffolding::no-variadic("testvalue2") +} +-- examples/provider/provider.tf -- +provider "scaffolding" { + # first example configuration here +} +-- examples/provider/provider2.tf -- +provider "scaffolding" { + # second example configuration here +} +-- examples/resources/scaffolding_example/import-by-string-id.tf -- +import { + to = scaffolding_example.example + id = "id-123" +} + +resource "scaffolding_example" "example" { + configurable_attribute = "some-value" +} +-- examples/resources/scaffolding_example/import-by-identity.tf -- +import { + to = scaffolding_example.example + identity = { + name = "resource-abc" + project = "test-project-123" # can also be read via SCAFFOLDING_PROJECT environment variable + region = "us-east-2" # can also be read via SCAFFOLDING_REGION environment variable + } +} + +resource "scaffolding_example" "example" { + configurable_attribute = "some-value" +} +-- examples/resources/scaffolding_example/resource.tf -- +resource "scaffolding_example" "example" { + configurable_attribute = "some-value1" +} +-- examples/resources/scaffolding_example/resource2.tf -- +resource "scaffolding_example" "example" { + configurable_attribute = "some-value2" +} +-- examples/ephemeral-resources/scaffolding_example/ephemeral-resource.tf -- +resource "scaffolding_example" "example" { + configurable_attribute = "some-value1" +} +-- examples/ephemeral-resources/scaffolding_example/ephemeral-resource2.tf -- +resource "scaffolding_example" "example" { + configurable_attribute = "some-value2" +} +-- schema.json -- +{ + "format_version": "1.0", + "provider_schemas": { + "registry.terraform.io/hashicorp/scaffolding": { + "provider": { + "version": 0, + "block": { + "attributes": { + "endpoint": { + "type": "string", + "description": "Example provider attribute", + "description_kind": "markdown", + "optional": true + } + }, + "description": "Example provider", + "description_kind": "markdown" + } + }, + "resource_schemas": { + "scaffolding_example": { + "version": 0, + "block": { + "attributes": { + "configurable_attribute": { + "type": "string", + "description": "Example configurable attribute", + "description_kind": "markdown", + "optional": true + }, + "defaulted": { + "type": "string", + "description": "Example configurable attribute with default value", + "description_kind": "markdown", + "optional": true, + "computed": true + }, + "id": { + "type": "string", + "description": "Example identifier", + "description_kind": "markdown", + "computed": true + } + }, + "description": "Example resource", + "description_kind": "markdown" + } + } + }, + "resource_identity_schemas": { + "scaffolding_example": { + "version": 0, + "attributes": { + "name": { + "type": "string", + "description": "Name of resource.", + "required_for_import": true + }, + "project": { + "type": "string", + "description": "Project of resource, can also be sourced via `SCAFFOLDING_PROJECT` environment variable.", + "optional_for_import": true + }, + "region": { + "type": "string", + "description": "Region of resource, can also be sourced via `SCAFFOLDING_REGION` environment variable.", + "optional_for_import": true + } + } + } + }, + "ephemeral_resource_schemas": { + "scaffolding_example": { + "version": 0, + "block": { + "attributes": { + "configurable_attribute": { + "type": "string", + "description": "Example configurable attribute", + "description_kind": "markdown", + "optional": true + }, + "defaulted": { + "type": "string", + "description": "Example configurable attribute with default value", + "description_kind": "markdown", + "optional": true, + "computed": true + }, + "id": { + "type": "string", + "description": "Example identifier", + "description_kind": "markdown", + "computed": true + } + }, + "description": "Example ephemeral resource", + "description_kind": "markdown" + } + } + }, + "data_source_schemas": { + "scaffolding_example": { + "version": 0, + "block": { + "attributes": { + "configurable_attribute": { + "type": "string", + "description": "Example configurable attribute", + "description_kind": "markdown", + "optional": true + }, + "id": { + "type": "string", + "description": "Example identifier", + "description_kind": "markdown", + "computed": true + } + }, + "description": "Example data source", + "description_kind": "markdown" + } + } + }, + "functions": { + "example": { + "description": "Given a string value, returns the same value.", + "summary": "Echo a string", + "return_type": "string", + "parameters": [ + { + "name": "input", + "description": "Value to echo.", + "type": "string" + } + ], + "variadic_parameter": { + "name": "variadicInput", + "description": "Variadic input to echo.", + "type": "string" + } + }, + "no-variadic": { + "description": "Given a string value, returns the same value.", + "summary": "Echo a string", + "return_type": "string", + "parameters": [ + { + "name": "input", + "description": "Value to echo.", + "type": "string" + } + ] + } + } + } + } +} \ No newline at end of file diff --git a/cmd/tfplugindocs/testdata/scripts/schema-json/generate/framework_provider_success_no_templates.txtar b/cmd/tfplugindocs/testdata/scripts/schema-json/generate/framework_provider_success_no_templates_single_example.txtar similarity index 100% rename from cmd/tfplugindocs/testdata/scripts/schema-json/generate/framework_provider_success_no_templates.txtar rename to cmd/tfplugindocs/testdata/scripts/schema-json/generate/framework_provider_success_no_templates_single_example.txtar diff --git a/internal/provider/generate.go b/internal/provider/generate.go index 85370946..2110657e 100644 --- a/internal/provider/generate.go +++ b/internal/provider/generate.go @@ -625,11 +625,20 @@ func (g *generator) renderStaticWebsite(providerSchema *tfjson.ProviderSchema) e switch relDir { case "data-sources/": resSchema, resName := resourceSchema(providerSchema.DataSourceSchemas, shortName, relFile) - exampleFilePath := filepath.Join(g.ProviderExamplesDir(), "data-sources", resName, "data-source.tf") if resSchema != nil { + exampleFilePath := filepath.Join(g.ProviderExamplesDir(), "data-sources", resName, "data-source.tf") + exampleFilesPattern := filepath.Join(g.ProviderExamplesDir(), "data-sources", resName, "data-source*.tf") + exampleFiles, err := filepath.Glob(exampleFilesPattern) + + if err != nil { + return fmt.Errorf("unable to glob example files with pattern %q: %w", exampleFilesPattern, err) + } + + slices.Sort(exampleFiles) + tmpl := resourceTemplate(tmplData) - render, err := tmpl.Render(g.providerDir, resName, g.providerName, g.renderedProviderName, "Data Source", exampleFilePath, "", "", "", resSchema, nil) + render, err := tmpl.Render(g.providerDir, resName, g.providerName, g.renderedProviderName, "Data Source", exampleFilePath, exampleFiles, "", "", "", resSchema, nil) if err != nil { return fmt.Errorf("unable to render data source template %q: %w", rel, err) } @@ -643,14 +652,24 @@ func (g *generator) renderStaticWebsite(providerSchema *tfjson.ProviderSchema) e case "resources/": resSchema, resName := resourceSchema(providerSchema.ResourceSchemas, shortName, relFile) resIdentitySchema := resourceIdentitySchema(providerSchema.ResourceIdentitySchemas, shortName, relFile) - exampleFilePath := filepath.Join(g.ProviderExamplesDir(), "resources", resName, "resource.tf") - importFilePath := filepath.Join(g.ProviderExamplesDir(), "resources", resName, "import.sh") - importIDConfigFilePath := filepath.Join(g.ProviderExamplesDir(), "resources", resName, "import-by-string-id.tf") - importIdentityConfigFilePath := filepath.Join(g.ProviderExamplesDir(), "resources", resName, "import-by-identity.tf") if resSchema != nil { + exampleFilePath := filepath.Join(g.ProviderExamplesDir(), "resources", resName, "resource.tf") + exampleFilesPattern := filepath.Join(g.ProviderExamplesDir(), "resources", resName, "resource*.tf") + exampleFiles, err := filepath.Glob(exampleFilesPattern) + + if err != nil { + return fmt.Errorf("unable to glob example files with pattern %q: %w", exampleFilesPattern, err) + } + + slices.Sort(exampleFiles) + + importFilePath := filepath.Join(g.ProviderExamplesDir(), "resources", resName, "import.sh") + importIDConfigFilePath := filepath.Join(g.ProviderExamplesDir(), "resources", resName, "import-by-string-id.tf") + importIdentityConfigFilePath := filepath.Join(g.ProviderExamplesDir(), "resources", resName, "import-by-identity.tf") + tmpl := resourceTemplate(tmplData) - render, err := tmpl.Render(g.providerDir, resName, g.providerName, g.renderedProviderName, "Resource", exampleFilePath, importIDConfigFilePath, importIdentityConfigFilePath, importFilePath, resSchema, resIdentitySchema) + render, err := tmpl.Render(g.providerDir, resName, g.providerName, g.renderedProviderName, "Resource", exampleFilePath, exampleFiles, importIDConfigFilePath, importIdentityConfigFilePath, importFilePath, resSchema, resIdentitySchema) if err != nil { return fmt.Errorf("unable to render resource template %q: %w", rel, err) } @@ -665,9 +684,17 @@ func (g *generator) renderStaticWebsite(providerSchema *tfjson.ProviderSchema) e funcName := removeAllExt(relFile) if signature, ok := providerSchema.Functions[funcName]; ok { exampleFilePath := filepath.Join(g.ProviderExamplesDir(), "functions", funcName, "function.tf") + exampleFilesPattern := filepath.Join(g.ProviderExamplesDir(), "functions", funcName, "function*.tf") + exampleFiles, err := filepath.Glob(exampleFilesPattern) + + if err != nil { + return fmt.Errorf("unable to glob example files with pattern %q: %w", exampleFilesPattern, err) + } + + slices.Sort(exampleFiles) tmpl := functionTemplate(tmplData) - render, err := tmpl.Render(g.providerDir, funcName, g.providerName, g.renderedProviderName, "function", exampleFilePath, signature) + render, err := tmpl.Render(g.providerDir, funcName, g.providerName, g.renderedProviderName, "function", exampleFilePath, exampleFiles, signature) if err != nil { return fmt.Errorf("unable to render function template %q: %w", rel, err) } @@ -681,11 +708,20 @@ func (g *generator) renderStaticWebsite(providerSchema *tfjson.ProviderSchema) e g.warnf("function entitled %q does not exist", funcName) case "ephemeral-resources/": resSchema, resName := resourceSchema(providerSchema.EphemeralResourceSchemas, shortName, relFile) - exampleFilePath := filepath.Join(g.ProviderExamplesDir(), "ephemeral-resources", resName, "ephemeral-resource.tf") if resSchema != nil { + exampleFilePath := filepath.Join(g.ProviderExamplesDir(), "ephemeral-resources", resName, "ephemeral-resource.tf") + exampleFilesPattern := filepath.Join(g.ProviderExamplesDir(), "ephemeral-resources", resName, "ephemeral-resource*.tf") + exampleFiles, err := filepath.Glob(exampleFilesPattern) + + if err != nil { + return fmt.Errorf("unable to glob example files with pattern %q: %w", exampleFilesPattern, err) + } + + slices.Sort(exampleFiles) + tmpl := resourceTemplate(tmplData) - render, err := tmpl.Render(g.providerDir, resName, g.providerName, g.renderedProviderName, "Ephemeral Resource", exampleFilePath, "", "", "", resSchema, nil) + render, err := tmpl.Render(g.providerDir, resName, g.providerName, g.renderedProviderName, "Ephemeral Resource", exampleFilePath, exampleFiles, "", "", "", resSchema, nil) if err != nil { return fmt.Errorf("unable to render ephemeral resource template %q: %w", rel, err) } @@ -698,9 +734,18 @@ func (g *generator) renderStaticWebsite(providerSchema *tfjson.ProviderSchema) e g.warnf("ephemeral resource entitled %q, or %q does not exist", shortName, resName) case "": // provider if relFile == "index.md.tmpl" { - tmpl := providerTemplate(tmplData) exampleFilePath := filepath.Join(g.ProviderExamplesDir(), "provider", "provider.tf") - render, err := tmpl.Render(g.providerDir, g.providerName, g.renderedProviderName, exampleFilePath, providerSchema.ConfigSchema) + exampleFilesPattern := filepath.Join(g.ProviderExamplesDir(), "provider", "provider*.tf") + exampleFiles, err := filepath.Glob(exampleFilesPattern) + + if err != nil { + return fmt.Errorf("unable to glob example files with pattern %q: %w", exampleFilesPattern, err) + } + + slices.Sort(exampleFiles) + + tmpl := providerTemplate(tmplData) + render, err := tmpl.Render(g.providerDir, g.providerName, g.renderedProviderName, exampleFilePath, exampleFiles, providerSchema.ConfigSchema) if err != nil { return fmt.Errorf("unable to render provider template %q: %w", rel, err) } diff --git a/internal/provider/template.go b/internal/provider/template.go index 5912ac7a..db76ff95 100644 --- a/internal/provider/template.go +++ b/internal/provider/template.go @@ -120,7 +120,7 @@ func (t docTemplate) Render(providerDir string, out io.Writer) error { return renderTemplate(providerDir, "docTemplate", s, out, nil) } -func (t providerTemplate) Render(providerDir, providerName, renderedProviderName, exampleFile string, schema *tfjson.Schema) (string, error) { +func (t providerTemplate) Render(providerDir, providerName, renderedProviderName, exampleFile string, exampleFiles []string, schema *tfjson.Schema) (string, error) { schemaBuffer := bytes.NewBuffer(nil) err := schemamd.Render(schema, schemaBuffer) if err != nil { @@ -135,8 +135,10 @@ func (t providerTemplate) Render(providerDir, providerName, renderedProviderName return renderStringTemplate(providerDir, "providerTemplate", s, struct { Description string - HasExample bool - ExampleFile string + HasExample bool + HasExamples bool + ExampleFile string + ExampleFiles []string ProviderName string ProviderShortName string @@ -146,8 +148,10 @@ func (t providerTemplate) Render(providerDir, providerName, renderedProviderName }{ Description: schema.Block.Description, - HasExample: exampleFile != "" && fileExists(exampleFile), - ExampleFile: exampleFile, + HasExample: exampleFile != "" && fileExists(exampleFile), + HasExamples: len(exampleFiles) > 0, + ExampleFile: exampleFile, + ExampleFiles: exampleFiles, ProviderName: providerName, ProviderShortName: providerShortName(renderedProviderName), @@ -158,7 +162,7 @@ func (t providerTemplate) Render(providerDir, providerName, renderedProviderName }) } -func (t resourceTemplate) Render(providerDir, name, providerName, renderedProviderName, typeName, exampleFile, importIDConfigFile, importIdentityConfigFile, importCmdFile string, schema *tfjson.Schema, identitySchema *tfjson.IdentitySchema) (string, error) { +func (t resourceTemplate) Render(providerDir, name, providerName, renderedProviderName, typeName, exampleFile string, exampleFiles []string, importIDConfigFile, importIdentityConfigFile, importCmdFile string, schema *tfjson.Schema, identitySchema *tfjson.IdentitySchema) (string, error) { schemaBuffer := bytes.NewBuffer(nil) err := schemamd.Render(schema, schemaBuffer) if err != nil { @@ -194,8 +198,10 @@ func (t resourceTemplate) Render(providerDir, name, providerName, renderedProvid Name string Description string - HasExample bool - ExampleFile string + HasExample bool + HasExamples bool + ExampleFile string + ExampleFiles []string HasImport bool ImportFile string @@ -218,8 +224,10 @@ func (t resourceTemplate) Render(providerDir, name, providerName, renderedProvid Name: name, Description: schema.Block.Description, - HasExample: exampleFile != "" && fileExists(exampleFile), - ExampleFile: exampleFile, + HasExample: exampleFile != "" && fileExists(exampleFile), + HasExamples: len(exampleFiles) > 0, + ExampleFile: exampleFile, + ExampleFiles: exampleFiles, HasImport: importCmdFile != "" && fileExists(importCmdFile), ImportFile: importCmdFile, @@ -240,7 +248,7 @@ func (t resourceTemplate) Render(providerDir, name, providerName, renderedProvid }) } -func (t functionTemplate) Render(providerDir, name, providerName, renderedProviderName, typeName, exampleFile string, signature *tfjson.FunctionSignature) (string, error) { +func (t functionTemplate) Render(providerDir, name, providerName, renderedProviderName, typeName, exampleFile string, exampleFiles []string, signature *tfjson.FunctionSignature) (string, error) { funcSig, err := functionmd.RenderSignature(name, signature) if err != nil { return "", fmt.Errorf("unable to render function signature: %w", err) @@ -267,8 +275,10 @@ func (t functionTemplate) Render(providerDir, name, providerName, renderedProvid Description string Summary string - HasExample bool - ExampleFile string + HasExample bool + HasExamples bool + ExampleFile string + ExampleFiles []string ProviderName string ProviderShortName string @@ -286,8 +296,10 @@ func (t functionTemplate) Render(providerDir, name, providerName, renderedProvid Description: signature.Description, Summary: signature.Summary, - HasExample: exampleFile != "" && fileExists(exampleFile), - ExampleFile: exampleFile, + HasExample: exampleFile != "" && fileExists(exampleFile), + HasExamples: len(exampleFiles) > 0, + ExampleFile: exampleFile, + ExampleFiles: exampleFiles, ProviderName: providerName, ProviderShortName: providerShortName(renderedProviderName), @@ -314,10 +326,13 @@ description: |- {{ .Description | trimspace }} -{{ if .HasExample -}} +{{ if .HasExamples -}} ## Example Usage -{{tffile .ExampleFile }} +{{- range .ExampleFiles }} + +{{ tffile . }} +{{- end }} {{- end }} {{ .SchemaMarkdown | trimspace }} @@ -361,10 +376,13 @@ description: |- {{ .Description | trimspace }} -{{ if .HasExample -}} +{{ if .HasExamples -}} ## Example Usage -{{tffile .ExampleFile }} +{{- range .ExampleFiles }} + +{{ tffile . }} +{{- end }} {{- end }} ## Signature @@ -390,10 +408,13 @@ description: |- {{ .Description | trimspace }} -{{ if .HasExample -}} +{{ if .HasExamples -}} ## Example Usage -{{tffile .ExampleFile }} +{{- range .ExampleFiles }} + +{{ tffile . }} +{{- end }} {{- end }} {{ .SchemaMarkdown | trimspace }} diff --git a/internal/provider/template_test.go b/internal/provider/template_test.go index d60e3992..426e4ff2 100644 --- a/internal/provider/template_test.go +++ b/internal/provider/template_test.go @@ -93,7 +93,7 @@ provider "scaffolding" { }, } - result, err := tpl.Render("testdata/test-provider-dir", "testTemplate", "test-provider", "test-provider", "Resource", "provider.tf", "", "", "", &schema, nil) + result, err := tpl.Render("testdata/test-provider-dir", "testTemplate", "test-provider", "test-provider", "Resource", "provider.tf", []string{"provider.tf"}, "", "", "", &schema, nil) if err != nil { t.Error(err) } @@ -133,7 +133,7 @@ provider "scaffolding" { }, } - result, err := tpl.Render("testdata/test-provider-dir", "testTemplate", "test-provider", "provider.tf", &schema) + result, err := tpl.Render("testdata/test-provider-dir", "testTemplate", "test-provider", "provider.tf", []string{"provider.tf"}, &schema) if err != nil { t.Error(err) } From 56a9ac81599dc078286e729498ab15d986fe20d3 Mon Sep 17 00:00:00 2001 From: Austin Valle Date: Wed, 24 Sep 2025 15:37:21 -0400 Subject: [PATCH 2/3] test fix after recent merge --- ...amework_provider_success_no_templates_multiple_examples.txtar | 1 + 1 file changed, 1 insertion(+) diff --git a/cmd/tfplugindocs/testdata/scripts/schema-json/generate/framework_provider_success_no_templates_multiple_examples.txtar b/cmd/tfplugindocs/testdata/scripts/schema-json/generate/framework_provider_success_no_templates_multiple_examples.txtar index 713c0918..40da7337 100644 --- a/cmd/tfplugindocs/testdata/scripts/schema-json/generate/framework_provider_success_no_templates_multiple_examples.txtar +++ b/cmd/tfplugindocs/testdata/scripts/schema-json/generate/framework_provider_success_no_templates_multiple_examples.txtar @@ -26,6 +26,7 @@ generating new template for function "example" generating new template for function "no-variadic" generating missing ephemeral resource content generating new template for "scaffolding_example" +generating missing action content generating missing provider content generating new template for "terraform-provider-scaffolding" rendering static website From 3ec6e3e999a4c1702d90228909729320781407f6 Mon Sep 17 00:00:00 2001 From: Austin Valle Date: Wed, 24 Sep 2025 15:54:15 -0400 Subject: [PATCH 3/3] fix merge conflict --- internal/provider/template.go | 15 +-------------- 1 file changed, 1 insertion(+), 14 deletions(-) diff --git a/internal/provider/template.go b/internal/provider/template.go index 57ae0b90..2d1bfc8a 100644 --- a/internal/provider/template.go +++ b/internal/provider/template.go @@ -198,20 +198,7 @@ func (t providerTemplate) Render(providerDir, providerName, renderedProviderName return "", nil } - return renderStringTemplate(providerDir, "providerTemplate", s, struct { - Description string - - HasExample bool - HasExamples bool - ExampleFile string - ExampleFiles []string - - ProviderName string - ProviderShortName string - SchemaMarkdown string - - RenderedProviderName string - }{ + return renderStringTemplate(providerDir, "providerTemplate", s, ProviderTemplateType{ Description: schema.Block.Description, HasExample: exampleFile != "" && fileExists(exampleFile),