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

Open
wants to merge 6 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
}
source = "```" + lang + "\n" + source + "```"
return source, err
Expand Down
74 changes: 66 additions & 8 deletions pkg/tfgen/installation_docs.go
Original file line number Diff line number Diff line change
Expand Up @@ -66,9 +66,15 @@ func plainDocsParser(docFile *DocFile, g *Generator) ([]byte, error) {
return nil, err
}

// If the code translation resulted in an empty examples section, remove it
content, err = removeEmptySection("Example Usage", []byte(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)
contentBytes, err = g.editRules.apply(docFile.FileName, content, info.PostCodeTranslation)
if err != nil {
return nil, err
}
Expand Down Expand Up @@ -221,9 +227,6 @@ func translateCodeBlocks(contentStr string, g *Generator) (string, error) {

// 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 @@ func convertExample(g *Generator, code string, exampleNumber int) (string, error
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,35 @@ func getProviderDisplayName(g *Generator) string {
capitalize := cases.Title(language.English)
return capitalize.String(providerName)
}

func removeEmptySection(title string, contentBytes []byte) ([]byte, error) {
if !isMarkdownSectionEmpty(title, contentBytes) {
return contentBytes, nil
}
return SkipSectionByHeaderContent(contentBytes, func(headerText string) bool {
return headerText == title
})
}

func isMarkdownSectionEmpty(title string, contentBytes []byte) bool {
gm := goldmark.New(goldmark.WithExtensions(parse.TFRegistryExtension))
astNode := gm.Parser().Parse(text.NewReader(contentBytes))

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(contentBytes)
// A little confusingly, we check if the section text _only_ contains the title, "Example Usage".
// Non-empty sections contain the title + content, so if we see only the title, the section is empty.
if string(sectionText) == title {
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 @@ -599,6 +645,28 @@ func TestSkipDefaultSectionHeaders(t *testing.T) {
}
}

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 := removeEmptySection("Example Usage", []byte(tc.input))
require.NoError(t, err)
assertEqualHTML(t, tc.expected, string(actual))
})
}

// Helper func to determine if the HTML rendering is equal.
// This helps in cases where the processed Markdown is slightly different from the expected Markdown
// due to goldmark making some (insignificant to the final HTML) changes when parsing and rendering.
Expand Down
Loading
Loading