[codegen/nodejs] SDK generator fixes. (#4618)

- Add support for constant values in unions (e.g. `"foo" | "bar"`)
- Generate `Input<T | U>` instead of `Input<T> | Input<U>`
- Emit deprecation messages for resources
- Handle nil state inputs
- Allow packages to provide a README
- Remove sync invoke support

These changes are part of
https://github.com/pulumi/pulumi-terraform-bridge/issues/179, and
restore functional parity with the current `tfgen` generator.
This commit is contained in:
Pat Gavlin 2020-05-13 10:55:37 -07:00 committed by GitHub
parent 54f5a0ac36
commit 99852bc18e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 108 additions and 46 deletions

View file

@ -79,7 +79,7 @@ func (d DocLanguageHelper) GetLanguageTypeString(pkg *schema.Package, moduleName
pkg: pkg, pkg: pkg,
mod: moduleName, mod: moduleName,
} }
typeName := modCtx.typeString(t, input, false, optional) typeName := modCtx.typeString(t, input, false, optional, nil)
// Remove any package qualifiers from the type name. // Remove any package qualifiers from the type name.
typeQualifierPackage := "inputs" typeQualifierPackage := "inputs"

View file

@ -87,6 +87,9 @@ type modContext struct {
typeDetails map[*schema.ObjectType]*typeDetails typeDetails map[*schema.ObjectType]*typeDetails
children []*modContext children []*modContext
tool string tool string
// Name overrides set in NodeJSInfo
modToPkg map[string]string // Module name -> package name
} }
func (mod *modContext) details(t *schema.ObjectType) *typeDetails { func (mod *modContext) details(t *schema.ObjectType) *typeDetails {
@ -109,6 +112,9 @@ func (mod *modContext) tokenToType(tok string, input bool) string {
contract.Assertf(len(components) == 3, "malformed token %v", tok) contract.Assertf(len(components) == 3, "malformed token %v", tok)
modName, name := mod.pkg.TokenToModule(tok), title(components[2]) modName, name := mod.pkg.TokenToModule(tok), title(components[2])
if override, ok := mod.modToPkg[modName]; ok {
modName = override
}
root := "outputs." root := "outputs."
if input { if input {
@ -139,13 +145,13 @@ func tokenToFunctionName(tok string) string {
return camel(tokenToName(tok)) return camel(tokenToName(tok))
} }
func (mod *modContext) typeString(t schema.Type, input, wrapInput, optional bool) string { func (mod *modContext) typeString(t schema.Type, input, wrapInput, optional bool, constValue interface{}) string {
var typ string var typ string
switch t := t.(type) { switch t := t.(type) {
case *schema.ArrayType: case *schema.ArrayType:
typ = mod.typeString(t.ElementType, input, wrapInput, false) + "[]" typ = mod.typeString(t.ElementType, input, wrapInput, false, constValue) + "[]"
case *schema.MapType: case *schema.MapType:
typ = fmt.Sprintf("{[key: string]: %v}", mod.typeString(t.ElementType, input, wrapInput, false)) typ = fmt.Sprintf("{[key: string]: %v}", mod.typeString(t.ElementType, input, wrapInput, false, constValue))
case *schema.ObjectType: case *schema.ObjectType:
typ = mod.tokenToType(t.Token, input) typ = mod.tokenToType(t.Token, input)
case *schema.TokenType: case *schema.TokenType:
@ -153,9 +159,15 @@ func (mod *modContext) typeString(t schema.Type, input, wrapInput, optional bool
case *schema.UnionType: case *schema.UnionType:
var elements []string var elements []string
for _, e := range t.ElementTypes { for _, e := range t.ElementTypes {
elements = append(elements, mod.typeString(e, input, wrapInput, false)) t := mod.typeString(e, input, wrapInput, false, constValue)
if wrapInput && strings.HasPrefix(t, "pulumi.Input<") {
contract.Assert(t[len(t)-1] == '>')
// Strip off the leading `pulumi.Input<` and the trailing `>`
t = t[len("pulumi.Input<") : len(t)-1]
} }
return strings.Join(elements, " | ") elements = append(elements, t)
}
typ = strings.Join(elements, " | ")
default: default:
switch t { switch t {
case schema.BoolType: case schema.BoolType:
@ -179,6 +191,9 @@ func (mod *modContext) typeString(t schema.Type, input, wrapInput, optional bool
if optional { if optional {
return typ + " | undefined" return typ + " | undefined"
} }
if constValue != nil && typ == "string" {
typ = constValue.(string)
}
return typ return typ
} }
@ -239,7 +254,7 @@ func (mod *modContext) genPlainType(w io.Writer, name, comment string, propertie
sigil = "?" sigil = "?"
} }
fmt.Fprintf(w, "%s %s%s%s: %s;\n", indent, prefix, p.Name, sigil, mod.typeString(p.Type, input, wrapInput, false)) fmt.Fprintf(w, "%s %s%s%s: %s;\n", indent, prefix, p.Name, sigil, mod.typeString(p.Type, input, wrapInput, false, nil))
} }
fmt.Fprintf(w, "%s}\n", indent) fmt.Fprintf(w, "%s}\n", indent)
} }
@ -269,6 +284,13 @@ func tsPrimitiveValue(value interface{}) (string, error) {
} }
} }
func (mod *modContext) getConstValue(cv interface{}) (string, error) {
if cv == nil {
return "", nil
}
return tsPrimitiveValue(cv)
}
func (mod *modContext) getDefaultValue(dv *schema.DefaultValue, t schema.Type) (string, error) { func (mod *modContext) getDefaultValue(dv *schema.DefaultValue, t schema.Type) (string, error) {
var val string var val string
if dv.Value != nil { if dv.Value != nil {
@ -339,7 +361,7 @@ func (mod *modContext) genResource(w io.Writer, r *schema.Resource) error {
name := resourceName(r) name := resourceName(r)
// Write the TypeDoc/JSDoc for the resource class // Write the TypeDoc/JSDoc for the resource class
printComment(w, codegen.StripNonRelevantExamples(r.Comment, "typescript"), "", "") printComment(w, codegen.StripNonRelevantExamples(r.Comment, "typescript"), r.DeprecationMessage, "")
baseType := "CustomResource" baseType := "CustomResource"
if r.IsProvider { if r.IsProvider {
@ -361,7 +383,7 @@ func (mod *modContext) genResource(w io.Writer, r *schema.Resource) error {
fmt.Fprintf(w, " * @param state Any extra arguments used during the lookup.\n") fmt.Fprintf(w, " * @param state Any extra arguments used during the lookup.\n")
fmt.Fprintf(w, " */\n") fmt.Fprintf(w, " */\n")
stateParam, stateRef := "", "undefined" stateParam, stateRef := "", "undefined, "
if r.StateInputs != nil { if r.StateInputs != nil {
stateParam, stateRef = fmt.Sprintf("state?: %s, ", stateType), "<any>state, " stateParam, stateRef = fmt.Sprintf("state?: %s, ", stateType), "<any>state, "
} }
@ -412,7 +434,7 @@ func (mod *modContext) genResource(w io.Writer, r *schema.Resource) error {
outcomment = "/*out*/ " outcomment = "/*out*/ "
} }
fmt.Fprintf(w, " public %sreadonly %s!: pulumi.Output<%s>;\n", outcomment, prop.Name, mod.typeString(prop.Type, false, false, !prop.IsRequired)) fmt.Fprintf(w, " public %sreadonly %s!: pulumi.Output<%s>;\n", outcomment, prop.Name, mod.typeString(prop.Type, false, false, !prop.IsRequired, nil))
} }
fmt.Fprintf(w, "\n") fmt.Fprintf(w, "\n")
@ -438,6 +460,9 @@ func (mod *modContext) genResource(w io.Writer, r *schema.Resource) error {
if r.IsProvider { if r.IsProvider {
trailingBrace, optionsType = " {", "ResourceOptions" trailingBrace, optionsType = " {", "ResourceOptions"
} }
if r.StateInputs == nil {
trailingBrace = " {"
}
if r.DeprecationMessage != "" { if r.DeprecationMessage != "" {
fmt.Fprintf(w, " /** @deprecated %s */\n", r.DeprecationMessage) fmt.Fprintf(w, " /** @deprecated %s */\n", r.DeprecationMessage)
@ -446,18 +471,21 @@ func (mod *modContext) genResource(w io.Writer, r *schema.Resource) error {
optionsType, trailingBrace) optionsType, trailingBrace)
if !r.IsProvider { if !r.IsProvider {
if r.StateInputs != nil {
if r.DeprecationMessage != "" { if r.DeprecationMessage != "" {
fmt.Fprintf(w, " /** @deprecated %s */\n", r.DeprecationMessage) fmt.Fprintf(w, " /** @deprecated %s */\n", r.DeprecationMessage)
} }
// Now write out a general purpose constructor implementation that can handle the public signautre as well as the // Now write out a general purpose constructor implementation that can handle the public signature as well as the
// signature to support construction via `.get`. And then emit the body preamble which will pluck out the // signature to support construction via `.get`. And then emit the body preamble which will pluck out the
// conditional state into sensible variables using dynamic type tests. // conditional state into sensible variables using dynamic type tests.
fmt.Fprintf(w, " constructor(name: string, argsOrState?: %s | %s, opts?: pulumi.CustomResourceOptions) {\n", fmt.Fprintf(w, " constructor(name: string, argsOrState?: %s | %s, opts?: pulumi.CustomResourceOptions) {\n",
argsType, stateType) argsType, stateType)
}
if r.DeprecationMessage != "" { if r.DeprecationMessage != "" {
fmt.Fprintf(w, " pulumi.log.warn(\"%s is deprecated: %s\")\n", name, r.DeprecationMessage) fmt.Fprintf(w, " pulumi.log.warn(\"%s is deprecated: %s\")\n", name, r.DeprecationMessage)
} }
fmt.Fprintf(w, " let inputs: pulumi.Inputs = {};\n") fmt.Fprintf(w, " let inputs: pulumi.Inputs = {};\n")
if r.StateInputs != nil {
// The lookup case: // The lookup case:
fmt.Fprintf(w, " if (opts && opts.id) {\n") fmt.Fprintf(w, " if (opts && opts.id) {\n")
fmt.Fprintf(w, " const state = argsOrState as %[1]s | undefined;\n", stateType) fmt.Fprintf(w, " const state = argsOrState as %[1]s | undefined;\n", stateType)
@ -467,10 +495,13 @@ func (mod *modContext) genResource(w io.Writer, r *schema.Resource) error {
// The creation case (with args): // The creation case (with args):
fmt.Fprintf(w, " } else {\n") fmt.Fprintf(w, " } else {\n")
fmt.Fprintf(w, " const args = argsOrState as %s | undefined;\n", argsType) fmt.Fprintf(w, " const args = argsOrState as %s | undefined;\n", argsType)
}
} else { } else {
fmt.Fprintf(w, " let inputs: pulumi.Inputs = {};\n") fmt.Fprintf(w, " let inputs: pulumi.Inputs = {};\n")
if r.StateInputs != nil {
fmt.Fprintf(w, " {\n") fmt.Fprintf(w, " {\n")
} }
}
for _, prop := range r.InputProperties { for _, prop := range r.InputProperties {
if prop.IsRequired { if prop.IsRequired {
fmt.Fprintf(w, " if (!args || args.%s === undefined) {\n", prop.Name) fmt.Fprintf(w, " if (!args || args.%s === undefined) {\n", prop.Name)
@ -480,6 +511,19 @@ func (mod *modContext) genResource(w io.Writer, r *schema.Resource) error {
} }
for _, prop := range r.InputProperties { for _, prop := range r.InputProperties {
arg := fmt.Sprintf("args ? args.%[1]s : undefined", prop.Name) arg := fmt.Sprintf("args ? args.%[1]s : undefined", prop.Name)
prefix := " "
if r.StateInputs == nil {
prefix = " "
}
if prop.ConstValue != nil {
cv, err := mod.getConstValue(prop.ConstValue)
if err != nil {
return err
}
arg = cv
} else {
if prop.DefaultValue != nil { if prop.DefaultValue != nil {
dv, err := mod.getDefaultValue(prop.DefaultValue, prop.Type) dv, err := mod.getDefaultValue(prop.DefaultValue, prop.Type)
if err != nil { if err != nil {
@ -490,17 +534,23 @@ func (mod *modContext) genResource(w io.Writer, r *schema.Resource) error {
// provider properties must be marshaled as JSON strings. // provider properties must be marshaled as JSON strings.
if r.IsProvider && !isStringType(prop.Type) { if r.IsProvider && !isStringType(prop.Type) {
arg = fmt.Sprintf("pulumi.output(%s).apply(JSON.stringify)\n", arg) arg = fmt.Sprintf("pulumi.output(%s).apply(JSON.stringify)", arg)
} }
}
fmt.Fprintf(w, " inputs[\"%s\"] = %s;\n", prop.Name, arg) fmt.Fprintf(w, "%sinputs[\"%s\"] = %s;\n", prefix, prop.Name, arg)
} }
for _, prop := range r.Properties { for _, prop := range r.Properties {
prefix := " "
if r.StateInputs == nil {
prefix = " "
}
if !ins.has(prop.Name) { if !ins.has(prop.Name) {
fmt.Fprintf(w, " inputs[\"%s\"] = undefined /*out*/;\n", prop.Name) fmt.Fprintf(w, "%sinputs[\"%s\"] = undefined /*out*/;\n", prefix, prop.Name)
} }
} }
if r.StateInputs != nil {
fmt.Fprintf(w, " }\n") fmt.Fprintf(w, " }\n")
}
// If the caller didn't request a specific version, supply one using the version of this library. // If the caller didn't request a specific version, supply one using the version of this library.
fmt.Fprintf(w, " if (!opts) {\n") fmt.Fprintf(w, " if (!opts) {\n")
@ -577,7 +627,7 @@ func (mod *modContext) genFunction(w io.Writer, fun *schema.Function) {
} else { } else {
retty = title(name) + "Result" retty = title(name) + "Result"
} }
fmt.Fprintf(w, "export function %[1]s(%[2]sopts?: pulumi.InvokeOptions): Promise<%[3]s> & %[3]s {\n", name, argsig, retty) fmt.Fprintf(w, "export function %s(%sopts?: pulumi.InvokeOptions): Promise<%s> {\n", name, argsig, retty)
if fun.DeprecationMessage != "" { if fun.DeprecationMessage != "" {
fmt.Fprintf(w, " pulumi.log.warn(\"%s is deprecated: %s\")\n", name, fun.DeprecationMessage) fmt.Fprintf(w, " pulumi.log.warn(\"%s is deprecated: %s\")\n", name, fun.DeprecationMessage)
} }
@ -597,7 +647,7 @@ func (mod *modContext) genFunction(w io.Writer, fun *schema.Function) {
fmt.Fprintf(w, " }\n") fmt.Fprintf(w, " }\n")
// Now simply invoke the runtime function with the arguments, returning the results. // Now simply invoke the runtime function with the arguments, returning the results.
fmt.Fprintf(w, " const promise: Promise<%s> = pulumi.runtime.invoke(\"%s\", {\n", retty, fun.Token) fmt.Fprintf(w, " return pulumi.runtime.invoke(\"%s\", {\n", fun.Token)
if fun.Inputs != nil { if fun.Inputs != nil {
for _, p := range fun.Inputs.Properties { for _, p := range fun.Inputs.Properties {
// Pass the argument to the invocation. // Pass the argument to the invocation.
@ -605,8 +655,6 @@ func (mod *modContext) genFunction(w io.Writer, fun *schema.Function) {
} }
} }
fmt.Fprintf(w, " }, opts);\n") fmt.Fprintf(w, " }, opts);\n")
fmt.Fprintf(w, "\n")
fmt.Fprintf(w, " return pulumi.utils.liftProperties(promise, opts);\n")
fmt.Fprintf(w, "}\n") fmt.Fprintf(w, "}\n")
// If there are argument and/or return types, emit them. // If there are argument and/or return types, emit them.
@ -766,7 +814,7 @@ func (mod *modContext) genConfig(w io.Writer, variables []*schema.Property) erro
getfunc := "get" getfunc := "get"
if p.Type != schema.StringType { if p.Type != schema.StringType {
// Only try to parse a JSON object if the config isn't a straight string. // Only try to parse a JSON object if the config isn't a straight string.
getfunc = fmt.Sprintf("getObject<%s>", mod.typeString(p.Type, false, false, false)) getfunc = fmt.Sprintf("getObject<%s>", mod.typeString(p.Type, false, false, false, nil))
} }
printComment(w, p.Comment, "", "") printComment(w, p.Comment, "", "")
@ -781,7 +829,7 @@ func (mod *modContext) genConfig(w io.Writer, variables []*schema.Property) erro
} }
fmt.Fprintf(w, "export let %s: %s = %s;\n", fmt.Fprintf(w, "export let %s: %s = %s;\n",
p.Name, mod.typeString(p.Type, false, false, true), configFetch) p.Name, mod.typeString(p.Type, false, false, true, nil), configFetch)
} }
return nil return nil
@ -914,10 +962,22 @@ func (mod *modContext) gen(fs fs) error {
} }
// Ensure that the target module directory contains a README.md file. // Ensure that the target module directory contains a README.md file.
readme := mod.pkg.Description readme := mod.pkg.Language["nodejs"].(NodePackageInfo).Readme
if readme == "" {
readme = mod.pkg.Description
if readme != "" && readme[len(readme)-1] != '\n' { if readme != "" && readme[len(readme)-1] != '\n' {
readme += "\n" readme += "\n"
} }
if mod.pkg.Attribution != "" {
if len(readme) != 0 {
readme += "\n"
}
readme += mod.pkg.Attribution
}
if readme != "" && readme[len(readme)-1] != '\n' {
readme += "\n"
}
}
fs.add(path.Join(mod.mod, "README.md"), []byte(readme)) fs.add(path.Join(mod.mod, "README.md"), []byte(readme))
// Utilities, config // Utilities, config

View file

@ -26,6 +26,8 @@ type NodePackageInfo struct {
PackageName string `json:"packageName,omitempty"` PackageName string `json:"packageName,omitempty"`
// Description for the NPM package. // Description for the NPM package.
PackageDescription string `json:"packageDescription,omitempty"` PackageDescription string `json:"packageDescription,omitempty"`
// Readme contains the text for the package's README.md files.
Readme string `json:"readme,omityempty"`
// NPM dependencies to add to package.json. // NPM dependencies to add to package.json.
Dependencies map[string]string `json:"dependencies,omitempty"` Dependencies map[string]string `json:"dependencies,omitempty"`
// NPM dev-dependencies to add to package.json. // NPM dev-dependencies to add to package.json.