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:
Praneet Loke 2020-03-20 08:17:58 -07:00 committed by GitHub
parent 5ecf24ee81
commit 861d568eb2
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 351 additions and 45 deletions

View file

@ -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 {

View 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)
}
})
}
}

View file

@ -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>

View file

@ -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

View file

@ -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 {