pulumi/pkg/codegen/dotnet/gen.go
Praneet Loke 7b17463031
[codegen/docs] Fix the generation of Function names, args, and result type names (#4490)
* Add a new DocLangHelper interface method to generate function names per language. Use the functionNames override map in Go to correctly generate a function name.

* Add kong to the title lookup map.

* Check if item was found in map.

* Use title case for the nodejs Function args type name. Use title case for the function name in the index page.

* Also fix the result name for a Function in nodejs to use title case.

* Use the module name to lookup the package.

* Use the title-case name for the Function name in the index page. Fix the language value passed to the lang chooser in function.tmpl.

* Use title case for nested type titles.

* Add title lookup for GitHub.
2020-04-27 17:47:01 -07:00

1373 lines
38 KiB
Go

// 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 dotnet
import (
"bytes"
"fmt"
"io"
"io/ioutil"
"net/http"
"path"
"path/filepath"
"reflect"
"strconv"
"strings"
"unicode"
"github.com/pkg/errors"
"github.com/pulumi/pulumi/pkg/v2/codegen"
"github.com/pulumi/pulumi/pkg/v2/codegen/schema"
"github.com/pulumi/pulumi/sdk/v2/go/common/util/contract"
)
type stringSet map[string]struct{}
func (ss stringSet) add(s string) {
ss[s] = struct{}{}
}
func (ss stringSet) has(s string) bool {
_, ok := ss[s]
return ok
}
type typeDetails struct {
outputType bool
inputType bool
stateType bool
functionType bool
}
// Title converts the input string to a title case
// where only the initial letter is upper-cased.
func Title(s string) string {
if s == "" {
return ""
}
runes := []rune(s)
return string(append([]rune{unicode.ToUpper(runes[0])}, runes[1:]...))
}
func csharpIdentifier(s string) string {
switch s {
case "abstract", "as", "base", "bool",
"break", "byte", "case", "catch",
"char", "checked", "class", "const",
"continue", "decimal", "default", "delegate",
"do", "double", "else", "enum",
"event", "explicit", "extern", "false",
"finally", "fixed", "float", "for",
"foreach", "goto", "if", "implicit",
"in", "int", "interface", "internal",
"is", "lock", "long", "namespace",
"new", "null", "object", "operator",
"out", "override", "params", "private",
"protected", "public", "readonly", "ref",
"return", "sbyte", "sealed", "short",
"sizeof", "stackalloc", "static", "string",
"struct", "switch", "this", "throw",
"true", "try", "typeof", "uint",
"ulong", "unchecked", "unsafe", "ushort",
"using", "virtual", "void", "volatile", "while":
return "@" + s
default:
return s
}
}
func isImmutableArrayType(t schema.Type, wrapInput bool) bool {
_, isArray := t.(*schema.ArrayType)
return isArray && !wrapInput
}
func isValueType(t schema.Type) bool {
switch t {
case schema.BoolType, schema.IntType, schema.NumberType:
return true
default:
return false
}
}
func namespaceName(namespaces map[string]string, name string) string {
if ns, ok := namespaces[name]; ok {
return ns
}
return Title(name)
}
type modContext struct {
pkg *schema.Package
mod string
propertyNames map[*schema.Property]string
types []*schema.ObjectType
resources []*schema.Resource
functions []*schema.Function
typeDetails map[*schema.ObjectType]*typeDetails
children []*modContext
tool string
namespaceName string
namespaces map[string]string
}
func (mod *modContext) propertyName(p *schema.Property) string {
if n, ok := mod.propertyNames[p]; ok {
return n
}
return Title(p.Name)
}
func (mod *modContext) details(t *schema.ObjectType) *typeDetails {
details, ok := mod.typeDetails[t]
if !ok {
details = &typeDetails{}
mod.typeDetails[t] = details
}
return details
}
func tokenToName(tok string) string {
// token := pkg : module : member
// module := path/to/module
components := strings.Split(tok, ":")
contract.Assertf(len(components) == 3, "malformed token %v", tok)
return Title(components[2])
}
func resourceName(r *schema.Resource) string {
if r.IsProvider {
return "Provider"
}
return tokenToName(r.Token)
}
func tokenToFunctionName(tok string) string {
return tokenToName(tok)
}
func (mod *modContext) tokenToNamespace(tok string) string {
components := strings.Split(tok, ":")
contract.Assertf(len(components) == 3, "malformed token %v", tok)
pkg, nsName := "Pulumi."+namespaceName(mod.namespaces, components[0]), mod.pkg.TokenToModule(tok)
if nsName == "" {
return pkg
}
return pkg + "." + namespaceName(mod.namespaces, nsName)
}
func (mod *modContext) typeString(t schema.Type, qualifier string, input, state, wrapInput, requireInitializers, optional bool) string {
var typ string
switch t := t.(type) {
case *schema.ArrayType:
var listFmt string
switch {
case wrapInput:
listFmt, optional = "InputList<%v>", false
case requireInitializers:
listFmt = "List<%v>"
default:
listFmt, optional = "ImmutableArray<%v>", false
}
wrapInput = false
typ = fmt.Sprintf(listFmt, mod.typeString(t.ElementType, qualifier, input, state, false, false, false))
case *schema.MapType:
var mapFmt string
switch {
case wrapInput:
mapFmt, optional = "InputMap<%v>", false
case requireInitializers:
mapFmt = "Dictionary<string, %v>"
default:
mapFmt = "ImmutableDictionary<string, %v>"
}
wrapInput = false
typ = fmt.Sprintf(mapFmt, mod.typeString(t.ElementType, qualifier, input, state, false, false, false))
case *schema.ObjectType:
typ = mod.tokenToNamespace(t.Token)
if typ == mod.namespaceName {
typ = qualifier
} else if qualifier != "" {
typ += "." + qualifier
}
if typ != "" {
typ += "."
}
typ += tokenToName(t.Token)
switch {
case state:
typ += "GetArgs"
case input:
typ += "Args"
case mod.details(t).functionType:
typ += "Result"
}
case *schema.TokenType:
// Use the underlying type for now.
if t.UnderlyingType != nil {
return mod.typeString(t.UnderlyingType, qualifier, input, state, wrapInput, requireInitializers, optional)
}
typ = tokenToName(t.Token)
if ns := mod.tokenToNamespace(t.Token); ns != mod.namespaceName {
typ = ns + "." + typ
}
case *schema.UnionType:
unionT := "Union"
if wrapInput {
unionT = "InputUnion"
}
elementTypeSet := stringSet{}
var elementTypes []string
for _, e := range t.ElementTypes {
et := mod.typeString(e, qualifier, input, state, false, false, false)
if !elementTypeSet.has(et) {
elementTypeSet.add(et)
elementTypes = append(elementTypes, et)
}
}
if len(elementTypes) == 1 {
return mod.typeString(t.ElementTypes[0], qualifier, input, state, wrapInput, requireInitializers, optional)
}
for _, e := range elementTypes[:len(elementTypes)-1] {
typ = fmt.Sprintf("%s%s<%s, ", typ, unionT, e)
}
last := elementTypes[len(elementTypes)-1]
term := strings.Repeat(">", len(elementTypes)-1)
wrapInput = false
typ += last + term
default:
switch t {
case schema.BoolType:
typ = "bool"
case schema.IntType:
typ = "int"
case schema.NumberType:
typ = "double"
case schema.StringType:
typ = "string"
case schema.ArchiveType:
typ = "Archive"
case schema.AssetType:
typ = "AssetOrArchive"
case schema.AnyType:
typ = "object"
}
}
if wrapInput {
typ = fmt.Sprintf("Input<%s>", typ)
}
if optional {
typ += "?"
}
return typ
}
var docCommentEscaper = strings.NewReplacer(
`&`, "&amp;",
`<`, "&lt;",
`>`, "&gt;",
)
func printComment(w io.Writer, comment string, indent string) {
lines := strings.Split(docCommentEscaper.Replace(comment), "\n")
for len(lines) > 0 && lines[len(lines)-1] == "" {
lines = lines[:len(lines)-1]
}
if len(lines) > 0 {
fmt.Fprintf(w, "%s/// <summary>\n", indent)
for _, l := range lines {
fmt.Fprintf(w, "%s/// %s\n", indent, l)
}
fmt.Fprintf(w, "%s/// </summary>\n", indent)
}
}
type plainType struct {
mod *modContext
res *schema.Resource
name string
comment string
baseClass string
propertyTypeQualifier string
properties []*schema.Property
wrapInput bool
state bool
}
func (pt *plainType) genInputProperty(w io.Writer, prop *schema.Property, indent string) {
wireName := prop.Name
propertyName := pt.mod.propertyName(prop)
propertyType := pt.mod.typeString(prop.Type, pt.propertyTypeQualifier, true, pt.state, pt.wrapInput, false, !prop.IsRequired)
// First generate the input attribute.
attributeArgs := ""
if prop.IsRequired {
attributeArgs = ", required: true"
}
if pt.res != nil && pt.res.IsProvider && prop.Type != schema.StringType {
attributeArgs += ", json: true"
}
// Next generate the input property itself. The way this is generated depends on the type of the property:
// complex types like lists and maps need a backing field.
switch prop.Type.(type) {
case *schema.ArrayType, *schema.MapType:
backingFieldName := "_" + prop.Name
requireInitializers := !pt.wrapInput
backingFieldType := pt.mod.typeString(prop.Type, pt.propertyTypeQualifier, true, pt.state, pt.wrapInput, requireInitializers, false)
fmt.Fprintf(w, "%s [Input(\"%s\"%s)]\n", indent, wireName, attributeArgs)
fmt.Fprintf(w, "%s private %s? %s;\n", indent, backingFieldType, backingFieldName)
if prop.Comment != "" {
fmt.Fprintf(w, "\n")
printComment(w, prop.Comment, indent+" ")
}
if prop.DeprecationMessage != "" {
fmt.Fprintf(w, "%s [Obsolete(@\"%s\")]\n", indent, strings.Replace(prop.DeprecationMessage, `"`, `""`, -1))
}
// Note that we use the backing field type--which is just the property type without any nullable annotation--to
// ensure that the user does not see warnings when initializing these properties using object or collection
// initializers.
fmt.Fprintf(w, "%s public %s %s\n", indent, backingFieldType, propertyName)
fmt.Fprintf(w, "%s {\n", indent)
fmt.Fprintf(w, "%s get => %[2]s ?? (%[2]s = new %[3]s());\n", indent, backingFieldName, backingFieldType)
fmt.Fprintf(w, "%s set => %s = value;\n", indent, backingFieldName)
fmt.Fprintf(w, "%s }\n", indent)
default:
initializer := ""
if prop.IsRequired && (!isValueType(prop.Type) || pt.wrapInput) {
initializer = " = null!;"
}
printComment(w, prop.Comment, indent+" ")
fmt.Fprintf(w, "%s [Input(\"%s\"%s)]\n", indent, wireName, attributeArgs)
fmt.Fprintf(w, "%s public %s %s { get; set; }%s\n", indent, propertyType, propertyName, initializer)
}
}
func (pt *plainType) genInputType(w io.Writer, level int) error {
indent := strings.Repeat(" ", level)
fmt.Fprintf(w, "\n")
// Open the class.
printComment(w, pt.comment, indent)
fmt.Fprintf(w, "%spublic sealed class %s : Pulumi.%s\n", indent, pt.name, pt.baseClass)
fmt.Fprintf(w, "%s{\n", indent)
// Declare each input property.
for _, p := range pt.properties {
pt.genInputProperty(w, p, indent)
fmt.Fprintf(w, "\n")
}
// Generate a constructor that will set default values.
fmt.Fprintf(w, "%s public %s()\n", indent, pt.name)
fmt.Fprintf(w, "%s {\n", indent)
for _, prop := range pt.properties {
if prop.DefaultValue != nil {
dv, err := pt.mod.getDefaultValue(prop.DefaultValue, prop.Type)
if err != nil {
return err
}
propertyName := pt.mod.propertyName(prop)
fmt.Fprintf(w, "%s %s = %s;\n", indent, propertyName, dv)
}
}
fmt.Fprintf(w, "%s }\n", indent)
// Close the class.
fmt.Fprintf(w, "%s}\n", indent)
return nil
}
func (pt *plainType) genOutputType(w io.Writer, level int) {
indent := strings.Repeat(" ", level)
fmt.Fprintf(w, "\n")
// Open the class and attribute it appropriately.
fmt.Fprintf(w, "%s[OutputType]\n", indent)
fmt.Fprintf(w, "%spublic sealed class %s\n", indent, pt.name)
fmt.Fprintf(w, "%s{\n", indent)
// Generate each output field.
for _, prop := range pt.properties {
fieldName := pt.mod.propertyName(prop)
fieldType := pt.mod.typeString(prop.Type, pt.propertyTypeQualifier, false, false, false, false, !prop.IsRequired)
printComment(w, prop.Comment, indent+" ")
fmt.Fprintf(w, "%s public readonly %s %s;\n", indent, fieldType, fieldName)
}
if len(pt.properties) > 0 {
fmt.Fprintf(w, "\n")
}
// Generate an appropriately-attributed constructor that will set this types' fields.
fmt.Fprintf(w, "%s [OutputConstructor]\n", indent)
fmt.Fprintf(w, "%s private %s(", indent, pt.name)
// Generate the constructor parameters.
for i, prop := range pt.properties {
paramName := csharpIdentifier(prop.Name)
paramType := pt.mod.typeString(prop.Type, pt.propertyTypeQualifier, false, false, false, false, !prop.IsRequired)
terminator := ""
if i != len(pt.properties)-1 {
terminator = ",\n"
}
paramDef := fmt.Sprintf("%s %s%s", paramType, paramName, terminator)
if len(pt.properties) > 1 {
paramDef = fmt.Sprintf("\n%s %s", indent, paramDef)
}
fmt.Fprint(w, paramDef)
}
fmt.Fprintf(w, ")\n")
// Generate the constructor body.
fmt.Fprintf(w, "%s {\n", indent)
for _, prop := range pt.properties {
paramName := csharpIdentifier(prop.Name)
fieldName := pt.mod.propertyName(prop)
fmt.Fprintf(w, "%s %s = %s;\n", indent, fieldName, paramName)
}
fmt.Fprintf(w, "%s }\n", indent)
// Close the class.
fmt.Fprintf(w, "%s}\n", indent)
}
func primitiveValue(value interface{}) (string, error) {
v := reflect.ValueOf(value)
if v.Kind() == reflect.Interface {
v = v.Elem()
}
switch v.Kind() {
case reflect.Bool:
if v.Bool() {
return "true", nil
}
return "false", nil
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32:
return strconv.FormatInt(v.Int(), 10), nil
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32:
return strconv.FormatUint(v.Uint(), 10), nil
case reflect.Float32, reflect.Float64:
return strconv.FormatFloat(v.Float(), 'f', -1, 64), nil
case reflect.String:
return fmt.Sprintf("%q", v.String()), nil
default:
return "", errors.Errorf("unsupported default value of type %T", value)
}
}
func (mod *modContext) getDefaultValue(dv *schema.DefaultValue, t schema.Type) (string, error) {
var val string
if dv.Value != nil {
v, err := primitiveValue(dv.Value)
if err != nil {
return "", err
}
val = v
}
if len(dv.Environment) != 0 {
getType := ""
switch t {
case schema.BoolType:
getType = "Boolean"
case schema.IntType:
getType = "Int32"
case schema.NumberType:
getType = "Double"
}
envVars := fmt.Sprintf("%q", dv.Environment[0])
for _, e := range dv.Environment[1:] {
envVars += fmt.Sprintf(", %q", e)
}
getEnv := fmt.Sprintf("Utilities.GetEnv%s(%s)", getType, envVars)
if val != "" {
val = fmt.Sprintf("%s ?? %s", getEnv, val)
} else {
val = getEnv
}
}
return val, nil
}
func genAlias(w io.Writer, alias *schema.Alias) {
fmt.Fprintf(w, "new Alias { ")
parts := []string{}
if alias.Name != nil {
parts = append(parts, fmt.Sprintf("Name = \"%v\"", *alias.Name))
}
if alias.Project != nil {
parts = append(parts, fmt.Sprintf("Project = \"%v\"", *alias.Project))
}
if alias.Type != nil {
parts = append(parts, fmt.Sprintf("Type = \"%v\"", *alias.Type))
}
for i, part := range parts {
if i > 0 {
fmt.Fprintf(w, ", ")
}
fmt.Fprintf(w, "%s", part)
}
fmt.Fprintf(w, "}")
}
func (mod *modContext) genResource(w io.Writer, r *schema.Resource) error {
// Create a resource module file into which all of this resource's types will go.
name := resourceName(r)
// Open the namespace.
fmt.Fprintf(w, "namespace %s\n", mod.namespaceName)
fmt.Fprintf(w, "{\n")
// Write the TypeDoc/JSDoc for the resource class
printComment(w, codegen.StripNonRelevantExamples(r.Comment, "csharp"), " ")
// Open the class.
className := name
baseType := "Pulumi.CustomResource"
if r.IsProvider {
baseType = "Pulumi.ProviderResource"
}
if r.DeprecationMessage != "" {
fmt.Fprintf(w, " [Obsolete(@\"%s\")]\n", strings.Replace(r.DeprecationMessage, `"`, `""`, -1))
}
fmt.Fprintf(w, " public partial class %s : %s\n", className, baseType)
fmt.Fprintf(w, " {\n")
// Emit all output properties.
for _, prop := range r.Properties {
// Write the property attribute
wireName := prop.Name
propertyName := mod.propertyName(prop)
propertyType := mod.typeString(prop.Type, "Outputs", false, false, false, false, !prop.IsRequired)
// Workaround the fact that provider inputs come back as strings.
if r.IsProvider && !schema.IsPrimitiveType(prop.Type) {
propertyType = "string"
if !prop.IsRequired {
propertyType += "?"
}
}
printComment(w, prop.Comment, " ")
fmt.Fprintf(w, " [Output(\"%s\")]\n", wireName)
fmt.Fprintf(w, " public Output<%s> %s { get; private set; } = null!;\n", propertyType, propertyName)
fmt.Fprintf(w, "\n")
}
if len(r.Properties) > 0 {
fmt.Fprintf(w, "\n")
}
// Emit the class constructor.
argsType := className + "Args"
var argsDefault string
allOptionalInputs := true
for _, prop := range r.InputProperties {
allOptionalInputs = allOptionalInputs && !prop.IsRequired
}
if allOptionalInputs {
// If the number of required input properties was zero, we can make the args object optional.
argsDefault = " = null"
argsType += "?"
}
optionsType := "CustomResourceOptions"
tok := r.Token
if r.IsProvider {
tok = mod.pkg.Name
}
// Write a comment prior to the constructor.
fmt.Fprintf(w, " /// <summary>\n")
fmt.Fprintf(w, " /// Create a %s resource with the given unique name, arguments, and options.\n", className)
fmt.Fprintf(w, " /// </summary>\n")
fmt.Fprintf(w, " ///\n")
fmt.Fprintf(w, " /// <param name=\"name\">The unique name of the resource</param>\n")
fmt.Fprintf(w, " /// <param name=\"args\">The arguments used to populate this resource's properties</param>\n")
fmt.Fprintf(w, " /// <param name=\"options\">A bag of options that control this resource's behavior</param>\n")
fmt.Fprintf(w, " public %s(string name, %s args%s, %s? options = null)\n", className, argsType, argsDefault, optionsType)
fmt.Fprintf(w, " : base(\"%s\", name, args ?? new %sArgs(), MakeResourceOptions(options, \"\"))\n", tok, className)
fmt.Fprintf(w, " {\n")
fmt.Fprintf(w, " }\n")
// Write a private constructor for the use of `Get`.
if !r.IsProvider {
stateParam, stateRef := "", "null"
if r.StateInputs != nil {
stateParam, stateRef = fmt.Sprintf("%sState? state = null, ", className), "state"
}
fmt.Fprintf(w, "\n")
fmt.Fprintf(w, " private %s(string name, Input<string> id, %s%s? options = null)\n", className, stateParam, optionsType)
fmt.Fprintf(w, " : base(\"%s\", name, %s, MakeResourceOptions(options, id))\n", tok, stateRef)
fmt.Fprintf(w, " {\n")
fmt.Fprintf(w, " }\n")
}
// Write the method that will calculate the resource options.
fmt.Fprintf(w, "\n")
fmt.Fprintf(w, " private static %[1]s MakeResourceOptions(%[1]s? options, Input<string>? id)\n", optionsType)
fmt.Fprintf(w, " {\n")
fmt.Fprintf(w, " var defaultOptions = new %s\n", optionsType)
fmt.Fprintf(w, " {\n")
fmt.Fprintf(w, " Version = Utilities.Version,")
switch len(r.Aliases) {
case 0:
fmt.Fprintf(w, "\n")
case 1:
fmt.Fprintf(w, " Aliases = { ")
genAlias(w, r.Aliases[0])
fmt.Fprintf(w, " },\n")
default:
fmt.Fprintf(w, " Aliases =\n")
fmt.Fprintf(w, " {\n")
for _, alias := range r.Aliases {
fmt.Fprintf(w, " ")
genAlias(w, alias)
fmt.Fprintf(w, ",\n")
}
fmt.Fprintf(w, " },\n")
}
fmt.Fprintf(w, " };\n")
fmt.Fprintf(w, " var merged = %s.Merge(defaultOptions, options);\n", optionsType)
fmt.Fprintf(w, " // Override the ID if one was specified for consistency with other language SDKs.\n")
fmt.Fprintf(w, " merged.Id = id ?? merged.Id;\n")
fmt.Fprintf(w, " return merged;\n")
fmt.Fprintf(w, " }\n")
// Write the `Get` method for reading instances of this resource unless this is a provider resource.
if !r.IsProvider {
stateParam, stateRef := "", ""
if r.StateInputs != nil {
stateParam, stateRef = fmt.Sprintf("%sState? state = null, ", className), "state, "
}
fmt.Fprintf(w, " /// <summary>\n")
fmt.Fprintf(w, " /// Get an existing %s resource's state with the given name, ID, and optional extra\n", className)
fmt.Fprintf(w, " /// properties used to qualify the lookup.\n")
fmt.Fprintf(w, " /// </summary>\n")
fmt.Fprintf(w, " ///\n")
fmt.Fprintf(w, " /// <param name=\"name\">The unique name of the resulting resource.</param>\n")
fmt.Fprintf(w, " /// <param name=\"id\">The unique provider ID of the resource to lookup.</param>\n")
fmt.Fprintf(w, " /// <param name=\"state\">Any extra arguments used during the lookup.</param>\n")
fmt.Fprintf(w, " /// <param name=\"options\">A bag of options that control this resource's behavior</param>\n")
fmt.Fprintf(w, " public static %s Get(string name, Input<string> id, %s%s? options = null)\n", className, stateParam, optionsType)
fmt.Fprintf(w, " {\n")
fmt.Fprintf(w, " return new %s(name, id, %soptions);\n", className, stateRef)
fmt.Fprintf(w, " }\n")
}
// Close the class.
fmt.Fprintf(w, " }\n")
// Generate the resource args type.
args := &plainType{
mod: mod,
res: r,
name: name + "Args",
baseClass: "ResourceArgs",
propertyTypeQualifier: "Inputs",
properties: r.InputProperties,
wrapInput: true,
}
if err := args.genInputType(w, 1); err != nil {
return err
}
// Generate the `Get` args type, if any.
if r.StateInputs != nil {
state := &plainType{
mod: mod,
res: r,
name: name + "State",
baseClass: "ResourceArgs",
propertyTypeQualifier: "Inputs",
properties: r.StateInputs.Properties,
wrapInput: true,
state: true,
}
if err := state.genInputType(w, 1); err != nil {
return err
}
}
// Close the namespace.
fmt.Fprintf(w, "}\n")
return nil
}
func (mod *modContext) genFunction(w io.Writer, fun *schema.Function) error {
className := tokenToFunctionName(fun.Token)
fmt.Fprintf(w, "namespace %s\n", mod.tokenToNamespace(fun.Token))
fmt.Fprintf(w, "{\n")
var typeParameter string
if fun.Outputs != nil {
typeParameter = fmt.Sprintf("<%sResult>", className)
}
var argsParamDef string
argsParamRef := "InvokeArgs.Empty"
if fun.Inputs != nil {
allOptionalInputs := true
for _, prop := range fun.Inputs.Properties {
allOptionalInputs = allOptionalInputs && !prop.IsRequired
}
var argsDefault, sigil string
if allOptionalInputs {
// If the number of required input properties was zero, we can make the args object optional.
argsDefault, sigil = " = null", "?"
}
argsParamDef = fmt.Sprintf("%sArgs%s args%s, ", className, sigil, argsDefault)
argsParamRef = fmt.Sprintf("args ?? new %sArgs()", className)
}
if fun.DeprecationMessage != "" {
fmt.Fprintf(w, " [Obsolete(@\"%s\")]\n", strings.Replace(fun.DeprecationMessage, `"`, `""`, -1))
}
// Open the class we'll use for datasources.
fmt.Fprintf(w, " public static class %s\n", className)
fmt.Fprintf(w, " {\n")
// Emit the doc comment, if any.
printComment(w, fun.Comment, " ")
// Emit the datasource method.
fmt.Fprintf(w, " public static Task%s InvokeAsync(%sInvokeOptions? options = null)\n",
typeParameter, argsParamDef)
fmt.Fprintf(w, " => Pulumi.Deployment.Instance.InvokeAsync%s(\"%s\", %s, options.WithVersion());\n",
typeParameter, fun.Token, argsParamRef)
// Close the class.
fmt.Fprintf(w, " }\n")
// Emit the args and result types, if any.
if fun.Inputs != nil {
fmt.Fprintf(w, "\n")
args := &plainType{
mod: mod,
name: className + "Args",
baseClass: "InvokeArgs",
propertyTypeQualifier: "Inputs",
properties: fun.Inputs.Properties,
}
if err := args.genInputType(w, 1); err != nil {
return err
}
}
if fun.Outputs != nil {
fmt.Fprintf(w, "\n")
res := &plainType{
mod: mod,
name: className + "Result",
propertyTypeQualifier: "Outputs",
properties: fun.Outputs.Properties,
}
res.genOutputType(w, 1)
}
// Close the namespace.
fmt.Fprintf(w, "}\n")
return nil
}
func visitObjectTypes(t schema.Type, visitor func(*schema.ObjectType)) {
switch t := t.(type) {
case *schema.ArrayType:
visitObjectTypes(t.ElementType, visitor)
case *schema.MapType:
visitObjectTypes(t.ElementType, visitor)
case *schema.ObjectType:
for _, p := range t.Properties {
visitObjectTypes(p.Type, visitor)
}
visitor(t)
case *schema.UnionType:
for _, e := range t.ElementTypes {
visitObjectTypes(e, visitor)
}
}
}
func (mod *modContext) genType(w io.Writer, obj *schema.ObjectType, propertyTypeQualifier string, input, state bool, level int) error {
name := tokenToName(obj.Token)
switch {
case state:
name += "GetArgs"
case input:
name += "Args"
case mod.details(obj).functionType:
name += "Result"
}
pt := &plainType{
mod: mod,
name: name,
comment: obj.Comment,
propertyTypeQualifier: propertyTypeQualifier,
properties: obj.Properties,
state: state,
}
if input {
pt.baseClass, pt.wrapInput = "ResourceArgs", true
if mod.details(obj).functionType {
pt.baseClass, pt.wrapInput = "InvokeArgs", false
}
return pt.genInputType(w, level)
}
pt.genOutputType(w, level)
return nil
}
func (mod *modContext) genPulumiHeader(w io.Writer) {
mod.genHeader(w, []string{
"System",
"System.Collections.Generic",
"System.Collections.Immutable",
"System.Threading.Tasks",
"Pulumi.Serialization",
})
}
func (mod *modContext) genHeader(w io.Writer, using []string) {
fmt.Fprintf(w, "// *** WARNING: this file was generated by %v. ***\n", mod.tool)
fmt.Fprintf(w, "// *** Do not edit by hand unless you're certain you know what you are doing! ***\n")
fmt.Fprintf(w, "\n")
for _, u := range using {
fmt.Fprintf(w, "using %s;\n", u)
}
if len(using) > 0 {
fmt.Fprintf(w, "\n")
}
}
func (mod *modContext) getConfigProperty(schemaType schema.Type) (string, string) {
propertyType := mod.typeString(
schemaType, "Types", false, false, false /*wrapInputs*/, false /*requireInitializers*/, false)
var getFunc string
nullableSigil := "?"
switch schemaType {
case schema.StringType:
getFunc = "Get"
case schema.BoolType:
getFunc = "GetBoolean"
case schema.IntType:
getFunc = "GetInt32"
case schema.NumberType:
getFunc = "GetDouble"
default:
switch t := schemaType.(type) {
case *schema.TokenType:
if t.UnderlyingType != nil {
return mod.getConfigProperty(t.UnderlyingType)
}
}
getFunc = "GetObject<" + propertyType + ">"
if _, ok := schemaType.(*schema.ArrayType); ok {
nullableSigil = ""
}
}
return propertyType + nullableSigil, getFunc
}
func (mod *modContext) genConfig(variables []*schema.Property) (string, error) {
w := &bytes.Buffer{}
mod.genHeader(w, []string{"System.Collections.Immutable"})
// Use the root namespace to avoid `Pulumi.Provider.Config.Config.VarName` usage.
fmt.Fprintf(w, "namespace %s\n", mod.namespaceName)
fmt.Fprintf(w, "{\n")
// Open the config class.
fmt.Fprintf(w, " public static class Config\n")
fmt.Fprintf(w, " {\n")
// Create a config bag for the variables to pull from.
fmt.Fprintf(w, " private static readonly Pulumi.Config __config = new Pulumi.Config(\"%v\");", mod.pkg.Name)
fmt.Fprintf(w, "\n")
// Emit an entry for all config variables.
for _, p := range variables {
propertyType, getFunc := mod.getConfigProperty(p.Type)
propertyName := strings.Title(p.Name)
initializer := fmt.Sprintf("__config.%s(\"%s\")", getFunc, p.Name)
if p.DefaultValue != nil {
dv, err := mod.getDefaultValue(p.DefaultValue, p.Type)
if err != nil {
return "", err
}
initializer += " ?? " + dv
}
printComment(w, p.Comment, " ")
fmt.Fprintf(w, " public static %s %s { get; set; } = %s;\n", propertyType, propertyName, initializer)
fmt.Fprintf(w, "\n")
}
// Emit any nested types.
if len(mod.types) > 0 {
fmt.Fprintf(w, " public static class Types\n")
fmt.Fprintf(w, " {\n")
for _, typ := range mod.types {
fmt.Fprintf(w, "\n")
// Open the class.
fmt.Fprintf(w, " public class %s\n", tokenToName(typ.Token))
fmt.Fprintf(w, " {\n")
// Generate each output field.
for _, prop := range typ.Properties {
name := mod.propertyName(prop)
typ := mod.typeString(prop.Type, "Types", false, false, false /*wrapInput*/, false, !prop.IsRequired)
initializer := ""
if !prop.IsRequired && !isValueType(prop.Type) && !isImmutableArrayType(prop.Type, false) {
initializer = " = null!;"
}
printComment(w, prop.Comment, " ")
fmt.Fprintf(w, " public %s %s { get; set; }%s\n", typ, name, initializer)
}
// Close the class.
fmt.Fprintf(w, " }\n")
}
fmt.Fprintf(w, " }\n")
}
// Close the config class and namespace.
fmt.Fprintf(w, " }\n")
// Close the namespace.
fmt.Fprintf(w, "}\n")
return w.String(), nil
}
type fs map[string][]byte
func (fs fs) add(path string, contents []byte) {
_, has := fs[path]
contract.Assertf(!has, "duplicate file: %s", path)
fs[path] = contents
}
func (mod *modContext) genUtilities() (string, error) {
// Strip any 'v' off of the version.
w := &bytes.Buffer{}
err := csharpUtilitiesTemplate.Execute(w, csharpUtilitiesTemplateContext{
Namespace: mod.namespaceName,
ClassName: "Utilities",
Tool: mod.tool,
Version: mod.pkg.Version.String(),
})
if err != nil {
return "", err
}
return w.String(), nil
}
func (mod *modContext) gen(fs fs) error {
nsComponents := strings.Split(mod.namespaceName, ".")
if len(nsComponents) > 0 {
// Trim off "Pulumi.Pkg"
nsComponents = nsComponents[2:]
}
dir := path.Join(nsComponents...)
if mod.mod == "config" {
dir = "Config"
}
var files []string
for p := range fs {
d := path.Dir(p)
if d == "." {
d = ""
}
if d == dir {
files = append(files, p)
}
}
addFile := func(name, contents string) {
p := path.Join(dir, name)
files = append(files, p)
fs.add(p, []byte(contents))
}
// Ensure that the target module directory contains a README.md file.
readme := mod.pkg.Description
if readme != "" && readme[len(readme)-1] != '\n' {
readme += "\n"
}
fs.add(filepath.Join(dir, "README.md"), []byte(readme))
// Utilities, config
switch mod.mod {
case "":
utilities, err := mod.genUtilities()
if err != nil {
return err
}
fs.add("Utilities.cs", []byte(utilities))
case "config":
if len(mod.pkg.Config) > 0 {
config, err := mod.genConfig(mod.pkg.Config)
if err != nil {
return err
}
addFile("Config.cs", config)
return nil
}
}
// Resources
for _, r := range mod.resources {
buffer := &bytes.Buffer{}
mod.genPulumiHeader(buffer)
if err := mod.genResource(buffer, r); err != nil {
return err
}
addFile(resourceName(r)+".cs", buffer.String())
}
// Functions
for _, f := range mod.functions {
buffer := &bytes.Buffer{}
mod.genPulumiHeader(buffer)
if err := mod.genFunction(buffer, f); err != nil {
return err
}
addFile(tokenToName(f.Token)+".cs", buffer.String())
}
// Nested types
for _, t := range mod.types {
if mod.details(t).inputType {
buffer := &bytes.Buffer{}
mod.genPulumiHeader(buffer)
fmt.Fprintf(buffer, "namespace %s.Inputs\n", mod.namespaceName)
fmt.Fprintf(buffer, "{\n")
if err := mod.genType(buffer, t, "Inputs", true, false, 1); err != nil {
return err
}
fmt.Fprintf(buffer, "}\n")
addFile(path.Join("Inputs", tokenToName(t.Token)+"Args.cs"), buffer.String())
}
if mod.details(t).stateType {
buffer := &bytes.Buffer{}
mod.genPulumiHeader(buffer)
fmt.Fprintf(buffer, "namespace %s.Inputs\n", mod.namespaceName)
fmt.Fprintf(buffer, "{\n")
if err := mod.genType(buffer, t, "Inputs", true, true, 1); err != nil {
return err
}
fmt.Fprintf(buffer, "}\n")
addFile(path.Join("Inputs", tokenToName(t.Token)+"GetArgs.cs"), buffer.String())
}
if mod.details(t).outputType {
buffer := &bytes.Buffer{}
mod.genPulumiHeader(buffer)
fmt.Fprintf(buffer, "namespace %s.Outputs\n", mod.namespaceName)
fmt.Fprintf(buffer, "{\n")
if err := mod.genType(buffer, t, "Outputs", false, false, 1); err != nil {
return err
}
fmt.Fprintf(buffer, "}\n")
suffix := ""
if mod.details(t).functionType {
suffix = "Result"
}
addFile(path.Join("Outputs", tokenToName(t.Token)+suffix+".cs"), buffer.String())
}
}
return nil
}
// genPackageMetadata generates all the non-code metadata required by a Pulumi package.
func genPackageMetadata(pkg *schema.Package, assemblyName string, packageReferences map[string]string, files fs) error {
projectFile, err := genProjectFile(pkg, assemblyName, packageReferences)
if err != nil {
return err
}
logo, err := getLogo(pkg)
if err != nil {
return err
}
files.add(assemblyName+".csproj", projectFile)
files.add("logo.png", logo)
return nil
}
// genProjectFile emits a C# project file into the configured output directory.
func genProjectFile(pkg *schema.Package, assemblyName string, packageReferences map[string]string) ([]byte, error) {
w := &bytes.Buffer{}
err := csharpProjectFileTemplate.Execute(w, csharpProjectFileTemplateContext{
XMLDoc: fmt.Sprintf(`.\%s.xml`, assemblyName),
Package: pkg,
PackageReferences: packageReferences,
})
if err != nil {
return nil, err
}
return w.Bytes(), nil
}
// emitLogo downloads an image and saves it as logo.png into the configured output directory.
func getLogo(pkg *schema.Package) ([]byte, error) {
url := pkg.LogoURL
if url == "" {
// Default to a generic Pulumi logo from the parent repository.
url = "https://raw.githubusercontent.com/pulumi/pulumi/394c91d7f6ab7a4096f4454827690a460f665433/sdk/dotnet/pulumi_logo_64x64.png"
}
// Get the data.
// nolint: gosec
resp, err := http.Get(url)
if err != nil {
return nil, err
}
defer contract.IgnoreClose(resp.Body)
return ioutil.ReadAll(resp.Body)
}
func computePropertyNames(props []*schema.Property, names map[*schema.Property]string) {
for _, p := range props {
if info, ok := p.Language["csharp"].(CSharpPropertyInfo); ok && info.Name != "" {
names[p] = info.Name
}
}
}
func GeneratePackage(tool string, pkg *schema.Package, extraFiles map[string][]byte) (map[string][]byte, error) {
// Decode .NET-specific info
if err := pkg.ImportLanguages(map[string]schema.Language{"csharp": Importer}); err != nil {
return nil, err
}
info, _ := pkg.Language["csharp"].(CSharpPackageInfo)
propertyNames := map[*schema.Property]string{}
for _, r := range pkg.Resources {
computePropertyNames(r.Properties, propertyNames)
computePropertyNames(r.InputProperties, propertyNames)
if r.StateInputs != nil {
computePropertyNames(r.StateInputs.Properties, propertyNames)
}
}
for _, f := range pkg.Functions {
if f.Inputs != nil {
computePropertyNames(f.Inputs.Properties, propertyNames)
}
if f.Outputs != nil {
computePropertyNames(f.Outputs.Properties, propertyNames)
}
}
for _, t := range pkg.Types {
if obj, ok := t.(*schema.ObjectType); ok {
computePropertyNames(obj.Properties, propertyNames)
}
}
// group resources, types, and functions into Go packages
modules := map[string]*modContext{}
details := map[*schema.ObjectType]*typeDetails{}
assemblyName := "Pulumi." + namespaceName(info.Namespaces, pkg.Name)
var getMod func(token string) *modContext
getMod = func(token string) *modContext {
modName := pkg.TokenToModule(token)
mod, ok := modules[modName]
if !ok {
ns := assemblyName
if modName != "" {
ns += "." + namespaceName(info.Namespaces, modName)
}
mod = &modContext{
pkg: pkg,
mod: modName,
tool: tool,
namespaceName: ns,
namespaces: info.Namespaces,
typeDetails: details,
propertyNames: propertyNames,
}
if modName != "" {
parentName := path.Dir(modName)
if parentName == "." || parentName == "" {
parentName = ":index:"
}
parent := getMod(parentName)
parent.children = append(parent.children, mod)
}
modules[modName] = mod
}
return mod
}
// Create the config module if necessary.
if len(pkg.Config) > 0 {
cfg := getMod(":config/config:")
cfg.namespaceName = assemblyName
}
for _, v := range pkg.Config {
visitObjectTypes(v.Type, func(t *schema.ObjectType) { getMod(t.Token).details(t).outputType = true })
}
// Find input and output types referenced by resources.
scanResource := func(r *schema.Resource) {
mod := getMod(r.Token)
mod.resources = append(mod.resources, r)
for _, p := range r.Properties {
visitObjectTypes(p.Type, func(t *schema.ObjectType) { getMod(t.Token).details(t).outputType = true })
}
for _, p := range r.InputProperties {
visitObjectTypes(p.Type, func(t *schema.ObjectType) {
if r.IsProvider {
getMod(t.Token).details(t).outputType = true
}
getMod(t.Token).details(t).inputType = true
})
}
if r.StateInputs != nil {
visitObjectTypes(r.StateInputs, func(t *schema.ObjectType) {
getMod(t.Token).details(t).inputType = true
getMod(t.Token).details(t).stateType = true
})
}
}
scanResource(pkg.Provider)
for _, r := range pkg.Resources {
scanResource(r)
}
// Find input and output types referenced by functions.
for _, f := range pkg.Functions {
mod := getMod(f.Token)
mod.functions = append(mod.functions, f)
if f.Inputs != nil {
visitObjectTypes(f.Inputs, func(t *schema.ObjectType) {
getMod(t.Token).details(t).inputType = true
getMod(t.Token).details(t).functionType = true
})
}
if f.Outputs != nil {
visitObjectTypes(f.Outputs, func(t *schema.ObjectType) {
getMod(t.Token).details(t).outputType = true
getMod(t.Token).details(t).functionType = true
})
}
}
// Find nested types.
for _, t := range pkg.Types {
if obj, ok := t.(*schema.ObjectType); ok {
mod := getMod(obj.Token)
mod.types = append(mod.types, obj)
}
}
// Generate each module.
files := fs{}
for p, f := range extraFiles {
files.add(p, f)
}
for _, mod := range modules {
if err := mod.gen(files); err != nil {
return nil, err
}
}
// Finally emit the package metadata (NPM, TypeScript, and so on).
if err := genPackageMetadata(pkg, assemblyName, info.PackageReferences, files); err != nil {
return nil, err
}
return files, nil
}