Add test for resource docs to confirm Python casing (#4126)
* Generate Go package maps at the beginning, so that the resource docs can use the appropriate package context for generating a property type string name.
This commit is contained in:
parent
5ecf24ee81
commit
861d568eb2
|
@ -22,6 +22,7 @@ package docs
|
|||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"html"
|
||||
"html/template"
|
||||
|
@ -59,13 +60,13 @@ func init() {
|
|||
for _, lang := range supportedLanguages {
|
||||
switch lang {
|
||||
case "csharp":
|
||||
docHelpers[lang] = dotnet.DocLanguageHelper{}
|
||||
docHelpers[lang] = &dotnet.DocLanguageHelper{}
|
||||
case "go":
|
||||
docHelpers[lang] = go_gen.DocLanguageHelper{}
|
||||
docHelpers[lang] = &go_gen.DocLanguageHelper{}
|
||||
case "nodejs":
|
||||
docHelpers[lang] = nodejs.DocLanguageHelper{}
|
||||
docHelpers[lang] = &nodejs.DocLanguageHelper{}
|
||||
case "python":
|
||||
docHelpers[lang] = python.DocLanguageHelper{}
|
||||
docHelpers[lang] = &python.DocLanguageHelper{}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -80,6 +81,8 @@ type header struct {
|
|||
|
||||
// property represents an input or an output property.
|
||||
type property struct {
|
||||
// DisplayName is the property name with word-breaks.
|
||||
DisplayName string
|
||||
Name string
|
||||
Comment string
|
||||
Type propertyType
|
||||
|
@ -517,7 +520,8 @@ func (mod *modContext) getProperties(properties []*schema.Property, lang string,
|
|||
}
|
||||
|
||||
docProperties = append(docProperties, property{
|
||||
Name: wbr(propLangName),
|
||||
DisplayName: wbr(propLangName),
|
||||
Name: propLangName,
|
||||
Comment: prop.Comment,
|
||||
DeprecationMessage: prop.DeprecationMessage,
|
||||
IsRequired: prop.IsRequired,
|
||||
|
@ -1050,27 +1054,8 @@ 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)
|
||||
},
|
||||
"pyName": func(str string) string {
|
||||
return python.PyName(str)
|
||||
},
|
||||
})
|
||||
|
||||
for name, b := range packagedTemplates {
|
||||
template.Must(templates.New(name).Parse(string(b)))
|
||||
}
|
||||
|
||||
// group resources, types, and functions into modules
|
||||
func generateModulesFromSchemaPackage(tool string, pkg *schema.Package) map[string]*modContext {
|
||||
// Group resources, types, and functions into modules.
|
||||
modules := map[string]*modContext{}
|
||||
|
||||
var getMod func(token string) *modContext
|
||||
|
@ -1098,9 +1083,19 @@ func GeneratePackage(tool string, pkg *schema.Package) (map[string][]byte, error
|
|||
return mod
|
||||
}
|
||||
|
||||
goLangHelper := getLanguageDocHelper("go").(*go_gen.DocLanguageHelper)
|
||||
var goInfo go_gen.GoInfo
|
||||
if golang, ok := pkg.Language["go"]; ok {
|
||||
if err := json.Unmarshal(golang, &goInfo); err != nil {
|
||||
panic(fmt.Errorf("decoding go package info: %v", err))
|
||||
}
|
||||
}
|
||||
// Generate the Go package map info now, so we can use that to get the type string
|
||||
// names later.
|
||||
goLangHelper.GeneratePackagesMap(pkg, tool, goInfo)
|
||||
|
||||
pyLangHelper := getLanguageDocHelper("python").(*python.DocLanguageHelper)
|
||||
types := &modContext{pkg: pkg, mod: "types", tool: tool}
|
||||
docHelper := getLanguageDocHelper("python")
|
||||
pyHelper := docHelper.(python.DocLanguageHelper)
|
||||
|
||||
for _, v := range pkg.Config {
|
||||
visitObjectTypes(v.Type, func(t *schema.ObjectType) { types.details(t).outputType = true })
|
||||
|
@ -1111,13 +1106,13 @@ func GeneratePackage(tool string, pkg *schema.Package) (map[string][]byte, error
|
|||
mod.resources = append(mod.resources, r)
|
||||
|
||||
for _, p := range r.Properties {
|
||||
pyHelper.GenPropertyCaseMap(mod.pkg, mod.mod, tool, p, snakeCaseToCamelCase, camelCaseToSnakeCase)
|
||||
pyLangHelper.GenPropertyCaseMap(mod.pkg, mod.mod, tool, p, snakeCaseToCamelCase, camelCaseToSnakeCase)
|
||||
|
||||
visitObjectTypes(p.Type, func(t *schema.ObjectType) { types.details(t).outputType = true })
|
||||
}
|
||||
|
||||
for _, p := range r.InputProperties {
|
||||
pyHelper.GenPropertyCaseMap(mod.pkg, mod.mod, tool, p, snakeCaseToCamelCase, camelCaseToSnakeCase)
|
||||
pyLangHelper.GenPropertyCaseMap(mod.pkg, mod.mod, tool, p, snakeCaseToCamelCase, camelCaseToSnakeCase)
|
||||
|
||||
visitObjectTypes(p.Type, func(t *schema.ObjectType) {
|
||||
if r.IsProvider {
|
||||
|
@ -1153,7 +1148,32 @@ func GeneratePackage(tool string, pkg *schema.Package) (map[string][]byte, error
|
|||
})
|
||||
}
|
||||
}
|
||||
return modules
|
||||
}
|
||||
|
||||
// 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)
|
||||
},
|
||||
"pyName": func(str string) string {
|
||||
return python.PyName(str)
|
||||
},
|
||||
})
|
||||
|
||||
for name, b := range packagedTemplates {
|
||||
template.Must(templates.New(name).Parse(string(b)))
|
||||
}
|
||||
|
||||
// Generate the modules from the schema, and for every module
|
||||
// run the generator functions to generate markdown files.
|
||||
modules := generateModulesFromSchemaPackage(tool, pkg)
|
||||
files := fs{}
|
||||
for _, mod := range modules {
|
||||
if err := mod.gen(files); err != nil {
|
||||
|
|
275
pkg/codegen/docs/gen_test.go
Normal file
275
pkg/codegen/docs/gen_test.go
Normal file
|
@ -0,0 +1,275 @@
|
|||
// 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 (
|
||||
"encoding/json"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/pulumi/pulumi/pkg/codegen/python"
|
||||
"github.com/pulumi/pulumi/pkg/codegen/schema"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
const (
|
||||
unitTestTool = "Pulumi Resource Docs Unit Test"
|
||||
providerPackage = "prov"
|
||||
)
|
||||
|
||||
var (
|
||||
simpleProperties = map[string]schema.PropertySpec{
|
||||
"stringProp": {
|
||||
Description: "A string prop.",
|
||||
TypeSpec: schema.TypeSpec{
|
||||
Type: "string",
|
||||
},
|
||||
},
|
||||
"boolProp": {
|
||||
Description: "A bool prop.",
|
||||
TypeSpec: schema.TypeSpec{
|
||||
Type: "boolean",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// testPackageSpec represents a fake package spec for a Provider used for testing.
|
||||
testPackageSpec schema.PackageSpec
|
||||
)
|
||||
|
||||
func initTestPackageSpec(t *testing.T) {
|
||||
t.Helper()
|
||||
|
||||
pythonMapCase := map[string]json.RawMessage{
|
||||
"python": json.RawMessage(`{"mapCase":false}`),
|
||||
}
|
||||
testPackageSpec = schema.PackageSpec{
|
||||
Name: providerPackage,
|
||||
Description: "A fake provider package used for testing.",
|
||||
Meta: &schema.MetadataSpec{
|
||||
ModuleFormat: "(.*)(?:/[^/]*)",
|
||||
},
|
||||
Types: map[string]schema.ObjectTypeSpec{
|
||||
// Package-level types.
|
||||
"prov:/getPackageResourceOptions:getPackageResourceOptions": {
|
||||
Description: "Options object for the package-level function getPackageResource.",
|
||||
Type: "object",
|
||||
Properties: simpleProperties,
|
||||
},
|
||||
|
||||
// Module-level types.
|
||||
"prov:module/getModuleResourceOptions:getModuleResourceOptions": {
|
||||
Description: "Options object for the module-level function getModuleResource.",
|
||||
Type: "object",
|
||||
Properties: simpleProperties,
|
||||
},
|
||||
"prov:module/ResourceOptions:ResourceOptions": {
|
||||
Description: "The resource options object.",
|
||||
Type: "object",
|
||||
Properties: map[string]schema.PropertySpec{
|
||||
"stringProp": {
|
||||
Description: "A string prop.",
|
||||
Language: pythonMapCase,
|
||||
TypeSpec: schema.TypeSpec{
|
||||
Type: "string",
|
||||
},
|
||||
},
|
||||
"boolProp": {
|
||||
Description: "A bool prop.",
|
||||
Language: pythonMapCase,
|
||||
TypeSpec: schema.TypeSpec{
|
||||
Type: "boolean",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
"prov:module/ResourceOptions2:ResourceOptions2": {
|
||||
Description: "The resource options object.",
|
||||
Type: "object",
|
||||
Properties: map[string]schema.PropertySpec{
|
||||
"uniqueProp": {
|
||||
Description: "This is a property unique to this type.",
|
||||
Language: pythonMapCase,
|
||||
TypeSpec: schema.TypeSpec{
|
||||
Type: "number",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
Resources: map[string]schema.ResourceSpec{
|
||||
"prov:module/resource:Resource": {
|
||||
InputProperties: map[string]schema.PropertySpec{
|
||||
"integerProp": {
|
||||
Description: "This is integerProp's description.",
|
||||
TypeSpec: schema.TypeSpec{
|
||||
Type: "integer",
|
||||
},
|
||||
},
|
||||
"stringProp": {
|
||||
Description: "This is stringProp's description.",
|
||||
TypeSpec: schema.TypeSpec{
|
||||
Type: "string",
|
||||
},
|
||||
},
|
||||
"boolProp": {
|
||||
Description: "A bool prop.",
|
||||
TypeSpec: schema.TypeSpec{
|
||||
Type: "boolean",
|
||||
},
|
||||
},
|
||||
"optionsProp": {
|
||||
TypeSpec: schema.TypeSpec{
|
||||
Ref: "#/types/prov:module/ResourceOptions:ResourceOptions",
|
||||
},
|
||||
},
|
||||
"options2Prop": {
|
||||
TypeSpec: schema.TypeSpec{
|
||||
Ref: "#/types/prov:module/ResourceOptions2:ResourceOptions2",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
Functions: map[string]schema.FunctionSpec{
|
||||
// Package-level Functions.
|
||||
"prov:/getPackageResource:getPackageResource": {
|
||||
Description: "A package-level function.",
|
||||
Inputs: &schema.ObjectTypeSpec{
|
||||
Description: "Inputs for getPackageResource.",
|
||||
Type: "object",
|
||||
Properties: map[string]schema.PropertySpec{
|
||||
"options": {
|
||||
TypeSpec: schema.TypeSpec{
|
||||
Ref: "#/types/prov:/getPackageResourceOptions:getPackageResourceOptions",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
Outputs: &schema.ObjectTypeSpec{
|
||||
Description: "Outputs for getPackageResource.",
|
||||
Properties: simpleProperties,
|
||||
Type: "object",
|
||||
},
|
||||
},
|
||||
|
||||
// Module-level Functions.
|
||||
"prov:module/getModuleResource:getModuleResource": {
|
||||
Description: "A module-level function.",
|
||||
Inputs: &schema.ObjectTypeSpec{
|
||||
Description: "Inputs for getModuleResource.",
|
||||
Type: "object",
|
||||
Properties: map[string]schema.PropertySpec{
|
||||
"options": {
|
||||
TypeSpec: schema.TypeSpec{
|
||||
Ref: "#/types/prov:module/getModuleResource:getModuleResource",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
Outputs: &schema.ObjectTypeSpec{
|
||||
Description: "Outputs for getModuleResource.",
|
||||
Properties: simpleProperties,
|
||||
Type: "object",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// TestResourceNestedPropertyPythonCasing tests that the properties
|
||||
// of a nested object have the expected casing.
|
||||
func TestResourceNestedPropertyPythonCasing(t *testing.T) {
|
||||
initTestPackageSpec(t)
|
||||
|
||||
schemaPkg, err := schema.ImportSpec(testPackageSpec)
|
||||
assert.NoError(t, err, "importing spec")
|
||||
|
||||
modules := generateModulesFromSchemaPackage(unitTestTool, schemaPkg)
|
||||
mod := modules["module"]
|
||||
for _, r := range mod.resources {
|
||||
nestedTypes := mod.genNestedTypes(r, true)
|
||||
if len(nestedTypes) == 0 {
|
||||
t.Error("did not find any nested types")
|
||||
return
|
||||
}
|
||||
|
||||
t.Run("InputPropertiesAreSnakeCased", func(t *testing.T) {
|
||||
props := mod.getProperties(r.InputProperties, "python", true, false)
|
||||
for _, p := range props {
|
||||
assert.True(t, strings.Contains(p.Name, "_"), "input property name in python must use snake_case")
|
||||
}
|
||||
})
|
||||
|
||||
// Non-unique nested properties are ones that have names that occur as direct input properties
|
||||
// of the resource or elsewhere in the package and are mapped as snake_case even if the property
|
||||
// itself has a "Language" spec with the `MapCase` value of `false`.
|
||||
t.Run("NonUniqueNestedProperties", func(t *testing.T) {
|
||||
n := nestedTypes[0]
|
||||
assert.Equal(t, "ResourceOptions", n.Name, "got %v instead of ResourceOptions", n.Name)
|
||||
|
||||
pyProps := n.Properties["python"]
|
||||
nestedObject, ok := testPackageSpec.Types["prov:module/ResourceOptions:ResourceOptions"]
|
||||
if !ok {
|
||||
t.Error("sample schema package spec does not contain known object type")
|
||||
return
|
||||
}
|
||||
|
||||
for name := range nestedObject.Properties {
|
||||
found := false
|
||||
pyName := python.PyName(name)
|
||||
for _, prop := range pyProps {
|
||||
if prop.Name == pyName {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
assert.True(t, found, "expected to find %q", pyName)
|
||||
}
|
||||
})
|
||||
|
||||
// Unique nested properties are those that only appear inside a nested object and therefore
|
||||
// are never mapped to their snake_case. Therefore, such properties must be rendered with a
|
||||
// camelCase.
|
||||
t.Run("UniqueNestedProperties", func(t *testing.T) {
|
||||
n := nestedTypes[1]
|
||||
assert.Equal(t, "ResourceOptions2", n.Name, "got %v instead of ResourceOptions2", n.Name)
|
||||
|
||||
pyProps := n.Properties["python"]
|
||||
nestedObject, ok := testPackageSpec.Types["prov:module/ResourceOptions2:ResourceOptions2"]
|
||||
if !ok {
|
||||
t.Error("sample schema package spec does not contain known object type")
|
||||
return
|
||||
}
|
||||
|
||||
for name := range nestedObject.Properties {
|
||||
found := false
|
||||
for _, prop := range pyProps {
|
||||
if prop.Name == name {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
assert.True(t, found, "expected to find %q", name)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
2
pkg/codegen/docs/templates/properties.tmpl
vendored
2
pkg/codegen/docs/templates/properties.tmpl
vendored
|
@ -14,7 +14,7 @@
|
|||
<tbody>
|
||||
{{ range . }}
|
||||
<tr>
|
||||
<td class="align-top">{{ htmlSafe .Name }}</td>
|
||||
<td class="align-top">{{ htmlSafe .DisplayName }}</td>
|
||||
<td class="align-top">
|
||||
{{ if eq .Type.Link "#" "" }}
|
||||
<code>{{ htmlSafe .Type.Name }}</code>
|
||||
|
|
|
@ -27,7 +27,9 @@ import (
|
|||
)
|
||||
|
||||
// DocLanguageHelper is the Go-specific implementation of the DocLanguageHelper.
|
||||
type DocLanguageHelper struct{}
|
||||
type DocLanguageHelper struct {
|
||||
packages map[string]*pkgContext
|
||||
}
|
||||
|
||||
var _ codegen.DocLanguageHelper = DocLanguageHelper{}
|
||||
|
||||
|
@ -74,10 +76,13 @@ func GetDocLinkForBuiltInType(typeName string) string {
|
|||
|
||||
// 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)
|
||||
modPkg := d.packages[moduleName]
|
||||
return modPkg.plainType(t, optional)
|
||||
}
|
||||
|
||||
// GeneratePackagesMap generates a map of Go packages for resources, functions and types.
|
||||
func (d *DocLanguageHelper) GeneratePackagesMap(pkg *schema.Package, tool string, goInfo GoInfo) {
|
||||
d.packages = generatePackageContextMap(tool, pkg, goInfo)
|
||||
}
|
||||
|
||||
// GetResourceFunctionResultName returns the name of the result type when a function is used to lookup
|
||||
|
|
|
@ -955,15 +955,8 @@ type GoInfo struct {
|
|||
PackageImportAliases map[string]string `json:"packageImportAliases,omitempty"`
|
||||
}
|
||||
|
||||
func GeneratePackage(tool string, pkg *schema.Package) (map[string][]byte, error) {
|
||||
var goInfo GoInfo
|
||||
if golang, ok := pkg.Language["go"]; ok {
|
||||
if err := json.Unmarshal(golang, &goInfo); err != nil {
|
||||
return nil, errors.Wrap(err, "decoding go package info")
|
||||
}
|
||||
}
|
||||
|
||||
// group resources, types, and functions into Go packages
|
||||
// generatePackageContextMap groups resources, types, and functions into Go packages.
|
||||
func generatePackageContextMap(tool string, pkg *schema.Package, goInfo GoInfo) map[string]*pkgContext {
|
||||
packages := map[string]*pkgContext{}
|
||||
getPkg := func(token string) *pkgContext {
|
||||
mod := pkg.TokenToModule(token)
|
||||
|
@ -1067,6 +1060,19 @@ func GeneratePackage(tool string, pkg *schema.Package) (map[string][]byte, error
|
|||
}
|
||||
}
|
||||
|
||||
return packages
|
||||
}
|
||||
|
||||
func GeneratePackage(tool string, pkg *schema.Package) (map[string][]byte, error) {
|
||||
var goInfo GoInfo
|
||||
if golang, ok := pkg.Language["go"]; ok {
|
||||
if err := json.Unmarshal(golang, &goInfo); err != nil {
|
||||
return nil, errors.Wrap(err, "decoding go package info")
|
||||
}
|
||||
}
|
||||
|
||||
packages := generatePackageContextMap(tool, pkg, goInfo)
|
||||
|
||||
// emit each package
|
||||
var pkgMods []string
|
||||
for mod := range packages {
|
||||
|
|
Loading…
Reference in a new issue