From 69163cd34b1dd82989d338020dcf066e779b6f34 Mon Sep 17 00:00:00 2001 From: Selena Goods Date: Tue, 23 Jan 2024 16:10:38 -0500 Subject: [PATCH] Add Support for Provider-defined Function Documentation (#328) * Implement `functionmd` package * Implement provider-defined function support for `generate` command * Fix spacing for `functionmd.RenderFunctions()` output * Update README.md * Implement provider-defined function support for `migrate` command * Add copywrite headers * Add `templates` directory to list of accepted directory names in `validate` * Update `provider-build` acceptance tests * Add Changelog entries * Update README.md to include Terraform binary requirement for functions --- .../ENHANCEMENTS-20240122-160317.yaml | 6 + .../unreleased/FEATURES-20240122-160026.yaml | 5 + .../unreleased/FEATURES-20240122-160050.yaml | 5 + README.md | 62 +++++--- ...k_provider_success_generic_templates.txtar | 1 + ...ork_provider_success_named_templates.txtar | 1 + ...mework_provider_success_no_templates.txtar | 1 + .../generate/null_provider_success.txtar | 1 + ...k_provider_success_generic_templates.txtar | 117 +++++++++++++++ ...amework_provider_success_legacy_docs.txtar | 50 +++++++ ...ork_provider_success_named_templates.txtar | 116 +++++++++++++++ ...mework_provider_success_no_templates.txtar | 61 ++++++++ ...mework_provider_success_static_files.txtar | 50 +++++++ .../generate/null_provider_success.txtar | 1 + .../time_provider_success_docs_website.txtar | 82 ++++++++++- ...time_provider_success_legacy_website.txtar | 82 ++++++++++- functionmd/render.go | 96 +++++++++++++ functionmd/render_test.go | 136 ++++++++++++++++++ functionmd/testdata/example_arguments.md | 5 + functionmd/testdata/example_signature.md | 3 + functionmd/testdata/example_vararg.md | 1 + .../testdata/function_signature.schema.json | 49 +++++++ internal/provider/generate.go | 88 +++++++++++- internal/provider/migrate.go | 17 ++- internal/provider/template.go | 108 +++++++++++++- internal/provider/validate.go | 2 + 26 files changed, 1107 insertions(+), 39 deletions(-) create mode 100644 .changes/unreleased/ENHANCEMENTS-20240122-160317.yaml create mode 100644 .changes/unreleased/FEATURES-20240122-160026.yaml create mode 100644 .changes/unreleased/FEATURES-20240122-160050.yaml create mode 100644 functionmd/render.go create mode 100644 functionmd/render_test.go create mode 100644 functionmd/testdata/example_arguments.md create mode 100644 functionmd/testdata/example_signature.md create mode 100644 functionmd/testdata/example_vararg.md create mode 100644 functionmd/testdata/function_signature.schema.json diff --git a/.changes/unreleased/ENHANCEMENTS-20240122-160317.yaml b/.changes/unreleased/ENHANCEMENTS-20240122-160317.yaml new file mode 100644 index 00000000..1ad48423 --- /dev/null +++ b/.changes/unreleased/ENHANCEMENTS-20240122-160317.yaml @@ -0,0 +1,6 @@ +kind: ENHANCEMENTS +body: 'validate: Add `functions` to list of allowed template and rendered website + subdirectories' +time: 2024-01-22T16:03:17.035362-05:00 +custom: + Issue: "328" diff --git a/.changes/unreleased/FEATURES-20240122-160026.yaml b/.changes/unreleased/FEATURES-20240122-160026.yaml new file mode 100644 index 00000000..8a6c3bb5 --- /dev/null +++ b/.changes/unreleased/FEATURES-20240122-160026.yaml @@ -0,0 +1,5 @@ +kind: FEATURES +body: 'generate: Add support for Provider-defined Function documentation' +time: 2024-01-22T16:00:26.052538-05:00 +custom: + Issue: "328" diff --git a/.changes/unreleased/FEATURES-20240122-160050.yaml b/.changes/unreleased/FEATURES-20240122-160050.yaml new file mode 100644 index 00000000..4b61a7cb --- /dev/null +++ b/.changes/unreleased/FEATURES-20240122-160050.yaml @@ -0,0 +1,5 @@ +kind: FEATURES +body: 'migrate: Add support for Provider-defined Function documentation' +time: 2024-01-22T16:00:50.279982-05:00 +custom: + Issue: "328" diff --git a/README.md b/README.md index b9cbb287..f7f8b40b 100644 --- a/README.md +++ b/README.md @@ -105,6 +105,7 @@ When you run `tfplugindocs`, by default from the root directory of a provider co * Generate a default provider template file, if missing (**index.md**) * Generate resource template files, if missing * Generate data source template files, if missing +* Generate function template files, if missing (Requires Terraform v1.8.0+) * Copy all non-template files to the output website directory * Process all the remaining templates to generate files for the output website directory @@ -165,19 +166,21 @@ The `migrate` subcommand takes the following actions: The generation of missing documentation is based on a number of assumptions / conventional paths. -> **NOTE:** In the following conventional paths, `` and `` include the provider prefix as well. +> **NOTE:** In the following conventional paths, `` 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` For templates: -| Path | Description | -|-----------------------------------------------------------|----------------------------------------| -| `templates/` | Root of templated docs | -| `templates/index.md[.tmpl]` | Docs index page (or template) | -| `templates/data-sources.md[.tmpl]` | Generic data source page (or template) | -| `templates/data-sources/.md[.tmpl]` | Data source page (or template) | -| `templates/resources.md[.tmpl]` | Generic resource page (or template) | -| `templates/resources/.md[.tmpl]` | Resource page (or template) | +| Path | Description | +|-------------------------------------------------------|----------------------------------------| +| `templates/` | Root of templated docs | +| `templates/index.md[.tmpl]` | Docs index page (or template) | +| `templates/data-sources.md[.tmpl]` | Generic data source page (or template) | +| `templates/data-sources/.md[.tmpl]` | Data source page (or template) | +| `templates/functions.md[.tmpl]` | Generic function page (or template) | +| `templates/functions/.md[.tmpl]` | Function page (or template) | +| `templates/resources.md[.tmpl]` | Generic resource page (or template) | +| `templates/resources/.md[.tmpl]` | Resource page (or template) | Note: the `.tmpl` extension is necessary, for the file to be correctly handled as a template. @@ -188,6 +191,7 @@ For examples: | `examples/` | Root of examples | | `examples/provider/provider.tf` | Provider example config | | `examples/data-sources//data-source.tf` | Data source example config | +| `examples/functions//function.tf` | Function example config | | `examples/resources//resource.tf` | Resource example config | | `examples/resources//import.sh` | Resource example import command | @@ -197,13 +201,14 @@ The `migrate` subcommand assumes the following conventional paths for the render Legacy website directory structure: -| Path | Description | -|---------------------------------------------------|-----------------------------| -| `website/` | Root of website docs | -| `website/docs/guides` | Root of guides subdirectory | -| `website/docs/index.html.markdown` | Docs index page | -| `website/docs/d/.html.markdown` | Data source page | -| `website/docs/r/.html.markdown` | Resource page | +| Path | Description | +|-------------------------------------------------------|-----------------------------| +| `website/` | Root of website docs | +| `website/docs/guides` | Root of guides subdirectory | +| `website/docs/index.html.markdown` | Docs index page | +| `website/docs/d/.html.markdown` | Data source page | +| `website/docs/functons/.html.markdown` | Functions page | +| `website/docs/r/.html.markdown` | Resource page | Docs website directory structure: @@ -213,6 +218,7 @@ Docs website directory structure: | `docs/guides` | Root of guides subdirectory | | `docs/index.html.markdown` | Docs index page | | `docs/data-sources/.html.markdown` | Data source page | +| `docs/functions/.html.markdown` | Function page | | `docs/resources/.html.markdown` | Resource page | Files named `index` (before the first `.`) in the website docs root directory and files in the `website/docs/d/`, `website/docs/r/`, `docs/data-sources/`, @@ -229,7 +235,7 @@ using the following data fields and functions: #### Data fields -##### Provider +##### Provider Fields | Field | Type | Description | |------------------------:|:------:|-------------------------------------------------------------------------------------------| @@ -241,7 +247,7 @@ using the following data fields and functions: | `.RenderedProviderName` | string | Value provided via argument `--rendered-provider-name`, otherwise same as `.ProviderName` | | `.SchemaMarkdown` | string | a Markdown formatted Provider Schema definition | -##### Resources / Data Source +##### Resources / Data Source Fields | Field | Type | Description | |------------------------:|:------:|-------------------------------------------------------------------------------------------| @@ -257,7 +263,25 @@ using the following data fields and functions: | `.RenderedProviderName` | string | Value provided via argument `--rendered-provider-name`, otherwise same as `.ProviderName` | | `.SchemaMarkdown` | string | a Markdown formatted Resource / Data Source Schema definition | -#### Functions +##### Provider-defined Function Fields + +| Field | Type | Description | +|------------------------------------:|:------:|-------------------------------------------------------------------------------------------| +| `.Name` | string | Name of the function (ex. `echo`) | +| `.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 | +| `.ProviderName` | string | Canonical provider name (ex. `terraform-provider-random`) | +| `.ProviderShortName` | string | Short version of the provider name (ex. `random`) | +| `.RenderedProviderName` | string | Value provided via argument `--rendered-provider-name`, otherwise same as `.ProviderName` | +| `.FunctionSignatureMarkdown` | string | a Markdown formatted Function signature | +| `.FunctionArgumentsMarkdown` | string | a Markdown formatted Function arguments definition | +| `.HasVariadic` | bool | Does this function have a variadic argument? | +| `.FunctionVariadicArgumentMarkdown` | string | a Markdown formatted Function variadic argument definition | + +#### Template Functions | Function | Description | |-----------------|---------------------------------------------------------------------------------------------------| diff --git a/cmd/tfplugindocs/testdata/scripts/provider-build/generate/framework_provider_success_generic_templates.txtar b/cmd/tfplugindocs/testdata/scripts/provider-build/generate/framework_provider_success_generic_templates.txtar index 6738e4a4..3f6010cd 100644 --- a/cmd/tfplugindocs/testdata/scripts/provider-build/generate/framework_provider_success_generic_templates.txtar +++ b/cmd/tfplugindocs/testdata/scripts/provider-build/generate/framework_provider_success_generic_templates.txtar @@ -25,6 +25,7 @@ generating missing resource content resource "scaffolding_example" fallback template exists, creating template generating missing data source content data-source "scaffolding_example" fallback template exists, creating template +generating missing function content generating missing provider content provider "terraform-provider-scaffolding" template exists, skipping rendering static website diff --git a/cmd/tfplugindocs/testdata/scripts/provider-build/generate/framework_provider_success_named_templates.txtar b/cmd/tfplugindocs/testdata/scripts/provider-build/generate/framework_provider_success_named_templates.txtar index 35e77b7f..e4b3d497 100644 --- a/cmd/tfplugindocs/testdata/scripts/provider-build/generate/framework_provider_success_named_templates.txtar +++ b/cmd/tfplugindocs/testdata/scripts/provider-build/generate/framework_provider_success_named_templates.txtar @@ -25,6 +25,7 @@ generating missing resource content resource "scaffolding_example" template exists, skipping generating missing data source content data-source "scaffolding_example" template exists, skipping +generating missing function content generating missing provider content provider "terraform-provider-scaffolding" template exists, skipping rendering static website diff --git a/cmd/tfplugindocs/testdata/scripts/provider-build/generate/framework_provider_success_no_templates.txtar b/cmd/tfplugindocs/testdata/scripts/provider-build/generate/framework_provider_success_no_templates.txtar index 0460419c..01e3829e 100644 --- a/cmd/tfplugindocs/testdata/scripts/provider-build/generate/framework_provider_success_no_templates.txtar +++ b/cmd/tfplugindocs/testdata/scripts/provider-build/generate/framework_provider_success_no_templates.txtar @@ -23,6 +23,7 @@ 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 missing provider content generating new template for "terraform-provider-scaffolding" rendering static website diff --git a/cmd/tfplugindocs/testdata/scripts/provider-build/generate/null_provider_success.txtar b/cmd/tfplugindocs/testdata/scripts/provider-build/generate/null_provider_success.txtar index 5caa782e..0c8fa4eb 100644 --- a/cmd/tfplugindocs/testdata/scripts/provider-build/generate/null_provider_success.txtar +++ b/cmd/tfplugindocs/testdata/scripts/provider-build/generate/null_provider_success.txtar @@ -22,6 +22,7 @@ generating missing resource content resource "null_resource" fallback template exists, creating template generating missing data source content data-source "null_data_source" fallback template exists, creating template +generating missing function content generating missing provider content provider "terraform-provider-null" template exists, skipping rendering static website diff --git a/cmd/tfplugindocs/testdata/scripts/schema-json/generate/framework_provider_success_generic_templates.txtar b/cmd/tfplugindocs/testdata/scripts/schema-json/generate/framework_provider_success_generic_templates.txtar index 156b7442..187ea8b9 100644 --- a/cmd/tfplugindocs/testdata/scripts/schema-json/generate/framework_provider_success_generic_templates.txtar +++ b/cmd/tfplugindocs/testdata/scripts/schema-json/generate/framework_provider_success_generic_templates.txtar @@ -9,6 +9,8 @@ cmp stdout expected-output.txt cmpenv docs/index.md expected-index.md cmpenv docs/data-sources/example.md expected-datasource.md cmpenv docs/resources/example.md expected-resource.md +cmpenv docs/functions/example.md expected-function.md + -- expected-output.txt -- rendering website for provider "terraform-provider-scaffolding" (as "terraform-provider-scaffolding") @@ -20,12 +22,15 @@ generating missing resource content resource "scaffolding_example" fallback template exists, creating template generating missing data source content data-source "scaffolding_example" fallback template exists, creating template +generating missing function content +function "example" fallback template exists, creating template generating missing provider content provider "terraform-provider-scaffolding" template exists, skipping rendering static website cleaning rendered website dir rendering templated website to static markdown rendering "data-sources/example.md.tmpl" +rendering "functions/example.md.tmpl" rendering "index.md.tmpl" rendering "resources/example.md.tmpl" -- expected-datasource.md -- @@ -84,6 +89,53 @@ data "scaffolding_example" "example" { configurable_attribute = "some-value" } ``` +-- expected-function.md -- +# Data Fields + +Name: example +Type: function +Description: Given a string value, returns the same value. +Summary: Echo a string +HasExample: true +ExampleFile: $WORK/examples/functions/example/function.tf +ProviderName: terraform-provider-scaffolding +ProviderShortName: scaffolding +RenderedProviderName: terraform-provider-scaffolding +FunctionSignatureMarkdown: +```text +example(input string, variadicInput string...) string +``` +FunctionArgumentsMarkdown: +1. `input` (String) Value to echo. +HasVariadic: true +FunctionVariadicArgumentMarkdown: +1. `variadicInput` (Variadic, String) Variadic input to echo. + +# Functions + +lower: function +plainmarkdown: function +prefixlines: Prefix: function +split: [example] +title: Function +trimspace: function +upper: FUNCTION + +# Conditionals and File Functions + +printf tffile: +## Example Usage + +{{tffile "$WORK/examples/functions/example/function.tf"}} + +tffile: +## Example Usage + +```terraform +output "test" { + value = provider::scaffolding::example("testvalue1", "testvalue2") +} +``` -- expected-index.md -- # Data Fields @@ -250,6 +302,48 @@ tffile: {{ if .HasExample -}} ## Example Usage +{{tffile .ExampleFile }} +{{- end }} +-- templates/functions.md.tmpl -- +# Data Fields + +Name: {{.Name}} +Type: {{.Type}} +Description: {{.Description}} +Summary: {{.Summary}} +HasExample: {{.HasExample}} +ExampleFile: {{.ExampleFile}} +ProviderName: {{.ProviderName}} +ProviderShortName: {{.ProviderShortName}} +RenderedProviderName: {{.RenderedProviderName}} +FunctionSignatureMarkdown: {{.FunctionSignatureMarkdown}} +FunctionArgumentsMarkdown: {{.FunctionArgumentsMarkdown}} +HasVariadic: {{.HasVariadic}} +FunctionVariadicArgumentMarkdown: {{.FunctionVariadicArgumentMarkdown}} + +# Functions + +lower: {{ .Type | lower }} +plainmarkdown: {{ .Type | plainmarkdown }} +prefixlines: {{ .Type | prefixlines "Prefix: " }} +split: {{ split .Name "_" }} +title: {{ .Type | title }} +trimspace: {{ .Type | trimspace }} +upper: {{ .Type | upper }} + +# Conditionals and File Functions + +printf tffile: +{{ if .HasExample -}} +## Example Usage + +{{ printf "{{tffile %q}}" .ExampleFile }} +{{- end }} + +tffile: +{{ if .HasExample -}} +## Example Usage + {{tffile .ExampleFile }} {{- end }} -- templates/index.md.tmpl -- @@ -360,6 +454,10 @@ The document generation tool looks for files in the following locations by defau data "scaffolding_example" "example" { configurable_attribute = "some-value" } +-- examples/functions/example/function.tf -- +output "test" { + value = provider::scaffolding::example("testvalue1", "testvalue2") +} -- examples/provider/provider.tf -- provider "scaffolding" { # example configuration here @@ -442,6 +540,25 @@ terraform import scaffolding_example.example "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" + } + } } } } diff --git a/cmd/tfplugindocs/testdata/scripts/schema-json/generate/framework_provider_success_legacy_docs.txtar b/cmd/tfplugindocs/testdata/scripts/schema-json/generate/framework_provider_success_legacy_docs.txtar index 374dd8b6..03973af1 100644 --- a/cmd/tfplugindocs/testdata/scripts/schema-json/generate/framework_provider_success_legacy_docs.txtar +++ b/cmd/tfplugindocs/testdata/scripts/schema-json/generate/framework_provider_success_legacy_docs.txtar @@ -17,6 +17,11 @@ cmp templates/d/example.markdown docs/d/example.markdown cmp templates/d/example.html.markdown docs/d/example.html.markdown cmp templates/d/example.html.md docs/d/example.html.md +cmp templates/functions/example.md docs/functions/example.md +cmp templates/functions/example.markdown docs/functions/example.markdown +cmp templates/functions/example.html.markdown docs/functions/example.html.markdown +cmp templates/functions/example.html.md docs/functions/example.html.md + cmp templates/index.md docs/index.md cmp templates/index.markdown docs/index.markdown cmp templates/index.html.markdown docs/index.html.markdown @@ -31,6 +36,8 @@ generating missing resource content resource "scaffolding_example" static file exists, skipping generating missing data source content data-source "scaffolding_example" static file exists, skipping +generating missing function content +function "example" static file exists, skipping generating missing provider content provider "terraform-provider-scaffolding" static file exists, skipping rendering static website @@ -40,6 +47,10 @@ copying non-template file: "d/example.html.markdown" copying non-template file: "d/example.html.md" copying non-template file: "d/example.markdown" copying non-template file: "d/example.md" +copying non-template file: "functions/example.html.markdown" +copying non-template file: "functions/example.html.md" +copying non-template file: "functions/example.markdown" +copying non-template file: "functions/example.md" copying non-template file: "index.html.markdown" copying non-template file: "index.html.md" copying non-template file: "index.markdown" @@ -86,6 +97,26 @@ Type: {{.Type}} -- templates/d/example.html.md -- # Data Fields +Name: {{.Name}} +Type: {{.Type}} +-- templates/functions/example.md -- +# Data Fields + +Name: {{.Name}} +Type: {{.Type}} +-- templates/functions/example.markdown -- +# Data Fields + +Name: {{.Name}} +Type: {{.Type}} +-- templates/functions/example.html.markdown -- +# Data Fields + +Name: {{.Name}} +Type: {{.Type}} +-- templates/functions/example.html.md -- +# Data Fields + Name: {{.Name}} Type: {{.Type}} -- templates/index.md -- @@ -204,6 +235,25 @@ terraform import scaffolding_example.example "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" + } + } } } } diff --git a/cmd/tfplugindocs/testdata/scripts/schema-json/generate/framework_provider_success_named_templates.txtar b/cmd/tfplugindocs/testdata/scripts/schema-json/generate/framework_provider_success_named_templates.txtar index 1bae5cd3..15680887 100644 --- a/cmd/tfplugindocs/testdata/scripts/schema-json/generate/framework_provider_success_named_templates.txtar +++ b/cmd/tfplugindocs/testdata/scripts/schema-json/generate/framework_provider_success_named_templates.txtar @@ -9,6 +9,7 @@ cmp stdout expected-output.txt cmpenv docs/index.md expected-index.md cmpenv docs/data-sources/example.md expected-datasource.md cmpenv docs/resources/example.md expected-resource.md +cmpenv docs/functions/example.md expected-function.md -- expected-output.txt -- rendering website for provider "terraform-provider-scaffolding" (as "terraform-provider-scaffolding") @@ -20,12 +21,15 @@ generating missing resource content resource "scaffolding_example" template exists, skipping generating missing data source content data-source "scaffolding_example" template exists, skipping +generating missing function content +function "example" template exists, skipping generating missing provider content provider "terraform-provider-scaffolding" template exists, skipping rendering static website cleaning rendered website dir rendering templated website to static markdown rendering "data-sources/example.md.tmpl" +rendering "functions/example.md.tmpl" rendering "index.md.tmpl" rendering "resources/example.md.tmpl" -- expected-datasource.md -- @@ -84,6 +88,53 @@ data "scaffolding_example" "example" { configurable_attribute = "some-value" } ``` +-- expected-function.md -- +# Data Fields + +Name: example +Type: function +Description: Given a string value, returns the same value. +Summary: Echo a string +HasExample: true +ExampleFile: $WORK/examples/functions/example/function.tf +ProviderName: terraform-provider-scaffolding +ProviderShortName: scaffolding +RenderedProviderName: terraform-provider-scaffolding +FunctionSignatureMarkdown: +```text +example(input string, variadicInput string...) string +``` +FunctionArgumentsMarkdown: +1. `input` (String) Value to echo. +HasVariadic: true +FunctionVariadicArgumentMarkdown: +1. `variadicInput` (Variadic, String) Variadic input to echo. + +# Functions + +lower: function +plainmarkdown: function +prefixlines: Prefix: function +split: [example] +title: Function +trimspace: function +upper: FUNCTION + +# Conditionals and File Functions + +printf tffile: +## Example Usage + +{{tffile "$WORK/examples/functions/example/function.tf"}} + +tffile: +## Example Usage + +```terraform +output "test" { + value = provider::scaffolding::example("testvalue1", "testvalue2") +} +``` -- expected-index.md -- # Data Fields @@ -286,6 +337,48 @@ tffile: {{ if .HasExample -}} ## Example Usage +{{tffile .ExampleFile }} +{{- end }} +-- templates/functions/example.md.tmpl -- +# Data Fields + +Name: {{.Name}} +Type: {{.Type}} +Description: {{.Description}} +Summary: {{.Summary}} +HasExample: {{.HasExample}} +ExampleFile: {{.ExampleFile}} +ProviderName: {{.ProviderName}} +ProviderShortName: {{.ProviderShortName}} +RenderedProviderName: {{.RenderedProviderName}} +FunctionSignatureMarkdown: {{.FunctionSignatureMarkdown}} +FunctionArgumentsMarkdown: {{.FunctionArgumentsMarkdown}} +HasVariadic: {{.HasVariadic}} +FunctionVariadicArgumentMarkdown: {{.FunctionVariadicArgumentMarkdown}} + +# Functions + +lower: {{ .Type | lower }} +plainmarkdown: {{ .Type | plainmarkdown }} +prefixlines: {{ .Type | prefixlines "Prefix: " }} +split: {{ split .Name "_" }} +title: {{ .Type | title }} +trimspace: {{ .Type | trimspace }} +upper: {{ .Type | upper }} + +# Conditionals and File Functions + +printf tffile: +{{ if .HasExample -}} +## Example Usage + +{{ printf "{{tffile %q}}" .ExampleFile }} +{{- end }} + +tffile: +{{ if .HasExample -}} +## Example Usage + {{tffile .ExampleFile }} {{- end }} -- templates/resources/example.md.tmpl -- @@ -360,6 +453,10 @@ The document generation tool looks for files in the following locations by defau data "scaffolding_example" "example" { configurable_attribute = "some-value" } +-- examples/functions/example/function.tf -- +output "test" { + value = provider::scaffolding::example("testvalue1", "testvalue2") +} -- examples/provider/provider.tf -- provider "scaffolding" { # example configuration here @@ -442,6 +539,25 @@ terraform import scaffolding_example.example "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" + } + } } } } 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.txtar index b8fe1392..8ac9b9ad 100644 --- 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.txtar @@ -8,6 +8,7 @@ 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 -- expected-output.txt -- rendering website for provider "terraform-provider-scaffolding" (as "terraform-provider-scaffolding") @@ -18,12 +19,15 @@ 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 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 "functions/example.md.tmpl" rendering "index.md.tmpl" rendering "resources/example.md.tmpl" -- expected-datasource.md -- @@ -57,6 +61,40 @@ data "scaffolding_example" "example" { ### Read-Only - `id` (String) Example identifier +-- expected-function.md -- +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "example function - terraform-provider-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") +} +``` + +## Signature + + +```text +example(input string, variadicInput string...) string +``` + +## Arguments + + +1. `input` (String) Value to echo. + +1. `variadicInput` (Variadic, String) Variadic input to echo. -- expected-index.md -- --- # generated by https://github.com/hashicorp/terraform-plugin-docs @@ -130,6 +168,10 @@ The document generation tool looks for files in the following locations by defau data "scaffolding_example" "example" { configurable_attribute = "some-value" } +-- examples/functions/example/function.tf -- +output "test" { + value = provider::scaffolding::example("testvalue1", "testvalue2") +} -- examples/provider/provider.tf -- provider "scaffolding" { # example configuration here @@ -211,6 +253,25 @@ resource "scaffolding_example" "example" { "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" + } + } } } } diff --git a/cmd/tfplugindocs/testdata/scripts/schema-json/generate/framework_provider_success_static_files.txtar b/cmd/tfplugindocs/testdata/scripts/schema-json/generate/framework_provider_success_static_files.txtar index a01fd1e4..bc0309b3 100644 --- a/cmd/tfplugindocs/testdata/scripts/schema-json/generate/framework_provider_success_static_files.txtar +++ b/cmd/tfplugindocs/testdata/scripts/schema-json/generate/framework_provider_success_static_files.txtar @@ -17,6 +17,11 @@ cmp templates/data-sources/example.markdown docs/data-sources/example.markdown cmp templates/data-sources/example.html.markdown docs/data-sources/example.html.markdown cmp templates/data-sources/example.html.md docs/data-sources/example.html.md +cmp templates/functions/example.md docs/functions/example.md +cmp templates/functions/example.markdown docs/functions/example.markdown +cmp templates/functions/example.html.markdown docs/functions/example.html.markdown +cmp templates/functions/example.html.md docs/functions/example.html.md + cmp templates/index.md docs/index.md cmp templates/index.markdown docs/index.markdown cmp templates/index.html.markdown docs/index.html.markdown @@ -31,6 +36,8 @@ generating missing resource content resource "scaffolding_example" static file exists, skipping generating missing data source content data-source "scaffolding_example" static file exists, skipping +generating missing function content +function "example" static file exists, skipping generating missing provider content provider "terraform-provider-scaffolding" static file exists, skipping rendering static website @@ -40,6 +47,10 @@ copying non-template file: "data-sources/example.html.markdown" copying non-template file: "data-sources/example.html.md" copying non-template file: "data-sources/example.markdown" copying non-template file: "data-sources/example.md" +copying non-template file: "functions/example.html.markdown" +copying non-template file: "functions/example.html.md" +copying non-template file: "functions/example.markdown" +copying non-template file: "functions/example.md" copying non-template file: "index.html.markdown" copying non-template file: "index.html.md" copying non-template file: "index.markdown" @@ -86,6 +97,26 @@ Type: {{.Type}} -- templates/data-sources/example.html.md -- # Data Fields +Name: {{.Name}} +Type: {{.Type}} +-- templates/functions/example.md -- +# Data Fields + +Name: {{.Name}} +Type: {{.Type}} +-- templates/functions/example.markdown -- +# Data Fields + +Name: {{.Name}} +Type: {{.Type}} +-- templates/functions/example.html.markdown -- +# Data Fields + +Name: {{.Name}} +Type: {{.Type}} +-- templates/functions/example.html.md -- +# Data Fields + Name: {{.Name}} Type: {{.Type}} -- templates/index.md -- @@ -204,6 +235,25 @@ terraform import scaffolding_example.example "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" + } + } } } } diff --git a/cmd/tfplugindocs/testdata/scripts/schema-json/generate/null_provider_success.txtar b/cmd/tfplugindocs/testdata/scripts/schema-json/generate/null_provider_success.txtar index 71e98aae..91597c24 100644 --- a/cmd/tfplugindocs/testdata/scripts/schema-json/generate/null_provider_success.txtar +++ b/cmd/tfplugindocs/testdata/scripts/schema-json/generate/null_provider_success.txtar @@ -17,6 +17,7 @@ generating missing resource content resource "null_resource" fallback template exists, creating template generating missing data source content data-source "null_data_source" fallback template exists, creating template +generating missing function content generating missing provider content provider "terraform-provider-null" template exists, skipping rendering static website diff --git a/cmd/tfplugindocs/testdata/scripts/schema-json/migrate/time_provider_success_docs_website.txtar b/cmd/tfplugindocs/testdata/scripts/schema-json/migrate/time_provider_success_docs_website.txtar index 15b8c754..1ac1f8d4 100644 --- a/cmd/tfplugindocs/testdata/scripts/schema-json/migrate/time_provider_success_docs_website.txtar +++ b/cmd/tfplugindocs/testdata/scripts/schema-json/migrate/time_provider_success_docs_website.txtar @@ -10,6 +10,7 @@ cmpenv stdout expected-output.txt # Check template files cmpenv templates/index.md.tmpl exp-templates/index.md.tmpl +cmpenv templates/functions/rfc3339_parse.md.tmpl exp-templates/functions/rfc3339_parse.md.tmpl cmpenv templates/resources/offset.md.tmpl exp-templates/resources/offset.md.tmpl cmpenv templates/resources/rotating.md.tmpl exp-templates/resources/rotating.md.tmpl cmpenv templates/resources/sleep.md.tmpl exp-templates/resources/sleep.md.tmpl @@ -18,6 +19,8 @@ cmpenv templates/resources/static.md.tmpl exp-templates/resources/static.md.tmpl # Check generated example files cmpenv examples/example_1.tf examples/example_1.tf +cmpenv examples/functions/rfc3339_parse/example_1.tf exp-examples/functions/rfc3339_parse/example_1.tf + cmpenv examples/resources/offset/example_1.tf exp-examples/resources/offset/example_1.tf cmpenv examples/resources/offset/example_2.tf exp-examples/resources/offset/example_2.tf cmpenv examples/resources/offset/import_1.sh exp-examples/resources/offset/import_1.sh @@ -38,6 +41,13 @@ cmpenv examples/resources/static/import_1.sh examples/resources/static/import_1. -- expected-output.txt -- migrating website from "$WORK/docs" to "$WORK/templates" +migrating functons directory: functions +migrating file "rfc3339_parse.html.markdown" +extracting YAML frontmatter to "$WORK/templates/functions/rfc3339_parse.md.tmpl" +extracting code examples from "rfc3339_parse.html.markdown" +creating example file "$WORK/examples/functions/rfc3339_parse/example_1.tf" +skipping code block with unknown language "text" +finished creating template "$WORK/templates/functions/rfc3339_parse.md.tmpl" migrating provider index: index.html.markdown migrating file "index.html.markdown" extracting YAML frontmatter to "$WORK/templates/index.md.tmpl" @@ -125,6 +135,35 @@ resource "aws_instance" "server" { `triggers` are *not* treated as sensitive attributes; a value used for `triggers` will be displayed in Terraform UI output as plaintext. To force a these actions to reoccur without updating `triggers`, the [`terraform taint` command](https://www.terraform.io/docs/commands/taint.html) can be used to produce the action on the next run. +-- docs/functions/rfc3339_parse.html.markdown -- +--- +page_title: "rfc3339_parse Function - Time" +subcategory: "" +description: |- + Parse an RFC3339 timestamp string +--- + +# rfc3339_parse Function + +Given an RFC3339 timestamp string, will parse and return an object representation of that date and time. + +## Example Usage + +```terraform +output "test" { + value = provider::time::rfc3339_parse("2023-07-25T23:43:16-00:00") +} +``` + +## Signature + +```text +rfc3339_parse(timestamp string) Object +``` + +## Arguments + +1. `timestamp` (string) RFC3339 timestamp string to parse -- docs/resources/offset.html.markdown -- --- layout: "time" @@ -495,6 +534,10 @@ resource "aws_instance" "server" { # ... (other aws_instance arguments) ... } +-- exp-examples/functions/rfc3339_parse/example_1.tf -- +output "test" { + value = provider::time::rfc3339_parse("2023-07-25T23:43:16-00:00") +} -- exp-examples/resources/offset/example_1.tf -- resource "time_offset" "example" { offset_days = 7 @@ -627,7 +670,7 @@ description: |- {{/* This template serves as a starting point for documentation generation, and can be customized with hardcoded values and/or doc gen templates. -For example, the {{ SchemaMarkdown }} template can be used to replace manual schema documentation if descriptions of schema attributes are added in the provider source code. */ -}} +For example, the {{ .SchemaMarkdown }} template can be used to replace manual schema documentation if descriptions of schema attributes are added in the provider source code. */ -}} # Time Provider @@ -652,6 +695,35 @@ For example: `triggers` are *not* treated as sensitive attributes; a value used for `triggers` will be displayed in Terraform UI output as plaintext. To force a these actions to reoccur without updating `triggers`, the [`terraform taint` command](https://www.terraform.io/docs/commands/taint.html) can be used to produce the action on the next run. +-- exp-templates/functions/rfc3339_parse.md.tmpl -- +--- +page_title: "rfc3339_parse Function - Time" +subcategory: "" +description: |- + Parse an RFC3339 timestamp string +--- + +{{/* This template serves as a starting point for documentation generation, and can be customized with hardcoded values and/or doc gen templates. + +For example, the {{ .FunctionArgumentsMarkdown }} template can be used to replace manual argument documentation if descriptions of function arguments are added in the provider source code. */ -}} + +# rfc3339_parse Function + +Given an RFC3339 timestamp string, will parse and return an object representation of that date and time. + +## Example Usage + +{{tffile "$WORK/examples/functions/rfc3339_parse/example_1.tf"}} + +## Signature + +```text +rfc3339_parse(timestamp string) Object +``` + +## Arguments + +1. `timestamp` (string) RFC3339 timestamp string to parse -- exp-templates/resources/offset.md.tmpl -- --- page_title: "Time: time_offset" @@ -661,7 +733,7 @@ description: |- {{/* This template serves as a starting point for documentation generation, and can be customized with hardcoded values and/or doc gen templates. -For example, the {{ SchemaMarkdown }} template can be used to replace manual schema documentation if descriptions of schema attributes are added in the provider source code. */ -}} +For example, the {{ .SchemaMarkdown }} template can be used to replace manual schema documentation if descriptions of schema attributes are added in the provider source code. */ -}} # Resource: time_offset @@ -724,7 +796,7 @@ description: |- {{/* This template serves as a starting point for documentation generation, and can be customized with hardcoded values and/or doc gen templates. -For example, the {{ SchemaMarkdown }} template can be used to replace manual schema documentation if descriptions of schema attributes are added in the provider source code. */ -}} +For example, the {{ .SchemaMarkdown }} template can be used to replace manual schema documentation if descriptions of schema attributes are added in the provider source code. */ -}} # Resource: time_rotating @@ -786,7 +858,7 @@ description: |- {{/* This template serves as a starting point for documentation generation, and can be customized with hardcoded values and/or doc gen templates. -For example, the {{ SchemaMarkdown }} template can be used to replace manual schema documentation if descriptions of schema attributes are added in the provider source code. */ -}} +For example, the {{ .SchemaMarkdown }} template can be used to replace manual schema documentation if descriptions of schema attributes are added in the provider source code. */ -}} # Resource: time_sleep @@ -844,7 +916,7 @@ description: |- {{/* This template serves as a starting point for documentation generation, and can be customized with hardcoded values and/or doc gen templates. -For example, the {{ SchemaMarkdown }} template can be used to replace manual schema documentation if descriptions of schema attributes are added in the provider source code. */ -}} +For example, the {{ .SchemaMarkdown }} template can be used to replace manual schema documentation if descriptions of schema attributes are added in the provider source code. */ -}} # Resource: time_static diff --git a/cmd/tfplugindocs/testdata/scripts/schema-json/migrate/time_provider_success_legacy_website.txtar b/cmd/tfplugindocs/testdata/scripts/schema-json/migrate/time_provider_success_legacy_website.txtar index ec1af20e..5c48f6f6 100644 --- a/cmd/tfplugindocs/testdata/scripts/schema-json/migrate/time_provider_success_legacy_website.txtar +++ b/cmd/tfplugindocs/testdata/scripts/schema-json/migrate/time_provider_success_legacy_website.txtar @@ -10,6 +10,7 @@ cmpenv stdout expected-output.txt # Check template files cmpenv templates/index.md.tmpl exp-templates/index.md.tmpl +cmpenv templates/functions/rfc3339_parse.md.tmpl exp-templates/functions/rfc3339_parse.md.tmpl cmpenv templates/resources/offset.md.tmpl exp-templates/resources/offset.md.tmpl cmpenv templates/resources/rotating.md.tmpl exp-templates/resources/rotating.md.tmpl cmpenv templates/resources/sleep.md.tmpl exp-templates/resources/sleep.md.tmpl @@ -18,6 +19,8 @@ cmpenv templates/resources/static.md.tmpl exp-templates/resources/static.md.tmpl # Check generated example files cmpenv examples/example_1.tf examples/example_1.tf +cmpenv examples/functions/rfc3339_parse/example_1.tf exp-examples/functions/rfc3339_parse/example_1.tf + cmpenv examples/resources/offset/example_1.tf exp-examples/resources/offset/example_1.tf cmpenv examples/resources/offset/example_2.tf exp-examples/resources/offset/example_2.tf cmpenv examples/resources/offset/import_1.sh exp-examples/resources/offset/import_1.sh @@ -41,6 +44,13 @@ cmpenv examples/resources/static/import_1.sh examples/resources/static/import_1. -- expected-output.txt -- migrating website from "$WORK/website/docs" to "$WORK/templates" +migrating functons directory: functions +migrating file "rfc3339_parse.html.markdown" +extracting YAML frontmatter to "$WORK/templates/functions/rfc3339_parse.md.tmpl" +extracting code examples from "rfc3339_parse.html.markdown" +creating example file "$WORK/examples/functions/rfc3339_parse/example_1.tf" +skipping code block with unknown language "text" +finished creating template "$WORK/templates/functions/rfc3339_parse.md.tmpl" migrating provider index: index.html.markdown migrating file "index.html.markdown" extracting YAML frontmatter to "$WORK/templates/index.md.tmpl" @@ -128,6 +138,35 @@ resource "aws_instance" "server" { `triggers` are *not* treated as sensitive attributes; a value used for `triggers` will be displayed in Terraform UI output as plaintext. To force a these actions to reoccur without updating `triggers`, the [`terraform taint` command](https://www.terraform.io/docs/commands/taint.html) can be used to produce the action on the next run. +-- website/docs/functions/rfc3339_parse.html.markdown -- +--- +page_title: "rfc3339_parse Function - Time" +subcategory: "" +description: |- + Parse an RFC3339 timestamp string +--- + +# rfc3339_parse Function + +Given an RFC3339 timestamp string, will parse and return an object representation of that date and time. + +## Example Usage + +```terraform +output "test" { + value = provider::time::rfc3339_parse("2023-07-25T23:43:16-00:00") +} +``` + +## Signature + +```text +rfc3339_parse(timestamp string) Object +``` + +## Arguments + +1. `timestamp` (string) RFC3339 timestamp string to parse -- website/docs/r/offset.html.markdown -- --- layout: "time" @@ -499,6 +538,10 @@ resource "aws_instance" "server" { # ... (other aws_instance arguments) ... } +-- exp-examples/functions/rfc3339_parse/example_1.tf -- +output "test" { + value = provider::time::rfc3339_parse("2023-07-25T23:43:16-00:00") +} -- exp-examples/resources/offset/example_1.tf -- resource "time_offset" "example" { offset_days = 7 @@ -631,7 +674,7 @@ description: |- {{/* This template serves as a starting point for documentation generation, and can be customized with hardcoded values and/or doc gen templates. -For example, the {{ SchemaMarkdown }} template can be used to replace manual schema documentation if descriptions of schema attributes are added in the provider source code. */ -}} +For example, the {{ .SchemaMarkdown }} template can be used to replace manual schema documentation if descriptions of schema attributes are added in the provider source code. */ -}} # Time Provider @@ -656,6 +699,35 @@ For example: `triggers` are *not* treated as sensitive attributes; a value used for `triggers` will be displayed in Terraform UI output as plaintext. To force a these actions to reoccur without updating `triggers`, the [`terraform taint` command](https://www.terraform.io/docs/commands/taint.html) can be used to produce the action on the next run. +-- exp-templates/functions/rfc3339_parse.md.tmpl -- +--- +page_title: "rfc3339_parse Function - Time" +subcategory: "" +description: |- + Parse an RFC3339 timestamp string +--- + +{{/* This template serves as a starting point for documentation generation, and can be customized with hardcoded values and/or doc gen templates. + +For example, the {{ .FunctionArgumentsMarkdown }} template can be used to replace manual argument documentation if descriptions of function arguments are added in the provider source code. */ -}} + +# rfc3339_parse Function + +Given an RFC3339 timestamp string, will parse and return an object representation of that date and time. + +## Example Usage + +{{tffile "$WORK/examples/functions/rfc3339_parse/example_1.tf"}} + +## Signature + +```text +rfc3339_parse(timestamp string) Object +``` + +## Arguments + +1. `timestamp` (string) RFC3339 timestamp string to parse -- exp-templates/resources/offset.md.tmpl -- --- page_title: "Time: time_offset" @@ -665,7 +737,7 @@ description: |- {{/* This template serves as a starting point for documentation generation, and can be customized with hardcoded values and/or doc gen templates. -For example, the {{ SchemaMarkdown }} template can be used to replace manual schema documentation if descriptions of schema attributes are added in the provider source code. */ -}} +For example, the {{ .SchemaMarkdown }} template can be used to replace manual schema documentation if descriptions of schema attributes are added in the provider source code. */ -}} # Resource: time_offset @@ -728,7 +800,7 @@ description: |- {{/* This template serves as a starting point for documentation generation, and can be customized with hardcoded values and/or doc gen templates. -For example, the {{ SchemaMarkdown }} template can be used to replace manual schema documentation if descriptions of schema attributes are added in the provider source code. */ -}} +For example, the {{ .SchemaMarkdown }} template can be used to replace manual schema documentation if descriptions of schema attributes are added in the provider source code. */ -}} # Resource: time_rotating @@ -790,7 +862,7 @@ description: |- {{/* This template serves as a starting point for documentation generation, and can be customized with hardcoded values and/or doc gen templates. -For example, the {{ SchemaMarkdown }} template can be used to replace manual schema documentation if descriptions of schema attributes are added in the provider source code. */ -}} +For example, the {{ .SchemaMarkdown }} template can be used to replace manual schema documentation if descriptions of schema attributes are added in the provider source code. */ -}} # Resource: time_sleep @@ -848,7 +920,7 @@ description: |- {{/* This template serves as a starting point for documentation generation, and can be customized with hardcoded values and/or doc gen templates. -For example, the {{ SchemaMarkdown }} template can be used to replace manual schema documentation if descriptions of schema attributes are added in the provider source code. */ -}} +For example, the {{ .SchemaMarkdown }} template can be used to replace manual schema documentation if descriptions of schema attributes are added in the provider source code. */ -}} # Resource: time_static diff --git a/functionmd/render.go b/functionmd/render.go new file mode 100644 index 00000000..fdea3a82 --- /dev/null +++ b/functionmd/render.go @@ -0,0 +1,96 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package functionmd + +import ( + "bytes" + "fmt" + "strings" + + tfjson "github.com/hashicorp/terraform-json" + + "github.com/hashicorp/terraform-plugin-docs/schemamd" +) + +// RenderArguments returns a Markdown formatted string of the function arguments. +func RenderArguments(signature *tfjson.FunctionSignature) (string, error) { + argBuffer := bytes.NewBuffer(nil) + for i, p := range signature.Parameters { + name := p.Name + desc := strings.TrimSpace(p.Description) + + typeBuffer := bytes.NewBuffer(nil) + err := schemamd.WriteType(typeBuffer, p.Type) + if err != nil { + return "", err + } + + if p.IsNullable { + argBuffer.WriteString(fmt.Sprintf("1. `%s` (%s, Nullable) %s", name, typeBuffer.String(), desc)) + } else { + argBuffer.WriteString(fmt.Sprintf("1. `%s` (%s) %s", name, typeBuffer.String(), desc)) + } + + if i != len(signature.Parameters)-1 { + argBuffer.WriteString("\n") + } + + } + return argBuffer.String(), nil + +} + +// RenderSignature returns a Markdown formatted string of the function signature. +func RenderSignature(funcName string, signature *tfjson.FunctionSignature) (string, error) { + + returnType := signature.ReturnType.FriendlyName() + + paramBuffer := bytes.NewBuffer(nil) + for i, p := range signature.Parameters { + if i != 0 { + paramBuffer.WriteString(", ") + } + + paramBuffer.WriteString(fmt.Sprintf("%s %s", p.Name, p.Type.FriendlyName())) + } + + if signature.VariadicParameter != nil { + if signature.Parameters != nil { + paramBuffer.WriteString(", ") + } + + paramBuffer.WriteString(fmt.Sprintf("%s %s...", signature.VariadicParameter.Name, + signature.VariadicParameter.Type.FriendlyName())) + + } + + return fmt.Sprintf("```text\n"+ + "%s(%s) %s\n"+ + "```", + funcName, paramBuffer.String(), returnType), nil +} + +// RenderVariadicArg returns a Markdown formatted string of the variadic argument if it exists, +// otherwise an empty string. +func RenderVariadicArg(signature *tfjson.FunctionSignature) (string, error) { + if signature.VariadicParameter == nil { + return "", nil + } + + name := signature.VariadicParameter.Name + desc := strings.TrimSpace(signature.VariadicParameter.Description) + + typeBuffer := bytes.NewBuffer(nil) + err := schemamd.WriteType(typeBuffer, signature.VariadicParameter.Type) + if err != nil { + return "", err + } + + if signature.VariadicParameter.IsNullable { + return fmt.Sprintf("1. `%s` (Variadic, %s, Nullable) %s", name, typeBuffer.String(), desc), nil + } else { + return fmt.Sprintf("1. `%s` (Variadic, %s) %s", name, typeBuffer.String(), desc), nil + } + +} diff --git a/functionmd/render_test.go b/functionmd/render_test.go new file mode 100644 index 00000000..c76c37a7 --- /dev/null +++ b/functionmd/render_test.go @@ -0,0 +1,136 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package functionmd_test + +import ( + "encoding/json" + "os" + "strings" + "testing" + + "github.com/google/go-cmp/cmp" + tfjson "github.com/hashicorp/terraform-json" + + "github.com/hashicorp/terraform-plugin-docs/functionmd" +) + +func TestRenderArguments(t *testing.T) { + t.Parallel() + + inputFile := "testdata/function_signature.schema.json" + expectedFile := "testdata/example_arguments.md" + + input, err := os.ReadFile(inputFile) + if err != nil { + t.Fatal(err) + } + + expected, err := os.ReadFile(expectedFile) + if err != nil { + t.Fatal(err) + } + + var signature tfjson.FunctionSignature + + err = json.Unmarshal(input, &signature) + if err != nil { + t.Fatal(err) + } + + argStr, err := functionmd.RenderArguments(&signature) + if err != nil { + t.Fatal(err) + } + + // Remove \r characters so tests don't fail on windows + expectedStr := strings.ReplaceAll(string(expected), "\r", "") + + // Remove trailing newlines before comparing (some text editors remove them). + expectedStr = strings.TrimRight(expectedStr, "\n") + actual := strings.TrimRight(argStr, "\n") + if diff := cmp.Diff(expectedStr, actual); diff != "" { + t.Fatalf("Unexpected diff (-wanted, +got): %s", diff) + } + +} + +func TestRenderSignature(t *testing.T) { + t.Parallel() + + inputFile := "testdata/function_signature.schema.json" + expectedFile := "testdata/example_signature.md" + + input, err := os.ReadFile(inputFile) + if err != nil { + t.Fatal(err) + } + + expected, err := os.ReadFile(expectedFile) + if err != nil { + t.Fatal(err) + } + + var signature tfjson.FunctionSignature + + err = json.Unmarshal(input, &signature) + if err != nil { + t.Fatal(err) + } + + argStr, err := functionmd.RenderSignature("example", &signature) + if err != nil { + t.Fatal(err) + } + + // Remove \r characters so tests don't fail on windows + expectedStr := strings.ReplaceAll(string(expected), "\r", "") + + // Remove trailing newlines before comparing (some text editors remove them). + expectedStr = strings.TrimRight(expectedStr, "\n") + actual := strings.TrimRight(argStr, "\n") + if diff := cmp.Diff(expectedStr, actual); diff != "" { + t.Fatalf("Unexpected diff (-wanted, +got): %s", diff) + } + +} + +func TestRenderVariadicArg(t *testing.T) { + inputFile := "testdata/function_signature.schema.json" + expectedFile := "testdata/example_vararg.md" + + t.Parallel() + + input, err := os.ReadFile(inputFile) + if err != nil { + t.Fatal(err) + } + + expected, err := os.ReadFile(expectedFile) + if err != nil { + t.Fatal(err) + } + + var signature tfjson.FunctionSignature + + err = json.Unmarshal(input, &signature) + if err != nil { + t.Fatal(err) + } + + argStr, err := functionmd.RenderVariadicArg(&signature) + if err != nil { + t.Fatal(err) + } + + // Remove \r characters so tests don't fail on windows + expectedStr := strings.ReplaceAll(string(expected), "\r", "") + + // Remove trailing newlines before comparing (some text editors remove them). + expectedStr = strings.TrimRight(expectedStr, "\n") + actual := strings.TrimRight(argStr, "\n") + if diff := cmp.Diff(expectedStr, actual); diff != "" { + t.Fatalf("Unexpected diff (-wanted, +got): %s", diff) + } + +} diff --git a/functionmd/testdata/example_arguments.md b/functionmd/testdata/example_arguments.md new file mode 100644 index 00000000..643eb579 --- /dev/null +++ b/functionmd/testdata/example_arguments.md @@ -0,0 +1,5 @@ +1. `input` (String) Value to echo. +1. `int64Input` (Number) Int64 Value to echo. +1. `listStringInput` (List of String) List of strings to echo. +1. `mapStringInput` (Map of String) Map of strings to echo. +1. `objectInput` (Object) Object to echo. \ No newline at end of file diff --git a/functionmd/testdata/example_signature.md b/functionmd/testdata/example_signature.md new file mode 100644 index 00000000..7a76d2bb --- /dev/null +++ b/functionmd/testdata/example_signature.md @@ -0,0 +1,3 @@ +```text +example(input string, int64Input number, listStringInput list of string, mapStringInput map of string, objectInput object, variadicInput string...) string +``` \ No newline at end of file diff --git a/functionmd/testdata/example_vararg.md b/functionmd/testdata/example_vararg.md new file mode 100644 index 00000000..4b3061c6 --- /dev/null +++ b/functionmd/testdata/example_vararg.md @@ -0,0 +1 @@ +1. `variadicInput` (Variadic, String) Variadic input to echo. \ No newline at end of file diff --git a/functionmd/testdata/function_signature.schema.json b/functionmd/testdata/function_signature.schema.json new file mode 100644 index 00000000..9853832e --- /dev/null +++ b/functionmd/testdata/function_signature.schema.json @@ -0,0 +1,49 @@ +{ + "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" + }, + { + "name": "int64Input", + "description": "Int64 Value to echo.", + "type": "number" + }, + { + "name": "listStringInput", + "description": "List of strings to echo.", + "type": [ + "list", + "string" + ] + }, + { + "name": "mapStringInput", + "description": "Map of strings to echo.", + "type": [ + "map", + "string" + ] + }, + { + "name": "objectInput", + "description": "Object to echo.", + "type": [ + "object", + { + "attr1": "string", + "attr2": "number" + } + ] + } + ], + "variadic_parameter": { + "name": "variadicInput", + "description": "Variadic input to echo.", + "type": "string" + } +} \ No newline at end of file diff --git a/internal/provider/generate.go b/internal/provider/generate.go index 85a12320..d0c53a54 100644 --- a/internal/provider/generate.go +++ b/internal/provider/generate.go @@ -51,6 +51,14 @@ var ( "d/%s.html.markdown", "d/%s.html.md", } + websiteFunctionFile = "functions/%s.md.tmpl" + websiteFunctionFallbackFile = "functions.md.tmpl" + websiteFunctionFileStaticCandidates = []string{ + "functions/%s.md", + "functions/%s.markdown", + "functions/%s.html.markdown", + "functions/%s.html.md", + } websiteProviderFile = "index.md.tmpl" websiteProviderFileStaticCandidates = []string{ "index.markdown", @@ -63,6 +71,7 @@ var ( "data-sources", "guides", "resources", + "functions", } managedWebsiteFiles = []string{ @@ -328,6 +337,42 @@ func (g *generator) generateMissingDataSourceTemplate(datasourceName string) err return nil } +func (g *generator) generateMissingFunctionTemplate(functionName string) error { + templatePath := fmt.Sprintf(websiteFunctionFile, resourceShortName(functionName, g.providerName)) + templatePath = filepath.Join(g.TempTemplatesDir(), templatePath) + if fileExists(templatePath) { + g.infof("function %q template exists, skipping", functionName) + return nil + } + + fallbackTemplatePath := filepath.Join(g.TempTemplatesDir(), websiteFunctionFallbackFile) + if fileExists(fallbackTemplatePath) { + g.infof("function %q fallback template exists, creating template", functionName) + err := cp(fallbackTemplatePath, templatePath) + if err != nil { + return fmt.Errorf("unable to copy fallback template for %q: %w", functionName, err) + } + return nil + } + + for _, candidate := range websiteFunctionFileStaticCandidates { + candidatePath := fmt.Sprintf(candidate, resourceShortName(functionName, g.providerName)) + candidatePath = filepath.Join(g.TempTemplatesDir(), candidatePath) + if fileExists(candidatePath) { + g.infof("function %q static file exists, skipping", functionName) + return nil + } + } + + g.infof("generating new template for function %q", functionName) + err := writeFile(templatePath, string(defaultFunctionTemplate)) + if err != nil { + return fmt.Errorf("unable to write template for %q: %w", functionName, err) + } + + return nil +} + func (g *generator) generateMissingProviderTemplate() error { templatePath := filepath.Join(g.TempTemplatesDir(), websiteProviderFile) if fileExists(templatePath) { @@ -377,6 +422,18 @@ func (g *generator) generateMissingTemplates(providerSchema *tfjson.ProviderSche } } + g.infof("generating missing function content") + for name, signature := range providerSchema.Functions { + if g.ignoreDeprecated && signature.DeprecationMessage != "" { + continue + } + + err := g.generateMissingFunctionTemplate(name) + if err != nil { + return fmt.Errorf("unable to generate template for function %q: %w", name, err) + } + } + g.infof("generating missing provider content") err := g.generateMissingProviderTemplate() if err != nil { @@ -420,8 +477,11 @@ func (g *generator) renderStaticWebsite(providerSchema *tfjson.ProviderSchema) e g.infof("rendering templated website to static markdown") - err = filepath.Walk(g.websiteTmpDir, func(path string, info os.FileInfo, _ error) error { - if info.IsDir() { + err = filepath.WalkDir(g.websiteTmpDir, func(path string, d os.DirEntry, err error) error { + if err != nil { + return fmt.Errorf("unable to walk path %q: %w", path, err) + } + if d.IsDir() { // skip directories return nil } @@ -435,8 +495,8 @@ func (g *generator) renderStaticWebsite(providerSchema *tfjson.ProviderSchema) e relDir, relFile := filepath.Split(rel) relDir = filepath.ToSlash(relDir) - // skip special top-level generic resource and data source templates - if relDir == "" && (relFile == "resources.md.tmpl" || relFile == "data-sources.md.tmpl") { + // skip special top-level generic resource, data source, and function templates + if relDir == "" && (relFile == "resources.md.tmpl" || relFile == "data-sources.md.tmpl" || relFile == "functions.md.tmpl") { return nil } @@ -497,11 +557,29 @@ func (g *generator) renderStaticWebsite(providerSchema *tfjson.ProviderSchema) e } _, err = out.WriteString(render) if err != nil { - return fmt.Errorf("unable to write regindered string: %w", err) + return fmt.Errorf("unable to write rendered string: %w", err) } return nil } g.warnf("resource entitled %q, or %q does not exist", shortName, resName) + case "functions/": + funcName := removeAllExt(relFile) + if signature, ok := providerSchema.Functions[funcName]; ok { + exampleFilePath := filepath.Join(g.ProviderExamplesDir(), "functions", funcName, "function.tf") + + tmpl := functionTemplate(tmplData) + render, err := tmpl.Render(g.providerDir, funcName, g.providerName, g.renderedProviderName, "function", exampleFilePath, signature) + if err != nil { + return fmt.Errorf("unable to render function template %q: %w", rel, err) + } + _, err = out.WriteString(render) + if err != nil { + return fmt.Errorf("unable to write rendered string: %w", err) + } + return nil + } + + g.warnf("function entitled %q does not exist", funcName) case "": // provider if relFile == "index.md.tmpl" { tmpl := providerTemplate(tmplData) diff --git a/internal/provider/migrate.go b/internal/provider/migrate.go index c5efda7d..e8a77f46 100644 --- a/internal/provider/migrate.go +++ b/internal/provider/migrate.go @@ -110,6 +110,13 @@ func (m *migrator) Migrate() error { return err } return filepath.SkipDir + case "functions": + m.infof("migrating functons directory: %s", d.Name()) + err := filepath.WalkDir(path, m.MigrateTemplate("functions")) + if err != nil { + return err + } + return filepath.SkipDir case "guides": m.infof("copying guides directory: %s", d.Name()) err := cp(path, filepath.Join(m.ProviderTemplatesDir(), "guides")) @@ -180,7 +187,7 @@ func (m *migrator) MigrateTemplate(relDir string) fs.WalkDirFunc { } m.infof("extracting YAML frontmatter to %q", templateFilePath) - err = m.ExtractFrontMatter(data, templateFilePath) + err = m.ExtractFrontMatter(data, relDir, templateFilePath) if err != nil { return fmt.Errorf("unable to extract front matter to %q: %w", templateFilePath, err) } @@ -196,7 +203,7 @@ func (m *migrator) MigrateTemplate(relDir string) fs.WalkDirFunc { } -func (m *migrator) ExtractFrontMatter(content []byte, templateFilePath string) error { +func (m *migrator) ExtractFrontMatter(content []byte, relDir string, templateFilePath string) error { templateFile, err := os.OpenFile(templateFilePath, os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0600) if err != nil { return fmt.Errorf("unable to open file %q: %w", templateFilePath, err) @@ -241,7 +248,11 @@ func (m *migrator) ExtractFrontMatter(content []byte, templateFilePath string) e } // add comment to end of front matter briefly explaining template functionality - _, err = templateFile.WriteString(migrateProviderTemplateComment + "\n") + if relDir == "functions" { + _, err = templateFile.WriteString(migrateFunctionTemplateComment + "\n") + } else { + _, err = templateFile.WriteString(migrateProviderTemplateComment + "\n") + } if err != nil { return fmt.Errorf("unable to append template comment to %q: %w", templateFilePath, err) } diff --git a/internal/provider/template.go b/internal/provider/template.go index 809d7411..10f2e655 100644 --- a/internal/provider/template.go +++ b/internal/provider/template.go @@ -16,18 +16,24 @@ import ( tfjson "github.com/hashicorp/terraform-json" + "github.com/hashicorp/terraform-plugin-docs/functionmd" "github.com/hashicorp/terraform-plugin-docs/internal/mdplain" "github.com/hashicorp/terraform-plugin-docs/internal/tmplfuncs" "github.com/hashicorp/terraform-plugin-docs/schemamd" ) const ( - schemaComment = "" + schemaComment = "" + signatureComment = "" + argumentComment = "" + variadicComment = "" + frontmatterComment = "# generated by https://github.com/hashicorp/terraform-plugin-docs" ) type ( resourceTemplate string + functionTemplate string providerTemplate string docTemplate string @@ -200,6 +206,68 @@ func (t resourceTemplate) Render(providerDir, name, providerName, renderedProvid }) } +func (t functionTemplate) Render(providerDir, name, providerName, renderedProviderName, typeName, exampleFile 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) + } + + funcArgs, err := functionmd.RenderArguments(signature) + if err != nil { + return "", fmt.Errorf("unable to render function arguments: %w", err) + } + + funcVarArg, err := functionmd.RenderVariadicArg(signature) + if err != nil { + return "", fmt.Errorf("unable to render variadic argument: %w", err) + } + + s := string(t) + if s == "" { + return "", nil + } + + return renderStringTemplate(providerDir, "resourceTemplate", s, struct { + Type string + Name string + Description string + Summary string + + HasExample bool + ExampleFile string + + ProviderName string + ProviderShortName string + + FunctionSignatureMarkdown string + FunctionArgumentsMarkdown string + + HasVariadic bool + FunctionVariadicArgumentMarkdown string + + RenderedProviderName string + }{ + Type: typeName, + Name: name, + Description: signature.Description, + Summary: signature.Summary, + + HasExample: exampleFile != "" && fileExists(exampleFile), + ExampleFile: exampleFile, + + ProviderName: providerName, + ProviderShortName: providerShortName(providerName), + + FunctionSignatureMarkdown: signatureComment + "\n" + funcSig, + FunctionArgumentsMarkdown: argumentComment + "\n" + funcArgs, + + HasVariadic: signature.VariadicParameter != nil, + FunctionVariadicArgumentMarkdown: variadicComment + "\n" + funcVarArg, + + RenderedProviderName: renderedProviderName, + }) +} + const defaultResourceTemplate resourceTemplate = `--- ` + frontmatterComment + ` page_title: "{{.Name}} {{.Type}} - {{.ProviderName}}" @@ -229,6 +297,36 @@ Import is supported using the following syntax: {{- end }} ` +const defaultFunctionTemplate functionTemplate = `--- +` + frontmatterComment + ` +page_title: "{{.Name}} {{.Type}} - {{.ProviderName}}" +subcategory: "" +description: |- +{{ .Summary | plainmarkdown | trimspace | prefixlines " " }} +--- + +# {{.Type}}: {{.Name}} + +{{ .Description | trimspace }} + +{{ if .HasExample -}} +## Example Usage + +{{tffile .ExampleFile }} +{{- end }} + +## Signature + +{{ .FunctionSignatureMarkdown }} + +## Arguments + +{{ .FunctionArgumentsMarkdown }} +{{ if .HasVariadic -}} +{{ .FunctionVariadicArgumentMarkdown }} +{{- end }} +` + const defaultProviderTemplate providerTemplate = `--- ` + frontmatterComment + ` page_title: "{{.ProviderShortName}} Provider" @@ -253,5 +351,11 @@ description: |- const migrateProviderTemplateComment string = ` {{/* This template serves as a starting point for documentation generation, and can be customized with hardcoded values and/or doc gen templates. -For example, the {{ SchemaMarkdown }} template can be used to replace manual schema documentation if descriptions of schema attributes are added in the provider source code. */ -}} +For example, the {{ .SchemaMarkdown }} template can be used to replace manual schema documentation if descriptions of schema attributes are added in the provider source code. */ -}} +` + +const migrateFunctionTemplateComment string = ` +{{/* This template serves as a starting point for documentation generation, and can be customized with hardcoded values and/or doc gen templates. + +For example, the {{ .FunctionArgumentsMarkdown }} template can be used to replace manual argument documentation if descriptions of function arguments are added in the provider source code. */ -}} ` diff --git a/internal/provider/validate.go b/internal/provider/validate.go index 48bdde47..c93ff3ec 100644 --- a/internal/provider/validate.go +++ b/internal/provider/validate.go @@ -62,6 +62,7 @@ func validateTemplates(ui cli.Ui, dir string) error { checkAllowedDirs( "data-sources", "guides", + "functions", "resources", ), checkBlockedExtensions( @@ -97,6 +98,7 @@ func validateStaticDocs(ui cli.Ui, dir string) error { checkAllowedDirs( "data-sources", "guides", + "functions", "resources", "cdktf", ),