Skip to content

Commit 3ca48ac

Browse files
Do not render empty code choosers on generated index page (#2822)
This pull request does the following: 1. Prevents the generation of code choosers with empty content 1. Writes a YAML config file with no code choosers if the example is config-only 1. Checks the `pclExample` object for empty pulumi YAML or PCL explicitly and before attempting to convert, removing the empty string check from the converter functions 1. Adds tests to illustrate all four situations: - valid provider config + example - invalid code - valid provider config but no example - valid example but no provider config 1. Adds `exampleUnavailable` placeholder constant for use when conversion fails on an individual language 1. Adds a `successfulConversion` flag that flips to True as soon as any one language registers as having content other than the `exampleUnavailable` message. The idea is that if even only one pulumi language converted, we want to display the code chooser. Fixes #2819
1 parent 5c26c22 commit 3ca48ac

File tree

11 files changed

+421
-36
lines changed

11 files changed

+421
-36
lines changed

pkg/tfgen/convert_cli.go

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,10 @@ import (
4545
"github.com/pulumi/pulumi-terraform-bridge/v3/pkg/tfgen/internal/autofill"
4646
)
4747

48+
const (
49+
exampleUnavailable = "Example currently unavailable in this language\n"
50+
)
51+
4852
func cliConverterEnabled() bool {
4953
return cmdutil.IsTruthy(os.Getenv("PULUMI_CONVERT"))
5054
}
@@ -645,14 +649,14 @@ func (cc *cliConverter) singleExampleFromHCLToPCL(path, hclCode string) (transla
645649
func (cc *cliConverter) singleExampleFromPCLToLanguage(example translatedExample, lang string) (string, error) {
646650
var err error
647651

648-
if example.PCL == "" {
649-
return "", nil
650-
}
651652
source, diags, _ := cc.convertPCL(example.PCL, lang)
652653
diags = cc.postProcessDiagnostics(diags.Extend(example.Diagnostics))
653654
if diags.HasErrors() {
654-
source = "Example currently unavailable in this language\n"
655-
err = fmt.Errorf("failed to convert an example: %s", diags.Error())
655+
err = fmt.Errorf("conversion errors: %s", diags.Error())
656+
}
657+
658+
if source == "" {
659+
source = exampleUnavailable
656660
}
657661
source = "```" + lang + "\n" + source + "```"
658662
return source, err

pkg/tfgen/installation_docs.go

Lines changed: 68 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -66,9 +66,15 @@ func plainDocsParser(docFile *DocFile, g *Generator) ([]byte, error) {
6666
return nil, err
6767
}
6868

69+
// If the code translation resulted in an empty examples section, remove it
70+
content, err = removeEmptySection("Example Usage", []byte(contentStr))
71+
if err != nil {
72+
return nil, err
73+
}
74+
6975
// Apply post-code translation edit rules. This applies all default edit rules and provider-supplied edit rules in
7076
// the post-code translation phase.
71-
contentBytes, err = g.editRules.apply(docFile.FileName, []byte(contentStr), info.PostCodeTranslation)
77+
contentBytes, err = g.editRules.apply(docFile.FileName, content, info.PostCodeTranslation)
7278
if err != nil {
7379
return nil, err
7480
}
@@ -221,9 +227,6 @@ func translateCodeBlocks(contentStr string, g *Generator) (string, error) {
221227

222228
// This function renders the Pulumi.yaml config file for a given language if configuration is included in the example.
223229
func processConfigYaml(pulumiYAML, lang string) string {
224-
if pulumiYAML == "" {
225-
return pulumiYAML
226-
}
227230
// Replace the project name from the default `/` to a more descriptive name
228231
nameRegex := regexp.MustCompile(`name: /*`)
229232
pulumiYAMLFile := nameRegex.ReplaceAllString(pulumiYAML, "name: configuration-example")
@@ -253,30 +256,53 @@ func convertExample(g *Generator, code string, exampleNumber int) (string, error
253256
return "", err
254257
}
255258

259+
// If both PCL and PulumiYAML fields are empty, we can return.
260+
if pclExample.PulumiYAML == "" && pclExample.PCL == "" {
261+
return "", nil
262+
}
263+
264+
// If we have a valid provider config but no additional code, we only render a YAML configuration block
265+
// with no choosers and an empty language runtime field
266+
if pclExample.PulumiYAML != "" && pclExample.PCL == "" {
267+
if pclExample.PCL == "" {
268+
return processConfigYaml(pclExample.PulumiYAML, ""), nil
269+
}
270+
}
271+
256272
langs := genLanguageToSlice(g.language)
257273
const (
258274
chooserStart = `{{< chooser language "typescript,python,go,csharp,java,yaml" >}}` + "\n"
259275
chooserEnd = "{{< /chooser >}}\n"
260276
choosableEnd = "\n{{% /choosable %}}\n"
261277
)
262278
exampleContent := chooserStart
279+
successfulConversion := false
263280

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

268285
// Generate the Pulumi.yaml config file for each language
269-
configFile := pclExample.PulumiYAML
270-
pulumiYAML := processConfigYaml(configFile, lang)
286+
var pulumiYAML string
287+
if pclExample.PulumiYAML != "" {
288+
pulumiYAML = processConfigYaml(pclExample.PulumiYAML, lang)
289+
}
290+
271291
// Generate language example
272292
convertedLang, err := converter.singleExampleFromPCLToLanguage(pclExample, lang)
273293
if err != nil {
274294
g.warn(err.Error())
275295
}
296+
if convertedLang != exampleUnavailable {
297+
successfulConversion = true
298+
}
276299
exampleContent += choosableStart + pulumiYAML + convertedLang + choosableEnd
277300
}
278-
exampleContent += chooserEnd
279-
return exampleContent, nil
301+
302+
if successfulConversion {
303+
return exampleContent + chooserEnd, nil
304+
}
305+
return "", nil
280306
}
281307

282308
type titleRemover struct{}
@@ -477,3 +503,37 @@ func getProviderDisplayName(g *Generator) string {
477503
capitalize := cases.Title(language.English)
478504
return capitalize.String(providerName)
479505
}
506+
507+
func removeEmptySection(title string, contentBytes []byte) ([]byte, error) {
508+
if !isMarkdownSectionEmpty(title, contentBytes) {
509+
return contentBytes, nil
510+
}
511+
return SkipSectionByHeaderContent(contentBytes, func(headerText string) bool {
512+
return headerText == title
513+
})
514+
}
515+
516+
func isMarkdownSectionEmpty(title string, contentBytes []byte) bool {
517+
gm := goldmark.New(goldmark.WithExtensions(parse.TFRegistryExtension))
518+
astNode := gm.Parser().Parse(text.NewReader(contentBytes))
519+
520+
isEmpty := false
521+
522+
err := ast.Walk(astNode, func(n ast.Node, entering bool) (ast.WalkStatus, error) {
523+
if section, ok := n.(*section.Section); ok && entering {
524+
if section.HasChildren() {
525+
// A titled section is empty if it has only one child - the title.
526+
// If the child's text matches the title, the section is empty.
527+
sectionText := string(section.FirstChild().Text(contentBytes))
528+
if section.FirstChild() == section.LastChild() && sectionText == title {
529+
isEmpty = true
530+
return ast.WalkStop, nil
531+
}
532+
}
533+
}
534+
return ast.WalkContinue, nil
535+
})
536+
contract.AssertNoErrorf(err, "impossible: ast.Walk should never error")
537+
538+
return isEmpty
539+
}

pkg/tfgen/installation_docs_test.go

Lines changed: 91 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -449,32 +449,78 @@ func TestTranslateCodeBlocks(t *testing.T) {
449449
}
450450
pclsMap := make(map[string]translatedExample)
451451

452-
tc := testCase{
453-
name: "Translates HCL from examples ",
454-
contentStr: readfile(t, "test_data/installation-docs/configuration.md"),
455-
expected: readfile(t, "test_data/installation-docs/configuration-expected.md"),
456-
g: &Generator{
457-
sink: mockSink{},
458-
cliConverterState: &cliConverter{
459-
info: p,
460-
pcls: pclsMap,
452+
testCases := []testCase{
453+
{
454+
name: "Translates HCL from examples ",
455+
contentStr: readfile(t, "test_data/installation-docs/configuration.md"),
456+
expected: readfile(t, "test_data/installation-docs/configuration-expected.md"),
457+
g: &Generator{
458+
sink: mockSink{},
459+
cliConverterState: &cliConverter{
460+
info: p,
461+
pcls: pclsMap,
462+
},
463+
language: RegistryDocs,
464+
},
465+
},
466+
{
467+
name: "Does not translate an invalid example and leaves example block blank",
468+
contentStr: readfile(t, "test_data/installation-docs/invalid-example.md"),
469+
expected: readfile(t, "test_data/installation-docs/invalid-example-expected.md"),
470+
g: &Generator{
471+
sink: mockSink{},
472+
cliConverterState: &cliConverter{
473+
info: p,
474+
pcls: pclsMap,
475+
},
476+
language: RegistryDocs,
477+
},
478+
},
479+
{
480+
name: "Translates standalone provider config into Pulumi config YAML",
481+
contentStr: readfile(t, "test_data/installation-docs/provider-config-only.md"),
482+
expected: readfile(t, "test_data/installation-docs/provider-config-only-expected.md"),
483+
g: &Generator{
484+
sink: mockSink{},
485+
cliConverterState: &cliConverter{
486+
info: p,
487+
pcls: pclsMap,
488+
},
489+
language: RegistryDocs,
490+
},
491+
},
492+
{
493+
name: "Translates standalone example into languages",
494+
contentStr: readfile(t, "test_data/installation-docs/example-only.md"),
495+
expected: readfile(t, "test_data/installation-docs/example-only-expected.md"),
496+
g: &Generator{
497+
sink: mockSink{},
498+
cliConverterState: &cliConverter{
499+
info: p,
500+
pcls: pclsMap,
501+
},
502+
language: RegistryDocs,
461503
},
462-
language: RegistryDocs,
463504
},
464505
}
465-
t.Run(tc.name, func(t *testing.T) {
466-
if runtime.GOOS == "windows" {
467-
// Currently there is a test issue in CI/test setup:
468-
//
469-
// convertViaPulumiCLI: failed to clean up temp bridge-examples.json file: The
470-
// process cannot access the file because it is being used by another process.
471-
t.Skipf("Skipping on Windows due to a test setup issue")
472-
}
473-
t.Setenv("PULUMI_CONVERT", "1")
474-
actual, err := translateCodeBlocks(tc.contentStr, tc.g)
475-
require.NoError(t, err)
476-
require.Equal(t, tc.expected, actual)
477-
})
506+
507+
for _, tt := range testCases {
508+
tt := tt
509+
510+
t.Run(tt.name, func(t *testing.T) {
511+
if runtime.GOOS == "windows" {
512+
// Currently there is a test issue in CI/test setup:
513+
//
514+
// convertViaPulumiCLI: failed to clean up temp bridge-examples.json file: The
515+
// process cannot access the file because it is being used by another process.
516+
t.Skipf("Skipping on Windows due to a test setup issue")
517+
}
518+
t.Setenv("PULUMI_CONVERT", "1")
519+
actual, err := translateCodeBlocks(tt.contentStr, tt.g)
520+
require.NoError(t, err)
521+
require.Equal(t, tt.expected, actual)
522+
})
523+
}
478524
}
479525

480526
func TestSkipSectionHeadersByContent(t *testing.T) {
@@ -599,6 +645,28 @@ func TestSkipDefaultSectionHeaders(t *testing.T) {
599645
}
600646
}
601647

648+
func TestRemoveEmptyExamples(t *testing.T) {
649+
t.Parallel()
650+
type testCase struct {
651+
name string
652+
input string
653+
expected string
654+
}
655+
656+
tc := testCase{
657+
name: "An empty Example Usage section is skipped",
658+
input: readTestFile(t, "skip-empty-examples/input.md"),
659+
expected: readTestFile(t, "skip-empty-examples/expected.md"),
660+
}
661+
662+
t.Run(tc.name, func(t *testing.T) {
663+
t.Parallel()
664+
actual, err := removeEmptySection("Example Usage", []byte(tc.input))
665+
require.NoError(t, err)
666+
assertEqualHTML(t, tc.expected, string(actual))
667+
})
668+
}
669+
602670
// Helper func to determine if the HTML rendering is equal.
603671
// This helps in cases where the processed Markdown is slightly different from the expected Markdown
604672
// due to goldmark making some (insignificant to the final HTML) changes when parsing and rendering.

0 commit comments

Comments
 (0)