-
Notifications
You must be signed in to change notification settings - Fork 16
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* Generate API doc examples * Github actions to init submodules
- Loading branch information
1 parent
a26289c
commit 4756990
Showing
10 changed files
with
432 additions
and
82 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -124,6 +124,8 @@ jobs: | |
repo: pulumi/pulumictl | ||
- name: Install Pulumi CLI | ||
uses: pulumi/[email protected] | ||
- name: Initialize submodules | ||
run: make init_submodules | ||
- name: Build codegen binaries | ||
run: make codegen | ||
- name: Build Schema | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -41,6 +41,8 @@ jobs: | |
repo: pulumi/pulumictl | ||
- name: Install Pulumi CLI | ||
uses: pulumi/[email protected] | ||
- name: Initialize submodules | ||
run: make init_submodules | ||
- name: Build codegen binaries | ||
run: make codegen | ||
- name: Build Schema | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
[submodule "aws-cloudformation-user-guide"] | ||
path = aws-cloudformation-user-guide | ||
url = https://github.com/awsdocs/aws-cloudformation-user-guide |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Submodule aws-cloudformation-user-guide
added at
f57297
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,293 @@ | ||
// Copyright 2016-2021, Pulumi Corporation. | ||
|
||
package main | ||
|
||
import ( | ||
"bufio" | ||
"bytes" | ||
"fmt" | ||
"github.com/blang/semver" | ||
"github.com/hashicorp/hcl/v2" | ||
"github.com/pkg/errors" | ||
"github.com/pulumi/pulumi-aws-native/provider/pkg/cf2pulumi" | ||
"github.com/pulumi/pulumi/pkg/v3/codegen/dotnet" | ||
gogen "github.com/pulumi/pulumi/pkg/v3/codegen/go" | ||
"github.com/pulumi/pulumi/pkg/v3/codegen/hcl2" | ||
"github.com/pulumi/pulumi/pkg/v3/codegen/hcl2/syntax" | ||
"github.com/pulumi/pulumi/pkg/v3/codegen/nodejs" | ||
"github.com/pulumi/pulumi/pkg/v3/codegen/python" | ||
"github.com/pulumi/pulumi/pkg/v3/codegen/schema" | ||
"os" | ||
"path" | ||
"path/filepath" | ||
"strings" | ||
"text/template" | ||
) | ||
|
||
func generateExamples(pkgSpec *schema.PackageSpec, languages []string) error { | ||
// Find all snippets in the AWS CloudFormation Docs repo. | ||
folder := path.Join(".", "aws-cloudformation-user-guide", "doc_source") | ||
examples, err := findAllExamples(folder) | ||
if err != nil { | ||
return err | ||
} | ||
|
||
// Cache to speed up code generation. | ||
hcl2Cache := hcl2.Cache(hcl2.NewPackageCache()) | ||
pkg, err := schema.ImportSpec(*pkgSpec, nil) | ||
if err != nil { | ||
return err | ||
} | ||
loaderOption := hcl2.Loader(inMemoryPackageLoader(map[string]*schema.Package{ | ||
"aws-native": pkg, | ||
})) | ||
|
||
// Render examples to SDK languages. | ||
examplesRenderData := map[string][]exampleRenderData{} | ||
for _, yaml := range examples { | ||
example, err := generateExample(yaml, languages, hcl2Cache, loaderOption) | ||
if err != nil { | ||
// Skip all snippets that don't produce valid examples. | ||
continue | ||
} | ||
var existing []exampleRenderData | ||
if other, ok := examplesRenderData[example.ResourceType]; ok { | ||
existing = other | ||
} | ||
examplesRenderData[example.ResourceType] = append(existing, exampleRenderData{ | ||
ExampleDescription: "Example", | ||
LanguageToExampleProgram: example.LanguageToExampleProgram, | ||
}) | ||
} | ||
|
||
// Write examples to the schema docs. | ||
for resourceType, data := range examplesRenderData { | ||
err = renderExampleToSchema(pkgSpec, resourceType, data) | ||
if err != nil { | ||
return errors.Wrapf(err, "rendering %s %+v", resourceType, data) | ||
} | ||
} | ||
return nil | ||
} | ||
|
||
func generateExample(yaml string, languages []string, bindOpts ...hcl2.BindOption) (*resourceExample, error) { | ||
body, err := cf2pulumi.RenderText(yaml) | ||
if err != nil { | ||
return nil, errors.Wrapf(err, "rendering YAML") | ||
} | ||
|
||
cf2pulumi.FormatBody(body) | ||
text := fmt.Sprintf("%v", body) | ||
|
||
parser := syntax.NewParser() | ||
if err := parser.ParseFile(strings.NewReader(text), "program.pp"); err != nil { | ||
return nil, errors.Wrapf(err, "parsing IR") | ||
} | ||
if parser.Diagnostics.HasErrors() { | ||
buf := new(bytes.Buffer) | ||
_ = parser.NewDiagnosticWriter(buf, 0, false).WriteDiagnostics(parser.Diagnostics) | ||
return nil, errors.Errorf("parser diagnostic errors: %s", buf) | ||
} | ||
|
||
program, diags, err := hcl2.BindProgram(parser.Files, bindOpts...) | ||
if err != nil { | ||
return nil, errors.Wrapf(err, "binding program") | ||
} | ||
if diags.HasErrors() { | ||
buf := new(bytes.Buffer) | ||
_ = program.NewDiagnosticWriter(buf, 0, false).WriteDiagnostics(diags) | ||
return nil, errors.Errorf("bind diagnostic errors: %s", buf) | ||
} | ||
|
||
resourceType := "" | ||
for _, node := range program.Nodes { | ||
if res, ok := node.(*hcl2.Resource); ok { | ||
resourceType = res.Token | ||
break | ||
} | ||
} | ||
if resourceType == "" { | ||
return nil, errors.New("no resource node found") | ||
} | ||
|
||
perLanguage := languageToExampleProgram{} | ||
for _, target := range languages { | ||
var files map[string][]byte | ||
switch target { | ||
case "dotnet": | ||
files, diags, err = dotnet.GenerateProgram(program) | ||
case "go": | ||
files, diags, err = recoverableProgramGen(program, gogen.GenerateProgram) | ||
case "nodejs": | ||
files, diags, err = nodejs.GenerateProgram(program) | ||
case "python": | ||
files, diags, err = python.GenerateProgram(program) | ||
} | ||
if err != nil { | ||
return nil, errors.Wrapf(err, "generating program") | ||
} | ||
if diags.HasErrors() { | ||
buf := new(bytes.Buffer) | ||
_ = program.NewDiagnosticWriter(buf, 0, true).WriteDiagnostics(diags) | ||
return nil, errors.Errorf("generate diagnostic errors: %s", buf) | ||
} | ||
|
||
var sb strings.Builder | ||
for _, f := range files { | ||
sb.WriteString(string(f)) | ||
} | ||
perLanguage[language(target)] = programText(sb.String()) | ||
} | ||
return &resourceExample{ | ||
ResourceType: resourceType, | ||
LanguageToExampleProgram: perLanguage, | ||
}, nil | ||
} | ||
|
||
func findAllExamples(folder string) ([]string, error) { | ||
var fileNames, result []string | ||
err := filepath.Walk(folder, func(path string, info os.FileInfo, err error) error { | ||
if !info.IsDir() && strings.Contains(path, "aws-resource-") { | ||
fileNames = append(fileNames, path) | ||
} | ||
return nil | ||
}) | ||
if err != nil { | ||
return nil, err | ||
} | ||
for _, fileName := range fileNames { | ||
examples, err := findExamples(fileName) | ||
if err != nil { | ||
return nil, err | ||
} | ||
result = append(result, examples...) | ||
} | ||
return result, nil | ||
} | ||
|
||
func findExamples(fileName string) ([]string, error) { | ||
docFile, err := os.Open(fileName) | ||
if err != nil { | ||
return nil, errors.Wrapf(err, "opening file") | ||
} | ||
defer func() { | ||
_ = docFile.Close() | ||
}() | ||
|
||
var result []string | ||
var buf strings.Builder | ||
snippet := false | ||
|
||
scanner := bufio.NewScanner(docFile) | ||
for scanner.Scan() { | ||
line := scanner.Text() | ||
if strings.HasPrefix(line, "```") { | ||
if snippet && buf.Len() > 0 { | ||
result = append(result, buf.String()) | ||
} | ||
snippet = !snippet | ||
continue | ||
} | ||
if snippet { | ||
buf.WriteString(line) | ||
buf.WriteRune('\n') | ||
} else { | ||
buf.Reset() | ||
} | ||
} | ||
return result, scanner.Err() | ||
} | ||
|
||
type programGenFn func(*hcl2.Program) (map[string][]byte, hcl.Diagnostics, error) | ||
|
||
func recoverableProgramGen(program *hcl2.Program, fn programGenFn) (files map[string][]byte, d hcl.Diagnostics, err error) { | ||
defer func() { | ||
if r := recover(); r != nil { | ||
err = fmt.Errorf("panic recovered during generation: %v", r) | ||
} | ||
}() | ||
|
||
return fn(program) | ||
} | ||
|
||
type programText string | ||
type language string | ||
|
||
type languageToExampleProgram map[language]programText | ||
type exampleRenderData struct { | ||
ExampleDescription string | ||
LanguageToExampleProgram languageToExampleProgram | ||
} | ||
type resourceExample struct { | ||
ResourceType string | ||
LanguageToExampleProgram languageToExampleProgram | ||
} | ||
|
||
func renderExampleToSchema(pkgSpec *schema.PackageSpec, resourceName string, | ||
examplesRenderData []exampleRenderData) error { | ||
const tmpl = ` | ||
{{"{{% examples %}}"}} | ||
## Example Usage | ||
{{- range . }} | ||
{{ "{{% example %}}" }} | ||
### {{ .ExampleDescription }} | ||
{{- range $lang, $example := .LanguageToExampleProgram }} | ||
{{ beginLanguage $lang }} | ||
{{ $example }} | ||
{{ endLanguage }} | ||
{{ end }} | ||
{{"{{% /example %}}"}} | ||
{{- end }} | ||
{{"{{% /examples %}}"}} | ||
` | ||
res, ok := pkgSpec.Resources[resourceName] | ||
if !ok { | ||
return fmt.Errorf("missing resource from schema: %s", resourceName) | ||
} | ||
|
||
t, err := template.New("examples").Funcs(template.FuncMap{ | ||
"beginLanguage": func(lang interface{}) string { | ||
l := fmt.Sprintf("%s", lang) | ||
switch l { | ||
case "nodejs": | ||
l = "typescript" | ||
case "dotnet": | ||
l = "csharp" | ||
} | ||
return fmt.Sprintf("```%s", l) | ||
}, | ||
"endLanguage": func() string { | ||
return "```" | ||
}, | ||
}).Parse(tmpl) | ||
if err != nil { | ||
return err | ||
} | ||
b := strings.Builder{} | ||
if err = t.Execute(&b, examplesRenderData); err != nil { | ||
return err | ||
} | ||
res.Description += b.String() | ||
pkgSpec.Resources[resourceName] = res | ||
return nil | ||
} | ||
|
||
// inMemoryPackageLoader prevents having to fetch the schema from | ||
// the provider every time which significantly speeds up codegen. | ||
func inMemoryPackageLoader(pkgs map[string]*schema.Package) schema.Loader { | ||
return &inMemoryLoader{pkgs: pkgs} | ||
} | ||
|
||
type inMemoryLoader struct { | ||
pkgs map[string]*schema.Package | ||
} | ||
|
||
func (l *inMemoryLoader) LoadPackage(pkg string, _ *semver.Version) (*schema.Package, error) { | ||
if p, ok := l.pkgs[pkg]; ok { | ||
return p, nil | ||
} | ||
|
||
return nil, errors.Errorf("package %s not found in the in-memory map", pkg) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.