Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Do not render empty code choosers on generated index page #2822

Merged
merged 7 commits into from
Jan 14, 2025
14 changes: 9 additions & 5 deletions pkg/tfgen/convert_cli.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,10 @@ import (
"github.com/pulumi/pulumi-terraform-bridge/v3/pkg/tfgen/internal/autofill"
)

const (
exampleUnavailable = "Example currently unavailable in this language\n"
)

func cliConverterEnabled() bool {
return cmdutil.IsTruthy(os.Getenv("PULUMI_CONVERT"))
}
Expand Down Expand Up @@ -645,14 +649,14 @@ func (cc *cliConverter) singleExampleFromHCLToPCL(path, hclCode string) (transla
func (cc *cliConverter) singleExampleFromPCLToLanguage(example translatedExample, lang string) (string, error) {
var err error

if example.PCL == "" {
return "", nil
}
source, diags, _ := cc.convertPCL(example.PCL, lang)
diags = cc.postProcessDiagnostics(diags.Extend(example.Diagnostics))
if diags.HasErrors() {
source = "Example currently unavailable in this language\n"
err = fmt.Errorf("failed to convert an example: %s", diags.Error())
err = fmt.Errorf("conversion errors: %s", diags.Error())
}

if source == "" {
source = exampleUnavailable
}
Comment on lines 654 to 660
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't understand why this change makes sense:

If there are no errors (!diag.HasErrors()), but source == "", should we really set source = exampleUnavailable.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Two reasons:

  1. It is possible for diag to have no errors (for example, when the input is empty), which results in source being empty. We can return an empty string, or declare that we currently have no example.

  2. It is also possible for diag to have errors but the conversion being successful regardless.

Therefore it makes more sense to check for "what are we looking to display to the user" and if it comes up empty, say that.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you put that in a comment on if source == "" {?

source = "```" + lang + "\n" + source + "```"
return source, err
Expand Down
76 changes: 69 additions & 7 deletions pkg/tfgen/installation_docs.go
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,12 @@
return nil, err
}

// If the code translation resulted in an empty examples section, remove it
contentStr, err = removeEmptyExamples(contentStr)
if err != nil {
return nil, err
}

// Apply post-code translation edit rules. This applies all default edit rules and provider-supplied edit rules in
// the post-code translation phase.
contentBytes, err = g.editRules.apply(docFile.FileName, []byte(contentStr), info.PostCodeTranslation)
Expand Down Expand Up @@ -221,9 +227,6 @@

// This function renders the Pulumi.yaml config file for a given language if configuration is included in the example.
func processConfigYaml(pulumiYAML, lang string) string {
if pulumiYAML == "" {
return pulumiYAML
}
// Replace the project name from the default `/` to a more descriptive name
nameRegex := regexp.MustCompile(`name: /*`)
pulumiYAMLFile := nameRegex.ReplaceAllString(pulumiYAML, "name: configuration-example")
Expand Down Expand Up @@ -253,30 +256,53 @@
return "", err
}

// If both PCL and PulumiYAML fields are empty, we can return.
if pclExample.PulumiYAML == "" && pclExample.PCL == "" {
return "", nil
}

// If we have a valid provider config but no additional code, we only render a YAML configuration block
// with no choosers and an empty language runtime field
if pclExample.PulumiYAML != "" && pclExample.PCL == "" {
if pclExample.PCL == "" {
return processConfigYaml(pclExample.PulumiYAML, ""), nil
}
}

langs := genLanguageToSlice(g.language)
const (
chooserStart = `{{< chooser language "typescript,python,go,csharp,java,yaml" >}}` + "\n"
chooserEnd = "{{< /chooser >}}\n"
choosableEnd = "\n{{% /choosable %}}\n"
)
exampleContent := chooserStart
successfulConversion := false

// Generate each language in turn and mark up the output with the correct Hugo shortcodes.
for _, lang := range langs {
choosableStart := fmt.Sprintf("{{%% choosable language %s %%}}\n", lang)

// Generate the Pulumi.yaml config file for each language
configFile := pclExample.PulumiYAML
pulumiYAML := processConfigYaml(configFile, lang)
var pulumiYAML string
if pclExample.PulumiYAML != "" {
pulumiYAML = processConfigYaml(pclExample.PulumiYAML, lang)
}

// Generate language example
convertedLang, err := converter.singleExampleFromPCLToLanguage(pclExample, lang)
if err != nil {
g.warn(err.Error())
}
if convertedLang != exampleUnavailable {
successfulConversion = true
}
exampleContent += choosableStart + pulumiYAML + convertedLang + choosableEnd
}
exampleContent += chooserEnd
return exampleContent, nil

if successfulConversion {
return exampleContent + chooserEnd, nil
}
return "", nil
}

type titleRemover struct{}
Expand Down Expand Up @@ -477,3 +503,39 @@
capitalize := cases.Title(language.English)
return capitalize.String(providerName)
}

func removeEmptyExamples(contentStr string) (string, error) {
if checkExamplesEmpty(contentStr) {
mybytes, err := SkipSectionByHeaderContent([]byte(contentStr), func(headerText string) bool {
return headerText == "Example Usage"
})
contentStr = string(mybytes)
return contentStr, err
}
return contentStr, nil

}

Check failure on line 517 in pkg/tfgen/installation_docs.go

View workflow job for this annotation

GitHub Actions / Test and Lint / lint

unnecessary trailing newline (whitespace)
guineveresaenger marked this conversation as resolved.
Show resolved Hide resolved

func checkExamplesEmpty(contentStr string) bool {

Check failure on line 519 in pkg/tfgen/installation_docs.go

View workflow job for this annotation

GitHub Actions / Test and Lint / lint

unnecessary leading newline (whitespace)
guineveresaenger marked this conversation as resolved.
Show resolved Hide resolved

gm := goldmark.New(goldmark.WithExtensions(parse.TFRegistryExtension))
astNode := gm.Parser().Parse(text.NewReader([]byte(contentStr)))

isEmpty := false

err := ast.Walk(astNode, func(n ast.Node, entering bool) (ast.WalkStatus, error) {
if section, ok := n.(*section.Section); ok && entering {
sectionText := section.Text([]byte(contentStr))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of converting to text and then checking if nothing came out, can we do this with a more structured check:

A section is empty if it has one child (the title). We can then get the content of the title and validate that.

// A little confusingly, we check if the section text _only_ contains the title, "Example Usage".
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// A little confusingly, we check if the section text _only_ contains the title, "Example Usage".
// A little confusingly, we check if the section text _only_ contains the title.

// Non-empty sections contain the title + content, so if we see only the title, the section is empty.
if string(sectionText) == "Example Usage" {
isEmpty = true
return ast.WalkStop, nil
}
}
return ast.WalkContinue, nil
})
contract.AssertNoErrorf(err, "impossible: ast.Walk should never error")

return isEmpty
}
114 changes: 91 additions & 23 deletions pkg/tfgen/installation_docs_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -449,32 +449,78 @@ func TestTranslateCodeBlocks(t *testing.T) {
}
pclsMap := make(map[string]translatedExample)

tc := testCase{
name: "Translates HCL from examples ",
contentStr: readfile(t, "test_data/installation-docs/configuration.md"),
expected: readfile(t, "test_data/installation-docs/configuration-expected.md"),
g: &Generator{
sink: mockSink{},
cliConverterState: &cliConverter{
info: p,
pcls: pclsMap,
testCases := []testCase{
{
name: "Translates HCL from examples ",
contentStr: readfile(t, "test_data/installation-docs/configuration.md"),
expected: readfile(t, "test_data/installation-docs/configuration-expected.md"),
g: &Generator{
sink: mockSink{},
cliConverterState: &cliConverter{
info: p,
pcls: pclsMap,
},
language: RegistryDocs,
},
},
{
name: "Does not translate an invalid example and leaves example block blank",
contentStr: readfile(t, "test_data/installation-docs/invalid-example.md"),
expected: readfile(t, "test_data/installation-docs/invalid-example-expected.md"),
g: &Generator{
sink: mockSink{},
cliConverterState: &cliConverter{
info: p,
pcls: pclsMap,
},
language: RegistryDocs,
},
},
{
name: "Translates standalone provider config into Pulumi config YAML",
contentStr: readfile(t, "test_data/installation-docs/provider-config-only.md"),
expected: readfile(t, "test_data/installation-docs/provider-config-only-expected.md"),
g: &Generator{
sink: mockSink{},
cliConverterState: &cliConverter{
info: p,
pcls: pclsMap,
},
language: RegistryDocs,
},
},
{
name: "Translates standalone example into languages",
contentStr: readfile(t, "test_data/installation-docs/example-only.md"),
expected: readfile(t, "test_data/installation-docs/example-only-expected.md"),
g: &Generator{
sink: mockSink{},
cliConverterState: &cliConverter{
info: p,
pcls: pclsMap,
},
language: RegistryDocs,
},
language: RegistryDocs,
},
}
t.Run(tc.name, func(t *testing.T) {
if runtime.GOOS == "windows" {
// Currently there is a test issue in CI/test setup:
//
// convertViaPulumiCLI: failed to clean up temp bridge-examples.json file: The
// process cannot access the file because it is being used by another process.
t.Skipf("Skipping on Windows due to a test setup issue")
}
t.Setenv("PULUMI_CONVERT", "1")
actual, err := translateCodeBlocks(tc.contentStr, tc.g)
require.NoError(t, err)
require.Equal(t, tc.expected, actual)
})

for _, tt := range testCases {
tt := tt

t.Run(tt.name, func(t *testing.T) {
if runtime.GOOS == "windows" {
// Currently there is a test issue in CI/test setup:
//
// convertViaPulumiCLI: failed to clean up temp bridge-examples.json file: The
// process cannot access the file because it is being used by another process.
t.Skipf("Skipping on Windows due to a test setup issue")
}
t.Setenv("PULUMI_CONVERT", "1")
actual, err := translateCodeBlocks(tt.contentStr, tt.g)
require.NoError(t, err)
require.Equal(t, tt.expected, actual)
})
}
}

func TestSkipSectionHeadersByContent(t *testing.T) {
Expand Down Expand Up @@ -617,3 +663,25 @@ func assertEqualHTML(t *testing.T, expected, actual string) bool {
}
return assert.Equal(t, expectedBuf.String(), outputBuf.String())
}

func TestRemoveEmptyExamples(t *testing.T) {
t.Parallel()
type testCase struct {
name string
input string
expected string
}

tc := testCase{
name: "An empty Example Usage section is skipped",
input: readTestFile(t, "skip-empty-examples/input.md"),
expected: readTestFile(t, "skip-empty-examples/expected.md"),
}

t.Run(tc.name, func(t *testing.T) {
t.Parallel()
actual, err := removeEmptyExamples(tc.input)
require.NoError(t, err)
assertEqualHTML(t, tc.expected, actual)
})
}
Loading
Loading