diff --git a/.chronus/changes/go-ptr-2026-8-8-10-5-9.md b/.chronus/changes/go-ptr-2026-8-8-10-5-9.md new file mode 100644 index 0000000000..76b91b76e1 --- /dev/null +++ b/.chronus/changes/go-ptr-2026-8-8-10-5-9.md @@ -0,0 +1,7 @@ +--- +changeKind: internal +packages: + - "@azure-tools/typespec-go" +--- + +Add Ptr to the code model to explicitly model pointer types. \ No newline at end of file diff --git a/packages/typespec-go/src/codegen/core/example.ts b/packages/typespec-go/src/codegen/core/example.ts index 279b533710..70427bb6f3 100644 --- a/packages/typespec-go/src/codegen/core/example.ts +++ b/packages/typespec-go/src/codegen/core/example.ts @@ -119,7 +119,7 @@ export function generateExamples( }); } } - exampleText += `${indent.get()}clientFactory, err := ${go.getPackageName(pkg.src)}.NewClientFactory(${clientFactoryParamsExample.map((p) => getExampleValue(pkg, p.value, "\t", imports, p.parameter.byValue)).join(", ")}, nil)\n`; + exampleText += `${indent.get()}clientFactory, err := ${go.getPackageName(pkg.src)}.NewClientFactory(${clientFactoryParamsExample.map((p) => getExampleValue(pkg, p.value, "\t", imports, p.parameter.type.kind !== "ptr")).join(", ")}, nil)\n`; exampleText += `${indent.get()}if err != nil {\n`; exampleText += `${indent.push().get()}log.Fatalf("failed to create client: %v", err)\n`; exampleText += `${indent.pop().get()}}\n`; @@ -141,11 +141,11 @@ export function generateExamples( } } if (clientPrivateParameters.length > 0) { - clientRef += `${clientPrivateParameters.map((p) => getExampleValue(pkg, p.value, "\t", imports, p.parameter.byValue).slice(1)).join(", ")}`; + clientRef += `${clientPrivateParameters.map((p) => getExampleValue(pkg, p.value, "\t", imports, p.parameter.type.kind !== "ptr").slice(1)).join(", ")}`; } clientRef += `)`; } else { - exampleText += `${indent.get()}client, err := ${go.getPackageName(client.instance.constructors[0].pkg)}.${client.instance.constructors[0].name}(${clientParameters.map((p) => getExampleValue(pkg, p.value, "\t", imports, p.parameter.byValue).slice(1)).join(", ")}, cred, nil)\n`; + exampleText += `${indent.get()}client, err := ${go.getPackageName(client.instance.constructors[0].pkg)}.${client.instance.constructors[0].name}(${clientParameters.map((p) => getExampleValue(pkg, p.value, "\t", imports, p.parameter.type.kind !== "ptr").slice(1)).join(", ")}, cred, nil)\n`; exampleText += `${indent.get()}if err != nil {\n`; exampleText += `${indent.push().get()}log.Fatalf("failed to create client: %v", err)\n`; exampleText += `${indent.pop().get()}}\n`; @@ -281,7 +281,7 @@ export function generateExamples( ? fieldName : (example.responseEnvelope?.result.type as go.Model).name; if (method.returns.result?.kind === "monomorphicResult") { - resultByValue = method.returns.result.byValue; + resultByValue = method.returns.result.monomorphicType.kind !== "ptr"; } else if (method.returns.result?.kind === "polymorphicResult") { resultFieldName = method.returns.result.interface.name; resultByValue = false; @@ -365,7 +365,7 @@ function getExampleValue( case "any": return jsonToGo(example.value, indent); case "array": { - const isElementByValue = example.type.elementTypeByValue; + const isElementByValue = example.type.elementType.kind !== "ptr"; // if polymorphic, need to add type name in array, so inArray will be set to false // if other case, no need to add type name in array, so inArray will be set to true const isElementPolymorphic = example.type.elementType.kind === "interface"; @@ -378,7 +378,7 @@ function getExampleValue( } case "dictionary": { let exampleText = `${indent}${getRef(byValue)}${go.getTypeDeclaration(example.type, pkg)}{\n`; - const isValueByValue = example.type.valueTypeByValue; + const isValueByValue = example.type.valueType.kind !== "ptr"; const isValuePolymorphic = example.type.valueType.kind === "interface"; for (const key in example.value) { exampleText += `${indent}\t"${key}": ${getExampleValue(pkg, example.value[key], indent + "\t", imports, isValueByValue && !isValuePolymorphic).slice(indent.length + 1)},\n`; @@ -393,7 +393,7 @@ function getExampleValue( } for (const field in example.value) { const goField = example.type.fields.find((f) => f.name === field)!; - const isFieldByValue = goField.byValue ?? false; + const isFieldByValue = goField.type.kind !== "ptr"; const isFieldPolymorphic = goField.type.kind === "interface"; exampleText += `${indent}\t${field}: ${getExampleValue(pkg, example.value[field], indent + "\t", imports, isFieldByValue && !isFieldPolymorphic).slice(indent.length + 1)},\n`; } @@ -402,10 +402,10 @@ function getExampleValue( go.isAdditionalProperties(f), )!; const isAdditionalPropertiesFieldByValue = - additionalPropertiesField.type.valueTypeByValue ?? false; + additionalPropertiesField.type.valueType.kind !== "ptr"; const isAdditionalPropertiesPolymorphic = additionalPropertiesField.type.valueType.kind === "interface"; - exampleText += `${indent}\t${additionalPropertiesField.name}: ${getRef(additionalPropertiesField.byValue)}${go.getTypeDeclaration(additionalPropertiesField.type, pkg)}{\n`; + exampleText += `${indent}\t${additionalPropertiesField.name}: ${getRef(isAdditionalPropertiesFieldByValue)}${go.getTypeDeclaration(additionalPropertiesField.type, pkg)}{\n`; for (const key in example.additionalProperties) { exampleText += `${indent}\t"${key}": ${getExampleValue(pkg, example.additionalProperties[key], indent + "\t", imports, isAdditionalPropertiesFieldByValue && !isAdditionalPropertiesPolymorphic).slice(indent.length + 1)},\n`; } @@ -805,7 +805,7 @@ function isParamByValue(p: go.ParameterExample): boolean { case "interface": return p.value.kind === "null"; default: - return p.parameter.byValue; + return p.parameter.type.kind !== "ptr"; } } @@ -841,5 +841,5 @@ function getParamExampleValue( ).slice(1); } const fakeValue = generateFakeExample(param.type, param.name); - return getExampleValue(pkg, fakeValue, "\t", imports, param.byValue).slice(1); + return getExampleValue(pkg, fakeValue, "\t", imports, param.type.kind !== "ptr").slice(1); } diff --git a/packages/typespec-go/src/codegen/core/helpers.ts b/packages/typespec-go/src/codegen/core/helpers.ts index 49a89adfde..5d7ea85ec4 100644 --- a/packages/typespec-go/src/codegen/core/helpers.ts +++ b/packages/typespec-go/src/codegen/core/helpers.ts @@ -121,25 +121,16 @@ export function formatParameterTypeName( param: go.ClientOptionsType | go.ClientParameter | go.ParameterGroup, ): string { let typeName: string; - let required: boolean; switch (param.kind) { case "armClientOptions": - typeName = go.getTypeDeclaration(param, scope); - required = false; - break; case "clientOptions": - typeName = go.getTypeDeclaration(param, scope); - required = false; - break; case "paramGroup": typeName = go.getTypeDeclaration(param, scope); - required = param.required; break; default: typeName = go.getTypeDeclaration(param.type, scope); - required = param.byValue; } - return required ? typeName : `*${typeName}`; + return typeName; } // sorts parameters by their required state, ordering required before optional @@ -333,12 +324,7 @@ export function getParamName(param: go.MethodParameter): string { if (param.location === "client") { paramName = `client.${paramName}`; } - // client parameters with default values aren't emitted as pointer-to-type - if ( - !go.isRequiredParameter(param.style) && - !(param.location === "client" && go.isClientSideDefault(param.style)) && - !param.byValue - ) { + if (param.type.kind === "ptr") { paramName = `*${paramName}`; } return paramName; @@ -406,13 +392,14 @@ export function formatParamValue( return content; }; - switch (param.type.elementType.kind) { + const unwrappedElement = go.unwrapPtr(param.type.elementType); + switch (unwrappedElement.kind) { case "encodedBytes": imports.add("encoding/base64"); imports.add("strings"); return emitConvertOver( param.name, - `base64.${formatBytesEncoding(param.type.elementType.encoding)}Encoding.EncodeToString(${param.name}[i])`, + `base64.${formatBytesEncoding(unwrappedElement.encoding)}Encoding.EncodeToString(${param.name}[i])`, ); case "string": imports.add("strings"); @@ -420,12 +407,10 @@ export function formatParamValue( case "time": { imports.add("strings"); imports.add("github.com/Azure/azure-sdk-for-go/sdk/azcore/runtime/datetime"); - const elemVal = param.type.elementType.utc - ? `${param.name}[i].UTC()` - : `${param.name}[i]`; + const elemVal = unwrappedElement.utc ? `${param.name}[i].UTC()` : `${param.name}[i]`; return emitConvertOver( param.name, - `datetime.${param.type.elementType.format}(${elemVal}).String()`, + `datetime.${unwrappedElement.format}(${elemVal}).String()`, ); } default: @@ -436,7 +421,9 @@ export function formatParamValue( } } - return formatValue(paramName, param.type, imports); + // when we called getParamName it includes any dereference for a Ptr + // parameter, so we unwrap it here else we end up with a double deref + return formatValue(paramName, go.unwrapPtr(param.type), imports); } /** @@ -559,23 +546,16 @@ export function emitTimeParsing( return text; } -export function formatValue( - paramName: string, - type: go.WireType, - imports: ImportManager, - deref?: boolean, -): string { +export function formatValue(paramName: string, type: go.WireType, imports: ImportManager): string { // callers don't have enough context to know if paramName needs to be // dereferenced so we track that here when specified. note that not all // cases will require paramName to be dereferenced. - let star = ""; - if (deref === true) { - star = "*"; - } + const star = deref(type); - switch (type.kind) { + const unwrappedType = go.unwrapPtr(type); + switch (unwrappedType.kind) { case "constant": - if (type.type === "string") { + if (unwrappedType.type === "string") { return `string(${star}${paramName})`; } imports.add("fmt"); @@ -583,19 +563,19 @@ export function formatValue( case "encodedBytes": // a base-64 encoded value in string format imports.add("encoding/base64"); - return `base64.${formatBytesEncoding(type.encoding)}Encoding.EncodeToString(${paramName})`; + return `base64.${formatBytesEncoding(unwrappedType.encoding)}Encoding.EncodeToString(${paramName})`; case "etag": return `string(${star}${paramName})`; case "literal": // cannot use formatLiteralValue() since all values are treated as strings - switch (type.type.kind) { + switch (unwrappedType.type.kind) { case "constantDef": - return type.type.name; + return unwrappedType.type.name; default: - return `"${type.literal}"`; + return `"${unwrappedType.literal}"`; } case "scalar": - switch (type.type) { + switch (unwrappedType.type) { case "bool": imports.add("strconv"); return `strconv.FormatBool(${star}${paramName})`; @@ -612,12 +592,12 @@ export function formatValue( imports.add("strconv"); return `strconv.FormatInt(${star}${paramName}, 10)`; default: - throw new CodegenError("InternalError", `unhandled scalar type ${type.type}`); + throw new CodegenError("InternalError", `unhandled scalar type ${unwrappedType.type}`); } case "time": { imports.add("github.com/Azure/azure-sdk-for-go/sdk/azcore/runtime/datetime"); - const timeVal = type.utc ? `(${star}${paramName}).UTC()` : `${star}${paramName}`; - return `datetime.${type.format}(${timeVal}).String()`; + const timeVal = unwrappedType.utc ? `(${star}${paramName}).UTC()` : `${star}${paramName}`; + return `datetime.${unwrappedType.format}(${timeVal}).String()`; } default: return `${star}${paramName}`; @@ -1013,6 +993,8 @@ export function recursiveUnwrapMapSlice(item: go.WireType): go.WireType { switch (item.kind) { case "map": return recursiveUnwrapMapSlice(item.valueType); + case "ptr": + return recursiveUnwrapMapSlice(item.ptrType); case "slice": return recursiveUnwrapMapSlice(item.elementType); default: @@ -1020,14 +1002,9 @@ export function recursiveUnwrapMapSlice(item: go.WireType): go.WireType { } } -/** - * returns a * character when byValue is false - * - * @param byValue indicates if the type is passed by value - * @returns a * or the empty string - */ -export function star(byValue: boolean): string { - return byValue ? "" : "*"; +/** returns a * character when needing to dereference */ +export function deref(type: go.WireType): "*" | "" { + return type.kind === "ptr" ? "*" : ""; } /** diff --git a/packages/typespec-go/src/codegen/core/imports.ts b/packages/typespec-go/src/codegen/core/imports.ts index 3dce10acbb..6f00200408 100644 --- a/packages/typespec-go/src/codegen/core/imports.ts +++ b/packages/typespec-go/src/codegen/core/imports.ts @@ -103,6 +103,9 @@ export class ImportManager { case "map": this.addForType(type.valueType); break; + case "ptr": + this.addForType(type.ptrType); + break; case "slice": this.addForType(type.elementType); break; diff --git a/packages/typespec-go/src/codegen/core/models.ts b/packages/typespec-go/src/codegen/core/models.ts index d024e8d99d..104dac6c13 100644 --- a/packages/typespec-go/src/codegen/core/models.ts +++ b/packages/typespec-go/src/codegen/core/models.ts @@ -385,29 +385,30 @@ function generateModelDefs( const modelDefs = new Array(); for (const model of models) { for (const field of model.fields) { + const fieldType = go.unwrapPtr(field.type); const descriptionMods = new Array(); if (field.annotations.readOnly) { descriptionMods.push("READ-ONLY"); } else if ( field.annotations.required && - (field.type.kind !== "literal" || model.usage === go.UsageFlags.Output) + (fieldType.kind !== "literal" || model.usage === go.UsageFlags.Output) ) { descriptionMods.push("REQUIRED"); - } else if (field.type.kind === "literal") { + } else if (fieldType.kind === "literal") { if (!field.annotations.required) { descriptionMods.push("FLAG"); } descriptionMods.push("CONSTANT"); } - if (field.type.kind === "literal" && model.usage !== go.UsageFlags.Output) { + if (fieldType.kind === "literal" && model.usage !== go.UsageFlags.Output) { // add a comment with the const value for const properties that are sent over the wire if (field.docs.description) { field.docs.description += "\n"; } else { field.docs.description = ""; } - field.docs.description += `Field has constant value ${helpers.formatLiteralValue(field.type, false)}, any specified value is ignored.`; - } else if (field.type.kind === "rawJSON") { + field.docs.description += `Field has constant value ${helpers.formatLiteralValue(fieldType, false)}, any specified value is ignored.`; + } else if (fieldType.kind === "rawJSON") { // raw JSON is emitted as []byte, so document that the field contains raw // JSON and that the caller is responsible for marshaling their data structure. if (field.docs.description) { @@ -444,14 +445,15 @@ function generateModelDefs( let needsDateTimeMarshalling = false; let byteArrayFormat = false; for (const field of model.fields) { - if (field.type.kind !== "etag") { + const fieldType = go.unwrapPtr(field.type); + if (fieldType.kind !== "etag") { // azcore.ETag un/marshals on its own so no need to // import azcore as we don't explicitly reference the type - serdeImports.addForType(field.type); + serdeImports.addForType(fieldType); } - if (field.type.kind === "time") { + if (fieldType.kind === "time") { needsDateTimeMarshalling = true; - } else if (field.type.kind === "encodedBytes") { + } else if (fieldType.kind === "encodedBytes") { byteArrayFormat = true; } } @@ -555,15 +557,12 @@ function generateToMultipartForm(modelDef: ModelDef, indent: helpers.Indentation let method = `func (${receiver} ${modelDef.Model.name}) toMultipartFormData() (map[string]any, error) {\n`; method += `${indent.get()}objectMap := make(map[string]any)\n`; for (const field of modelDef.Model.fields) { - const fieldType = helpers.recursiveUnwrapMapSlice(field.type); - let star = ""; - if (!field.byValue) { - star = "*"; - } - if (!field.byValue) { + const star = helpers.deref(field.type); + if (field.type.kind === "ptr") { method += `${indent.get()}if ${receiver}.${field.name} != nil {\n`; indent.push(); } + const fieldType = helpers.recursiveUnwrapMapSlice(field.type); if (fieldType.kind === "model" && !fieldType.annotations.multipartFormData) { method += `${indent.get()}if err := populateMultipartJSON(objectMap, "${field.serializedName}", ${star}${receiver}.${field.name}); err != nil {\n`; method += `${indent.push().get()}return nil, err\n`; @@ -571,7 +570,7 @@ function generateToMultipartForm(modelDef: ModelDef, indent: helpers.Indentation } else { method += `${indent.get()}objectMap["${field.serializedName}"] = ${star}${receiver}.${field.name}\n`; } - if (!field.byValue) { + if (field.type.kind === "ptr") { indent.pop(); method += `${indent.get()}}\n`; } @@ -639,6 +638,10 @@ function generateJSONMarshallerBody( addlProps = field.type; continue; } + // pointer-ness is captured in the code model, so dispatch on the unwrapped + // type and read isPtr to decide pointer-to-type emission. + const fieldType = go.unwrapPtr(field.type); + const deref = helpers.deref(field.type); if (field.annotations.isDiscriminator) { if (field.defaultValue) { marshaller += `${indent.get()}objectMap["${field.serializedName}"] = ${helpers.formatLiteralValue(field.defaultValue, true)}\n`; @@ -647,29 +650,26 @@ function generateJSONMarshallerBody( // this will enable support for custom types that aren't (yet) described in the swagger. marshaller += `${indent.get()}objectMap["${field.serializedName}"] = ${receiver}.${field.name}\n`; } - } else if (field.type.kind === "encodedBytes") { + } else if (fieldType.kind === "encodedBytes") { imports.add("github.com/Azure/azure-sdk-for-go/sdk/azcore/runtime"); marshaller += `${indent.get()}populateByteArray(objectMap, "${field.serializedName}", ${receiver}.${field.name}, func() any {\n`; - marshaller += `${indent.push().get()}return runtime.EncodeByteArray(${receiver}.${field.name}, runtime.Base64${field.type.encoding}Format)\n`; + marshaller += `${indent.push().get()}return runtime.EncodeByteArray(${receiver}.${field.name}, runtime.Base64${fieldType.encoding}Format)\n`; marshaller += `${indent.pop().get()}})\n`; modelDef.SerDe.needsJSONPopulateByteArray = true; - } else if (go.isSlice(field.type, "encodedBytes")) { + } else if (go.isSlice(fieldType, "encodedBytes")) { imports.add("github.com/Azure/azure-sdk-for-go/sdk/azcore/runtime"); marshaller += `${indent.get()}populateByteArray(objectMap, "${field.serializedName}", ${receiver}.${field.name}, func() any {\n`; marshaller += `${indent.push().get()}encodedValue := make([]string, len(${receiver}.${field.name}))\n`; marshaller += `${indent.get()}for i := 0; i < len(${receiver}.${field.name}); i++ {\n`; - marshaller += `${indent.push().get()}encodedValue[i] = runtime.EncodeByteArray(${receiver}.${field.name}[i], runtime.Base64${field.type.elementType.encoding}Format)\n`; + marshaller += `${indent.push().get()}encodedValue[i] = runtime.EncodeByteArray(${receiver}.${field.name}[i], runtime.Base64${fieldType.elementType.encoding}Format)\n`; marshaller += `${indent.pop().get()}}\n`; marshaller += `${indent.get()}return encodedValue\n`; marshaller += `${indent.pop().get()}})\n`; modelDef.SerDe.needsJSONPopulateByteArray = true; - } else if (go.isSlice(field.type, "time")) { + } else if (go.isSlice(fieldType, "time")) { const source = `${receiver}.${field.name}`; - const elementType = field.type.elementType; - let elementPtr = "*"; - if (field.type.elementTypeByValue) { - elementPtr = ""; - } + const elementType = go.unwrapPtr(fieldType.elementType); + const elementPtr = helpers.deref(fieldType.elementType); imports.add("github.com/Azure/azure-sdk-for-go/sdk/azcore/runtime/datetime"); marshaller += `${indent.get()}aux := make([]${elementPtr}datetime.${elementType.format}, len(${source}), len(${source}))\n`; marshaller += `${indent.get()}for i := 0; i < len(${source}); i++ {\n`; @@ -688,8 +688,8 @@ function generateJSONMarshallerBody( marshaller += `${indent.get()}}\n`; marshaller += `${indent.get()}populate(objectMap, "${field.serializedName}", aux)\n`; modelDef.SerDe.needsJSONPopulate = true; - } else if (field.type.kind === "literal") { - const setter = `objectMap["${field.serializedName}"] = ${helpers.formatLiteralValue(field.type, true)}`; + } else if (fieldType.kind === "literal") { + const setter = `objectMap["${field.serializedName}"] = ${helpers.formatLiteralValue(fieldType, true)}`; if (!field.annotations.required) { marshaller += `${indent.get()}if ${receiver}.${field.name} != nil {\n`; marshaller += `${indent.push().get()}${setter}\n`; @@ -697,7 +697,7 @@ function generateJSONMarshallerBody( } else { marshaller += `${indent.get()}${setter}\n`; } - } else if (field.type.kind === "rawJSON") { + } else if (fieldType.kind === "rawJSON") { marshaller += `${indent.get()}populate(objectMap, "${field.serializedName}", json.RawMessage(${receiver}.${field.name}))\n`; modelDef.SerDe.needsJSONPopulate = true; } else { @@ -709,7 +709,7 @@ function generateJSONMarshallerBody( } if ( go.isScalar( - field.type, + fieldType, "uint8", "uint16", "uint32", @@ -719,42 +719,42 @@ function generateJSONMarshallerBody( "int32", "int64", ) && - field.type.encodeAsString + fieldType.encodeAsString ) { imports.add("strconv"); marshaller += `${indent.get()}populateAsString(objectMap, "${field.serializedName}", ${receiver}.${field.name}, func() string {\n`; - const isSigned = field.type.type.startsWith("int"); - let fieldExpr = `*${receiver}.${field.name}`; + const isSigned = fieldType.type.startsWith("int"); + let fieldExpr = `${deref}${receiver}.${field.name}`; if ( - (field.type.type.startsWith("uint") && field.type.type !== "uint64") || - (field.type.type.startsWith("int") && field.type.type !== "int64") + (fieldType.type.startsWith("uint") && fieldType.type !== "uint64") || + (fieldType.type.startsWith("int") && fieldType.type !== "int64") ) { fieldExpr = `${isSigned ? "int64" : "uint64"}(${fieldExpr})`; } marshaller += `${indent.push().get()}return strconv.${isSigned ? "FormatInt" : "FormatUint"}(${fieldExpr}, 10)\n`; marshaller += `${indent.pop().get()}})\n`; modelDef.SerDe.needsJSONPopulateAsString = true; - } else if (go.isScalar(field.type, "bool") && field.type.encodeAsString) { + } else if (go.isScalar(fieldType, "bool") && fieldType.encodeAsString) { imports.add("strconv"); marshaller += `${indent.get()}populateAsString(objectMap, "${field.serializedName}", ${receiver}.${field.name}, func() string {\n`; - marshaller += `${indent.push().get()}return strconv.FormatBool(*${receiver}.${field.name})\n`; + marshaller += `${indent.push().get()}return strconv.FormatBool(${deref}${receiver}.${field.name})\n`; marshaller += `${indent.pop().get()}})\n`; modelDef.SerDe.needsJSONPopulateAsString = true; } else { let populate: string; // some helpers require extra args after the common ones let populateArgs = ""; - if (field.type.kind === "time") { + if (fieldType.kind === "time") { imports.add("github.com/Azure/azure-sdk-for-go/sdk/azcore/runtime/datetime"); - populate = `populateTime[datetime.${field.type.format}]`; - populateArgs = `, ${field.type.utc}`; + populate = `populateTime[datetime.${fieldType.format}]`; + populateArgs = `, ${fieldType.utc}`; modelDef.SerDe.needsJSONPopulateTime = true; - } else if (field.type.kind === "any") { + } else if (fieldType.kind === "any") { populate = "populateAny"; modelDef.SerDe.needsJSONPopulateAny = true; - } else if (field.type.kind === "sliceArray") { + } else if (fieldType.kind === "sliceArray") { populate = "populateStringArray"; - populateArgs = `, "${getSliceArrayDelimiter(field.type.delimiter)}"`; + populateArgs = `, "${getSliceArrayDelimiter(fieldType.delimiter)}"`; modelDef.SerDe.needsJSONPopulateStringArray = true; } else { populate = "populate"; @@ -767,19 +767,19 @@ function generateJSONMarshallerBody( if (addlProps) { marshaller += `${indent.get()}if ${receiver}.AdditionalProperties != nil {\n`; marshaller += `${indent.push().get()}for key, val := range ${receiver}.AdditionalProperties {\n`; - if (addlProps.valueType.kind === "time" && addlProps.valueType.utc) { + if (go.isPtr(addlProps.valueType, "time") && addlProps.valueType.ptrType.utc) { // normalize utc datetimes before casting the (pointer) map value imports.add("github.com/Azure/azure-sdk-for-go/sdk/azcore/runtime/datetime"); marshaller += `${indent.push().get()}if val != nil {\n`; marshaller += `${indent.push().get()}utcTime := val.UTC()\n`; - marshaller += `${indent.get()}objectMap[key] = (*datetime.${addlProps.valueType.format})(&utcTime)\n`; + marshaller += `${indent.get()}objectMap[key] = (*datetime.${addlProps.valueType.ptrType.format})(&utcTime)\n`; marshaller += `${indent.pop().get()}} else {\n`; marshaller += `${indent.push().get()}objectMap[key] = nil\n`; marshaller += `${indent.pop().get()}}\n`; } else { let assignment = "val"; - if (addlProps.valueType.kind === "time") { - assignment = `(*${addlProps.valueType.format})(val)`; + if (go.isPtr(addlProps.valueType, "time")) { + assignment = `(*${addlProps.valueType.ptrType.format})(val)`; } marshaller += `${indent.push().get()}objectMap[key] = ${assignment}\n`; } @@ -853,18 +853,15 @@ function generateJSONUnmarshallerBody( const emitAddlProps = function (addlProps: go.Map): string { // indent is at the case body level when called let addlPropsText = `${indent.get()}if ${receiver}.AdditionalProperties == nil {\n`; - let ref = ""; - if (!addlProps.valueTypeByValue) { - ref = "&"; - } + const ref = addlProps.valueType.kind === "ptr" ? "&" : ""; addlPropsText += `${indent.push().get()}${receiver}.AdditionalProperties = ${go.getTypeDeclaration(addlProps, modelDef.Model.pkg)}{}\n`; addlPropsText += `${indent.pop().get()}}\n`; addlPropsText += `${indent.get()}if val != nil {\n`; - let auxType = go.getTypeDeclaration(addlProps.valueType, modelDef.Model.pkg); + let auxType = go.getTypeDeclaration(go.unwrapPtr(addlProps.valueType), modelDef.Model.pkg); let assignment = `${ref}aux`; - if (addlProps.valueType.kind === "time") { + if (go.isPtr(addlProps.valueType, "time")) { imports.add("time"); - auxType = addlProps.valueType.format; + auxType = addlProps.valueType.ptrType.format; assignment = `(*time.Time)(${assignment})`; } addlPropsText += `${indent.push().get()}var aux ${auxType}\n`; @@ -886,40 +883,40 @@ function generateJSONUnmarshallerBody( addlProps = field.type; continue; } + // dispatch on the unwrapped type; pointer-ness is captured in the code model. + const fieldType = go.unwrapPtr(field.type); unmarshalBody += `${indent.get()}case "${field.serializedName}":\n`; indent.push(); // case body level - if (hasDiscriminatorInterface(field.type)) { + if (hasDiscriminatorInterface(fieldType)) { unmarshalBody += generateDiscriminatorUnmarshaller(modelDef.Model, field, receiver, indent); needsErrCheck = true; - } else if (field.type.kind === "sliceArray") { - unmarshalBody += `${indent.get()}err = unpopulateStringArray(val, "${field.name}", &${receiver}.${field.name}, "${getSliceArrayDelimiter(field.type.delimiter)}")\n`; + } else if (fieldType.kind === "sliceArray") { + unmarshalBody += `${indent.get()}err = unpopulateStringArray(val, "${field.name}", &${receiver}.${field.name}, "${getSliceArrayDelimiter(fieldType.delimiter)}")\n`; modelDef.SerDe.needsJSONUnpopulateStringArray = true; needsErrCheck = true; - } else if (field.type.kind === "time") { - unmarshalBody += `${indent.get()}err = unpopulateTime[datetime.${field.type.format}](val, "${field.name}", &${receiver}.${field.name})\n`; + } else if (fieldType.kind === "time") { + unmarshalBody += `${indent.get()}err = unpopulateTime[datetime.${fieldType.format}](val, "${field.name}", &${receiver}.${field.name})\n`; modelDef.SerDe.needsJSONUnpopulateTime = true; needsErrCheck = true; - } else if (go.isSlice(field.type, "time")) { + } else if (go.isSlice(fieldType, "time")) { imports.add("time"); imports.add("github.com/Azure/azure-sdk-for-go/sdk/azcore/runtime/datetime"); - let elementPtr = "*"; - if (field.type.elementTypeByValue) { - elementPtr = ""; - } - unmarshalBody += `${indent.get()}var aux []${elementPtr}datetime.${field.type.elementType.format}\n`; + const elementType = go.unwrapPtr(fieldType.elementType); + const elementPtr = helpers.deref(fieldType.elementType); + unmarshalBody += `${indent.get()}var aux []${elementPtr}datetime.${elementType.format}\n`; unmarshalBody += `${indent.get()}err = unpopulate(val, "${field.name}", &aux)\n`; unmarshalBody += `${indent.get()}for _, au := range aux {\n`; unmarshalBody += `${indent.push().get()}${receiver}.${field.name} = append(${receiver}.${field.name}, (${elementPtr}time.Time)(au))\n`; unmarshalBody += `${indent.pop().get()}}\n`; modelDef.SerDe.needsJSONUnpopulate = true; needsErrCheck = true; - } else if (field.type.kind === "encodedBytes") { + } else if (fieldType.kind === "encodedBytes") { imports.add("github.com/Azure/azure-sdk-for-go/sdk/azcore/runtime"); unmarshalBody += `${indent.get()}if val != nil && string(val) != "null" {\n`; - unmarshalBody += `${indent.push().get()}err = runtime.DecodeByteArray(string(val), &${receiver}.${field.name}, runtime.Base64${field.type.encoding}Format)\n`; + unmarshalBody += `${indent.push().get()}err = runtime.DecodeByteArray(string(val), &${receiver}.${field.name}, runtime.Base64${fieldType.encoding}Format)\n`; unmarshalBody += `${indent.pop().get()}}\n`; needsErrCheck = true; - } else if (go.isSlice(field.type, "encodedBytes")) { + } else if (go.isSlice(fieldType, "encodedBytes")) { imports.add("github.com/Azure/azure-sdk-for-go/sdk/azcore/runtime"); unmarshalBody += `${indent.get()}var encodedValue []string\n`; unmarshalBody += `${indent.get()}err = unpopulate(val, "${field.name}", &encodedValue)\n`; @@ -927,17 +924,17 @@ function generateJSONUnmarshallerBody( indent.push(); unmarshalBody += `${indent.get()}${receiver}.${field.name} = make([][]byte, len(encodedValue))\n`; unmarshalBody += `${indent.get()}for i := 0; i < len(encodedValue) && err == nil; i++ {\n`; - unmarshalBody += `${indent.push().get()}err = runtime.DecodeByteArray(encodedValue[i], &${receiver}.${field.name}[i], runtime.Base64${field.type.elementType.encoding}Format)\n`; + unmarshalBody += `${indent.push().get()}err = runtime.DecodeByteArray(encodedValue[i], &${receiver}.${field.name}[i], runtime.Base64${fieldType.elementType.encoding}Format)\n`; unmarshalBody += `${indent.pop().get()}}\n`; indent.pop(); unmarshalBody += `${indent.get()}}\n`; modelDef.SerDe.needsJSONUnpopulate = true; needsErrCheck = true; - } else if (field.type.kind === "rawJSON") { + } else if (fieldType.kind === "rawJSON") { unmarshalBody += `${indent.get()}if string(val) != "null" {\n`; unmarshalBody += `${indent.push().get()}${receiver}.${field.name} = val\n`; unmarshalBody += `${indent.pop().get()}}\n`; - } else if (go.isScalar(field.type, "bool") && field.type.encodeAsString) { + } else if (go.isScalar(fieldType, "bool") && fieldType.encodeAsString) { imports.add("strconv"); imports.add("strings"); imports.add("github.com/Azure/azure-sdk-for-go/sdk/azcore/to"); @@ -953,7 +950,7 @@ function generateJSONUnmarshallerBody( needsErrCheck = true; } else if ( go.isScalar( - field.type, + fieldType, "uint8", "uint16", "uint32", @@ -963,13 +960,13 @@ function generateJSONUnmarshallerBody( "int32", "int64", ) && - field.type.encodeAsString + fieldType.encodeAsString ) { - const scalarType = field.type.type; + const scalarType = fieldType.type; imports.add("strconv"); imports.add("github.com/Azure/azure-sdk-for-go/sdk/azcore/to"); unmarshalBody += `${indent.get()}err = unpopulateFromString(val, "${field.name}", func(encodedValue string) error {\n`; - unmarshalBody += `${indent.push().get()}v, parseErr := strconv.${field.type.type.startsWith("int") ? "ParseInt" : "ParseUint"}(encodedValue, 10, 0)\n`; + unmarshalBody += `${indent.push().get()}v, parseErr := strconv.${fieldType.type.startsWith("int") ? "ParseInt" : "ParseUint"}(encodedValue, 10, 0)\n`; unmarshalBody += `${indent.get()}${helpers.buildIfBlock(indent, { condition: "parseErr == nil", body: (indent) => { @@ -989,7 +986,7 @@ function generateJSONUnmarshallerBody( needsErrCheck = true; } else { const unpopulateField = `err = unpopulate(val, "${field.name}", &${receiver}.${field.name})\n`; - if (field.type.kind === "string" && field.annotations.unmarshalEmptyStringAsNil) { + if (fieldType.kind === "string" && field.annotations.unmarshalEmptyStringAsNil) { unmarshalBody += `${indent.get()}${helpers.buildIfBlock(indent, { condition: `string(val) != \`""\``, body: (indent) => `${indent.get()}${unpopulateField}`, @@ -1262,17 +1259,18 @@ function generateXMLMarshaller( } text += generateAliasType(modelDef.Model, receiver, true, imports, indent); for (const field of modelDef.Model.fields) { - if (field.type.kind === "slice") { + const fieldType = go.unwrapPtr(field.type); + if (fieldType.kind === "slice") { text += `${indent.get()}if ${receiver}.${field.name} != nil {\n`; text += `${indent.push().get()}aux.${field.name} = &${receiver}.${field.name}\n`; text += `${indent.pop().get()}}\n`; - } else if (go.isAdditionalProperties(field) || field.type.kind === "map") { + } else if (go.isAdditionalProperties(field) || fieldType.kind === "map") { text += `${indent.get()}aux.${field.name} = (additionalProperties)(${receiver}.${field.name})\n`; - } else if (field.type.kind === "encodedBytes") { + } else if (fieldType.kind === "encodedBytes") { imports.add("github.com/Azure/azure-sdk-for-go/sdk/azcore/runtime"); text += `${indent.get()}if ${receiver}.${field.name} != nil {\n`; indent.push(); - text += `${indent.get()}encoded${field.name} := runtime.EncodeByteArray(${receiver}.${field.name}, runtime.Base64${field.type.encoding}Format)\n`; + text += `${indent.get()}encoded${field.name} := runtime.EncodeByteArray(${receiver}.${field.name}, runtime.Base64${fieldType.encoding}Format)\n`; text += `${indent.get()}aux.${field.name} = &encoded${field.name}\n`; indent.pop(); text += `${indent.get()}}\n`; @@ -1304,17 +1302,18 @@ function generateXMLUnmarshaller( text += `${indent.push().get()}return err\n`; text += `${indent.pop().get()}}\n`; for (const field of modelDef.Model.fields) { - if (field.type.kind === "time") { + const fieldType = go.unwrapPtr(field.type); + if (fieldType.kind === "time") { text += `${indent.get()}if aux.${field.name} != nil && !(*time.Time)(aux.${field.name}).IsZero() {\n`; text += `${indent.push().get()}${receiver}.${field.name} = (*time.Time)(aux.${field.name})\n`; text += `${indent.pop().get()}}\n`; - } else if (go.isAdditionalProperties(field) || field.type.kind === "map") { + } else if (go.isAdditionalProperties(field) || fieldType.kind === "map") { text += `${indent.get()}${receiver}.${field.name} = (map[string]*string)(aux.${field.name})\n`; - } else if (field.type.kind === "encodedBytes") { + } else if (fieldType.kind === "encodedBytes") { imports.add("github.com/Azure/azure-sdk-for-go/sdk/azcore/runtime"); text += `${indent.get()}if aux.${field.name} != nil {\n`; indent.push(); - text += `${indent.get()}if err := runtime.DecodeByteArray(*aux.${field.name}, &${receiver}.${field.name}, runtime.Base64${field.type.encoding}Format); err != nil {\n`; + text += `${indent.get()}if err := runtime.DecodeByteArray(*aux.${field.name}, &${receiver}.${field.name}, runtime.Base64${fieldType.encoding}Format); err != nil {\n`; text += `${indent.push().get()}return err\n`; text += `${indent.pop().get()}}\n`; indent.pop(); @@ -1345,15 +1344,16 @@ function generateAliasType( text += `${indent.get()}aux := &struct {\n`; text += `${indent.push().get()}*alias\n`; for (const field of modelType.fields) { + const fieldType = go.unwrapPtr(field.type); const sn = getXMLSerialization(field); - if (field.type.kind === "time") { + if (fieldType.kind === "time") { imports.add("github.com/Azure/azure-sdk-for-go/sdk/azcore/runtime/datetime"); - text += `${indent.get()}${field.name} *datetime.${field.type.format} \`xml:"${sn}"\`\n`; - } else if (go.isAdditionalProperties(field) || field.type.kind === "map") { + text += `${indent.get()}${field.name} *datetime.${fieldType.format} \`xml:"${sn}"\`\n`; + } else if (go.isAdditionalProperties(field) || fieldType.kind === "map") { text += `${indent.get()}${field.name} additionalProperties \`xml:"${sn}"\`\n`; - } else if (field.type.kind === "slice") { - text += `${indent.get()}${field.name} *${go.getTypeDeclaration(field.type, modelType.pkg)} \`xml:"${sn}"\`\n`; - } else if (field.type.kind === "encodedBytes") { + } else if (fieldType.kind === "slice") { + text += `${indent.get()}${field.name} *${go.getTypeDeclaration(fieldType, modelType.pkg)} \`xml:"${sn}"\`\n`; + } else if (fieldType.kind === "encodedBytes") { text += `${indent.get()}${field.name} *string \`xml:"${sn}"\`\n`; } } @@ -1366,11 +1366,12 @@ function generateAliasType( if (forMarshal) { // emit code to initialize time fields for (const field of modelType.fields) { - if (field.type.kind !== "time") { + const fieldType = go.unwrapPtr(field.type); + if (fieldType.kind !== "time") { continue; } imports.add("github.com/Azure/azure-sdk-for-go/sdk/azcore/runtime/datetime"); - text += `${indent.get()}${field.name}: (*datetime.${field.type.format})(${receiver}.${field.name}),\n`; + text += `${indent.get()}${field.name}: (*datetime.${fieldType.format})(${receiver}.${field.name}),\n`; } } text += `${indent.pop().get()}}\n`; @@ -1464,9 +1465,10 @@ class ModelDef { text += helpers.formatDocComment(field.docs); } let typeName = go.getTypeDeclaration(field.type, this.Model.pkg); - if (field.type.kind === "literal") { - // for constants we use the underlying type name - typeName = go.getLiteralTypeDeclaration(field.type.type); + const fieldType = go.unwrapPtr(field.type); + if (fieldType.kind === "literal") { + // for constants we use the underlying type name; getTypeDeclaration above emits the pointer. + typeName = `${helpers.deref(field.type)}${go.getLiteralTypeDeclaration(fieldType.type)}`; } let serialization = field.serializedName; if (this.Format === "JSON") { @@ -1479,7 +1481,7 @@ class ModelDef { if (this.Format === "XML" && !go.isAdditionalProperties(field)) { tag = ` \`xml:"${serialization}"\``; } - text += `${indent.get()}${field.name} ${helpers.star(field.byValue)}${typeName}${tag}\n`; + text += `${indent.get()}${field.name} ${typeName}${tag}\n`; first = false; } diff --git a/packages/typespec-go/src/codegen/core/operations.ts b/packages/typespec-go/src/codegen/core/operations.ts index ed85751397..ebc60141e5 100644 --- a/packages/typespec-go/src/codegen/core/operations.ts +++ b/packages/typespec-go/src/codegen/core/operations.ts @@ -298,7 +298,7 @@ function generateConstructors( ): string { imports.add("github.com/Azure/azure-sdk-for-go/sdk/azcore/runtime"); let bodyText = `${indent.get()}if options == nil {\n`; - bodyText += `${indent.push().get()}options = &${optionsTypeName}{}\n`; + bodyText += `${indent.push().get()}options = ${optionsTypeName}{}\n`; bodyText += `${indent.pop().get()}}\n`; let apiVersionConfig = ""; // check if there's an api version parameter @@ -410,7 +410,7 @@ function generateConstructors( const tokenPolicy = `\n${indent.get()}PerCall: []policy.Policy{\n${indent.get()}runtime.NewBearerTokenPolicy(credential, []string{c.Audience + "${helpers.splitScope(credentialParam.type.scopes[0]).scope}"}, ${tokenPolicyOpts}),\n${indent.get()}},\n`; indent.pop(); // back to level 1 prolog = emitProlog( - go.getTypeDeclaration(clientOptions, client.pkg), + go.getTypeDeclaration(clientOptions, client.pkg, true), true, tokenPolicy, ); @@ -434,7 +434,7 @@ function generateConstructors( break; } } else { - prolog = emitProlog(go.getTypeDeclaration(clientOptions, client.pkg), false); + prolog = emitProlog(go.getTypeDeclaration(clientOptions, client.pkg, true), false); } // add client options last @@ -466,7 +466,7 @@ function generateConstructors( } ctorText += `${indent.get()}${param.name} := ${helpers.formatLiteralValue(param.style.defaultValue, false)}\n`; ctorText += `${indent.get()}if options.${name} != ${helpers.zeroValue(param)} {\n`; - ctorText += `${indent.push().get()}${param.name} = ${helpers.star(param.byValue)}options.${name}\n`; + ctorText += `${indent.push().get()}${param.name} = options.${name}\n`; ctorText += `${indent.pop().get()}}\n`; } }; diff --git a/packages/typespec-go/src/codegen/core/options.ts b/packages/typespec-go/src/codegen/core/options.ts index abdf115ed9..d15031ea3c 100644 --- a/packages/typespec-go/src/codegen/core/options.ts +++ b/packages/typespec-go/src/codegen/core/options.ts @@ -69,11 +69,7 @@ function emit(pkg: go.PackageContent, struct: go.Struct, imports: ImportManager) typeName = go.getLiteralTypeDeclaration(field.type.type); } - let pointer = "*"; - if (field.byValue) { - pointer = ""; - } - text += `${indent.get()}${naming.capitalize(field.name)} ${pointer}${typeName}\n`; + text += `${indent.get()}${naming.capitalize(field.name)} ${typeName}\n`; first = false; } } diff --git a/packages/typespec-go/src/codegen/core/request-handler.ts b/packages/typespec-go/src/codegen/core/request-handler.ts index 6b2c461b5a..2fe49d078b 100644 --- a/packages/typespec-go/src/codegen/core/request-handler.ts +++ b/packages/typespec-go/src/codegen/core/request-handler.ts @@ -160,7 +160,10 @@ export function createRequestHandler( text += emitParamGroupCheck(pp, indent); text += `${indent.push().get()}${defaultValue} = ${helpers.getParamName(pp)}\n`; text += `${indent.pop().get()}}\n`; - paramValue = helpers.formatValue(defaultValue, pp.type, imports); + // we've created a local var with the underlying type of the + // optional param, so we must unwrap before assigning the value + // to prevent attempting to dereference it. + paramValue = helpers.formatValue(defaultValue, go.unwrapPtr(pp.type), imports); } else { // param isn't required, so emit a local var with // the correct default value, then populate it with @@ -479,17 +482,13 @@ function emitBody( } text += `${indent.get()}XMLName xml.Name \`xml:"${tagName}"\`\n`; const fieldName = naming.capitalize(bodyParam.name); - let tag = go.getTypeDeclaration(bodyParam.type.elementType, method.receiver.type.pkg); + let tag = go.getTypeDeclaration(go.unwrapPtr(bodyParam.type.elementType), method.receiver.type.pkg); if (bodyParam.type.elementType.kind === "model" && bodyParam.type.elementType.xml?.name) { tag = bodyParam.type.elementType.xml.name; } text += `${indent.get()}${fieldName} *${go.getTypeDeclaration(bodyParam.type, method.receiver.type.pkg)} \`xml:"${tag}"\`\n`; text += `${indent.pop().get()}}\n`; - let addr = "&"; - if (!go.isRequiredParameter(bodyParam.style) && !bodyParam.byValue) { - addr = ""; - } - body = `wrapper{${fieldName}: ${addr}${body}}`; + body = `wrapper{${fieldName}: &${body}}`; } else if (bodyParam.type.kind === "time") { // utc datetimes are normalized to UTC before serialization. non-RFC3339 // formats are wrapped in the internal time type; RFC3339 relies on the @@ -505,8 +504,8 @@ function emitBody( go.isSlice(bodyParam.type, "time") && isSliceOfTimeForMarshalling(bodyParam.type) ) { - const timeType = bodyParam.type.elementType; - const elementPtr = bodyParam.type.elementTypeByValue ? "" : "*"; + const timeType = go.unwrapPtr(bodyParam.type.elementType); + const elementPtr = bodyParam.type.elementType.kind === "ptr" ? "*" : ""; imports.add("github.com/Azure/azure-sdk-for-go/sdk/azcore/runtime/datetime"); text += `${indent.get()}aux := make([]${elementPtr}datetime.${timeType.format}, len(${body}))\n`; text += `${indent.get()}for i := 0; i < len(${body}); i++ {\n`; @@ -524,7 +523,7 @@ function emitBody( text += `${indent.get()}}\n`; body = "aux"; } else if (go.isMap(bodyParam.type, "time")) { - const timeType = bodyParam.type.valueType; + const timeType = bodyParam.type.valueType.ptrType; imports.add("github.com/Azure/azure-sdk-for-go/sdk/azcore/runtime/datetime"); text += `${indent.get()}aux := map[string]*datetime.${timeType.format}{}\n`; text += `${indent.get()}for k, v := range ${body} {\n`; @@ -582,7 +581,7 @@ function emitBody( } else if (bodyParam.bodyFormat === "Text") { imports.add("strings"); imports.add("github.com/Azure/azure-sdk-for-go/sdk/azcore/streaming"); - const body = helpers.formatValue(helpers.getParamName(bodyParam), bodyParam.type, imports); + const body = helpers.formatParamValue(bodyParam, imports, indent); if (go.isRequiredParameter(bodyParam.style)) { text += `${indent.get()}body := streaming.NopCloser(strings.NewReader(${body}))\n`; text += emitSetBodyWithErrCheck( @@ -611,7 +610,7 @@ function emitBody( text += `${indent.get()}body := struct {\n`; indent.push(); for (const partialBodyParam of partialBodyParams) { - text += `${indent.get()}${naming.capitalize(partialBodyParam.serializedName)} ${helpers.star(partialBodyParam.byValue)}${go.getTypeDeclaration(partialBodyParam.type, method.receiver.type.pkg)} \`${partialBodyParam.format.toLowerCase()}:"${partialBodyParam.serializedName}"\`\n`; + text += `${indent.get()}${naming.capitalize(partialBodyParam.serializedName)} ${go.getTypeDeclaration(partialBodyParam.type, method.receiver.type.pkg)} \`${partialBodyParam.format.toLowerCase()}:"${partialBodyParam.serializedName}"\`\n`; } indent.pop(); text += `${indent.get()}}{\n`; @@ -748,9 +747,12 @@ function emitClientSideDefault( break; } + // we've created a local var with the underlying type of the + // optional param, so we must unwrap before assigning the value + // to prevent attempting to dereference it. const setterFormatText = setterFormat( `"${serializedName}"`, - helpers.formatValue(defaultVar, param.type, imports), + helpers.formatValue(defaultVar, go.unwrapPtr(param.type), imports), ); text += setterFormatText; // setterFormat can return the empty string in some cases. @@ -994,8 +996,9 @@ function getContentTypeValue( * @param type the slice of time.Time to inspect * @returns true if the slice needs custom marshalling */ -function isSliceOfTimeForMarshalling(type: go.Slice): boolean { - switch (type.elementType.format) { +function isSliceOfTimeForMarshalling(type: go.Slice | go.Time>): boolean { + const elementType = go.unwrapPtr(type.elementType); + switch (elementType.format) { case "PlainDate": case "RFC1123": case "RFC7231": @@ -1005,8 +1008,9 @@ function isSliceOfTimeForMarshalling(type: go.Slice): boolean { case "RFC3339": // RFC3339 normally uses the default time.Time marshaller, but utc slices // must be normalized to UTC, which requires building the wrapper slice. - return type.elementType.utc; + return elementType.utc; default: return false; } + return false; } diff --git a/packages/typespec-go/src/codegen/core/response-handler.ts b/packages/typespec-go/src/codegen/core/response-handler.ts index f233438d30..99be04e306 100644 --- a/packages/typespec-go/src/codegen/core/response-handler.ts +++ b/packages/typespec-go/src/codegen/core/response-handler.ts @@ -143,8 +143,8 @@ function generateResponseUnmarshaller( return unmarshallerText; } else if (go.isSlice(type, "time")) { // unmarshalling arrays of date/time is a little more involved - const timeType = type.elementType; - const elementPtr = type.elementTypeByValue ? "" : "*"; + const timeType = go.unwrapPtr(type.elementType); + const elementPtr = type.elementType.kind === "ptr" ? "*" : ""; imports.add("github.com/Azure/azure-sdk-for-go/sdk/azcore/runtime/datetime"); unmarshallerText += `${indent.get()}var aux []${elementPtr}datetime.${timeType.format}\n`; unmarshallerText += `${indent.get()}if err := runtime.UnmarshalAs${format}(resp, &aux); err != nil {\n`; @@ -157,7 +157,7 @@ function generateResponseUnmarshaller( unmarshallerText += `${indent.get()}${unmarshalTarget} = cp\n`; return unmarshallerText; } else if (go.isMap(type, "time")) { - const timeType = type.valueType; + const timeType = type.valueType.ptrType; imports.add("github.com/Azure/azure-sdk-for-go/sdk/azcore/runtime/datetime"); unmarshallerText += `${indent.get()}aux := map[string]*datetime.${timeType.format}{}\n`; unmarshallerText += `${indent.get()}if err := runtime.UnmarshalAs${format}(resp, &aux); err != nil {\n`; @@ -188,6 +188,7 @@ function generateResponseUnmarshaller( unmarshallerText += `${indent.push().get()}return ${zeroValue}, err\n`; unmarshallerText += `${indent.pop().get()}}\n`; let resultVar: string; + type = go.unwrapPtr(type); switch (type.kind) { case "scalar": resultVar = "parsedBody"; @@ -250,17 +251,18 @@ function formatHeaderResponseValue( indent.push(); let name = naming.uncapitalize(headerResp.fieldName); let byRef = "&"; - switch (headerResp.type.kind) { + const headerRespType = go.unwrapPtr(headerResp.type); + switch (headerRespType.kind) { case "constant": case "etag": - text += `${indent.get()}${respObj}.${headerResp.fieldName} = (*${go.getTypeDeclaration(headerResp.type, method.receiver.type.pkg)})(&val)\n`; + text += `${indent.get()}${respObj}.${headerResp.fieldName} = (${go.getTypeDeclaration(headerResp.type, method.receiver.type.pkg)})(&val)\n`; indent.pop(); text += `${indent.get()}}\n`; return text; case "encodedBytes": // a base-64 encoded value in string format imports.add("encoding/base64"); - text += `${indent.get()}${name}, err := base64.${helpers.formatBytesEncoding(headerResp.type.encoding)}Encoding.DecodeString(val)\n`; + text += `${indent.get()}${name}, err := base64.${helpers.formatBytesEncoding(headerRespType.encoding)}Encoding.DecodeString(val)\n`; byRef = ""; break; case "literal": @@ -269,20 +271,20 @@ function formatHeaderResponseValue( text += `${indent.get()}}\n`; return text; case "scalar": - text += helpers.emitScalarParsing(headerResp.type, "val", name, imports, indent); + text += helpers.emitScalarParsing(headerRespType, "val", name, imports, indent); break; case "string": text += `${indent.get()}${respObj}.${headerResp.fieldName} = &val\n`; text += `${indent.pop().get()}}\n`; return text; case "time": - if (headerResp.type.format === "Unix") { + if (headerRespType.format === "Unix") { imports.add("github.com/Azure/azure-sdk-for-go/sdk/azcore/to"); - text += helpers.emitTimeParsing("val", headerResp.type, "sec", imports, indent); + text += helpers.emitTimeParsing("val", headerRespType, "sec", imports, indent); name = "to.Ptr(time.Unix(sec, 0))"; byRef = ""; } else { - text += helpers.emitTimeParsing("val", headerResp.type, name, imports, indent); + text += helpers.emitTimeParsing("val", headerRespType, name, imports, indent); } } diff --git a/packages/typespec-go/src/codegen/core/responses.ts b/packages/typespec-go/src/codegen/core/responses.ts index 18f0ed97d3..82e5c5418e 100644 --- a/packages/typespec-go/src/codegen/core/responses.ts +++ b/packages/typespec-go/src/codegen/core/responses.ts @@ -187,27 +187,18 @@ function emit( } } - let byValue = true; - if (respEnv.result.kind === "monomorphicResult") { - byValue = respEnv.result.byValue; - } - fields.push({ docs: respEnv.result.docs, - field: `${indent.get()}${respEnv.result.fieldName} ${helpers.star(byValue)}${go.getTypeDeclaration(respType, respEnv.method.receiver.type.pkg)}${tag}\n`, + field: `${indent.get()}${respEnv.result.fieldName} ${go.getTypeDeclaration(respType, respEnv.method.receiver.type.pkg)}${tag}\n`, }); } } for (const header of respEnv.headers) { imports.addForType(header.type); - let byValue = true; - if (header.kind === "headerScalarResponse") { - byValue = header.byValue; - } fields.push({ docs: header.docs, - field: `${indent.get()}${header.fieldName} ${helpers.star(byValue)}${go.getTypeDeclaration(header.type, respEnv.method.receiver.type.pkg)}\n`, + field: `${indent.get()}${header.fieldName} ${go.getTypeDeclaration(header.type, respEnv.method.receiver.type.pkg)}\n`, }); } diff --git a/packages/typespec-go/src/codegen/core/unions.ts b/packages/typespec-go/src/codegen/core/unions.ts index c5d3ed6d9d..b1095cddd1 100644 --- a/packages/typespec-go/src/codegen/core/unions.ts +++ b/packages/typespec-go/src/codegen/core/unions.ts @@ -64,7 +64,7 @@ function generateUnionTypes( for (const field of goUnion.fields) { imports.addForType(field.type); text += helpers.formatDocCommentWithPrefix(field.name, field.docs); - text += `${indent.get()}${field.name} ${helpers.star(field.byValue)}${go.getTypeDeclaration(field.type, goUnion.pkg)}\n`; + text += `${indent.get()}${field.name} ${go.getTypeDeclaration(field.type, goUnion.pkg)}\n`; } text += `${indent.pop().get()}}\n\n`; } @@ -271,13 +271,13 @@ function generateUnmarshalObjects(goUnion: go.UnionStruct, indent: helpers.Inden const field = jsonObjects[i]; if (field.type.kind === "map") { break; - } else if (field.type.kind !== "model") { + } else if (!go.isPtr(field.type, "model")) { // we already validated this earlier. however it lets // the compiler treat field.type as a model type. throw new CodegenError("InternalError", `unexpected union type ${field.type.kind}`); } - const condition = `hasRequiredFields(rawMsg, ${getJsonFieldsForProbe(field.type)})`; + const condition = `hasRequiredFields(rawMsg, ${getJsonFieldsForProbe(field.type.ptrType)})`; const body = (indent: helpers.Indentation) => `${indent.get()}err = json.Unmarshal(data, &${receiver}.${field.name})\n`; if (i === 0) { @@ -466,7 +466,7 @@ function groupMixedJsonNumbers(goUnion: go.UnionStruct): Array { function groupJsonObjects(goUnion: go.UnionStruct): Array { const objectFields = new Array(); for (const field of goUnion.fields) { - if (field.type.kind === "map" || field.type.kind === "model") { + if (field.type.kind === "map" || go.isPtr(field.type, "model")) { objectFields.push(field); } } @@ -477,13 +477,13 @@ function groupJsonObjects(goUnion: go.UnionStruct): Array { // more fields have a better chance of hasRequiredFields() not // returning a false positive. maps always appears after models objectFields.sort((a, b) => { - if (a.type.kind === "model" && b.type.kind === "model") { - return b.type.fields.length - a.type.fields.length; + if (go.isPtr(a.type, "model") && go.isPtr(b.type, "model")) { + return b.type.ptrType.fields.length - a.type.ptrType.fields.length; } else if (a.type.kind === "map" && b.type.kind === "map") { return 0; } else if (a.type.kind === "map") { return 1; - } else if (a.type.kind === "model") { + } else if (go.isPtr(a.type, "model")) { return -1; } throw new CodegenError("InternalError", `unexpected union types ${a.type.kind} ${b.type.kind}`); @@ -501,6 +501,10 @@ function groupJsonObjects(goUnion: go.UnionStruct): Array { * @returns the ScalarType or undefined */ function isJsonNumberType(variantType: go.UnionVariantType): go.ScalarType | undefined { + if (variantType.kind !== "ptr") { + return undefined; + } + const isScalarJsonNubmer = function (scalar: go.ScalarType): go.ScalarType | undefined { switch (scalar) { case "bool": @@ -512,16 +516,17 @@ function isJsonNumberType(variantType: go.UnionVariantType): go.ScalarType | und } }; - switch (variantType.kind) { + const unwrapped = go.unwrapPtr(variantType); + switch (unwrapped.kind) { case "literal": - switch (variantType.type.kind) { + switch (unwrapped.type.kind) { case "scalar": - return isScalarJsonNubmer(variantType.type.type); + return isScalarJsonNubmer(unwrapped.type.type); default: return undefined; } case "scalar": - return isScalarJsonNubmer(variantType.type); + return isScalarJsonNubmer(unwrapped.type); default: return undefined; } @@ -543,9 +548,10 @@ function getJsonProbeKindForType(variantType: go.UnionVariantType): string { } }; - switch (variantType.kind) { + const unwrapped = go.unwrapPtr(variantType); + switch (unwrapped.kind) { case "constant": - switch (variantType.type) { + switch (unwrapped.type) { case "bool": return "jsonBool"; case "string": @@ -554,9 +560,9 @@ function getJsonProbeKindForType(variantType: go.UnionVariantType): string { return "jsonNumber"; } case "literal": - switch (variantType.type.kind) { + switch (unwrapped.type.kind) { case "scalar": - return getScalarProbe(variantType.type.type); + return getScalarProbe(unwrapped.type.type); default: return "jsonString"; } @@ -564,7 +570,7 @@ function getJsonProbeKindForType(variantType: go.UnionVariantType): string { case "model": return "jsonObject"; case "scalar": - return getScalarProbe(variantType.type); + return getScalarProbe(unwrapped.type); case "slice": return "jsonArray"; case "string": diff --git a/packages/typespec-go/src/codegen/fake/servers.ts b/packages/typespec-go/src/codegen/fake/servers.ts index 89fc7f182a..f5a34f5872 100644 --- a/packages/typespec-go/src/codegen/fake/servers.ts +++ b/packages/typespec-go/src/codegen/fake/servers.ts @@ -497,30 +497,33 @@ function generateServerTransportMethods( let contentToMarshal: string; const respField = getResultFieldName(method.returns.result); const getResponseField = `server.GetResponse(respr).${respField}`; - switch (method.returns.result.monomorphicType.kind) { - case "scalar": { - imports.add("github.com/Azure/azure-sdk-for-go/sdk/azcore/to"); - // create a local var that will hold the string-formatted scalar - contentToMarshal = `formatted${respField}`; - content += `${indent.get()}var ${contentToMarshal} *string\n`; - const localVar = naming.uncapitalize(respField); - const resultType = method.returns.result.monomorphicType; - // if value := server.GetResponse(respr).Value; value != nil {...format as string...} - content += `${indent.get()}${helpers.buildIfBlock(indent, { - condition: `${localVar} := ${getResponseField}; ${localVar} != nil`, - body: (indent) => - `${indent.get()}${contentToMarshal} = to.Ptr(${helpers.formatValue(localVar, resultType, imports, true)})\n`, - })}\n`; - break; + if (go.isPtr(method.returns.result.monomorphicType, "scalar", "string")) { + switch (method.returns.result.monomorphicType.ptrType.kind) { + case "scalar": { + imports.add("github.com/Azure/azure-sdk-for-go/sdk/azcore/to"); + // create a local var that will hold the string-formatted scalar + contentToMarshal = `formatted${respField}`; + content += `${indent.get()}var ${contentToMarshal} *string\n`; + const localVar = naming.uncapitalize(respField); + // we want the wrapped type so formatValue will deref it + const monomorphicType = method.returns.result.monomorphicType; + // if value := server.GetResponse(respr).Value; value != nil {...format as string...} + content += `${indent.get()}${helpers.buildIfBlock(indent, { + condition: `${localVar} := ${getResponseField}; ${localVar} != nil`, + body: (indent) => + `${indent.get()}${contentToMarshal} = to.Ptr(${helpers.formatValue(localVar, monomorphicType, imports)})\n`, + })}\n`; + break; + } + case "string": + contentToMarshal = getResponseField; + break; } - case "string": - contentToMarshal = getResponseField; - break; - default: - throw new CodegenError( - "UnsupportedTsp", - `unsupported text return kind ${method.returns.result.monomorphicType.kind} for method ${method.receiver.type.name}.${method.name}`, - ); + } else { + throw new CodegenError( + "UnsupportedTsp", + `unsupported text return kind ${method.returns.result.monomorphicType.kind} for method ${method.receiver.type.name}.${method.name}`, + ); } content += `${indent.get()}resp, err := server.MarshalResponseAsText(respContent, ${contentToMarshal}, req)\n`; } else { @@ -532,9 +535,9 @@ function generateServerTransportMethods( respField = ""; } let responseField = `server.GetResponse(respr)${respField}`; - if (method.returns.result.monomorphicType.kind === "time") { + if (go.isPtr(method.returns.result.monomorphicType, "time")) { imports.add("github.com/Azure/azure-sdk-for-go/sdk/azcore/runtime/datetime"); - responseField = `(*datetime.${method.returns.result.monomorphicType.format})(${responseField})`; + responseField = `(*datetime.${method.returns.result.monomorphicType.ptrType.format})(${responseField})`; } content += `${indent.get()}resp, err := server.MarshalResponseAs${method.returns.result.format}(respContent, ${responseField}, req)\n`; } @@ -561,7 +564,7 @@ function generateServerTransportMethods( content += `${indent.pop().get()}}\n`; } else { content += `${indent.get()}if val := server.GetResponse(respr).${header.fieldName}; val != nil {\n`; - content += `${indent.push().get()}resp.Header.Set("${helpers.canonicalizeHeaderName(header.headerName)}", ${helpers.formatValue("val", header.type, imports, true)})\n`; + content += `${indent.push().get()}resp.Header.Set("${helpers.canonicalizeHeaderName(header.headerName)}", ${helpers.formatValue("val", header.type, imports)})\n`; content += `${indent.pop().get()}}\n`; } } @@ -658,10 +661,11 @@ function dispatchForOperationBody( content += `${indent.get()}req.Body.Close()\n`; break; default: { - let bodyTypeName = go.getTypeDeclaration(bodyParam.type, pkg); - if (bodyParam.type.kind === "time") { + const bodyParamType = go.unwrapPtr(bodyParam.type); + let bodyTypeName = go.getTypeDeclaration(bodyParamType, pkg); + if (bodyParamType.kind === "time") { imports.add("github.com/Azure/azure-sdk-for-go/sdk/azcore/runtime/datetime"); - bodyTypeName = `datetime.${bodyParam.type.format}`; + bodyTypeName = `datetime.${bodyParamType.format}`; } content += `${indent.get()}body, err := server.UnmarshalRequestAs${bodyParam.bodyFormat}[${bodyTypeName}](req)\n`; content += `${indent.get()}if err != nil {\n${indent.push().get()}return nil, err\n${indent.pop().get()}}\n`; @@ -852,14 +856,20 @@ function dispatchForOperationBody( content += emitCase( field.serializedName, `${param.name}.${field.name}`, - field.type, - field.byValue, + go.unwrapPtr(field.type), + field.type.kind !== "ptr", ); } } else { - // for this case we've emitted local vars of the underlying - // type which is why we pass true for param destIsByValue - content += emitCase(param.name, param.name, param.type, true); + // optional params are wrapped in a pointer; the local var is declared + // as the pointer type, so dispatch on the unwrapped type and let + // emitCase wrap the value in to.Ptr when the destination is by-ref. + content += emitCase( + param.name, + param.name, + go.unwrapPtr(param.type), + !go.isPtr(param.type), + ); } } @@ -900,7 +910,7 @@ function dispatchForOperationBody( content += `${indent.get()}type partialBodyParams struct {\n`; indent.push(); for (const partialBodyParam of partialBodyParams) { - content += `${indent.get()}${naming.capitalize(partialBodyParam.name)} ${helpers.star(partialBodyParam.byValue)}${go.getTypeDeclaration(partialBodyParam.type, pkg)} \`json:"${partialBodyParam.serializedName}"\`\n`; + content += `${indent.get()}${naming.capitalize(partialBodyParam.name)} ${go.getTypeDeclaration(partialBodyParam.type, pkg)} \`json:"${partialBodyParam.serializedName}"\`\n`; } content += `${indent.pop().get()}}\n`; content += `${indent.get()}body, err := server.UnmarshalRequestAs${partialBodyParams[0].format}[partialBodyParams](req)\n`; @@ -914,7 +924,7 @@ function dispatchForOperationBody( for (const partialBodyParam of partialBodyParams) { result.params.set( partialBodyParam.name, - `${helpers.star(partialBodyParam.byValue)}body.${naming.capitalize(partialBodyParam.name)}`, + `${helpers.deref(partialBodyParam.type)}body.${naming.capitalize(partialBodyParam.name)}`, ); } @@ -935,7 +945,12 @@ function emitTextBodyUnmarshal( imports: ImportManager, indent: helpers.Indentation, ): string { - const typeName = go.getTypeDeclaration(bodyParam.type, pkg); + const bodyParamType = go.unwrapPtr(bodyParam.type); + + // we pass the unwrapped param type since we'll be declaring + // a local of the underlying type and we don't want it to be + // pointer-to-type + const typeName = go.getTypeDeclaration(bodyParamType, pkg); const optional = !go.isRequiredParameter(bodyParam.style); let content = ""; @@ -951,17 +966,17 @@ function emitTextBodyUnmarshal( const assignOrDecl = optional ? "=" : ":="; - switch (bodyParam.type.kind) { + switch (bodyParamType.kind) { case "string": content += `${indent.get()}body ${assignOrDecl} bodyRaw\n`; break; case "constant": imports.addForType(bodyParam.type); - if (bodyParam.type.type === "string") { + if (bodyParamType.type === "string") { content += `${indent.get()}body ${assignOrDecl} ${typeName}(bodyRaw)\n`; } else { content += helpers.emitScalarParsing( - bodyParam.type, + bodyParamType, "bodyRaw", "bodyParsed", imports, @@ -973,7 +988,7 @@ function emitTextBodyUnmarshal( break; case "scalar": content += helpers.emitScalarParsing( - bodyParam.type, + bodyParamType, "bodyRaw", optional ? "bodyParsed" : "body", imports, @@ -985,7 +1000,7 @@ function emitTextBodyUnmarshal( } break; case "time": - content += helpers.emitTimeParsing("bodyRaw", bodyParam.type, "bodyParsed", imports, indent); + content += helpers.emitTimeParsing("bodyRaw", bodyParamType, "bodyParsed", imports, indent); content += `${indent.get()}${helpers.buildErrCheck(indent, "err", "nil")}\n`; content += `${indent.get()}body ${assignOrDecl} bodyParsed\n`; break; @@ -1244,6 +1259,9 @@ function parseHeaderPathQueryParams( // contains the unescaped value. let paramValue = getRawParamValue(param); + // optional params are pointer-wrapped; dispatch on the unwrapped type. + const paramType = go.unwrapPtr(param.type); + // encoded path params are escaped, so we need to unescape them first. // non-encoded path params are already in their final form (the client // skips url.PathEscape for them), so they're passed through verbatim. @@ -1294,8 +1312,9 @@ function parseHeaderPathQueryParams( param.kind === "pathCollectionParam" || param.kind === "queryCollectionParam" ) { + const elementType = go.unwrapPtr(param.type.elementType); // any element type other than string will require some form of conversion/parsing - if (param.type.elementType.kind !== "string") { + if (elementType.kind !== "string") { if (param.collectionFormat !== "multi") { requiredHelpers.splitHelper = true; const elementsParam = createLocalVariableName(param, "Elements"); @@ -1305,22 +1324,19 @@ function parseHeaderPathQueryParams( const paramVar = createLocalVariableName(param, "Param"); let elementFormat: go.ScalarType | go.TimeFormat | go.BytesEncoding | "string"; - switch (param.type.elementType.kind) { + switch (elementType.kind) { case "constant": case "scalar": - elementFormat = param.type.elementType.type; + elementFormat = elementType.type; break; case "encodedBytes": - elementFormat = param.type.elementType.encoding; + elementFormat = elementType.encoding; break; case "time": - elementFormat = param.type.elementType.format; + elementFormat = elementType.format; break; default: - throw new CodegenError( - "InternalError", - `unhandled element kind ${param.type.elementType.kind}`, - ); + throw new CodegenError("InternalError", `unhandled element kind ${elementType.kind}`); } const toType = go.getTypeDeclaration(param.type.elementType, pkg); @@ -1387,7 +1403,7 @@ function parseHeaderPathQueryParams( requiredHelpers.splitHelper = true; content += `${indent.get()}${createLocalVariableName(param, "Param")} := splitHelper(${paramValue}, "${helpers.getDelimiterForCollectionFormat(param.collectionFormat)}")\n`; } - } else if (go.isScalar(param.type, "bool")) { + } else if (go.isScalar(paramType, "bool")) { imports.add("strconv"); let from = `strconv.ParseBool(${paramValue})`; if (!go.isRequiredParameter(param.style)) { @@ -1396,11 +1412,11 @@ function parseHeaderPathQueryParams( } content += `${indent.get()}${createLocalVariableName(param, "Param")}, err := ${from}\n`; content += `${indent.get()}if err != nil {\n${indent.push().get()}return nil, err\n${indent.pop().get()}}\n`; - } else if (param.type.kind === "encodedBytes") { + } else if (paramType.kind === "encodedBytes") { imports.add("encoding/base64"); - content += `${indent.get()}${createLocalVariableName(param, "Param")}, err := base64.${param.type.encoding}Encoding.DecodeString(${paramValue})\n`; + content += `${indent.get()}${createLocalVariableName(param, "Param")}, err := base64.${paramType.encoding}Encoding.DecodeString(${paramValue})\n`; content += `${indent.get()}if err != nil {\n${indent.push().get()}return nil, err\n${indent.pop().get()}}\n`; - } else if (param.type.kind === "time") { + } else if (paramType.kind === "time") { const formatMap: Record = { PlainDate: helpers.plainDateFormat, PlainTime: helpers.plainTimeFormat, @@ -1409,8 +1425,8 @@ function parseHeaderPathQueryParams( RFC7231: helpers.RFC1123Format, }; imports.add("time"); - if (param.type.format in formatMap) { - const format = formatMap[param.type.format]; + if (paramType.format in formatMap) { + const format = formatMap[paramType.format]; let from = `time.Parse(${format}, ${paramValue})`; if (!go.isRequiredParameter(param.style)) { requiredHelpers.parseOptional = true; @@ -1435,7 +1451,7 @@ function parseHeaderPathQueryParams( content += `${indent.get()}return time.Unix(p, 0), nil\n${indent.pop().get()}})\n`; content += `${indent.get()}if err != nil {\n${indent.push().get()}return nil, err\n${indent.pop().get()}}\n`; } - } else if (go.isScalar(param.type, "float32", "float64", "int32", "int64")) { + } else if (go.isScalar(paramType, "float32", "float64", "int32", "int64")) { let parser: string; if (!go.isRequiredParameter(param.style)) { requiredHelpers.parseOptional = true; @@ -1445,20 +1461,20 @@ function parseHeaderPathQueryParams( parser = "parseWithCast"; } if ( - param.type.type === "float32" || - param.type.type === "int32" || + paramType.type === "float32" || + paramType.type === "int32" || !go.isRequiredParameter(param.style) ) { - content += `${indent.get()}${createLocalVariableName(param, "Param")}, err := ${parser}(${paramValue}, func(v string) (${param.type.type}, error) {\n`; - content += `${indent.push().get()}p, parseErr := ${emitNumericConversion("v", param.type.type)}\n`; + content += `${indent.get()}${createLocalVariableName(param, "Param")}, err := ${parser}(${paramValue}, func(v string) (${paramType.type}, error) {\n`; + content += `${indent.push().get()}p, parseErr := ${emitNumericConversion("v", paramType.type)}\n`; content += `${indent.get()}if parseErr != nil {\n${indent.push().get()}return 0, parseErr\n${indent.pop().get()}}\n`; let result = "p"; - if (param.type.type === "float32" || param.type.type === "int32") { - result = `${param.type.type}(${result})`; + if (paramType.type === "float32" || paramType.type === "int32") { + result = `${paramType.type}(${result})`; } content += `${indent.get()}return ${result}, nil\n${indent.pop().get()}})\n`; } else { - content += `${indent.get()}${createLocalVariableName(param, "Param")}, err := ${emitNumericConversion(paramValue, param.type.type)}\n`; + content += `${indent.get()}${createLocalVariableName(param, "Param")}, err := ${emitNumericConversion(paramValue, paramType.type)}\n`; } content += `${indent.get()}if err != nil {\n${indent.push().get()}return nil, err\n${indent.pop().get()}}\n`; } else if (param.kind === "headerMapParam") { @@ -1473,7 +1489,7 @@ function parseHeaderPathQueryParams( content += `${indent.push().get()}if ${localVar} == nil {\n${indent.push().get()}${localVar} = map[string]*string{}\n${indent.pop().get()}}\n`; content += `${indent.get()}${localVar}[hh[len("${headerPrefix}"):]] = to.Ptr(getHeaderValue(req.Header, hh))\n`; content += `${indent.pop().get()}}\n${indent.pop().get()}}\n`; - } else if (go.isConstant(param.type, "bool", "float32", "float64", "int32", "int64")) { + } else if (go.isConstant(paramType, "bool", "float32", "float64", "int32", "int64")) { let parseHelper: string; if (!go.isRequiredParameter(param.style)) { requiredHelpers.parseOptional = true; @@ -1484,16 +1500,16 @@ function parseHeaderPathQueryParams( } let parse: string; let zeroValue: string; - if (param.type.type === "bool") { + if (paramType.type === "bool") { imports.add("strconv"); parse = "strconv.ParseBool(v)"; zeroValue = "false"; } else { // emitNumericConversion adds the necessary import of strconv - parse = emitNumericConversion("v", param.type.type); + parse = emitNumericConversion("v", paramType.type); zeroValue = "0"; } - const toConstType = go.getTypeDeclaration(param.type, pkg); + const toConstType = go.getTypeDeclaration(paramType, pkg); content += `${indent.get()}${createLocalVariableName(param, "Param")}, err := ${parseHelper}(${paramValue}, func(v string) (${toConstType}, error) {\n`; content += `${indent.push().get()}p, parseErr := ${parse}\n`; content += `${indent.get()}if parseErr != nil {\n${indent.push().get()}return ${zeroValue}, parseErr\n${indent.pop().get()}}\n`; @@ -1502,9 +1518,9 @@ function parseHeaderPathQueryParams( } else if (!go.isRequiredParameter(param.style)) { // we check this last as it's a superset of the previous conditions requiredHelpers.getOptional = true; - if (param.type.kind === "constant" || param.type.kind === "etag") { - imports.addForType(param.type); - paramValue = `${go.getTypeDeclaration(param.type, pkg)}(${paramValue})`; + if (paramType.kind === "constant" || paramType.kind === "etag") { + imports.addForType(paramType); + paramValue = `${go.getTypeDeclaration(paramType, pkg)}(${paramValue})`; } content += `${indent.get()}${createLocalVariableName(param, "Param")} := getOptional(${paramValue})\n`; } @@ -1524,7 +1540,7 @@ function parseHeaderPathQueryParams( } content += `${indent.get()}}\n`; } else { - content += `${indent.get()}var ${naming.uncapitalize(paramGroup.name)} *${go.getTypeDeclaration(paramGroup, pkg)}\n`; + content += `${indent.get()}var ${naming.uncapitalize(paramGroup.name)} ${go.getTypeDeclaration(paramGroup, pkg)}\n`; const params = paramGroups.get(paramGroup); const paramNilCheck = new Array(); if (params) { @@ -1551,13 +1567,13 @@ function parseHeaderPathQueryParams( } } content += `${indent.get()}if ${paramNilCheck.join(" || ")} {\n`; - content += `${indent.push().get()}${naming.uncapitalize(paramGroup.name)} = &${go.getTypeDeclaration(paramGroup, pkg)}{\n`; + content += `${indent.push().get()}${naming.uncapitalize(paramGroup.name)} = ${go.getTypeDeclaration(paramGroup, pkg, true)}{\n`; if (params) { indent.push(); for (const param of params) { let byRef = "&"; if ( - param.byValue || + param.type.kind !== "ptr" || (!go.isRequiredParameter(param.style) && param.kind !== "bodyParam" && !go.isFormBodyParameter(param) && @@ -1700,7 +1716,7 @@ function getFinalParamValue( (param.kind === "bodyParam" || go.isFormBodyParameter(param) || param.kind === "multipartFormBodyParam") && - param.type.kind === "time" && + go.unwrapPtr(param.type).kind === "time" && (param.kind !== "bodyParam" || param.bodyFormat !== "Text") ) { // time types in the body have been unmarshalled into our time helpers thus require a cast to time.Time diff --git a/packages/typespec-go/src/codemodel/client.ts b/packages/typespec-go/src/codemodel/client.ts index 44f05d2188..1f5cb9697b 100644 --- a/packages/typespec-go/src/codemodel/client.ts +++ b/packages/typespec-go/src/codemodel/client.ts @@ -430,7 +430,7 @@ export class ClientCredentialParameter implements ClientCredentialParameter { constructor(name: string, type: type.TokenCredential) { - super(name, type, true); + super(name, type); this.kind = "credentialParam"; this.style = "required"; } diff --git a/packages/typespec-go/src/codemodel/method.ts b/packages/typespec-go/src/codemodel/method.ts index bce19a5c41..12f70081b5 100644 --- a/packages/typespec-go/src/codemodel/method.ts +++ b/packages/typespec-go/src/codemodel/method.ts @@ -35,9 +35,6 @@ export interface Parameter { /** the parameter's type */ type: type.Type; - - /** indicates if the param is pointer-to-type or not */ - byValue: boolean; } /** a method's receiver parameter */ @@ -65,10 +62,9 @@ export class Method implements Method } export class Parameter implements Parameter { - constructor(name: string, type: type.Type, byValue: boolean) { + constructor(name: string, type: type.Type) { this.name = name; this.type = type; - this.byValue = byValue; this.docs = {}; } } diff --git a/packages/typespec-go/src/codemodel/param.ts b/packages/typespec-go/src/codemodel/param.ts index dd46c77590..3b3a65b54d 100644 --- a/packages/typespec-go/src/codemodel/param.ts +++ b/packages/typespec-go/src/codemodel/param.ts @@ -136,13 +136,14 @@ export interface HeaderScalarParameter extends HttpParameterBase { /** defines the possible types for a scalar header */ export type HeaderScalarType = - | type.Constant - | type.EncodedBytes - | type.ETag - | type.Literal - | type.Scalar - | type.String - | type.Time; + HeaderScalarPtrType | type.EncodedBytes | type.Ptr; + +/** the set of scalar header types wrapped in a Ptr */ +export type HeaderScalarPtrType = + type.Constant | type.ETag | type.Literal | type.Scalar | type.String | type.Time; + +/** the set of scalar header wire types */ +export type HeaderScalarWireType = type.EncodedBytes | HeaderScalarPtrType; /** parameter goes in multipart/form body */ export interface MultipartFormBodyParameter extends HttpParameterBase { @@ -248,7 +249,14 @@ export interface PathScalarParameter extends HttpParameterBase { /** defines the possible types for a PathScalarParameter */ export type PathScalarParameterType = - type.Constant | type.EncodedBytes | type.Literal | type.Scalar | type.String | type.Time; + PathScalarParameterWireType | type.Ptr; + +/** the set of scalar path types wrapped in a Ptr */ +export type PathScalarParameterPtrType = + type.Constant | type.Literal | type.Scalar | type.String | type.Time; + +/** the set of scalar path wire types */ +export type PathScalarParameterWireType = type.EncodedBytes | PathScalarParameterPtrType; /** a reference to an existing parameter */ export interface ParameterRef { @@ -297,7 +305,14 @@ export interface QueryScalarParameter extends HttpParameterBase { /** defines the possible types for a QueryScalarParameter */ export type QueryScalarParameterType = - type.Constant | type.EncodedBytes | type.Literal | type.Scalar | type.String | type.Time; + QueryScalarParameterWireType | type.Ptr; + +/** the set of scalar query types wrapped in a Ptr */ +export type QueryScalarParameterPtrType = + type.Constant | type.Literal | type.Scalar | type.String | type.Time; + +/** the set of scalar query wire types */ +export type QueryScalarParameterWireType = type.EncodedBytes | QueryScalarParameterPtrType; /** the synthesized resume token parameter for LROs */ export interface ResumeTokenParameter extends HttpParameterBase { @@ -322,7 +337,10 @@ export interface URIParameter extends HttpParameterBase { } /** defines the possible types for a URIParameter */ -export type URIParameterType = type.Constant | type.Scalar | type.String; +export type URIParameterType = URIParameterWireType | type.Ptr; + +/** the set of URI wire types */ +export type URIParameterWireType = type.Constant | type.Scalar | type.String; /** narrows style to a ClientSideDefault within the conditional block */ export function isClientSideDefault(style: ParameterStyle): style is ClientSideDefault { @@ -343,8 +361,10 @@ export function isHeaderParameter(param: MethodParameter): param is HeaderParame ); } -/** narrows type to a HeaderScalarType within the conditional block */ -export function isHeaderScalarType(type: type.WireType): type is HeaderScalarType { +/** narrows type to a HeaderScalarWireType within the conditional block */ +export function isHeaderScalarType( + type: Exclude, +): type is HeaderScalarWireType { switch (type.kind) { case "constant": case "encodedBytes": @@ -364,8 +384,10 @@ export function isPathParameter(param: MethodParameter): param is PathParameter return param.kind === "pathCollectionParam" || param.kind === "pathScalarParam"; } -/** narrows type to a PathScalarParameterType within the conditional block */ -export function isPathScalarParameterType(type: type.WireType): type is PathScalarParameterType { +/** narrows type to a PathScalarParameterWireType within the conditional block */ +export function isPathScalarParameterType( + type: type.WireType, +): type is PathScalarParameterWireType { switch (type.kind) { case "constant": case "encodedBytes": @@ -384,8 +406,10 @@ export function isQueryParameter(param: MethodParameter): param is QueryParamete return param.kind === "queryCollectionParam" || param.kind === "queryScalarParam"; } -/** narrows type to a QueryScalarParameterType within the conditional block */ -export function isQueryScalarParameterType(type: type.WireType): type is QueryScalarParameterType { +/** narrows type to a QueryScalarParameterWireType within the conditional block */ +export function isQueryScalarParameterType( + type: type.WireType, +): type is QueryScalarParameterWireType { switch (type.kind) { case "constant": case "encodedBytes": @@ -408,8 +432,8 @@ export function isRequiredParameter(paramStyle: ParameterStyle): boolean { return paramStyle === "required"; } -/** narrows type to a URIParameterType within the conditional block */ -export function isURIParameterType(type: type.WireType): type is URIParameterType { +/** narrows type to a URIParameterWireType within the conditional block */ +export function isURIParameterType(type: type.WireType): type is URIParameterWireType { switch (type.kind) { case "constant": case "scalar": @@ -454,10 +478,9 @@ class HttpParameterBase extends method.Parameter implements HttpParameterBase { name: string, type: type.WireType, style: ParameterStyle, - byValue: boolean, location: ParameterLocation, ) { - super(name, type, byValue); + super(name, type); this.style = style; this.location = location; this.docs = {}; @@ -474,9 +497,8 @@ export class BodyParameter extends HttpParameterBase implements BodyParameter { contentType: BodyParameterContentTypeKind, type: type.WireType, style: ParameterStyle, - byValue: boolean, ) { - super(name, type, style, byValue, "method"); + super(name, type, style, "method"); this.kind = "bodyParam"; this.bodyFormat = bodyFormat; this.contentType = contentType; @@ -499,9 +521,8 @@ export class FormBodyCollectionParameter type: type.Slice, collectionFormat: ExtendedCollectionFormat, style: ParameterStyle, - byValue: boolean, ) { - super(name, type, style, byValue, "method"); + super(name, type, style, "method"); this.kind = "formBodyCollectionParam"; this.formDataName = formDataName; this.collectionFormat = collectionFormat; @@ -509,14 +530,8 @@ export class FormBodyCollectionParameter } export class FormBodyScalarParameter extends HttpParameterBase implements FormBodyScalarParameter { - constructor( - name: string, - formDataName: string, - type: type.WireType, - style: ParameterStyle, - byValue: boolean, - ) { - super(name, type, style, byValue, "method"); + constructor(name: string, formDataName: string, type: type.WireType, style: ParameterStyle) { + super(name, type, style, "method"); this.kind = "formBodyScalarParam"; this.formDataName = formDataName; } @@ -532,10 +547,9 @@ export class HeaderCollectionParameter type: type.Slice, collectionFormat: CollectionFormat, style: ParameterStyle, - byValue: boolean, location: ParameterLocation, ) { - super(name, type, style, byValue, location); + super(name, type, style, location); this.kind = "headerCollectionParam"; this.headerName = headerName; this.collectionFormat = collectionFormat; @@ -548,10 +562,9 @@ export class HeaderMapParameter extends HttpParameterBase implements HeaderMapPa headerName: string, type: type.Map, style: ParameterStyle, - byValue: boolean, location: ParameterLocation, ) { - super(name, type, style, byValue, location); + super(name, type, style, location); this.kind = "headerMapParam"; this.headerName = headerName; } @@ -563,10 +576,9 @@ export class HeaderScalarParameter extends HttpParameterBase implements HeaderSc headerName: string, type: HeaderScalarType, style: ParameterStyle, - byValue: boolean, location: ParameterLocation, ) { - super(name, type, style, byValue, location); + super(name, type, style, location); this.kind = "headerScalarParam"; this.headerName = headerName; this.isApiVersion = false; @@ -577,8 +589,8 @@ export class MultipartFormBodyParameter extends HttpParameterBase implements MultipartFormBodyParameter { - constructor(name: string, type: type.WireType, style: ParameterStyle, byValue: boolean) { - super(name, type, style, byValue, "method"); + constructor(name: string, type: type.WireType, style: ParameterStyle) { + super(name, type, style, "method"); this.kind = "multipartFormBodyParam"; } } @@ -617,9 +629,8 @@ export class PartialBodyParameter extends HttpParameterBase implements PartialBo format: "JSON" | "XML", type: type.WireType, style: ParameterStyle, - byValue: boolean, ) { - super(name, type, style, byValue, "method"); + super(name, type, style, "method"); this.kind = "partialBodyParam"; this.format = format; this.serializedName = serializedName; @@ -634,10 +645,9 @@ export class PathCollectionParameter extends HttpParameterBase implements PathCo type: type.Slice, collectionFormat: CollectionFormat, style: ParameterStyle, - byValue: boolean, location: ParameterLocation, ) { - super(name, type, style, byValue, location); + super(name, type, style, location); this.kind = "pathCollectionParam"; this.pathSegment = pathSegment; this.isEncoded = isEncoded; @@ -652,10 +662,9 @@ export class PathScalarParameter extends HttpParameterBase implements PathScalar isEncoded: boolean, type: PathScalarParameterType, style: ParameterStyle, - byValue: boolean, location: ParameterLocation, ) { - super(name, type, style, byValue, location); + super(name, type, style, location); this.kind = "pathScalarParam"; this.pathSegment = pathSegment; this.isEncoded = isEncoded; @@ -675,10 +684,9 @@ export class QueryCollectionParameter type: type.Slice, collectionFormat: ExtendedCollectionFormat, style: ParameterStyle, - byValue: boolean, location: ParameterLocation, ) { - super(name, type, style, byValue, location); + super(name, type, style, location); this.kind = "queryCollectionParam"; this.queryParameter = queryParam; this.isEncoded = isEncoded; @@ -693,10 +701,9 @@ export class QueryScalarParameter extends HttpParameterBase implements QueryScal isEncoded: boolean, type: QueryScalarParameterType, style: ParameterStyle, - byValue: boolean, location: ParameterLocation, ) { - super(name, type, style, byValue, location); + super(name, type, style, location); this.kind = "queryScalarParam"; this.queryParameter = queryParam; this.isEncoded = isEncoded; @@ -706,7 +713,7 @@ export class QueryScalarParameter extends HttpParameterBase implements QueryScal export class ResumeTokenParameter extends HttpParameterBase implements ResumeTokenParameter { constructor() { - super("ResumeToken", new type.String(), "optional", true, "method"); + super("ResumeToken", new type.String(), "optional", "method"); this.kind = "resumeTokenParam"; this.docs.summary = "Resumes the long-running operation from the provided token."; } @@ -718,10 +725,9 @@ export class URIParameter extends HttpParameterBase implements URIParameter { uriPathSegment: string, type: URIParameterType, style: ParameterStyle, - byValue: boolean, location: ParameterLocation, ) { - super(name, type, style, byValue, location); + super(name, type, style, location); this.kind = "uriParam"; this.uriPathSegment = uriPathSegment; this.isApiVersion = false; diff --git a/packages/typespec-go/src/codemodel/result.ts b/packages/typespec-go/src/codemodel/result.ts index 558941fcf2..a82a4486f5 100644 --- a/packages/typespec-go/src/codemodel/result.ts +++ b/packages/typespec-go/src/codemodel/result.ts @@ -93,15 +93,15 @@ export interface HeaderScalarResponse { docs: type.Docs; /** the type of the response header */ - type: param.HeaderScalarType; - - /** indicates if the header is returned by value or by pointer */ - byValue: boolean; + type: HeaderScalarResponseType; /** the name of the header sent over the wire */ headerName: string; } +/** defines the possible types for a scalar response header */ +export type HeaderScalarResponseType = type.EncodedBytes | type.Ptr; + /** * used for methods that return a typed payload. * the type is anonymously embedded in the response envelope. @@ -145,24 +145,20 @@ export interface MonomorphicResult { /** the format in which the result is returned */ format: ResultFormat; - /** indicates if the response type is returned by value or by pointer */ - byValue: boolean; - /** optional XML schema metadata */ xml?: type.XMLInfo; } /** the possible monomorphic result types */ export type MonomorphicResultType = - | type.Any - | type.Constant - | type.EncodedBytes - | type.Map - | type.RawJSON - | type.Scalar - | type.Slice - | type.String - | type.Time; + Exclude | type.Ptr; + +/** the set of monomorphic result types wrapped in a Ptr */ +export type MonomorphicResultPtrType = type.Constant | type.Scalar | type.String | type.Time; + +/** the set of monomorphic result wire types */ +export type MonomorphicResultWireType = + type.Any | type.EncodedBytes | type.Map | type.RawJSON | type.Slice | MonomorphicResultPtrType; /** * used for methods that return a discriminated type. @@ -240,7 +236,9 @@ export function getResultType( } /** narrows type to a MonomorphicResultType within the conditional block */ -export function isMonomorphicResultType(type: type.WireType): type is MonomorphicResultType { +export function isMonomorphicResultType( + type: Exclude, +): type is MonomorphicResultWireType { switch (type.kind) { case "any": case "constant": @@ -297,16 +295,10 @@ export class HeaderMapResponse implements HeaderMapResponse { } export class HeaderScalarResponse implements HeaderScalarResponse { - constructor( - fieldName: string, - type: param.HeaderScalarType, - headerName: string, - byValue: boolean, - ) { + constructor(fieldName: string, type: HeaderScalarResponseType, headerName: string) { this.kind = "headerScalarResponse"; this.fieldName = fieldName; this.type = type; - this.byValue = byValue; this.headerName = headerName; this.docs = {}; } @@ -322,17 +314,11 @@ export class ModelResult implements ModelResult { } export class MonomorphicResult implements MonomorphicResult { - constructor( - fieldName: string, - format: ResultFormat, - type: MonomorphicResultType, - byValue: boolean, - ) { + constructor(fieldName: string, format: ResultFormat, type: MonomorphicResultType) { this.kind = "monomorphicResult"; this.fieldName = fieldName; this.format = format; this.monomorphicType = type; - this.byValue = byValue; this.docs = {}; } } diff --git a/packages/typespec-go/src/codemodel/type.ts b/packages/typespec-go/src/codemodel/type.ts index 546b5b67d9..28356da5d3 100644 --- a/packages/typespec-go/src/codemodel/type.ts +++ b/packages/typespec-go/src/codemodel/type.ts @@ -38,6 +38,7 @@ export type WireType = | Model | MultipartContent | PolymorphicModel + | Ptr | RawJSON | ReadCloser | ReadSeekCloser @@ -186,32 +187,26 @@ export type LiteralType = Constant | ConstantDef | EncodedBytes | Scalar | Strin export interface Map { kind: "map"; - /** the type of values in the map */ + /** + * the type of values in the map. + * note that the type is always pointer-to-type + * unless the type is implicitly nil-able. + */ valueType: T; - - /** indicates if the map's value type is pointer-to-type or not */ - valueTypeByValue: boolean; } /** the set of map value types */ export type MapValueType = | Any - | Constant | EncodedBytes | Interface | Map - | Model - | MultipartContent - | PolymorphicModel + | Ptr> | RawJSON | ReadCloser | ReadSeekCloser - | Scalar | Slice - | SliceArray - | String - | Time - | UnionStruct; + | SliceArray; /** a field within a model */ export interface ModelField extends StructField { @@ -286,6 +281,27 @@ export interface PolymorphicModel extends ModelBase { discriminatorValue?: Literal; } +/** defines possible Ptr types */ +export type PtrType = + | Constant + | ETag + | Literal + | Model + | MultipartContent + | PolymorphicModel + | Scalar + | String + | Time + | UnionStruct; + +/** a pointer to some type */ +export interface Ptr { + kind: "ptr"; + + /** the type being pointed to */ + ptrType: T; +} + /** a byte slice containing raw JSON */ export interface RawJSON { kind: "rawJSON"; @@ -334,31 +350,8 @@ export interface Slice { /** the element type for this slice */ elementType: T; - - /** indicates if the slice's element type is pointer-to-type or not */ - elementTypeByValue: boolean; -} - -/** specialized slice type for arrays represented as delimited strings */ -export interface SliceArray { - kind: "sliceArray"; - - /** the element type for this slice */ - elementType: SliceArrayElementType; - - /** indicates if the slice's element type is pointer-to-type or not */ - elementTypeByValue: boolean; - - /** the delimiter used to separate elements */ - delimiter: SliceArrayDelimiter; } -/** the set of slice array delimiters */ -export type SliceArrayDelimiter = "comma" | "newline" | "pipe" | "space"; - -/** the supported element types for arrays represented as delimited strings */ -export type SliceArrayElementType = Constant | String; - /** the set of slice element types */ export type SliceElementType = | Any @@ -369,6 +362,7 @@ export type SliceElementType = | Model | MultipartContent | PolymorphicModel + | Ptr> | RawJSON | ReadCloser | ReadSeekCloser @@ -379,6 +373,26 @@ export type SliceElementType = | Time | UnionStruct; +/** specialized slice type for arrays represented as delimited strings */ +export interface SliceArray { + kind: "sliceArray"; + + /** the element type for this slice */ + elementType: SliceArrayElementType; + + /** the delimiter used to separate elements */ + delimiter: SliceArrayDelimiter; +} + +/** the set of slice array delimiters */ +export type SliceArrayDelimiter = "comma" | "newline" | "pipe" | "space"; + +/** the supported element types for arrays represented as delimited strings */ +export type SliceArrayElementType = SliceArrayElementWireType | Ptr; + +/** the set of slice array wire types */ +export type SliceArrayElementWireType = Constant | String; + /** a Go string */ export interface String { kind: "string"; @@ -399,9 +413,6 @@ export interface StructField { /** the field's underlying type */ type: Type; - - /** indicates if the field is pointer-to-type or not */ - byValue: boolean; } /** a time.Time type from the standard library with a format specifier */ @@ -447,8 +458,15 @@ export interface UnionStruct extends StructBase { /** * the subset of WireType kinds that can appear as a variant within a non-discriminated union. + * pointer-capable variants are stored pointer-to-type. */ -export type UnionVariantType = Constant | Literal | Map | Model | Scalar | Slice | String; +export type UnionVariantType = Map | Slice | Ptr; + +/** the pointer-capable wire types that can be a union variant */ +export type UnionVariantPtrType = Constant | Literal | Model | Scalar | String; + +/** the wire types accepted as a union variant, prior to pointer-wrapping */ +export type UnionVariantWireType = Map | Slice | UnionVariantPtrType; /** bit flags indicating how a model/polymorphic type is used */ export enum UsageFlags { @@ -519,9 +537,24 @@ export function getLiteralTypeDeclaration(literal: LiteralType): string { * * @param type the type for which to emit the declaration * @param scope the scope in which the type declaration is emitted + * @param instance emit the type declaration used for defining an instance instead of a type. + * only useful for Ptr types so "&" is emitted instead of "*". the default is false. * @returns the Go type declaration */ -export function getTypeDeclaration(type: Client | Type, scope: PackageType): string { +export function getTypeDeclaration( + type: Client | Type, + scope: PackageType, + instance: boolean = false, +): string { + // client/method options are always emitted as pointer-to-type thus aren't wrapped in a go.Ptr + const byRef = + type.kind === "armClientOptions" || + type.kind === "clientOptions" || + (type.kind === "paramGroup" && !type.required) + ? instance + ? "&" + : "*" + : ""; switch (type.kind) { case "any": case "string": @@ -551,9 +584,9 @@ export function getTypeDeclaration(type: Client | Type, scope: PackageType): str if (pkg !== scope) { // type is being referenced from a different package // then where it's defined, so add its package prefix - return `${getPackageName(pkg)}.${typeName}`; + return `${byRef}${getPackageName(pkg)}.${typeName}`; } - return typeName; + return `${byRef}${typeName}`; } case "constantDef": return type.literal.type.kind; @@ -563,16 +596,14 @@ export function getTypeDeclaration(type: Client | Type, scope: PackageType): str case "literal": return getTypeDeclaration(type.type, scope); case "map": - return ( - `map[string]${type.valueTypeByValue ? "" : "*"}` + getTypeDeclaration(type.valueType, scope) - ); + return `map[string]${getTypeDeclaration(type.valueType, scope)}`; + case "ptr": + return `${instance ? "&" : "*"}${getTypeDeclaration(type.ptrType, scope)}`; case "scalar": return type.type; case "slice": case "sliceArray": - return ( - `[]${type.elementTypeByValue ? "" : "*"}` + getTypeDeclaration(type.elementType, scope) - ); + return `[]${getTypeDeclaration(type.elementType, scope)}`; case "time": return "time.Time"; case "armClientOptions": @@ -582,7 +613,7 @@ export function getTypeDeclaration(type: Client | Type, scope: PackageType): str case "readSeekCloser": case "tokenCredential": // strip module to just the leaf package as required - return `${path.basename(type.module)}.${type.name}`; + return `${byRef}${path.basename(type.module)}.${type.name}`; } } @@ -616,15 +647,36 @@ export function isLiteralValueType(type: WireType): type is LiteralType { } } +/** the inner (pointed-to) types allowed as a map value */ +type MapPtrType = Extract extends Ptr ? U : never; + /** narrows type to a map with one of the specified value type kinds (any map when no kinds are given) */ -export function isMap( +export function isMap( type: WireType, ...kinds: Array -): type is Map> { +): type is Map< + | Extract + | (Extract extends never ? never : Ptr>) +> { if (type.kind !== "map") { return false; } - return kinds.length === 0 || (kinds as Array).includes(type.valueType.kind); + return ( + kinds.length === 0 || + (kinds as Array).includes(type.valueType.kind) || + (kinds as Array).includes(unwrapPtr(type.valueType).kind) + ); +} + +/** narrows type to a ptr with one of the specified underlying types (any ptr when no types are given) */ +export function isPtr( + type: WireType, + ...kinds: Array +): type is Ptr> { + if (type.kind !== "ptr") { + return false; + } + return kinds.length === 0 || (kinds as Array).includes(type.ptrType.kind); } /** narrows type to a scalar with one of the specified underlying types (any scalar when no types are given) */ @@ -638,19 +690,32 @@ export function isScalar( return kinds.length === 0 || (kinds as Array).includes(type.type); } +type SlicePtrType = Extract extends Ptr ? U : never; + /** narrows type to a slice with one of the specified element type kinds (any slice when no kinds are given) */ -export function isSlice( +export function isSlice< + T extends SliceElementType["kind"] | SlicePtrType["kind"] = SliceElementType["kind"], +>( type: WireType, ...kinds: Array -): type is Slice> { +): type is Slice< + | Extract + | (Extract extends never + ? never + : Ptr>) +> { if (type.kind !== "slice") { return false; } - return kinds.length === 0 || (kinds as Array).includes(type.elementType.kind); + return ( + kinds.length === 0 || + (kinds as Array).includes(type.elementType.kind) || + (kinds as Array).includes(unwrapPtr(type.elementType).kind) + ); } -/** narrows type to a UnionVariantType within the conditional block */ -export function isUnionVariantType(type: WireType): type is UnionVariantType { +/** narrows type to a union variant wire type (prior to pointer-wrapping) within the conditional block */ +export function isUnionVariantType(type: Exclude): type is UnionVariantWireType { switch (type.kind) { case "constant": case "literal": @@ -665,15 +730,25 @@ export function isUnionVariantType(type: WireType): type is UnionVariantType { } } +/** the result of unwrapping a Ptr from T, distributing over unions */ +type UnwrappedPtr = T extends Ptr ? U : Exclude; + +/** unwraps type from a Ptr type, else returns type */ +export function unwrapPtr(type: T): UnwrappedPtr { + if (type.kind === "ptr") { + return type.ptrType as UnwrappedPtr; + } + return type as UnwrappedPtr; +} + /////////////////////////////////////////////////////////////////////////////////////////////////// // exported base types /////////////////////////////////////////////////////////////////////////////////////////////////// export class StructField implements StructField { - constructor(name: string, type: Type, byValue: boolean) { + constructor(name: string, type: Type) { this.name = name; this.type = type; - this.byValue = byValue; this.docs = {}; } } @@ -827,10 +902,9 @@ export class Literal implements Literal { } export class Map implements Map { - constructor(valueType: T, valueTypeByValue: boolean) { + constructor(valueType: T) { this.kind = "map"; this.valueType = valueType; - this.valueTypeByValue = valueTypeByValue; } } @@ -845,11 +919,10 @@ export class ModelField extends StructField implements ModelField { constructor( name: string, type: WireType, - byValue: boolean, serializedName: string, annotations: ModelFieldAnnotations, ) { - super(name, type, byValue); + super(name, type); this.serializedName = serializedName; this.annotations = annotations; } @@ -900,6 +973,13 @@ export class PolymorphicModel extends ModelBase implements PolymorphicModel { } } +export class Ptr implements Ptr { + constructor(ptrType: T) { + this.kind = "ptr"; + this.ptrType = ptrType; + } +} + export class RawJSON implements RawJSON { constructor() { this.kind = "rawJSON"; @@ -929,22 +1009,16 @@ export class Scalar implements Scalar { } export class Slice implements Slice { - constructor(elementType: T, elementTypeByValue: boolean) { + constructor(elementType: T) { this.kind = "slice"; this.elementType = elementType; - this.elementTypeByValue = elementTypeByValue; } } export class SliceArray implements SliceArray { - constructor( - elementType: SliceArrayElementType, - elementTypeByValue: boolean, - delimiter: SliceArrayDelimiter, - ) { + constructor(elementType: SliceArrayElementType, delimiter: SliceArrayDelimiter) { this.kind = "sliceArray"; this.elementType = elementType; - this.elementTypeByValue = elementTypeByValue; this.delimiter = delimiter; } } @@ -963,8 +1037,8 @@ export class Struct extends StructBase implements Struct { } export class UnionField extends StructField implements UnionField { - constructor(name: string, type: UnionVariantType, byValue: boolean) { - super(name, type, byValue); + constructor(name: string, type: UnionVariantType) { + super(name, type); } } diff --git a/packages/typespec-go/src/tcgcadapter/clients.ts b/packages/typespec-go/src/tcgcadapter/clients.ts index d5cd981cca..ae58d8da9f 100644 --- a/packages/typespec-go/src/tcgcadapter/clients.ts +++ b/packages/typespec-go/src/tcgcadapter/clients.ts @@ -481,7 +481,6 @@ export class ClientAdapter { const adaptedParam = new go.Parameter( getEscapedReservedName(helpers.getEffectiveName(param, true), "Param"), this.ta.getWireType(param.type, true, true), - true, ); adaptedParam.docs.summary = param.summary; adaptedParam.docs.description = param.doc; @@ -498,12 +497,9 @@ export class ClientAdapter { * @returns the adapted URI parameter */ private adaptURIParam(sdkParam: tcgc.SdkPathParameter, forceRequired: boolean): go.URIParameter { - let paramType: go.WireType; - if (sdkParam.isApiVersionParam) { - paramType = this.ta.getStringType(); - } else { - paramType = this.ta.getWireType(sdkParam.type, true, false); - } + const paramType = sdkParam.isApiVersionParam + ? this.ta.getStringType() + : this.ta.getWireType(sdkParam.type, true, false); if (go.isURIParameterType(paramType)) { const style = forceRequired ? "required" : this.adaptParameterStyle(sdkParam); @@ -517,9 +513,8 @@ export class ClientAdapter { const uriParam = new go.URIParameter( sdkParam.name, sdkParam.serializedName, - paramType, + sdkParam.optional ? this.ta.getPtrType(paramType) : paramType, style, - helpers.isTypePassedByValue(sdkParam.type) || !sdkParam.optional, "client", ); uriParam.docs.summary = sdkParam.summary; @@ -1052,9 +1047,7 @@ export class ClientAdapter { "Param", ); // if the param is required then it's always passed by value - const byVal = go.isRequiredParameter(paramStyle) - ? true - : helpers.isTypePassedByValue(param.type); + const isRequired = go.isRequiredParameter(paramStyle); const contentType = this.adaptContentType(opParam.defaultContentType); const getSerializedNameFromProperty = function ( property: tcgc.SdkModelPropertyType, @@ -1088,13 +1081,15 @@ export class ClientAdapter { param.__raw?.node, ); } + const paramType = this.ta.getWireType(param.type, true, true); adaptedParam = new go.PartialBodyParameter( paramName, serializedName, contentType, - this.ta.getWireType(param.type, true, true), + !isRequired && helpers.isPtrType(paramType) + ? this.ta.getPtrType(paramType) + : paramType, paramStyle, - byVal, ); break; } @@ -1104,7 +1099,7 @@ export class ClientAdapter { param.name, opParam.type, paramStyle, - byVal, + isRequired, ); } else { const contentTypeLiteral = this.ta.getLiteral( @@ -1117,7 +1112,6 @@ export class ClientAdapter { contentTypeLiteral, this.ta.getReadSeekCloser(false), paramStyle, - byVal, ); } break; @@ -1398,7 +1392,6 @@ export class ClientAdapter { opParam.serializedName, paramType, paramStyle, - true, paramLoc, ); break; @@ -1409,7 +1402,6 @@ export class ClientAdapter { true, paramType, paramType.kind === "literal" ? new go.ClientSideDefault(paramType) : paramStyle, - true, paramLoc, ); break; @@ -1420,7 +1412,6 @@ export class ClientAdapter { true, paramType, paramStyle, - true, paramLoc, ); break; @@ -1493,9 +1484,8 @@ export class ClientAdapter { helpers.getEffectiveName(methodParam, paramStyle === "required"), "Param", ); - const byVal = go.isRequiredParameter(paramStyle) - ? true - : helpers.isTypePassedByValue(methodParam.type); + // required and literal params are always passed by value + const byVal = go.isRequiredParameter(paramStyle) || paramStyle === "literal"; let adaptedParam: go.MethodParameter; switch (opParam.kind) { @@ -1522,9 +1512,8 @@ export class ClientAdapter { paramName, contentType, contentTypeLiteral, - bodyType, + !byVal && helpers.isPtrType(bodyType) ? this.ta.getPtrType(bodyType) : bodyType, paramStyle, - byVal, ); if (contentType === "XML" && methodParam.type.kind === "array") { // this is for compat with legacy behavior @@ -1540,7 +1529,7 @@ export class ClientAdapter { "unsupported parameter type cookie", opParam.__raw?.node, ); - case "header": + case "header": { const type = this.ta.getWireType(methodParam.type, true, false); if (type.kind === "map") { if (opParam.serializedName !== "x-ms-meta") { @@ -1555,7 +1544,6 @@ export class ClientAdapter { `${opParam.serializedName}-`, type, paramStyle, - byVal, location, ); } else if (opParam.collectionFormat) { @@ -1566,8 +1554,6 @@ export class ClientAdapter { opParam.__raw?.node, ); } - // TODO: is hard-coded false for element type by value correct? - const type = this.ta.getWireType(methodParam.type, true, false); if (type.kind !== "slice") { throw new AdapterError( "InternalError", @@ -1581,31 +1567,32 @@ export class ClientAdapter { type, opParam.collectionFormat === "simple" ? "csv" : opParam.collectionFormat, paramStyle, - byVal, location, ); } else { + const type = this.adaptHeaderScalarType(methodParam.type, true); adaptedParam = new go.HeaderScalarParameter( paramName, opParam.serializedName, - this.adaptHeaderScalarType(methodParam.type, true), + !byVal && helpers.isPtrType(type) ? this.ta.getPtrType(type) : type, paramStyle, - byVal, location, ); } break; - case "path": + } + case "path": { + const type = this.adaptPathScalarParameterType(methodParam.type); adaptedParam = new go.PathScalarParameter( paramName, opParam.serializedName, !opParam.allowReserved, - this.adaptPathScalarParameterType(methodParam.type), + !byVal && helpers.isPtrType(type) ? this.ta.getPtrType(type) : type, paramStyle, - byVal, location, ); break; + } case "query": if (opParam.collectionFormat) { const type = this.ta.getWireType(methodParam.type, true, false); @@ -1628,18 +1615,17 @@ export class ClientAdapter { ? "multi" : opParam.collectionFormat, paramStyle, - byVal, location, ); } else { // TODO: unencoded query param + const type = this.adaptQueryScalarParameterType(methodParam.type); adaptedParam = new go.QueryScalarParameter( paramName, opParam.serializedName, true, - this.adaptQueryScalarParameterType(methodParam.type), + !byVal && helpers.isPtrType(type) ? this.ta.getPtrType(type) : type, paramStyle, - byVal, location, ); } @@ -1703,13 +1689,16 @@ export class ClientAdapter { type = this.ta.getReadSeekCloser(false); } else { type = this.ta.getWireType(paramAsSdkType, true, true); + if (!byVal && helpers.isPtrType(type)) { + type = this.ta.getPtrType(type); + } } paramName = getEscapedReservedName( helpers.getEffectiveName({ name: paramName, isExactName }, paramStyle === "required"), "Param", ); - return new go.MultipartFormBodyParameter(paramName, type, paramStyle, byVal); + return new go.MultipartFormBodyParameter(paramName, type, paramStyle); } private getMethodNameForDocComment(method: go.MethodType): string { @@ -1796,11 +1785,11 @@ export class ClientAdapter { `${httpHeader.serializedName}-`, ); } else { + const type = this.adaptHeaderScalarType(httpHeader.type, false); headerResp = new go.HeaderScalarResponse( helpers.getEffectiveName(httpHeader), - this.adaptHeaderScalarType(httpHeader.type, false), + helpers.isPtrType(type) ? this.ta.getPtrType(type) : type, httpHeader.serializedName, - helpers.isTypePassedByValue(httpHeader.type), ); if (go.isPageableMethod(method)) { pageableRespHeadersMap.set(httpHeader, headerResp); @@ -1994,6 +1983,7 @@ export class ClientAdapter { respEnv.result.docs.summary = `Possible types are ${[...possibleTypes].sort().join(", ")}`; } else { const resultType = this.ta.getWireType(sdkResponseType, false, false); + if (go.isMonomorphicResultType(resultType)) { let fieldName: string | undefined; let xmlInfo: go.XMLInfo | undefined; @@ -2001,7 +1991,7 @@ export class ClientAdapter { // this is for compat with legacy behavior xmlInfo = new go.XMLInfo(); fieldName = sdkResponseType.name; - const elementType = (resultType).elementType; + const elementType = go.unwrapPtr((resultType).elementType); const elementTypeXmlName = helpers.hasXMLInfo(elementType)?.name; xmlInfo.wraps = elementTypeXmlName ?? go.getTypeDeclaration(elementType, method.receiver.type.pkg); @@ -2024,8 +2014,7 @@ export class ClientAdapter { respEnv.result = new go.MonomorphicResult( fieldName, contentType, - resultType, - helpers.isTypePassedByValue(sdkResponseType), + helpers.isPtrType(resultType) ? this.ta.getPtrType(resultType) : resultType, ); respEnv.result.xml = xmlInfo; } else { @@ -2117,22 +2106,14 @@ export class ClientAdapter { if (param.style === "literal") { continue; } - let byValue = - param.style === "required" || - (param.location === "client" && go.isClientSideDefault(param.style)); - // if the param isn't required, check if it should be passed by value or not. - // optional params that are implicitly nil-able shouldn't be pointer-to-type. - if (!byValue) { - byValue = param.byValue; - } - const field = new go.StructField(param.name, param.type, byValue); + const field = new go.StructField(param.name, param.type); field.docs = param.docs; structType.fields.push(field); } return structType; } - private adaptHeaderScalarType(sdkType: tcgc.SdkType, forParam: boolean): go.HeaderScalarType { + private adaptHeaderScalarType(sdkType: tcgc.SdkType, forParam: boolean): go.HeaderScalarWireType { // It would be ideal to force the ETag type for the known ETag headers per the RFC. // However, doing so introduces too many breaking changes (if-match and if-none-match). // TODO: If we decide to force ETag types by header name in the future, @@ -2151,7 +2132,7 @@ export class ClientAdapter { ); } - private adaptPathScalarParameterType(sdkType: tcgc.SdkType): go.PathScalarParameterType { + private adaptPathScalarParameterType(sdkType: tcgc.SdkType): go.PathScalarParameterWireType { const type = this.ta.getWireType(sdkType, false, false); if (go.isPathScalarParameterType(type)) { return type; @@ -2163,7 +2144,7 @@ export class ClientAdapter { ); } - private adaptQueryScalarParameterType(sdkType: tcgc.SdkType): go.QueryScalarParameterType { + private adaptQueryScalarParameterType(sdkType: tcgc.SdkType): go.QueryScalarParameterWireType { const type = this.ta.getWireType(sdkType, false, false); if (go.isQueryScalarParameterType(type)) { return type; @@ -2372,6 +2353,7 @@ export class ClientAdapter { exampleType: tcgc.SdkExampleValue, goType: go.WireType, ): Exclude { + goType = go.unwrapPtr(goType); switch (exampleType.kind) { case "string": switch (goType.kind) { diff --git a/packages/typespec-go/src/tcgcadapter/helpers.ts b/packages/typespec-go/src/tcgcadapter/helpers.ts index ef7b0d6053..b6a9e0ccdc 100644 --- a/packages/typespec-go/src/tcgcadapter/helpers.ts +++ b/packages/typespec-go/src/tcgcadapter/helpers.ts @@ -46,8 +46,10 @@ export function adaptXMLInfo(src: XMLSourceInfo): go.XMLInfo | undefined { xmlInfo.attribute = true; returnXMLInfo = true; } - if (src.type.kind === "slice") { - const elementXMLInfo = hasXMLInfo(src.type.elementType); + + const srcType = go.unwrapPtr(src.type); + if (srcType.kind === "slice") { + const elementXMLInfo = hasXMLInfo(srcType.elementType); if (src.xml?.unwrapped === false) { if (src.xml.itemsName) { xmlInfo.wraps = src.xml.itemsName; @@ -67,7 +69,7 @@ export function adaptXMLInfo(src: XMLSourceInfo): go.XMLInfo | undefined { xmlInfo.name = src.orTypeName; returnXMLInfo = true; } - } else if (src.xml?.unwrapped && src.type.kind === "string") { + } else if (src.xml?.unwrapped && srcType.kind === "string") { // an unwrapped string means it's text xmlInfo.text = true; // the ",chardata" tag is mutually exclusive @@ -110,24 +112,25 @@ export function isPolymorphicRoot(model: tcgc.SdkModelType): boolean { } } -/** - * returns true if the specified type doesn't need to be pointer-to-type - * because it's implicitly nil-able. - * - * @param type the type to inspect - * @returns true if the type is implicitly nil-able - */ -export function isTypePassedByValue(type: tcgc.SdkType): boolean { - if (type.kind === "nullable") { - type = type.type; +/** narrows type to a PtrType within the conditional block */ +export function isPtrType>( + type: T, +): type is Extract { + switch (type.kind) { + case "constant": + case "etag": + case "literal": + case "model": + case "multipartContent": + case "polymorphicModel": + case "scalar": + case "string": + case "time": + case "unionStruct": + return true; + default: + return false; } - return ( - type.kind === "unknown" || - type.kind === "array" || - type.kind === "bytes" || - type.kind === "dict" || - (type.kind === "model" && isPolymorphicRoot(type)) - ); } /** contains the set of client options */ diff --git a/packages/typespec-go/src/tcgcadapter/types.ts b/packages/typespec-go/src/tcgcadapter/types.ts index 029abdcad4..2e9e5ededb 100644 --- a/packages/typespec-go/src/tcgcadapter/types.ts +++ b/packages/typespec-go/src/tcgcadapter/types.ts @@ -146,17 +146,8 @@ export class TypeAdapter { } if (content.addlProps) { const annotations = new go.ModelFieldAnnotations(false, false, true, false); - const addlPropsType = new go.Map( - this.getMapValueType(content.addlProps, false, false), - helpers.isTypePassedByValue(content.addlProps), - ); - const addlProps = new go.ModelField( - "AdditionalProperties", - addlPropsType, - true, - "", - annotations, - ); + const addlPropsType = new go.Map(this.getMapValueType(content.addlProps, false, false)); + const addlProps = new go.ModelField("AdditionalProperties", addlPropsType, "", annotations); modelType.go.fields.push(addlProps); } this.getPkg().models.push(modelType.go); @@ -206,7 +197,7 @@ export class TypeAdapter { type: tcgc.SdkType, elementTypeByValue: boolean, substituteDiscriminator: boolean, - ): go.WireType { + ): Exclude { switch (type.kind) { case "boolean": case "bytes": @@ -231,11 +222,11 @@ export class TypeAdapter { case "url": return this.getBuiltInType(type); case "array": { - let elementType = type.valueType; + let valueType = type.valueType; let nullable = false; - if (elementType.kind === "nullable") { + if (valueType.kind === "nullable") { // unwrap the nullable type - elementType = elementType.type; + valueType = valueType.type; nullable = true; } // prefer elementTypeByValue. if false, then if the array elements have been explicitly marked as nullable then prefer that, else fall back to our usual algorithm @@ -243,34 +234,41 @@ export class TypeAdapter { ? true : nullable ? false - : this.codeModel.options["slice-elements-byval"] || - helpers.isTypePassedByValue(elementType); + : this.codeModel.options["slice-elements-byval"] === true; + const keyName = recursiveKeyName( `array-${myElementTypeByValue}`, - elementType, + valueType, substituteDiscriminator, ); + let arrayType = this.types.get(keyName); if (arrayType) { - return arrayType; + return arrayType; } - const goElementType = this.getWireType( - type.valueType, + + const elementType = this.getWireType( + valueType, elementTypeByValue, substituteDiscriminator, ); - switch (goElementType.kind) { + switch (elementType.kind) { case "constantDef": case "constantValue": case "etag": case "literal": throw new AdapterError( "UnsupportedTsp", - `unsupported kind ${goElementType.kind} for slice element type`, + `unsupported kind ${elementType.kind} for slice element type`, type.valueType.__raw?.node, ); } - arrayType = new go.Slice(goElementType, myElementTypeByValue); + + arrayType = new go.Slice( + !myElementTypeByValue && helpers.isPtrType(elementType) + ? this.getPtrType(elementType) + : elementType, + ); this.types.set(keyName, arrayType); return arrayType; } @@ -290,20 +288,21 @@ export class TypeAdapter { // forces GMT and Unix is absolute), so restrict it to that encoding. return this.getTimeType(type.encode, getDateTimeEncoding(type.encode) === "RFC3339"); case "dict": { - const valueTypeByValue = helpers.isTypePassedByValue(type.valueType); + const valueType = this.getMapValueType( + type.valueType, + elementTypeByValue, + substituteDiscriminator, + ); const keyName = recursiveKeyName( - `dict-${valueTypeByValue}`, + `dict${valueType.kind === "ptr" ? "-ptr" : ""}`, type.valueType, substituteDiscriminator, ); let mapType = this.types.get(keyName); if (mapType) { - return mapType; + return mapType; } - mapType = new go.Map( - this.getMapValueType(type.valueType, elementTypeByValue, substituteDiscriminator), - valueTypeByValue, - ); + mapType = new go.Map(valueType); this.types.set(keyName, mapType); return mapType; } @@ -369,7 +368,7 @@ export class TypeAdapter { } // returns the Go code model type for an io.ReadSeekCloser - getReadSeekCloser(sliceOf: boolean): go.WireType { + getReadSeekCloser(sliceOf: boolean): go.ReadSeekCloser | go.Slice { let keyName = "io-readseekcloser"; if (sliceOf) { keyName = "sliceof-" + keyName; @@ -378,11 +377,11 @@ export class TypeAdapter { if (!rsc) { rsc = new go.ReadSeekCloser(); if (sliceOf) { - rsc = new go.Slice(rsc, true); + rsc = new go.Slice(rsc); } this.types.set(keyName, rsc); } - return rsc; + return >rsc; } /** @@ -395,7 +394,10 @@ export class TypeAdapter { * @param contentType set when the request uses a fixed content type * @returns the go.MultipartContent instance */ - getMultipartContent(sliceOf: boolean, contentType?: string): go.WireType { + getMultipartContent( + sliceOf: boolean, + contentType?: string, + ): go.MultipartContent | go.Slice { let keyName = "streaming-multipartcontent"; if (contentType) { keyName += `-${contentType}`; @@ -408,21 +410,21 @@ export class TypeAdapter { const ct = contentType ? new go.Literal(this.getStringType(), `"${contentType}"`) : undefined; mpc = new go.MultipartContent(ct); if (sliceOf) { - mpc = new go.Slice(mpc, true); + mpc = new go.Slice(mpc); } this.types.set(keyName, mpc); } - return mpc; + return >mpc; } - private getBuiltInType(type: tcgc.SdkBuiltInType): go.WireType { + private getBuiltInType(type: tcgc.SdkBuiltInType): Exclude { switch (type.kind) { case "unknown": { if (this.codeModel.options["rawjson-as-bytes"]) { const anyRawJSONKey = "any-raw-json"; let anyRawJSON = this.types.get(anyRawJSONKey); if (anyRawJSON) { - return anyRawJSON; + return anyRawJSON; } anyRawJSON = new go.RawJSON(); this.types.set(anyRawJSONKey, anyRawJSON); @@ -430,7 +432,7 @@ export class TypeAdapter { } let anyType = this.types.get("any"); if (anyType) { - return anyType; + return anyType; } anyType = new go.Any(); this.types.set("any", anyType); @@ -440,7 +442,7 @@ export class TypeAdapter { const boolKey = type.encode === "string" ? "boolean-string" : "boolean"; let primitiveBool = this.types.get(boolKey); if (primitiveBool) { - return primitiveBool; + return >primitiveBool; } primitiveBool = new go.Scalar("bool", type.encode === "string"); this.types.set(boolKey, primitiveBool); @@ -452,7 +454,7 @@ export class TypeAdapter { const dateKey = "plainDate"; let date = this.types.get(dateKey); if (date) { - return date; + return date; } date = new go.Time("PlainDate", false); this.types.set(dateKey, date); @@ -463,7 +465,7 @@ export class TypeAdapter { const decimalKey = "float64"; let decimalType = this.types.get(decimalKey); if (decimalType) { - return decimalType; + return >decimalType; } decimalType = new go.Scalar(decimalKey, type.encode === "string"); this.types.set(decimalKey, decimalType); @@ -474,7 +476,7 @@ export class TypeAdapter { const float32Key = "float32"; let float32 = this.types.get(float32Key); if (float32) { - return float32; + return >float32; } float32 = new go.Scalar(float32Key, type.encode === "string"); this.types.set(float32Key, float32); @@ -484,7 +486,7 @@ export class TypeAdapter { const float64Key = "float64"; let float64 = this.types.get(float64Key); if (float64) { - return float64; + return >float64; } float64 = new go.Scalar(float64Key, type.encode === "string"); this.types.set(float64Key, float64); @@ -501,7 +503,7 @@ export class TypeAdapter { const keyName = type.encode === "string" ? `${type.kind}-string` : type.kind; let intType = this.types.get(keyName); if (intType) { - return intType; + return intType; } intType = new go.Scalar(type.kind, type.encode === "string"); this.types.set(keyName, intType); @@ -511,7 +513,7 @@ export class TypeAdapter { const safeintkey = type.encode === "string" ? "int64-string" : "int64"; let int64 = this.types.get(safeintkey); if (int64) { - return int64; + return >int64; } int64 = new go.Scalar("int64", type.encode === "string"); this.types.set(safeintkey, int64); @@ -528,7 +530,7 @@ export class TypeAdapter { const encoding = "PlainTime"; let time = this.types.get(encoding); if (time) { - return time; + return time; } time = new go.Time(encoding, false); this.types.set(encoding, time); @@ -628,6 +630,48 @@ export class TypeAdapter { return >literalType; } + /** returns a pointer to the specified type */ + getPtrType(goType: T): go.Ptr { + let ptrKey = "ptr-"; + switch (goType.kind) { + case "constant": + case "etag": + case "model": + case "multipartContent": + case "polymorphicModel": + case "unionStruct": + ptrKey += goType.name; + break; + case "literal": { + // enum-value literals hold a ConstantValue object; key by its unique name. + const literal = goType.literal; + ptrKey += + goType.type.kind === "constant" + ? `${goType.kind}-${(literal).name}` + : `${goType.kind}-${goType.type.kind}-${literal}`; + break; + } + case "scalar": + ptrKey += `${goType.kind}-${goType.type}-${goType.encodeAsString}`; + break; + case "string": + ptrKey += `${goType.kind}`; + break; + case "time": + ptrKey += `${goType.kind}-${goType.format}-${goType.utc}`; + break; + default: + goType satisfies never; + } + + let ptrType = this.types.get(ptrKey); + if (!ptrType) { + ptrType = new go.Ptr(goType); + this.types.set(ptrKey, ptrType); + } + return >ptrType; + } + /** returns a Go string type */ getStringType(): go.String { const stringKey = "string"; @@ -851,10 +895,16 @@ export class TypeAdapter { access: prop.access, }); - const fieldByValue = isMultipartFormData - ? !prop.optional - : helpers.isTypePassedByValue(prop.type); - const field = new go.ModelField(fieldName, type, fieldByValue, serializedName, annotations); + // pointer-capable types are wrapped in a pointer. the exception is multipart/form + // data fields, which default to by-value with only the optional ones being + // pointer-to-type (see the isMultipartFormData comment above). + const usePtr = isMultipartFormData ? prop.optional : true; + const field = new go.ModelField( + fieldName, + usePtr && helpers.isPtrType(type) ? this.getPtrType(type) : type, + serializedName, + annotations, + ); field.docs.summary = prop.summary; field.docs.description = prop.doc; @@ -864,16 +914,17 @@ export class TypeAdapter { annotations.isDiscriminator = true; field.defaultValue = this.getDiscriminatorLiteral(prop); } else if (prop.clientDefaultValue) { - if (!go.isLiteralValueType(type)) { + const unwrappedPtr = go.unwrapPtr(type); + if (!go.isLiteralValueType(unwrappedPtr)) { throw new AdapterError( "InternalError", - `unexpected client side default kind ${type.kind} for field ${field.name}`, + `unexpected client side default kind ${unwrappedPtr.kind} for field ${field.name}`, prop.__raw?.node, ); } field.defaultValue = this.getLiteral( - type, + unwrappedPtr, prop.clientDefaultValue, helpers.isExtensibleEnum(prop.type), ); @@ -1115,7 +1166,7 @@ export class TypeAdapter { sdkType.__raw?.node, ); default: - return valueType; + return helpers.isPtrType(valueType) ? this.getPtrType(valueType) : valueType; } } @@ -1166,8 +1217,7 @@ export class TypeAdapter { } sliceArray = new go.SliceArray( - elementType, - this.codeModel.options["slice-elements-byval"] ?? false, + this.codeModel.options["slice-elements-byval"] ? elementType : this.getPtrType(elementType), getSliceArrayDelimiter(encoding), ); @@ -1196,13 +1246,9 @@ export class TypeAdapter { variant.__raw?.node, ); } - goUnion.fields.push( - new go.UnionField( - recursiveVariantFieldName(type), - type, - helpers.isTypePassedByValue(variant), - ), - ); + + const fieldType = helpers.isPtrType(type) ? this.getPtrType(type) : type; + goUnion.fields.push(new go.UnionField(recursiveVariantFieldName(fieldType), fieldType)); } goUnion.docs.summary = sdkUnion.summary; @@ -1323,6 +1369,8 @@ function recursiveVariantFieldName(type: go.WireType): string { return `Literal${recursiveVariantFieldName(type.type)}`; case "map": return `MapOf${recursiveVariantFieldName(type.valueType)}`; + case "ptr": + return recursiveVariantFieldName(type.ptrType); case "slice": return `SliceOf${recursiveVariantFieldName(type.elementType)}`; case "scalar":