Update schema-based docs generator (#4035)

* Update properties.tmpl to render property comment as-is. WIP splitting out properties to lang-specific tables.

* Generate the constructor dynamically from the resource per language.

* Add doc functions in each language generator package for getting doc links for types..and later other functions too.

* Render the constructor params in the Go code and inject into the template.

* Generate nodejs types using the nodejs lang generator.

* Add a templates bundler. Added a new Make target for autogenerating a static bundle for the resource docs generator.

* Generate type links for all languages based on their schema type. Render the property type with a link if the underlying elements have a supporting type. Fix word-breaks for Python type names.

* Various changes including the introduction of an interface type under the codegen package to help with generating some language-specific information for the resource docs generator.

* Add a function to explicitly generate links for input types of nested types. Fix the resource doc link generator for Go. Don't replace the module name from the nodejs language type.

* Fix bug with C# property type html encoding.

* Fix some template formatting. Pass the state inputs for Python to generate the lookup function for it.
* Do not generate the examples section if there are none.

* Generating the property types per language.

* Formatting. Rename function for readability.

* Add comments. Update README.

* Use relative URLs for doc links within the main site
This commit is contained in:
Praneet Loke 2020-03-09 10:35:20 -07:00 committed by GitHub
parent 2a24470135
commit edbb05dddd
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
20 changed files with 1223 additions and 325 deletions

View file

@ -12,6 +12,12 @@ TESTPARALLELISM := 10
build-proto::
cd sdk/proto && ./generate.sh
.PHONY: generate
generate::
$(call STEP_MESSAGE)
echo "Generate static assets bundle for docs generator"
go generate ./pkg/codegen/docs/
build::
go install -ldflags "-X github.com/pulumi/pulumi/pkg/version.Version=${VERSION}" ${PROJECT}

27
pkg/codegen/docs.go Normal file
View file

@ -0,0 +1,27 @@
// Copyright 2016-2020, Pulumi Corporation.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package codegen
import (
"github.com/pulumi/pulumi/pkg/codegen/schema"
)
// DocLanguageHelper is an interface for extracting language-specific information from a Pulumi schema.
// See the implementation for this interface under each of the language code generators.
type DocLanguageHelper interface {
GetDocLinkForResourceType(packageName, moduleName, typeName string) string
GetDocLinkForInputType(packageName, moduleName, typeName string) string
GetLanguageTypeString(pkg *schema.Package, moduleName string, t schema.Type, input, optional bool) string
}

1
pkg/codegen/docs/.gitignore vendored Normal file
View file

@ -0,0 +1 @@
packaged.go

View file

@ -0,0 +1,40 @@
# Docs generator
This generator generates resource-level docs by utilizing the Pulumi schema.
## Crash course on templates
The templates use Go's built-in `html/template` package to process templates with data. The driver for this doc generator (e.g. tfbridge for TF-based providers) then persists each file from memory onto the disk as `.md` files.
Although we are using the `html/template` package, it has the same exact interface as the [`text/template`](https://golang.org/pkg/text/template) package, except for some HTML specific things. Therefore, all of the functions available in the `text/template` package are also available with the `html/template` package.
* Data can be injected using `{{.PropertyName}}`.
* Nested properties can be accessed using the dot notation, i.e. `{{.Property1.Property2}}`.
* Templates can inject other templates using the `{{template "template_name"}}` directive.
* For this to work, you will need to first define the named template using `{{define "template_name"}}`.
* You can pass data to nested templates by simply passing an argument after the template's name.
* To remove whitespace from injected values, use the `-` in the template tags.
* For example, `{{if .SomeBool}} some text {{- else}} some other text {{- end}}`. Note the use of `-` to eliminate whitespace from the enclosing text.
* Read more [here](https://golang.org/pkg/text/template/#hdr-Text_and_spaces).
* To render un-encoded content use the custom global function `htmlSafe`.
* **Note**: This should only be used if you know for sure you are not injecting any user-generated content, as it by-passes the HTML encoding.
* To print regular strings, that share the same syntax as the Go templating engine, use the built-in global function `print` [function](https://golang.org/pkg/text/template/#hdr-Functions).
* For example, if you need to render `{{% md %}}`, you will instead need to do `{{print "{{% md %}}"}}`.
Learn more from here: https://curtisvermeeren.github.io/2017/09/14/Golang-Templates-Cheatsheet
## `bundler.go`
This file contains a `main` function and is part of the `main` package. We run it using the `go generate` command (see the `Makefile` and the starting comment in `pkg/codegen/gen.go`).
> This file is ignored using a `+build ignore` comment at the top of the file, so it is not ignored during a `go build ...`.
## `packaged.go`
A file generated by `bundler.go` that contains formatted byte strings, that represent the string templates from the `./templates/` folder. This file is also git-ignored as it is intended to only be generated by the `docs` repo and is not used during runtime of the main Pulumi CLI. In fact, this whole package is not used during the runtime of the CLI itself.
## `go:generate`
> Read more [here](https://blog.golang.org/generate).
`go:generate` is a special code comment that can be used to run custom commands by simply running `go generate <package>`, which then scans for `go:generate` comments in all sources in the package `<package>`. It also serves as a way to document, that a certain file relies on a command to have been executed before it can be used.

128
pkg/codegen/docs/bundler.go Normal file
View file

@ -0,0 +1,128 @@
//+build ignore
// Copyright 2016-2020, Pulumi Corporation.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// Pulling out some of the repeated strings tokens into constants would harm readability, so we just ignore the
// goconst linter's warning.
//
// nolint: lll, goconst
package main
import (
"bytes"
"fmt"
"go/format"
"io/ioutil"
"log"
"os"
"strings"
"text/template"
)
const (
basePath = "."
docsTemplatesPath = basePath + "/templates"
generatedFileName = basePath + "/packaged.go"
)
var conv = map[string]interface{}{"conv": fmtByteSlice}
var tmpl = template.Must(template.New("").Funcs(conv).Parse(`
// AUTO-GENERATED FILE! DO NOT EDIT THIS FILE MANUALLY.
// Copyright 2016-2020, Pulumi Corporation.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// Pulling out some of the repeated strings tokens into constants would harm readability, so we just ignore the
// goconst linter's warning.
//
// nolint: lll, goconst
package docs
func init() {
packagedTemplates = make(map[string][]byte)
{{ range $key, $value := . }}
packagedTemplates["{{ $key }}"] = []byte{ {{ conv $value }} }
{{ println }}
{{- end }}
}
`))
// fmtByteSlice returns a formatted byte string for a given slice of bytes.
// We embed the raw bytes to avoid any formatting errors that can occur due to saving
// raw strings in a file.
func fmtByteSlice(s []byte) string {
builder := strings.Builder{}
for _, v := range s {
builder.WriteString(fmt.Sprintf("%d,", int(v)))
}
return builder.String()
}
// main reads files under the templates directory, and builds a map of filename to byte slice.
// Each file's contents are then written to a generated file.
//
// NOTE: Sub-directories are currently not supported.
func main() {
files, err := ioutil.ReadDir(docsTemplatesPath)
if err != nil {
log.Fatalf("Error reading the templates dir: %v", err)
}
contents := make(map[string][]byte)
for _, f := range files {
if f.IsDir() {
fmt.Printf("%q is a dir. Skipping...", f.Name())
}
b, err := ioutil.ReadFile(docsTemplatesPath + "/" + f.Name())
if err != nil {
log.Fatalf("Error reading file %s: %v", f.Name(), err)
}
contents[f.Name()] = b
}
// We overwrite the file every time the `go generate ...` command is run.
f, err := os.Create(generatedFileName)
if err != nil {
log.Fatal("Error creating blob file:", err)
}
defer f.Close()
builder := &bytes.Buffer{}
if err = tmpl.Execute(builder, contents); err != nil {
log.Fatal("Error executing template", err)
}
data, err := format.Source(builder.Bytes())
if err != nil {
log.Fatal("Error formatting generated code", err)
}
if err = ioutil.WriteFile(generatedFileName, data, os.ModePerm); err != nil {
log.Fatal("Error writing file", err)
}
}

View file

@ -1,3 +1,5 @@
//go:generate go run bundler.go
// Copyright 2016-2020, Pulumi Corporation.
//
// Licensed under the Apache License, Version 2.0 (the "License");
@ -21,17 +23,100 @@ package docs
import (
"bytes"
"fmt"
"html"
"html/template"
"io"
"path"
"sort"
"strings"
"unicode"
"github.com/pkg/errors"
"github.com/pulumi/pulumi/pkg/codegen"
"github.com/pulumi/pulumi/pkg/codegen/dotnet"
go_gen "github.com/pulumi/pulumi/pkg/codegen/go"
"github.com/pulumi/pulumi/pkg/codegen/nodejs"
"github.com/pulumi/pulumi/pkg/codegen/python"
"github.com/pulumi/pulumi/pkg/codegen/schema"
"github.com/pulumi/pulumi/pkg/util/contract"
)
var supportedLanguages = []string{"csharp", "go", "nodejs", "python"}
var templates *template.Template
var packagedTemplates map[string][]byte
// Header represents the header of each resource markdown file.
type Header struct {
Title string
}
type exampleUsage struct {
Heading string
Code string
}
// Property represents an input or an output property.
type Property struct {
Name string
Comment string
Type PropertyType
DeprecationMessage string
IsRequired bool
// IsInput is a flag to indicate if a property is an input
// property.
IsInput bool
}
// DocNestedType represents a complex type.
type DocNestedType struct {
Name string
APIDocLinks map[string]string
Properties map[string][]Property
}
// PropertyType represents the type of a property.
type PropertyType struct {
Name string
// Link can be a link to an anchor tag on the same
// page, or to another page/site.
Link string
}
// ConstructorParam represents the formal parameters of a constructor.
type ConstructorParam struct {
Name string
Type PropertyType
// This is the language specific optional type indicator.
// For example, in nodejs this is the character "?" and in Go
// it's "*".
OptionalFlag string
DefaultValue string
}
type resourceArgs struct {
Header
Comment string
Examples []exampleUsage
ConstructorParams map[string]string
// ConstructorResource is the resource that is being constructed or
// is the result of a constructor-like function.
ConstructorResource map[string]PropertyType
ArgsRequired bool
InputProperties map[string][]Property
OutputProperties map[string][]Property
StateInputs map[string][]Property
StateParam string
NestedTypes []DocNestedType
}
type stringSet map[string]struct{}
func (ss stringSet) add(s string) {
@ -44,32 +129,6 @@ type typeDetails struct {
functionType bool
}
// wbr inserts HTML <wbr> in between case changes, e.g. "fooBar" becomes "foo<wbr>Bar".
func wbr(s string) string {
var runes []rune
var prev rune
for i, r := range s {
if i != 0 && unicode.IsLower(prev) && unicode.IsUpper(r) {
runes = append(runes, []rune("<wbr>")...)
}
runes = append(runes, r)
prev = r
}
return string(runes)
}
func title(s string) string {
if s == "" {
return ""
}
runes := []rune(s)
return string(append([]rune{unicode.ToUpper(runes[0])}, runes[1:]...))
}
func lower(s string) string {
return strings.ToLower(s)
}
type modContext struct {
pkg *schema.Package
mod string
@ -80,6 +139,13 @@ type modContext struct {
tool string
}
func resourceName(r *schema.Resource) string {
if r.IsProvider {
return "Provider"
}
return tokenToName(r.Token)
}
func (mod *modContext) details(t *schema.ObjectType) *typeDetails {
details, ok := mod.typeDetails[t]
if !ok {
@ -92,336 +158,384 @@ func (mod *modContext) details(t *schema.ObjectType) *typeDetails {
return details
}
func tokenToName(tok string) string {
components := strings.Split(tok, ":")
contract.Assertf(len(components) == 3, "malformed token %v", tok)
return title(components[2])
}
func (mod *modContext) typeString(t schema.Type, lang string, input, optional bool, insertWordBreaks bool) PropertyType {
var langType string
func resourceName(r *schema.Resource) string {
if r.IsProvider {
return "Provider"
}
return tokenToName(r.Token)
}
func (mod *modContext) typeStringPulumi(t schema.Type, link bool) string {
var br string
lt := "<"
gt := ">"
// If we're linking, we're including HTML, so also include word breaks,
// and escape < and >.
if link {
br = "<wbr>"
lt = "&lt;<wbr>"
gt = "<wbr>&gt;"
var docLanguageHelper codegen.DocLanguageHelper
switch lang {
case "nodejs":
docLanguageHelper = nodejs.DocLanguageHelper{}
case "go":
docLanguageHelper = go_gen.DocLanguageHelper{}
case "csharp":
docLanguageHelper = dotnet.DocLanguageHelper{}
case "python":
docLanguageHelper = python.DocLanguageHelper{}
default:
panic(errors.Errorf("Unknown language (%q) passed!", lang))
}
var typ string
langType = docLanguageHelper.GetLanguageTypeString(mod.pkg, mod.mod, t, input, optional)
// If the type is an object type, let's also wrap it with a link to the supporting type
// on the same page using an anchor tag.
var href string
switch t := t.(type) {
case *schema.ArrayType:
typ = fmt.Sprintf("Array%s%s%s", lt, mod.typeStringPulumi(t.ElementType, link), gt)
case *schema.MapType:
typ = fmt.Sprintf("Map%s%s%s", lt, mod.typeStringPulumi(t.ElementType, link), gt)
elementLangType := mod.typeString(t.ElementType, lang, input, optional, false)
href = elementLangType.Link
case *schema.ObjectType:
if link {
typ = fmt.Sprintf("<a href=\"#%s\">%s</a>", lower(tokenToName(t.Token)), wbr(tokenToName(t.Token)))
} else {
typ = tokenToName(t.Token)
}
case *schema.TokenType:
typ = tokenToName(t.Token)
case *schema.UnionType:
var elements []string
for _, e := range t.ElementTypes {
elements = append(elements, mod.typeStringPulumi(e, link))
}
sep := fmt.Sprintf(", %s", br)
return fmt.Sprintf("Union%s%s%s", lt, strings.Join(elements, sep), gt)
default:
switch t {
case schema.BoolType:
typ = "boolean"
case schema.IntType, schema.NumberType:
typ = "number"
case schema.StringType:
typ = "string"
case schema.ArchiveType:
typ = "Archive"
case schema.AssetType:
typ = fmt.Sprintf("Union%sAsset, %sArchive%s", lt, br, gt)
case schema.AnyType:
typ = "any"
}
tokenName := tokenToName(t.Token)
// Links to anchor targs on the same page must be lower-cased.
href = "#" + lower(tokenName)
}
if insertWordBreaks {
if lang == "csharp" {
langType = html.EscapeString(langType)
}
langType = wbr(langType)
}
return PropertyType{
Link: href,
Name: langType,
}
return typ
}
func (mod *modContext) genConstructorTS(w io.Writer, r *schema.Resource) {
name := resourceName(r)
allOptionalInputs := true
for _, prop := range r.InputProperties {
allOptionalInputs = allOptionalInputs && !prop.IsRequired
}
var argsFlags string
if allOptionalInputs {
// If the number of required input properties was zero, we can make the args object optional.
argsFlags = "?"
}
argsType := name + "Args"
// TODO: The link to the class name and args type needs to factor in the package and module. Right now it's hardcoded to aws and s3.
fmt.Fprintf(w, "<span class=\"k\">new</span> <span class=\"nx\"><a href=/docs/reference/pkg/nodejs/pulumi/aws/s3/#%s>%s</a></span><span class=\"p\">(</span><span class=\"nx\">name</span>: <span class=\"kt\"><a href=https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String>string</a></span><span class=\"p\">,</span> <span class=\"nx\">args%s</span>: <span class=\"kt\"><a href=/docs/reference/pkg/nodejs/pulumi/aws/s3/#%s>%s</a></span><span class=\"p\">,</span> <span class=\"nx\">opts?</span>: <span class=\"kt\"><a href=/docs/reference/pkg/nodejs/pulumi/pulumi/#CustomResourceOptions>pulumi.CustomResourceOptions</a></span><span class=\"p\">);</span>", name, name, argsFlags, argsType, argsType)
}
func (mod *modContext) genConstructorPython(w io.Writer, r *schema.Resource) {
fmt.Fprintf(w, "def __init__(__self__, resource_name, opts=None")
for _, prop := range r.InputProperties {
fmt.Fprintf(w, ", %s=None", python.PyName(prop.Name))
}
// Note: We're excluding __name__ and __opts__ as those are only there for backwards compatibility and are
// deliberately not included in doc strings.
fmt.Fprintf(w, ", __props__=None)")
}
func (mod *modContext) genConstructorGo(w io.Writer, r *schema.Resource) {
func (mod *modContext) genConstructorTS(r *schema.Resource, argsOptional bool) []ConstructorParam {
name := resourceName(r)
argsType := name + "Args"
fmt.Fprintf(w, "func New%s(ctx *pulumi.Context, name string, args *%s, opts ...pulumi.ResourceOption) (*%s, error)\n", name, argsType, name)
argsFlag := ""
if argsOptional {
argsFlag = "?"
}
docLangHelper := nodejs.DocLanguageHelper{}
return []ConstructorParam{
{
Name: "name",
Type: PropertyType{
Name: "string",
Link: nodejs.GetDocLinkForBuiltInType("string"),
},
},
{
Name: "args",
OptionalFlag: argsFlag,
Type: PropertyType{
Name: argsType,
Link: docLangHelper.GetDocLinkForResourceType(mod.pkg.Name, mod.mod, argsType),
},
},
{
Name: "opts",
OptionalFlag: "?",
Type: PropertyType{
Name: "pulumi.CustomResourceOptions",
Link: docLangHelper.GetDocLinkForResourceType("pulumi", "pulumi", "CustomResourceOptions"),
},
},
}
}
func (mod *modContext) genConstructorCS(w io.Writer, r *schema.Resource) {
func (mod *modContext) genConstructorGo(r *schema.Resource, argsOptional bool) []ConstructorParam {
name := resourceName(r)
argsType := name + "Args"
argsFlag := ""
if argsOptional {
argsFlag = "*"
}
docLangHelper := go_gen.DocLanguageHelper{}
// return fmt.Sprintf("func New%s(ctx *pulumi.Context, name string, args *%s, opts ...pulumi.ResourceOption) (*%s, error)\n", name, argsType, name)
return []ConstructorParam{
{
Name: "ctx",
OptionalFlag: "*",
Type: PropertyType{
Name: "pulumi.Context",
Link: "https://pkg.go.dev/github.com/pulumi/pulumi/sdk/go/pulumi?tab=doc#Context",
},
},
{
Name: "name",
Type: PropertyType{
Name: "string",
Link: go_gen.GetDocLinkForBuiltInType("string"),
},
},
{
Name: "args",
OptionalFlag: argsFlag,
Type: PropertyType{
Name: argsType,
Link: docLangHelper.GetDocLinkForResourceType(mod.pkg.Name, mod.mod, argsType),
},
},
{
Name: "opts",
OptionalFlag: "...",
Type: PropertyType{
Name: "pulumi.ResourceOption",
Link: "https://pkg.go.dev/github.com/pulumi/pulumi/sdk/go/pulumi?tab=doc#ResourceOption",
},
},
}
}
func (mod *modContext) genConstructorCS(r *schema.Resource, argsOptional bool) []ConstructorParam {
name := resourceName(r)
argsType := name + "Args"
argsSchemaType := &schema.ObjectType{
Token: r.Token,
}
argLangType := mod.typeString(argsSchemaType, "csharp", true, argsOptional, false)
var argsFlag string
var argsDefault string
allOptionalInputs := true
for _, prop := range r.InputProperties {
allOptionalInputs = allOptionalInputs && !prop.IsRequired
}
if allOptionalInputs {
if argsOptional {
// If the number of required input properties was zero, we can make the args object optional.
argsDefault = " = null"
argsType += "?"
argsFlag = "?"
}
optionsType := "CustomResourceOptions"
optionsType := "Pulumi.CustomResourceOptions"
if r.IsProvider {
optionsType = "ResourceOptions"
optionsType = "Pulumi.ResourceOptions"
}
fmt.Fprintf(w, "public %s(string name, %s args%s, %s? options = null)\n", name, argsType, argsDefault, optionsType)
docLangHelper := dotnet.DocLanguageHelper{}
return []ConstructorParam{
{
Name: "name",
Type: PropertyType{
Name: "string",
Link: "https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/builtin-types/built-in-types",
},
},
{
Name: "args",
OptionalFlag: argsFlag,
DefaultValue: argsDefault,
Type: PropertyType{
Name: argsType,
Link: docLangHelper.GetDocLinkForResourceType(mod.pkg.Name, "", argLangType.Name),
},
},
{
Name: "opts",
OptionalFlag: "?",
DefaultValue: " = null",
Type: PropertyType{
Name: optionsType,
Link: docLangHelper.GetDocLinkForResourceType("", "", optionsType),
},
},
}
}
func (mod *modContext) genProperties(w io.Writer, properties []*schema.Property, input bool) {
if len(properties) == 0 {
return
}
fmt.Fprintf(w, "<table class=\"ml-6\">\n")
fmt.Fprintf(w, " <thead>\n")
fmt.Fprintf(w, " <tr>\n")
fmt.Fprintf(w, " <th>Argument</th>\n")
fmt.Fprintf(w, " <th>Type</th>\n")
fmt.Fprintf(w, " <th>Description</th>\n")
fmt.Fprintf(w, " </tr>\n")
fmt.Fprintf(w, " </thead>\n")
fmt.Fprintf(w, " <tbody>\n")
for _, prop := range properties {
var required string
if input {
required = "(Optional) "
if prop.IsRequired {
required = "(Required) "
}
}
// The comment contains markdown, so we must wrap it in our `{{% md %}}`` shortcode, which enables markdown
// to be rendered inside HTML tags (otherwise, Hugo's markdown renderer won't render it as markdown).
// Unfortunately, this injects an extra `<p>...</p>` around the rendered markdown content, which adds some margin
// to the top and bottom of the content which we don't want. So we inject some styles to remove the margins from
// those `p` tags.
fmt.Fprintf(w, " <tr>\n")
fmt.Fprintf(w, " <td class=\"align-top\">%s</td>\n", wbr(prop.Name))
fmt.Fprintf(w, " <td class=\"align-top\"><code>%s</code></td>\n", mod.typeStringPulumi(prop.Type, true))
fmt.Fprintf(w, " <td class=\"align-top\">{{%% md %%}}\n%s%s\n{{%% /md %%}}</td>\n", required, prop.Comment)
fmt.Fprintf(w, " </tr>\n")
}
fmt.Fprintf(w, " </tbody>\n")
fmt.Fprintf(w, "</table>\n\n")
}
func (mod *modContext) genNestedTypes(w io.Writer, properties []*schema.Property, input bool) {
func (mod *modContext) genNestedTypes(properties []*schema.Property, input bool) []DocNestedType {
tokens := stringSet{}
mod.getTypes(properties, tokens)
var objs []*schema.ObjectType
var objs []DocNestedType
for token := range tokens {
for _, t := range mod.pkg.Types {
if obj, ok := t.(*schema.ObjectType); ok && obj.Token == token {
objs = append(objs, obj)
if len(obj.Properties) == 0 {
continue
}
// Create maps to hold the per-language properties of this object and links to
// the API doc fpr each language.
props := make(map[string][]Property)
apiDocLinks := make(map[string]string)
for _, lang := range supportedLanguages {
var docLangHelper codegen.DocLanguageHelper
inputObjLangType := mod.typeString(t, lang, true /*input*/, true /*optional*/, false /*insertWordBreaks*/)
switch lang {
case "csharp":
docLangHelper = dotnet.DocLanguageHelper{}
case "go":
docLangHelper = go_gen.DocLanguageHelper{}
case "nodejs":
docLangHelper = nodejs.DocLanguageHelper{}
case "python":
docLangHelper = python.DocLanguageHelper{}
default:
panic(errors.Errorf("cannot generate nested type doc link for unhandled language %q", lang))
}
apiDocLinks[lang] = docLangHelper.GetDocLinkForInputType(mod.pkg.Name, mod.mod, inputObjLangType.Name)
props[lang] = mod.getProperties(obj.Properties, lang, true)
}
objs = append(objs, DocNestedType{
Name: tokenToName(obj.Token),
APIDocLinks: apiDocLinks,
Properties: props,
})
}
}
}
sort.Slice(objs, func(i, j int) bool {
return tokenToName(objs[i].Token) < tokenToName(objs[j].Token)
return objs[i].Name < objs[j].Name
})
for _, obj := range objs {
fmt.Fprintf(w, "#### %s\n\n", tokenToName(obj.Token))
mod.genProperties(w, obj.Properties, input)
}
return objs
}
func (mod *modContext) genGet(w io.Writer, r *schema.Resource) {
name := resourceName(r)
stateType := name + "State"
var stateParam string
if r.StateInputs != nil {
stateParam = fmt.Sprintf("state?: %s, ", stateType)
// getProperties returns a slice of properties that can be rendered for docs for
// the provided slice of properties in the schema.
func (mod *modContext) getProperties(properties []*schema.Property, lang string, isInput bool) []Property {
if len(properties) == 0 {
return nil
}
fmt.Fprintf(w, "{{< langchoose csharp >}}\n\n")
fmt.Fprintf(w, "```typescript\n")
fmt.Fprintf(w, "public static get(name: string, id: pulumi.Input<pulumi.ID>, %sopts?: pulumi.CustomResourceOptions): %s;\n", stateParam, name)
fmt.Fprintf(w, "```\n\n")
// TODO: This is currently hard coded for Bucket. Need to generalize for all resources.
fmt.Fprintf(w, "```python\n")
fmt.Fprintf(w, "def get(resource_name, id, opts=None, acceleration_status=None, acl=None, arn=None, bucket=None, bucket_domain_name=None, bucket_prefix=None, bucket_regional_domain_name=None, cors_rules=None, force_destroy=None, hosted_zone_id=None, lifecycle_rules=None, loggings=None, object_lock_configuration=None, policy=None, region=None, replication_configuration=None, request_payer=None, server_side_encryption_configuration=None, tags=None, versioning=None, website=None, website_domain=None, website_endpoint=None)\n")
fmt.Fprintf(w, "```\n\n")
// TODO: This is currently hard coded for Bucket. Need to generalize for all resources.
fmt.Fprintf(w, "```go\n")
fmt.Fprintf(w, "func GetBucket(ctx *pulumi.Context, name string, id pulumi.IDInput, state *BucketState, opts ...pulumi.ResourceOption) (*Bucket, error)\n")
fmt.Fprintf(w, "```\n\n")
// TODO: This is currently hard coded for Bucket. Need to generalize for all resources.
fmt.Fprintf(w, "```csharp\n")
fmt.Fprintf(w, "public static Bucket Get(string name, Input<string> id, BucketState? state = null, CustomResourceOptions? options = null);\n")
fmt.Fprintf(w, "```\n\n")
fmt.Fprintf(w, "Get an existing %s resource's state with the given name, ID, and optional extra\n", name)
fmt.Fprintf(w, "properties used to qualify the lookup.\n\n")
for _, lang := range []string{"nodejs", "go", "csharp"} {
fmt.Fprintf(w, "{{%% lang %s %%}}\n", lang)
fmt.Fprintf(w, "<ul class=\"pl-10\">\n")
fmt.Fprintf(w, " <li><strong>name</strong> &ndash; (Required) The unique name of the resulting resource.</li>\n")
fmt.Fprintf(w, " <li><strong>id</strong> &ndash; (Required) The _unique_ provider ID of the resource to lookup.</li>\n")
if stateParam != "" {
fmt.Fprintf(w, " <li><strong>state</strong> &ndash; (Optional) Any extra arguments used during the lookup.</li>\n")
docProperties := make([]Property, 0, len(properties))
for _, prop := range properties {
if prop == nil {
continue
}
fmt.Fprintf(w, " <li><strong>opts</strong> &ndash; (Optional) A bag of options that control this resource's behavior.</li>\n")
fmt.Fprintf(w, "</ul>\n")
fmt.Fprintf(w, "{{%% /lang %%}}\n\n")
docProperties = append(docProperties, Property{
Name: getLanguagePropertyName(prop.Name, lang, true),
Comment: prop.Comment,
DeprecationMessage: prop.DeprecationMessage,
IsRequired: prop.IsRequired,
IsInput: isInput,
Type: mod.typeString(prop.Type, lang, isInput, !prop.IsRequired, true),
})
}
// TODO: Unlike the other languages, Python does not have a separate state object. The state args are all just
// named parameters of the get function. Consider injecting `resource_name`, `id`, and `opts` as the first three
// items in the table of state input properties.
if r.StateInputs != nil {
fmt.Fprintf(w, "The following state arguments are supported:\n\n")
mod.genProperties(w, r.StateInputs.Properties, true)
}
return docProperties
}
func (mod *modContext) genResource(w io.Writer, r *schema.Resource) {
func (mod *modContext) genConstructors(r *schema.Resource, allOptionalInputs bool) map[string]string {
constructorParams := make(map[string]string)
for _, lang := range supportedLanguages {
var (
paramTemplate string
params []ConstructorParam
)
b := &bytes.Buffer{}
switch lang {
case "nodejs":
params = mod.genConstructorTS(r, allOptionalInputs)
paramTemplate = "ts_constructor_param"
case "go":
params = mod.genConstructorGo(r, allOptionalInputs)
paramTemplate = "go_constructor_param"
case "csharp":
params = mod.genConstructorCS(r, allOptionalInputs)
paramTemplate = "csharp_constructor_param"
}
n := len(params)
for i, p := range params {
if err := templates.ExecuteTemplate(b, paramTemplate, p); err != nil {
panic(err)
}
if i != n-1 {
if err := templates.ExecuteTemplate(b, "param_separator", nil); err != nil {
panic(err)
}
}
}
constructorParams[lang] = b.String()
}
return constructorParams
}
// getConstructorResourceInfo returns a map of per-language information about
// the resource being constructed.
func (mod *modContext) getConstructorResourceInfo(resourceTypeName string) map[string]PropertyType {
resourceMap := make(map[string]PropertyType)
resourceDisplayName := resourceTypeName
for _, lang := range supportedLanguages {
// Reset the type name back to the display name.
resourceTypeName = resourceDisplayName
var docLangHelper codegen.DocLanguageHelper
switch lang {
case "nodejs":
docLangHelper = nodejs.DocLanguageHelper{}
case "go":
docLangHelper = go_gen.DocLanguageHelper{}
case "csharp":
docLangHelper = dotnet.DocLanguageHelper{}
resourceTypeName = fmt.Sprintf("Pulumi.%s.%s.%s", strings.Title(mod.pkg.Name), strings.Title(mod.mod), resourceTypeName)
case "python":
// Pulumi's Python language SDK does not have "types" yet, so we will skip it for now.
continue
default:
panic(errors.Errorf("cannot generate constructor info for unhandled language %q", lang))
}
resourceMap[lang] = PropertyType{
Name: resourceDisplayName,
Link: docLangHelper.GetDocLinkForResourceType(mod.pkg.Name, mod.mod, resourceTypeName),
}
}
return resourceMap
}
// genResource is the entrypoint for generating a doc for a resource
// from its Pulumi schema.
func (mod *modContext) genResource(r *schema.Resource) resourceArgs {
// Create a resource module file into which all of this resource's types will go.
name := resourceName(r)
fmt.Fprintf(w, "%s\n\n", r.Comment)
// TODO: Remove this - it's just temporary to include some data we don't have available yet.
mod.genMockupExamples(w, r)
fmt.Fprintf(w, "## Create a %s Resource\n\n", name)
// TODO: In the examples on the page, we only want to show TypeScript and Python tabs for now, as initially
// we'll only have examples in those languages.
// However, lower on the page, we will be showing declarations and types in all of the supported languages.
// The default behavior of the lang chooser is to switch all lang tabs on the page when a tab is selected.
// This means, if Go is selected lower in the page, then the chooser tabs for the examples will try to show
// Go content, which won't be present. We should fix this somehow such that selecting Go lower in the page
// doesn't cause the example tabs to change. But if Python is selected, the example tabs should change since
// Python is available there.
fmt.Fprintf(w, "{{< langchoose csharp >}}\n\n")
fmt.Fprintf(w, "<div class=\"highlight\"><pre class=\"chroma\"><code class=\"language-typescript\" data-lang=\"typescript\">")
mod.genConstructorTS(w, r)
fmt.Fprintf(w, "</code></pre></div>\n\n")
fmt.Fprintf(w, "```python\n")
mod.genConstructorPython(w, r)
fmt.Fprintf(w, "\n```\n\n")
fmt.Fprintf(w, "```go\n")
mod.genConstructorGo(w, r)
fmt.Fprintf(w, "\n```\n\n")
fmt.Fprintf(w, "```csharp\n")
mod.genConstructorCS(w, r)
fmt.Fprintf(w, "\n```\n\n")
fmt.Fprintf(w, "Creates a %s resource with the given unique name, arguments, and options.\n\n", name)
// TODO: Unlike the other languages, Python does not have a separate Args object for inputs.
// The args are all just named parameters of the constructor. Consider injecting
// `resource_name` and `opts` as the first two items in the table of properties.
inputProps := make(map[string][]Property)
outputProps := make(map[string][]Property)
stateInputs := make(map[string][]Property)
for _, lang := range supportedLanguages {
inputProps[lang] = mod.getProperties(r.InputProperties, lang, true)
if r.IsProvider {
continue
}
outputProps[lang] = mod.getProperties(r.Properties, lang, false)
if r.StateInputs != nil {
stateInputs[lang] = mod.getProperties(r.StateInputs.Properties, lang, true)
}
}
allOptionalInputs := true
for _, prop := range r.InputProperties {
allOptionalInputs = allOptionalInputs && !prop.IsRequired
// If at least one prop is required, then break.
if prop.IsRequired {
allOptionalInputs = false
break
}
}
argsRequired := "Required"
if allOptionalInputs {
argsRequired = "Optional"
data := resourceArgs{
Header: Header{
Title: name,
},
Comment: r.Comment,
// TODO: This is just temporary to include some data we don't have available yet.
Examples: mod.getMockupExamples(r),
ConstructorParams: mod.genConstructors(r, allOptionalInputs),
ConstructorResource: mod.getConstructorResourceInfo(name),
ArgsRequired: !allOptionalInputs,
InputProperties: inputProps,
OutputProperties: outputProps,
StateInputs: stateInputs,
StateParam: name + "State",
NestedTypes: mod.genNestedTypes(r.InputProperties, true),
}
for _, lang := range []string{"nodejs", "go", "csharp"} {
fmt.Fprintf(w, "{{%% lang %s %%}}\n", lang)
fmt.Fprintf(w, "<ul class=\"pl-10\">\n")
fmt.Fprintf(w, " <li><strong>name</strong> &ndash; (Required) The unique name of the resulting resource.</li>\n")
fmt.Fprintf(w, " <li><strong>args</strong> &ndash; (%s) The arguments to use to populate this resource's properties.</li>\n", argsRequired)
fmt.Fprintf(w, " <li><strong>opts</strong> &ndash; (Optional) A bag of options that control this resource's behavior.</li>\n")
fmt.Fprintf(w, "</ul>\n")
fmt.Fprintf(w, "{{%% /lang %%}}\n\n")
}
fmt.Fprintf(w, "The following arguments are supported:\n\n")
// TODO: Unlike the other languages, Python does not have a separate Args object. The args are all just
// named parameters of the constructor. Consider injecting `resource_name` and `opts` as the first two items
// in the table of properties.
mod.genProperties(w, r.InputProperties, true)
fmt.Fprintf(w, "## %s Output Properties\n\n", name)
fmt.Fprintf(w, "The following output properties are available:\n\n")
mod.genProperties(w, r.Properties, false)
fmt.Fprintf(w, "## Look up an Existing %s Resource\n\n", name)
mod.genGet(w, r)
fmt.Fprintf(w, "## Import an Existing %s Resource\n\n", name)
// TODO: How do we want to show import? It will take a paragraph or two of explanation plus example, similar
// to the content at https://www.pulumi.com/docs/intro/concepts/programming-model/#import
fmt.Fprintf(w, "TODO\n\n")
fmt.Fprintf(w, "## Support Types\n\n")
mod.genNestedTypes(w, r.InputProperties, true)
return data
}
func (mod *modContext) genFunction(w io.Writer, fun *schema.Function) {
@ -543,12 +657,15 @@ func (mod *modContext) gen(fs fs) error {
// Resources
for _, r := range mod.resources {
data := mod.genResource(r)
title := resourceName(r)
buffer := &bytes.Buffer{}
mod.genHeader(buffer, resourceName(r))
mod.genResource(buffer, r)
addFile(lower(resourceName(r))+".md", buffer.String())
err := templates.ExecuteTemplate(buffer, "resource.tmpl", data)
if err != nil {
panic(err)
}
addFile(lower(title)+".md", buffer.String())
}
// Functions
@ -636,7 +753,23 @@ func (mod *modContext) genIndex(exports []string) string {
return w.String()
}
// GeneratePackage generates the docs package with docs for each resource given the Pulumi
// schema.
func GeneratePackage(tool string, pkg *schema.Package) (map[string][]byte, error) {
templates = template.New("").Funcs(template.FuncMap{
"htmlSafe": func(html string) template.HTML {
// Markdown fragments in the templates need to be rendered as-is,
// so that html/template package doesn't try to inject data into it,
// which will most certainly fail.
// nolint gosec
return template.HTML(html)
},
})
for name, b := range packagedTemplates {
template.Must(templates.New(name).Parse(string(b)))
}
// group resources, types, and functions into modules
modules := map[string]*modContext{}
@ -723,18 +856,13 @@ func GeneratePackage(tool string, pkg *schema.Package) (map[string][]byte, error
}
// TODO: Remove this when we have real examples available.
func (mod *modContext) genMockupExamples(w io.Writer, r *schema.Resource) {
func (mod *modContext) getMockupExamples(r *schema.Resource) []exampleUsage {
if resourceName(r) != "Bucket" {
return
return nil
}
fmt.Fprintf(w, "## Example Usage\n\n")
examples := []struct {
Heading string
Code string
}{
examples := []exampleUsage{
{
Heading: "Private Bucket w/ Tags",
Code: `import * as pulumi from "@pulumi/pulumi";
@ -1003,15 +1131,5 @@ const mybucket = new aws.s3.Bucket("mybucket", {
},
}
for _, example := range examples {
fmt.Fprintf(w, "### %s\n\n", example.Heading)
fmt.Fprintf(w, "{{< langchoose nojavascript nogo >}}\n\n")
fmt.Fprintf(w, "```typescript\n")
fmt.Fprintf(w, example.Code)
fmt.Fprintf(w, "```\n\n")
fmt.Fprintf(w, "```python\nComing soon\n```\n\n")
}
return examples
}

View file

@ -0,0 +1,7 @@
{{ define "constructor_args" }}
<ul class="pl-10">
<li><strong>name</strong> &ndash; (Required) The unique name of the resulting resource.</li>
<li><strong>args</strong> &ndash; {{ if . }} (Required) {{ else }} (Optional) {{ end }} The arguments to use to populate this resource's properties.</li>
<li><strong>opts</strong> &ndash; (Optional) A bag of options that control this resource's behavior.</li>
</ul>
{{ end }}

View file

@ -0,0 +1,9 @@
{{ define "param_separator" }}<span class="p">, </span>{{ end }}
{{ define "go_constructor_param" }}<span class="nx">{{ .Name }}</span> {{ .OptionalFlag }}{{ template "linkify_param" .Type }}{{ end }}
{{ define "ts_constructor_param" }}<span class="nx">{{ .Name }}</span>{{ .OptionalFlag }}: {{ template "linkify_param" .Type }}{{ end }}
{{ define "csharp_constructor_param" }}{{ template "linkify_param" .Type }}{{ .OptionalFlag }} <span class="nx">{{ .Name }}{{ .DefaultValue }}{{ end }}
{{ define "py_param" }}{{ range . }}, {{ htmlSafe .Name }}=None{{ end }}{{ end }}

View file

@ -0,0 +1,25 @@
{{ define "examples" }}
## Example Usage
{{ range . }}
### {{ .Heading }}
{{ htmlSafe "{{< langchoose nojavascript >}}" }}
```typescript
{{ htmlSafe .Code }}
```
```csharp
Coming soon!
```
```go
Coming soon!
```
```python
Coming soon!
```
{{ end }}
{{ end }}

View file

@ -0,0 +1,8 @@
{{ define "header" }}
---
title: "{{ .Title }}"
---
<style>
table td p { margin-top: 0; margin-bottom: 0; }
</style>
{{ end }}

View file

@ -0,0 +1,49 @@
{{ define "propIsRequired" }}
{{ if . }} (Required) {{- else }} (Optional){{- end }}
{{ end }}
{{ define "properties_table" }}
<table class="ml-6">
<thead>
<tr>
<th>Argument</th>
<th>Type</th>
<th>Description</th>
</tr>
</thead>
<tbody>
{{ range . }}
<tr>
<td class="align-top">{{ htmlSafe .Name }}</td>
<td class="align-top">
{{ if eq .Type.Link "#" "" }}
<code>{{ htmlSafe .Type.Name }}</code>
{{- else }}
<code>{{ template "linkify" .Type }}</code>
{{- end }}
</td>
<td class="align-top">
{{- htmlSafe "{{% md %}}" }} {{ if .IsInput }}{{ template "propIsRequired" .IsRequired }}{{- end }} {{- .Comment }} {{ htmlSafe "{{% /md %}}" }}
{{ .DeprecationMessage }}
</td>
</tr>
{{ end }}
</tbody>
</table>
{{ end }}
{{ define "properties" }}
{{ htmlSafe "{{< langchoose csharp nojavascript >}}" }}
{{ range $lang, $props := . }}
{{ print "{{% lang" }} {{ print $lang }} {{ print "%}}" }}
{{ template "properties_table" $props }}
{{ print "{{% /lang %}}" }}
{{ end }}
{{ end }}

View file

@ -0,0 +1,8 @@
{{ define "read_inputs" }}
<ul class="pl-10">
<li><strong>name</strong> &ndash; (Required) The unique name of the resulting resource.</li>
<li><strong>id</strong> &ndash; (Required) The _unique_ provider ID of the resource to lookup.</li>
<li><strong>state</strong> &ndash; (Optional) Any extra arguments used during the lookup.</li>
<li><strong>opts</strong> &ndash; (Optional) A bag of options that control this resource's behavior.</li>
</ul>
{{ end }}

110
pkg/codegen/docs/templates/resource.tmpl vendored Normal file
View file

@ -0,0 +1,110 @@
{{ template "header" .Header }}
{{ htmlSafe .Comment }}
<!-- Examples -->
{{ if ne (len .Examples) 0 }}{{ template "examples" .Examples }}{{ end }}
<!-- Create resource -->
## Create a {{ .Header.Title }} Resource
{{ htmlSafe "{{< langchoose csharp nojavascript >}}" }}
<div class="highlight"><pre class="chroma"><code class="language-typescript" data-lang="typescript"><span class="k">new </span>{{ template "linkify_param" .ConstructorResource.nodejs }}<span class="p">(</span>{{ htmlSafe .ConstructorParams.nodejs }}<span class="p">);</span></code></pre></div>
```python
def {{ .Header.Title}}(resource_name, id, opts=None{{ template "py_param" .InputProperties.python }}, __props__=None)
```
<div class="highlight"><pre class="chroma"><code class="language-go" data-lang="go"><span class="k">func </span>New{{ .Header.Title }}<span class="p">(</span>{{ htmlSafe .ConstructorParams.go }}<span class="p">) (*{{ template "linkify_param" .ConstructorResource.go }}, error)</span></code></pre></div>
<div class="highlight"><pre class="chroma"><code class="language-csharp" data-lang="csharp"><span class="k">public </span>{{ template "linkify_param" .ConstructorResource.csharp }}<span class="p">(</span>{{ htmlSafe .ConstructorParams.csharp }}<span class="p">)</span></code></pre></div>
Creates a {{ .Header.Title }} resource with the given unique name, arguments, and options.
{{ htmlSafe "{{% lang nodejs %}}" }}
{{ template "constructor_args" }}
{{ htmlSafe "{{% /lang %}}" }}
{{ htmlSafe "{{% lang go %}}" }}
{{ template "constructor_args" }}
{{ htmlSafe "{{% /lang %}}" }}
{{ htmlSafe "{{% lang csharp %}}" }}
{{ template "constructor_args" }}
{{ htmlSafe "{{% /lang %}}" }}
The following arguments are supported:
{{ template "properties" .InputProperties }}
<!-- Output properties -->
{{ if ne (len .OutputProperties) 0 }}
## {{.Header.Title}} Output Properties
The following output properties are available:
{{ template "properties" .OutputProperties }}
{{ end }}
<!-- Read resource -->
{{ if ne (len .StateInputs) 0 }}
## Look up an Existing {{.Header.Title}} Resource
{{ htmlSafe "{{< langchoose csharp nojavascript >}}" }}
```typescript
public static get(name: string, id: pulumi.Input<pulumi.ID>, state?: {{ .StateParam }}, opts?: pulumi.CustomResourceOptions): {{ .Header.Title }};
```
```python
def get(resource_name, id, opts=None{{ template "py_param" .StateInputs.python }})
```
```go
func Get{{.Header.Title}}(ctx *pulumi.Context, name string, id pulumi.IDInput, state *{{ .StateParam }}, opts ...pulumi.ResourceOption) (*Bucket, error)
```
```csharp
public static {{ .Header.Title }} Get(string name, Input<string> id, {{ .StateParam }}? state = null, CustomResourceOptions? options = null);
```
Get an existing {{.Header.Title}} resource's state with the given name, ID, and optional extra properties used to qualify the lookup.
{{ htmlSafe "{{% lang nodejs %}}" }}
{{ template "read_inputs" }}
{{ htmlSafe "{{% /lang %}}" }}
{{ htmlSafe "{{% lang go %}}" }}
{{ template "read_inputs" }}
{{ htmlSafe "{{% /lang %}}" }}
{{ htmlSafe "{{% lang csharp %}}" }}
{{ template "read_inputs" }}
{{ htmlSafe "{{% /lang %}}" }}
The following state arguments are supported:
{{ template "properties" .StateInputs }}
{{ end }}
<!-- Supporting types -->
{{ if ne (len .NestedTypes) 0 }}
## Supporting Types
{{ range .NestedTypes }}
#### {{ .Name }}
{{ htmlSafe "{{% lang nodejs %}}" }}
> See the <a href="{{ .APIDocLinks.nodejs }}">input</a> API doc.
{{ htmlSafe "{{% /lang %}}" }}
{{ htmlSafe "{{% lang go %}}" }}
> See the <a href="{{ .APIDocLinks.go }}">input</a> API doc.
{{ htmlSafe "{{% /lang %}}" }}
{{ htmlSafe "{{% lang csharp %}}" }}
> See the <a href="{{ .APIDocLinks.csharp }}">input</a> API doc.
{{ htmlSafe "{{% /lang %}}" }}
{{ template "properties" .Properties }}
{{ end }}
{{ end }}

3
pkg/codegen/docs/templates/utils.tmpl vendored Normal file
View file

@ -0,0 +1,3 @@
{{ define "linkify_param" }}<span class="nx"><a href="{{ .Link }}">{{ .Name }}</a></span>{{ end }}
{{ define "linkify" }}<a href="{{ .Link }}">{{ htmlSafe .Name }}</a>{{ end }}

82
pkg/codegen/docs/utils.go Normal file
View file

@ -0,0 +1,82 @@
// Copyright 2016-2020, Pulumi Corporation.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// Pulling out some of the repeated strings tokens into constants would harm readability, so we just ignore the
// goconst linter's warning.
//
// nolint: lll, goconst
package docs
import (
"strings"
"unicode"
"github.com/pulumi/pulumi/pkg/codegen/python"
"github.com/pulumi/pulumi/pkg/util/contract"
)
func isDotNetTypeNameBoundary(prev rune, next rune) bool {
// For C# type names, which are PascalCase are qualified using "." as the separator.
return prev == rune('.') && unicode.IsUpper(next)
}
func isPythonTypeNameBoundary(prev rune, next rune) bool {
// For Python, names are snake_cased (Duh?).
return (prev == rune('_') && unicode.IsLower(next))
}
// wbr inserts HTML <wbr> in between case changes, e.g. "fooBar" becomes "foo<wbr>Bar".
func wbr(s string) string {
var runes []rune
var prev rune
for i, r := range s {
if i != 0 &&
// For TS, JS and Go, property names are camelCase and types are PascalCase.
((unicode.IsLower(prev) && unicode.IsUpper(r)) ||
isDotNetTypeNameBoundary(prev, r) ||
isPythonTypeNameBoundary(prev, r)) {
runes = append(runes, []rune("<wbr>")...)
}
runes = append(runes, r)
prev = r
}
return string(runes)
}
func lower(s string) string {
return strings.ToLower(s)
}
// tokenToName returns the resource name from a Pulumi token.
func tokenToName(tok string) string {
components := strings.Split(tok, ":")
contract.Assertf(len(components) == 3, "malformed token %v", tok)
return strings.Title(components[2])
}
// getLanguagePropertyName returns a language-specific representation of a given
// property name.
func getLanguagePropertyName(name string, lang string, insertWordBreak bool) string {
switch lang {
case "python":
name = python.PyName(name)
case "go", "csharp":
name = strings.Title(name)
}
if !insertWordBreak {
return name
}
return wbr(name)
}

54
pkg/codegen/dotnet/doc.go Normal file
View file

@ -0,0 +1,54 @@
// Copyright 2016-2020, Pulumi Corporation.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// nolint: lll
package dotnet
import (
"fmt"
"strings"
"github.com/pulumi/pulumi/pkg/codegen"
"github.com/pulumi/pulumi/pkg/codegen/schema"
)
// DocLanguageHelper is the DotNet-specific implementation of the DocLanguageHelper.
type DocLanguageHelper struct{}
var _ codegen.DocLanguageHelper = DocLanguageHelper{}
// GetDocLinkForResourceType returns the .NET API doc URL for a type belonging to a resource provider.
func (d DocLanguageHelper) GetDocLinkForResourceType(packageName, _, typeName string) string {
typeName = strings.ReplaceAll(typeName, "?", "")
var packageNamespace string
if packageName != "" {
packageNamespace = "." + title(packageName)
}
return fmt.Sprintf("/docs/reference/pkg/dotnet/Pulumi%s/%s.html", packageNamespace, typeName)
}
// GetDocLinkForInputType is not implemented at this time for Python.
func (d DocLanguageHelper) GetDocLinkForInputType(packageName, moduleName, typeName string) string {
return d.GetDocLinkForResourceType(packageName, moduleName, typeName)
}
// GetLanguageTypeString returns the DotNet-specific type given a Pulumi schema type.
func (d DocLanguageHelper) GetLanguageTypeString(pkg *schema.Package, moduleName string, t schema.Type, input, optional bool) string {
typeDetails := map[*schema.ObjectType]*typeDetails{}
mod := &modContext{
pkg: pkg,
typeDetails: typeDetails,
}
return mod.typeString(t, "", input, false /*state*/, false /*wrapInput*/, true /*requireInitializers*/, optional)
}

59
pkg/codegen/go/doc.go Normal file
View file

@ -0,0 +1,59 @@
// Copyright 2016-2020, Pulumi Corporation.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// Pulling out some of the repeated strings tokens into constants would harm readability, so we just ignore the
// goconst linter's warning.
//
// nolint: lll, goconst
package gen
import (
"fmt"
"strings"
"github.com/pulumi/pulumi/pkg/codegen"
"github.com/pulumi/pulumi/pkg/codegen/schema"
)
// DocLanguageHelper is the Go-specific implementation of the DocLanguageHelper.
type DocLanguageHelper struct{}
var _ codegen.DocLanguageHelper = DocLanguageHelper{}
// GetDocLinkForResourceType returns the godoc URL for a type belonging to a resource provider.
func (d DocLanguageHelper) GetDocLinkForResourceType(packageName string, moduleName string, typeName string) string {
path := fmt.Sprintf("%s/%s", packageName, moduleName)
typeNameParts := strings.Split(typeName, ".")
typeName = typeNameParts[len(typeNameParts)-1]
return fmt.Sprintf("https://pkg.go.dev/github.com/pulumi/pulumi-%s/sdk/go/%s?tab=doc#%s", packageName, path, typeName)
}
// GetDocLinkForInputType returns the godoc URL for an input type.
func (d DocLanguageHelper) GetDocLinkForInputType(packageName, moduleName, typeName string) string {
name := d.GetDocLinkForResourceType(packageName, moduleName, typeName)
return name + "Args"
}
// GetDocLinkForBuiltInType returns the godoc URL for a built-in type.
func GetDocLinkForBuiltInType(typeName string) string {
return fmt.Sprintf("https://golang.org/pkg/builtin/#%s", typeName)
}
// GetLanguageTypeString returns the Go-specific type given a Pulumi schema type.
func (d DocLanguageHelper) GetLanguageTypeString(pkg *schema.Package, moduleName string, t schema.Type, input, optional bool) string {
mod := &pkgContext{
pkg: pkg,
}
return mod.plainType(t, optional)
}

73
pkg/codegen/nodejs/doc.go Normal file
View file

@ -0,0 +1,73 @@
// Copyright 2016-2020, Pulumi Corporation.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// Pulling out some of the repeated strings tokens into constants would harm readability, so we just ignore the
// goconst linter's warning.
//
// nolint: lll, goconst
package nodejs
import (
"fmt"
"strings"
"github.com/pulumi/pulumi/pkg/codegen"
"github.com/pulumi/pulumi/pkg/codegen/schema"
)
// DocLanguageHelper is the NodeJS-specific implementation of the DocLanguageHelper.
type DocLanguageHelper struct{}
var _ codegen.DocLanguageHelper = DocLanguageHelper{}
// GetDocLinkForResourceType returns the NodeJS API doc for a type belonging to a resource provider.
func (d DocLanguageHelper) GetDocLinkForResourceType(packageName, modName, typeName string) string {
path := fmt.Sprintf("%s/%s", packageName, modName)
typeName = strings.ReplaceAll(typeName, "?", "")
return fmt.Sprintf("/docs/reference/pkg/nodejs/pulumi/%s/#%s", path, typeName)
}
// GetDocLinkForInputType returns the doc link for an input type.
func (d DocLanguageHelper) GetDocLinkForInputType(packageName, modName, typeName string) string {
typeName = strings.ReplaceAll(typeName, "?", "")
return fmt.Sprintf("/docs/reference/pkg/nodejs/pulumi/%s/types/input/#%s", packageName, typeName)
}
// GetDocLinkForBuiltInType returns the URL for a built-in type.
func GetDocLinkForBuiltInType(typeName string) string {
return fmt.Sprintf("https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/%s", typeName)
}
// GetLanguageTypeString returns the language-specific type given a Pulumi schema type.
func (d DocLanguageHelper) GetLanguageTypeString(pkg *schema.Package, moduleName string, t schema.Type, input, optional bool) string {
modCtx := &modContext{
pkg: pkg,
mod: moduleName,
}
typeName := modCtx.typeString(t, input, false, optional)
// Remove any package qualifiers from the type name.
typeQualifierPackage := "inputs"
if !input {
typeQualifierPackage = "outputs"
}
typeName = strings.ReplaceAll(typeName, typeQualifierPackage+".", "")
// Remove the union with `undefined` for optional types,
// since we will show that information separately anyway.
if optional {
typeName = strings.ReplaceAll(typeName, " | undefined", "?")
}
return typeName
}

91
pkg/codegen/python/doc.go Normal file
View file

@ -0,0 +1,91 @@
// Copyright 2016-2018, Pulumi Corporation.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// Pulling out some of the repeated strings tokens into constants would harm readability,
// so we just ignore the goconst linter's warning.
//
// nolint: lll, goconst
package python
import (
"fmt"
"strings"
"github.com/pulumi/pulumi/pkg/codegen"
"github.com/pulumi/pulumi/pkg/codegen/schema"
)
// DocLanguageHelper is the Python-specific implementation of the DocLanguageHelper.
type DocLanguageHelper struct{}
var _ codegen.DocLanguageHelper = DocLanguageHelper{}
// GetDocLinkForResourceType is not implemented at this time for Python.
func (d DocLanguageHelper) GetDocLinkForResourceType(packageName, modName, typeName string) string {
return ""
}
// GetDocLinkForInputType is not implemented at this time for Python.
func (d DocLanguageHelper) GetDocLinkForInputType(packageName, modName, typeName string) string {
return ""
}
// GetLanguageTypeString returns the Python-specific type given a Pulumi schema type.
func (d DocLanguageHelper) GetLanguageTypeString(pkg *schema.Package, moduleName string, t schema.Type, input, optional bool) string {
name := pyType(t)
// The Python language generator will simply return
// "list" or "dict" for certain enumerables. Once the generator
// is updated with "types", the following code block ideally
// wouldn't run anymore.
switch name {
case "list":
arrTy := t.(*schema.ArrayType)
elType := arrTy.ElementType.String()
return getListWithTypeName(elementTypeToName(elType))
case "dict":
switch dictionaryTy := t.(type) {
case *schema.MapType:
elType := dictionaryTy.ElementType.String()
return getDictWithTypeName(elementTypeToName(elType))
case *schema.ObjectType:
return getDictWithTypeName(tokenToName(dictionaryTy.Token))
}
}
return name
}
// elementTypeToName returns the type name from an element type of the form
// package:module:_type, with its leading "_" stripped.
func elementTypeToName(el string) string {
parts := strings.Split(el, ":")
if len(parts) == 3 {
el = parts[2]
}
el = strings.TrimPrefix(el, "_")
return el
}
// getListWithTypeName returns a Python representation of a list containing
// items of `t`.
func getListWithTypeName(t string) string {
return fmt.Sprintf("list[%s]", PyName(t))
}
// getDictWithTypeName returns the Python representation of a dictionary
// where each item is of type `t`.
func getDictWithTypeName(t string) string {
return fmt.Sprintf("dict{%s}", PyName(t))
}