Skip to content

Commit

Permalink
Merge #10738
Browse files Browse the repository at this point in the history
10738: Go and Python codegen support symbols with hyphens in their names r=iwahbe a=mattolenik

# Description

This change enables the fix for pulumi/crd2pulumi#43, wherein hyphens `-` were mishandled and led to generation of invalid Go and Python. For example, code such as `func My-Thing() {}` or `class My-Thing`.

This change improves the existing casing functions and extracts them to a new subpackage, `cgstrings`.

Resolves pulumi/crd2pulumi#43. When this change is merged `go get -u` will fix the crd2pulumi issue.

## Checklist

<!--- Please provide details if the checkbox below is to be left unchecked. -->
- [ ] I have added tests that prove my fix is effective or that my feature works


Co-authored-by: Matthew Olenik <molenik@pulumi.com>
Co-authored-by: Ian Wahbe <ian@wahbe.com>
  • Loading branch information
3 people committed Oct 11, 2022
2 parents 145488b + 3630497 commit 9f1115c
Show file tree
Hide file tree
Showing 27 changed files with 1,351 additions and 44 deletions.
63 changes: 63 additions & 0 deletions pkg/codegen/cgstrings/cgstrings.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
// package cgstrings has various string processing functions that are useful during code generation.
package cgstrings

import (
"strings"
"unicode"
)

// Unhyphenate removes all hyphens from s, then uppercasing the letter following each hyphen.
// For example, "abc-def-ghi" becomes "abcDefGhi".
func Unhyphenate(str string) string {
return ModifyStringAroundDelimeter(str, "-", UppercaseFirst)
}

// Camel converts s to camelCase.
func Camel(s string) string {
if s == "" {
return ""
}
s = Unhyphenate(s)
runes := []rune(s)
res := make([]rune, 0, len(runes))
for i, r := range runes {
if unicode.IsLower(r) {
res = append(res, runes[i:]...)
break
}
res = append(res, unicode.ToLower(r))
}
return string(res)
}

// UppercaseFirst uppercases the first letter of s.
// E.g. "abc" -> "Abc"
func UppercaseFirst(s string) string {
if s == "" {
return ""
}
runes := []rune(s)
runes[0] = unicode.ToUpper(runes[0])
return string(runes)
}

func ModifyStringAroundDelimeter(str, delim string, modifyNext func(next string) string) string {
if delim == "" {
return str
}
i := strings.Index(str, delim)
if i < 0 {
return str
}
nextIdx := i + len(delim)
if nextIdx >= len(str) {
// Nothing left after the delimeter, it's at the end of the string.
return str[:len(str)-len(delim)]
}
prev := str[:nextIdx-1]
next := str[nextIdx:]
if next != "" {
next = modifyNext(next)
}
return prev + ModifyStringAroundDelimeter(next, delim, modifyNext)
}
46 changes: 46 additions & 0 deletions pkg/codegen/cgstrings/cgstrings_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package cgstrings

import (
"fmt"
"testing"

"github.com/stretchr/testify/assert"
)

func TestCamel(t *testing.T) {
t.Parallel()
assert := assert.New(t)

assert.Equal("", Camel(""))
assert.Equal("plugh", Camel("plugh"))
assert.Equal("waldoThudFred", Camel("WaldoThudFred"))
assert.Equal("graultBaz", Camel("Grault-Baz"))
assert.Equal("graultBaz", Camel("grault-baz"))
assert.Equal("graultBaz", Camel("graultBaz"))
assert.Equal("grault_Baz", Camel("Grault_Baz"))
assert.Equal("graultBaz", Camel("Grault-baz"))
}

func TestUnhyphenate(t *testing.T) {
t.Parallel()
testcases := []struct {
input, expected string
}{
{"", ""},
{"waldo", "waldo"},
{"waldo-thud-fred", "waldoThudFred"},
{"waldo-Thud-Fred", "waldoThudFred"},
{"waldo-Thud-Fred-", "waldoThudFred"},
{"-waldo-Thud-Fred", "WaldoThudFred"},
{"waldoThudFred", "waldoThudFred"},
{"WaldoThudFred", "WaldoThudFred"},
}
for _, tc := range testcases {
tc := tc
t.Run(fmt.Sprintf("Subtest:%q", tc.input), func(t *testing.T) {
t.Parallel()
assert := assert.New(t)
assert.Equal(tc.expected, Unhyphenate(tc.input))
})
}
}
72 changes: 29 additions & 43 deletions pkg/codegen/go/gen.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,9 @@ import (
"strconv"
"strings"
"sync"
"unicode"

"github.com/pulumi/pulumi/pkg/v3/codegen"
"github.com/pulumi/pulumi/pkg/v3/codegen/cgstrings"
"github.com/pulumi/pulumi/pkg/v3/codegen/schema"
"github.com/pulumi/pulumi/sdk/v3/go/common/diag"
"github.com/pulumi/pulumi/sdk/v3/go/common/resource/plugin"
Expand Down Expand Up @@ -93,24 +93,9 @@ func Title(s string) string {
if s[0] == '$' {
return Title(s[1:])
}
runes := []rune(s)
return string(append([]rune{unicode.ToUpper(runes[0])}, runes[1:]...))
}

func camel(s string) string {
if s == "" {
return ""
}
runes := []rune(s)
res := make([]rune, 0, len(runes))
for i, r := range runes {
if unicode.IsLower(r) {
res = append(res, runes[i:]...)
break
}
res = append(res, unicode.ToLower(r))
}
return string(res)
s = cgstrings.UppercaseFirst(s)
s = cgstrings.Unhyphenate(s)
return s
}

func tokenToPackage(pkg *schema.Package, overrides map[string]string, tok string) string {
Expand Down Expand Up @@ -1216,7 +1201,8 @@ func (pkg *pkgContext) genEnumOutputTypes(w io.Writer, name, elementArgsType, el
func (pkg *pkgContext) genEnumInputTypes(w io.Writer, name string, enumType *schema.EnumType, elementGoType string) {
pkg.genInputInterface(w, name)

fmt.Fprintf(w, "var %sPtrType = reflect.TypeOf((**%s)(nil)).Elem()\n", camel(name), name)
typeName := cgstrings.Camel(name)
fmt.Fprintf(w, "var %sPtrType = reflect.TypeOf((**%s)(nil)).Elem()\n", typeName, name)
fmt.Fprintln(w)

fmt.Fprintf(w, "type %sPtrInput interface {\n", name)
Expand All @@ -1226,25 +1212,25 @@ func (pkg *pkgContext) genEnumInputTypes(w io.Writer, name string, enumType *sch
fmt.Fprintf(w, "}\n")
fmt.Fprintln(w)

fmt.Fprintf(w, "type %sPtr %s\n", camel(name), elementGoType)
fmt.Fprintf(w, "type %sPtr %s\n", typeName, elementGoType)
fmt.Fprintln(w)

fmt.Fprintf(w, "func %[1]sPtr(v %[2]s) %[1]sPtrInput {\n", name, elementGoType)
fmt.Fprintf(w, "return (*%sPtr)(&v)\n", camel(name))
fmt.Fprintf(w, "return (*%sPtr)(&v)\n", typeName)
fmt.Fprintf(w, "}\n")
fmt.Fprintln(w)

fmt.Fprintf(w, "func (*%sPtr) ElementType() reflect.Type {\n", camel(name))
fmt.Fprintf(w, "return %sPtrType\n", camel(name))
fmt.Fprintf(w, "func (*%sPtr) ElementType() reflect.Type {\n", typeName)
fmt.Fprintf(w, "return %sPtrType\n", typeName)
fmt.Fprintf(w, "}\n")
fmt.Fprintln(w)

fmt.Fprintf(w, "func (in *%[1]sPtr) To%[2]sPtrOutput() %[2]sPtrOutput {\n", camel(name), name)
fmt.Fprintf(w, "func (in *%[1]sPtr) To%[2]sPtrOutput() %[2]sPtrOutput {\n", typeName, name)
fmt.Fprintf(w, "return pulumi.ToOutput(in).(%sPtrOutput)\n", name)
fmt.Fprintf(w, "}\n")
fmt.Fprintln(w)

fmt.Fprintf(w, "func (in *%[1]sPtr) To%[2]sPtrOutputWithContext(ctx context.Context) %[2]sPtrOutput {\n", camel(name), name)
fmt.Fprintf(w, "func (in *%[1]sPtr) To%[2]sPtrOutputWithContext(ctx context.Context) %[2]sPtrOutput {\n", cgstrings.Camel(name), name)
fmt.Fprintf(w, "return pulumi.ToOutputWithContext(ctx, in).(%sPtrOutput)\n", name)
fmt.Fprintf(w, "}\n")
fmt.Fprintln(w)
Expand Down Expand Up @@ -1311,7 +1297,7 @@ func (pkg *pkgContext) assignProperty(w io.Writer, p *schema.Property, object, v
}
fmt.Fprintf(w, "\t%s.%s = %s\n", object, Title(p.Name), value)
} else if indirectAssign {
tmpName := camel(p.Name) + "_"
tmpName := cgstrings.Camel(p.Name) + "_"
fmt.Fprintf(w, "%s := %s\n", tmpName, value)
fmt.Fprintf(w, "%s.%s = &%s\n", object, Title(p.Name), tmpName)
} else {
Expand Down Expand Up @@ -1417,7 +1403,7 @@ func (pkg *pkgContext) genInputTypes(w io.Writer, t *schema.ObjectType, details
if details.ptrInput {
pkg.genInputInterface(w, name+"Ptr")

ptrTypeName := camel(name) + "PtrType"
ptrTypeName := cgstrings.Camel(name) + "PtrType"

fmt.Fprintf(w, "type %s %sArgs\n\n", ptrTypeName, name)

Expand Down Expand Up @@ -1827,7 +1813,7 @@ func (pkg *pkgContext) genResource(w io.Writer, r *schema.Resource, generateReso

// Emit the state types for get methods.
fmt.Fprintf(w, "// Input properties used for looking up and filtering %s resources.\n", name)
fmt.Fprintf(w, "type %sState struct {\n", camel(name))
fmt.Fprintf(w, "type %sState struct {\n", cgstrings.Camel(name))
if r.StateInputs != nil {
for _, p := range r.StateInputs.Properties {
printCommentWithDeprecationMessage(w, p.Comment, p.DeprecationMessage, true)
Expand All @@ -1846,12 +1832,12 @@ func (pkg *pkgContext) genResource(w io.Writer, r *schema.Resource, generateReso
fmt.Fprintf(w, "}\n\n")

fmt.Fprintf(w, "func (%sState) ElementType() reflect.Type {\n", name)
fmt.Fprintf(w, "\treturn reflect.TypeOf((*%sState)(nil)).Elem()\n", camel(name))
fmt.Fprintf(w, "\treturn reflect.TypeOf((*%sState)(nil)).Elem()\n", cgstrings.Camel(name))
fmt.Fprintf(w, "}\n\n")
}

// Emit the args types.
fmt.Fprintf(w, "type %sArgs struct {\n", camel(name))
fmt.Fprintf(w, "type %sArgs struct {\n", cgstrings.Camel(name))
for _, p := range r.InputProperties {
printCommentWithDeprecationMessage(w, p.Comment, p.DeprecationMessage, true)
fmt.Fprintf(w, "\t%s %s `pulumi:\"%s\"`\n", Title(p.Name), pkg.typeString(codegen.ResolvedType(p.Type)), p.Name)
Expand All @@ -1877,7 +1863,7 @@ func (pkg *pkgContext) genResource(w io.Writer, r *schema.Resource, generateReso
fmt.Fprintf(w, "}\n\n")

fmt.Fprintf(w, "func (%sArgs) ElementType() reflect.Type {\n", name)
fmt.Fprintf(w, "\treturn reflect.TypeOf((*%sArgs)(nil)).Elem()\n", camel(name))
fmt.Fprintf(w, "\treturn reflect.TypeOf((*%sArgs)(nil)).Elem()\n", cgstrings.Camel(name))
fmt.Fprintf(w, "}\n")

// Emit resource methods.
Expand Down Expand Up @@ -1929,7 +1915,7 @@ func (pkg *pkgContext) genResource(w io.Writer, r *schema.Resource, generateReso
outputsType := "pulumi.AnyOutput"
if f.Outputs != nil {
if shouldLiftReturn {
outputsType = fmt.Sprintf("%s%sResultOutput", camel(name), methodName)
outputsType = fmt.Sprintf("%s%sResultOutput", cgstrings.Camel(name), methodName)
} else {
outputsType = fmt.Sprintf("%s%sResultOutput", name, methodName)
}
Expand All @@ -1944,7 +1930,7 @@ func (pkg *pkgContext) genResource(w io.Writer, r *schema.Resource, generateReso
fmt.Fprintf(w, "\t}\n")

// Get the name of the method to return the output
fmt.Fprintf(w, "\treturn %s.(%s).%s(), nil\n", resultVar, camel(outputsType), Title(f.Outputs.Properties[0].Name))
fmt.Fprintf(w, "\treturn %s.(%s).%s(), nil\n", resultVar, cgstrings.Camel(outputsType), Title(f.Outputs.Properties[0].Name))
} else {
// Check the error before proceeding.
fmt.Fprintf(w, "\tif err != nil {\n")
Expand All @@ -1959,7 +1945,7 @@ func (pkg *pkgContext) genResource(w io.Writer, r *schema.Resource, generateReso
// If there are argument and/or return types, emit them.
if len(args) > 0 {
fmt.Fprintf(w, "\n")
fmt.Fprintf(w, "type %s%sArgs struct {\n", camel(name), methodName)
fmt.Fprintf(w, "type %s%sArgs struct {\n", cgstrings.Camel(name), methodName)
for _, p := range args {
printCommentWithDeprecationMessage(w, p.Comment, p.DeprecationMessage, true)
fmt.Fprintf(w, "\t%s %s `pulumi:\"%s\"`\n", Title(p.Name), pkg.typeString(codegen.ResolvedType(p.Type)),
Expand All @@ -1976,15 +1962,15 @@ func (pkg *pkgContext) genResource(w io.Writer, r *schema.Resource, generateReso
fmt.Fprintf(w, "}\n\n")

fmt.Fprintf(w, "func (%s%sArgs) ElementType() reflect.Type {\n", name, methodName)
fmt.Fprintf(w, "\treturn reflect.TypeOf((*%s%sArgs)(nil)).Elem()\n", camel(name), methodName)
fmt.Fprintf(w, "\treturn reflect.TypeOf((*%s%sArgs)(nil)).Elem()\n", cgstrings.Camel(name), methodName)
fmt.Fprintf(w, "}\n\n")
}
if f.Outputs != nil {
outputStructName := name

// Don't export the result struct if we're lifting the value
if shouldLiftReturn {
outputStructName = camel(name)
outputStructName = cgstrings.Camel(name)
}

fmt.Fprintf(w, "\n")
Expand Down Expand Up @@ -2652,7 +2638,7 @@ func (pkg *pkgContext) genResourceRegistrations(w io.Writer, r *schema.Resource,
for _, method := range r.Methods {
if method.Function.Outputs != nil {
if pkg.liftSingleValueMethodReturns && len(method.Function.Outputs.Properties) == 1 {
fmt.Fprintf(w, "\tpulumi.RegisterOutputType(%s%sResultOutput{})\n", camel(name), Title(method.Name))
fmt.Fprintf(w, "\tpulumi.RegisterOutputType(%s%sResultOutput{})\n", cgstrings.Camel(name), Title(method.Name))
} else {
fmt.Fprintf(w, "\tpulumi.RegisterOutputType(%s%sResultOutput{})\n", name, Title(method.Name))
}
Expand Down Expand Up @@ -2873,7 +2859,7 @@ func (pkg *pkgContext) genConfig(w io.Writer, variables []*schema.Property) erro
}

printCommentWithDeprecationMessage(w, p.Comment, p.DeprecationMessage, false)
configKey := fmt.Sprintf("\"%s:%s\"", pkg.pkg.Name, camel(p.Name))
configKey := fmt.Sprintf("\"%s:%s\"", pkg.pkg.Name, cgstrings.Camel(p.Name))

fmt.Fprintf(w, "func Get%s(ctx *pulumi.Context) %s {\n", Title(p.Name), getType)
if p.DefaultValue != nil {
Expand Down Expand Up @@ -3230,11 +3216,11 @@ func generatePackageContextMap(tool string, pkg *schema.Package, goInfo GoPackag
names = append(names, rawResourceName(r)+suffix+"Input")
names = append(names, rawResourceName(r)+suffix+"Output")
names = append(names, rawResourceName(r)+suffix+"Args")
names = append(names, camel(rawResourceName(r))+suffix+"Args")
names = append(names, cgstrings.Camel(rawResourceName(r))+suffix+"Args")
names = append(names, "New"+rawResourceName(r)+suffix)
if !r.IsProvider && !r.IsComponent {
names = append(names, rawResourceName(r)+suffix+"State")
names = append(names, camel(rawResourceName(r))+suffix+"State")
names = append(names, cgstrings.Camel(rawResourceName(r))+suffix+"State")
names = append(names, "Get"+rawResourceName(r)+suffix)
}
return names
Expand Down Expand Up @@ -3644,7 +3630,7 @@ func GeneratePackage(tool string, pkg *schema.Package) (map[string][]byte, error
return nil, err
}

setFile(path.Join(mod, camel(rawResourceName(r))+".go"), buffer.String())
setFile(path.Join(mod, cgstrings.Camel(rawResourceName(r))+".go"), buffer.String())
}

// Functions
Expand All @@ -3654,7 +3640,7 @@ func GeneratePackage(tool string, pkg *schema.Package) (map[string][]byte, error
continue
}

fileName := path.Join(mod, camel(tokenToName(f.Token))+".go")
fileName := path.Join(mod, cgstrings.Camel(tokenToName(f.Token))+".go")
code, err := pkg.genFunctionCodeFile(f)
if err != nil {
return nil, err
Expand Down
13 changes: 13 additions & 0 deletions pkg/codegen/go/gen_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -403,3 +403,16 @@ import (
}
assert.Truef(t, found, `Didn't find a line that complies with "%v"`, autogenerated)
}
func TestTitle(t *testing.T) {
t.Parallel()
assert := assert.New(t)

assert.Equal("", Title(""))
assert.Equal("Plugh", Title("plugh"))
assert.Equal("WaldoThudFred", Title("WaldoThudFred"))
assert.Equal("WaldoThudFred", Title("waldoThudFred"))
assert.Equal("WaldoThudFred", Title("waldo-Thud-Fred"))
assert.Equal("WaldoThudFred", Title("waldo-ThudFred"))
assert.Equal("WaldoThud_Fred", Title("waldo-Thud_Fred"))
assert.Equal("WaldoThud_Fred", Title("waldo-thud_Fred"))
}
20 changes: 19 additions & 1 deletion pkg/codegen/go/utilities.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import (
"unicode"

"github.com/pulumi/pulumi/pkg/v3/codegen"
"github.com/pulumi/pulumi/pkg/v3/codegen/cgstrings"
)

// isReservedWord returns true if s is a Go reserved word as per
Expand Down Expand Up @@ -87,7 +88,8 @@ func makeSafeEnumName(name, typeName string) (string, error) {
}

// Capitalize and make a valid identifier.
safeName = makeValidIdentifier(Title(safeName))
safeName = enumTitle(safeName)
safeName = makeValidIdentifier(safeName)

// If there are multiple underscores in a row, replace with one.
regex := regexp.MustCompile(`_+`)
Expand All @@ -102,3 +104,19 @@ func makeSafeEnumName(name, typeName string) (string, error) {

return safeName, nil
}

// Title converts the input string to a title case
// where only the initial letter is upper-cased.
// It also removes $-prefix if any.
func enumTitle(s string) string {
if s == "" {
return ""
}
if s[0] == '$' {
return Title(s[1:])
}
s = cgstrings.UppercaseFirst(s)
return cgstrings.ModifyStringAroundDelimeter(s, "-", func(next string) string {
return "_" + cgstrings.UppercaseFirst(next)
})
}

0 comments on commit 9f1115c

Please sign in to comment.