// 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 { // Some schema field names may look like $ref or $schema. Remove the leading $ to make a valid identifier. // This could lead to a clash if both `$foo` and `foo` are defined, but we don't try to de-duplicate now. if strings.HasPrefix(s, "$") { s = s[1:] } 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" default: mapFmt = "ImmutableDictionary" } 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( `&`, "&", `<`, "<", `>`, ">", ) 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/// \n", indent) for _, l := range lines { fmt.Fprintf(w, "%s/// %s\n", indent, l) } fmt.Fprintf(w, "%s/// \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) if fieldName == paramName { // Avoid a no-op in case of field and property name collision. fieldName = "this." + fieldName } 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 hasConstInputs := false for _, prop := range r.InputProperties { allOptionalInputs = allOptionalInputs && !prop.IsRequired hasConstInputs = hasConstInputs || prop.ConstValue != nil } 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 } argsOverride := fmt.Sprintf("args ?? new %sArgs()", className) if hasConstInputs { argsOverride = "MakeArgs(args)" } // Write a comment prior to the constructor. fmt.Fprintf(w, " /// \n") fmt.Fprintf(w, " /// Create a %s resource with the given unique name, arguments, and options.\n", className) fmt.Fprintf(w, " /// \n") fmt.Fprintf(w, " ///\n") fmt.Fprintf(w, " /// The unique name of the resource\n") fmt.Fprintf(w, " /// The arguments used to populate this resource's properties\n") fmt.Fprintf(w, " /// A bag of options that control this resource's behavior\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, %s, MakeResourceOptions(options, \"\"))\n", tok, argsOverride) 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 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") } if hasConstInputs { // Write the method that will calculate the resource arguments. fmt.Fprintf(w, "\n") fmt.Fprintf(w, " private static %[1]s MakeArgs(%[1]s args)\n", argsType) fmt.Fprintf(w, " {\n") fmt.Fprintf(w, " args ??= new %sArgs();\n", className) for _, prop := range r.InputProperties { if prop.ConstValue != nil { v, err := primitiveValue(prop.ConstValue) if err != nil { return err } fmt.Fprintf(w, " args.%s = %s;\n", mod.propertyName(prop), v) } } fmt.Fprintf(w, " return args;\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? 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") default: fmt.Fprintf(w, "\n 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 { fmt.Fprintf(w, " /// \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, " /// \n") fmt.Fprintf(w, " ///\n") fmt.Fprintf(w, " /// The unique name of the resulting resource.\n") fmt.Fprintf(w, " /// The unique provider ID of the resource to lookup.\n") stateParam, stateRef := "", "" if r.StateInputs != nil { stateParam, stateRef = fmt.Sprintf("%sState? state = null, ", className), "state, " fmt.Fprintf(w, " /// Any extra arguments used during the lookup.\n") } fmt.Fprintf(w, " /// A bag of options that control this resource's behavior\n") fmt.Fprintf(w, " public static %s Get(string name, Input 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 visitObjectTypesAcc(t schema.Type, visitor func(*schema.ObjectType), visited codegen.Set) { if visited.Has(t) { return } visited.Add(t) switch t := t.(type) { case *schema.ArrayType: visitObjectTypesAcc(t.ElementType, visitor, visited) case *schema.MapType: visitObjectTypesAcc(t.ElementType, visitor, visited) case *schema.ObjectType: for _, p := range t.Properties { visitObjectTypesAcc(p.Type, visitor, visited) } visitor(t) case *schema.UnionType: for _, e := range t.ElementTypes { visitObjectTypesAcc(e, visitor, visited) } } } func visitObjectTypes(t schema.Type, visitor func(*schema.ObjectType)) { // Accumulator to avoid visiting the same node twice in case of recursive types. visited := codegen.Set{} visitObjectTypesAcc(t, visitor, visited) } 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 := mod.propertyName(p) 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{} computePropertyNames(pkg.Config, propertyNames) computePropertyNames(pkg.Provider.InputProperties, propertyNames) 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:") 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 }