Skip to content

Commit

Permalink
Add support for XML in tsp (#1449)
Browse files Browse the repository at this point in the history
Adapt various tsp decorators that contain XML info including attributes,
names, and unwrapped arrays.
Added support for marshaling maps in XML format.
Moved XMLInfo out of Parameter and into body param types.
  • Loading branch information
jhendrixMSFT authored Oct 23, 2024
1 parent 6343138 commit 8976aa6
Show file tree
Hide file tree
Showing 61 changed files with 4,460 additions and 11 deletions.
2 changes: 1 addition & 1 deletion packages/autorest.go/src/m4togocodemodel/clients.ts
Original file line number Diff line number Diff line change
Expand Up @@ -345,9 +345,9 @@ function adaptMethodParameter(op: m4.Operation, param: m4.Parameter): go.Paramet
} else {
const format = adaptBodyFormat(op.requests![0].protocol);
adaptedParam = new go.BodyParameter(param.language.go!.name, format, contentType, bodyType, placement, param.language.go!.byValue);
(<go.BodyParameter>adaptedParam).xml = adaptXMLInfo(param.schema);
}

adaptedParam.xml = adaptXMLInfo(param.schema);
break;
}
case 'header': {
Expand Down
26 changes: 26 additions & 0 deletions packages/autorest.go/test/autorest/xmlgroup/zz_models_serde.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

34 changes: 34 additions & 0 deletions packages/autorest.go/test/autorest/xmlgroup/zz_xml_helper.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

13 changes: 13 additions & 0 deletions packages/autorest.go/test/storage/azblob/zz_models_serde.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

34 changes: 34 additions & 0 deletions packages/autorest.go/test/storage/azblob/zz_xml_helper.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

11 changes: 7 additions & 4 deletions packages/codegen.go/src/models.ts
Original file line number Diff line number Diff line change
Expand Up @@ -200,7 +200,8 @@ function generateModelDefs(modelImports: ImportManager, serdeImports: ImportMana
if (needsDateTimeMarshalling || byteArrayFormat) {
generateXMLUnmarshaller(model, modelDef, serdeImports);
}
} else if (needsXMLDictionaryUnmarshalling(model)) {
} else if (needsXMLDictionaryHelper(model)) {
generateXMLMarshaller(model, modelDef, serdeImports);
generateXMLUnmarshaller(model, modelDef, serdeImports);
}
modelDefs.push(modelDef);
Expand All @@ -224,9 +225,9 @@ function generateModelDefs(modelImports: ImportManager, serdeImports: ImportMana
return modelDefs;
}

function needsXMLDictionaryUnmarshalling(modelType: go.ModelType): boolean {
function needsXMLDictionaryHelper(modelType: go.ModelType): boolean {
for (const field of values(modelType.fields)) {
// additional properties uses an internal wrapper type with its own unmarshaller
// additional properties uses an internal wrapper type with its own serde impl
if (go.isMapType(field.type) && !field.annotations.isAdditionalProperties) {
return true;
}
Expand Down Expand Up @@ -652,7 +653,7 @@ function recursivePopulateDiscriminator(item: go.PossibleType, receiver: string,
}

function generateXMLMarshaller(modelType: go.ModelType, modelDef: ModelDef, imports: ImportManager) {
// only needed for types with time.Time or where the XML name doesn't match the type name
// only needed for types with time.Time, maps, or where the XML name doesn't match the type name
const receiver = modelDef.receiverName();
const desc = `MarshalXML implements the xml.Marshaller interface for type ${modelDef.Name}.`;
let text = `func (${receiver} ${modelDef.Name}) MarshalXML(enc *xml.Encoder, start xml.StartElement) error {\n`;
Expand All @@ -665,6 +666,8 @@ function generateXMLMarshaller(modelType: go.ModelType, modelDef: ModelDef, impo
text += `\tif ${receiver}.${field.name} != nil {\n`;
text += `\t\taux.${field.name} = &${receiver}.${field.name}\n`;
text += '\t}\n';
} else if (field.annotations.isAdditionalProperties || go.isMapType(field.type)) {
text += `\taux.${field.name} = (additionalProperties)(${receiver}.${field.name})\n`;
} else if (go.isBytesType(field.type)) {
imports.add('github.com/Azure/azure-sdk-for-go/sdk/azcore/runtime');
text += `\tif ${receiver}.${field.name} != nil {\n`;
Expand Down
34 changes: 34 additions & 0 deletions packages/codegen.go/src/xmlAdditionalProps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,40 @@ export async function generateXMLAdditionalPropsHelpers(codeModel: go.CodeModel)
text += `
type additionalProperties map[string]*string
// MarshalXML implements the xml.Marshaler interface for additionalProperties.
func (ap additionalProperties) MarshalXML(e *xml.Encoder, start xml.StartElement) error {
if err := e.EncodeToken(start); err != nil {
return err
}
for k, v := range ap {
err := e.EncodeToken(xml.StartElement{
Name: xml.Name{
Local: k,
},
})
if err != nil {
return err
}
if v != nil {
err = e.EncodeToken(xml.CharData(*v))
if err != nil {
return err
}
}
err = e.EncodeToken(xml.EndElement{
Name: xml.Name{
Local: k,
},
})
if err != nil {
return err
}
}
return e.EncodeToken(xml.EndElement{
Name: start.Name,
})
}
// UnmarshalXML implements the xml.Unmarshaler interface for additionalProperties.
func (ap *additionalProperties) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
tokName := ""
Expand Down
6 changes: 4 additions & 2 deletions packages/codemodel.go/src/param.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,6 @@ export interface Parameter {
group?: ParameterGroup;

location: ParameterLocation;

xml?: type.XMLInfo;
}

export function isClientSideDefault(kind: ParameterKind): kind is ClientSideDefault {
Expand Down Expand Up @@ -149,6 +147,8 @@ export interface BodyParameter extends Parameter {

// "application/text" etc...
contentType: string;

xml?: type.XMLInfo;
}

// PartialBodyParameter is a field within a struct type sent in the body
Expand All @@ -157,6 +157,8 @@ export interface PartialBodyParameter extends Parameter {
serializedName: string;

format: 'JSON' | 'XML';

xml?: type.XMLInfo;
}

export interface FormBodyParameter extends Parameter {
Expand Down
1 change: 1 addition & 0 deletions packages/typespec-go/.scripts/tspcompile.js
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ const cadlRanch = {
'mediatypegroup': ['payload/media-type'],
//'multipartgroup': ['payload/multipart'], // TODO: https://github.com/Azure/autorest.go/issues/1445
'pageablegroup': ['payload/pageable'],
'xmlgroup': ['payload/xml', 'slice-elements-byval=true'],
'srvdrivenoldgroup': ['resiliency/srv-driven/old.tsp'],
'srvdrivennewgroup': ['resiliency/srv-driven'],
'jsongroup': ['serialization/encoded-name/json'],
Expand Down
4 changes: 4 additions & 0 deletions packages/typespec-go/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

## 0.3.2 (Unreleased)

### Features Added

* Add support for XML payloads.

### Bugs Fixed

* Fake servers will honor the caller's context in the `*http.Request`.
Expand Down
6 changes: 6 additions & 0 deletions packages/typespec-go/src/emitter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import { generateResponses } from '../../codegen.go/src/responses.js';
import { generateTimeHelpers } from '../../codegen.go/src/time.js';
import { generateServers } from '../../codegen.go/src/fake/servers.js';
import { generateServerFactory } from '../../codegen.go/src/fake/factory.js';
import { generateXMLAdditionalPropsHelpers } from '../../codegen.go/src/xmlAdditionalProps.js';
import { existsSync } from 'fs';
import { mkdir, readFile, writeFile } from 'fs/promises';
import { EmitContext } from '@typespec/compiler';
Expand Down Expand Up @@ -121,6 +122,11 @@ export async function $onEmit(context: EmitContext<GoEmitterOptions>) {
writeFile(`${context.emitterOutputDir}/${filePrefix}${helper.name.toLowerCase()}.go`, helper.content);
}

const xmlAddlProps = await generateXMLAdditionalPropsHelpers(codeModel);
if (xmlAddlProps.length > 0) {
writeFile(`${context.emitterOutputDir}/${filePrefix}xml_helper.go`, xmlAddlProps);
}

if (codeModel.options.generateFakes) {
const serverContent = await generateServers(codeModel);
if (serverContent.servers.length > 0) {
Expand Down
8 changes: 7 additions & 1 deletion packages/typespec-go/src/tcgcadapter/adapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,13 @@ export async function tcgcToGoCodeModel(context: EmitContext<GoEmitterOptions>):
options.azcoreVersion = context.options['azcore-version'];
}

const sdkContext = await tcgc.createSdkContext(context);
// @encodedName can be used in XML scenarios, it
// is effectively the same as TypeSpec.Xml.@name.
// however, it's filtered out by default so we need
// to add it to the allow list of decorators
const sdkContext = await tcgc.createSdkContext(context, '@azure-tools/typespec-go', {
additionalDecorators: ['TypeSpec\\.@encodedName'],
});
let codeModelType: go.CodeModelType = 'data-plane';
if (sdkContext.arm === true) {
codeModelType = 'azure-arm';
Expand Down
61 changes: 58 additions & 3 deletions packages/typespec-go/src/tcgcadapter/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -546,7 +546,7 @@ export class typeAdapter {
} else {
modelType = new go.ModelType(modelName, annotations, usage);
// polymorphic types don't have XMLInfo
// TODO: XMLInfo
modelType.xml = adaptXMLInfo(model.decorators);
}

modelType.docs.summary = model.summary;
Expand Down Expand Up @@ -616,8 +616,7 @@ export class typeAdapter {
field.defaultValue = this.getDiscriminatorLiteral(prop);
}

// TODO: XMLInfo
//field.xml = adaptXMLInfo(prop.schema);
field.xml = adaptXMLInfo(prop.decorators, field);

return field;
}
Expand Down Expand Up @@ -1072,3 +1071,59 @@ function aggregateProperties(model: tcgc.SdkModelType): {props: Array<tcgc.SdkMo
}
return {props: allProps, addlProps: addlProps};
}

// called for models and model fields. for the former, the field param will be undefined
export function adaptXMLInfo(decorators: Array<tcgc.DecoratorInfo>, field?: go.ModelField): go.XMLInfo | undefined {
// if there are no decorators and this isn't a slice
// type in a model field then do nothing
if (decorators.length === 0 && (!field || (!go.isSliceType(field.type)))) {
return undefined;
}

const xmlInfo = new go.XMLInfo();
if (field && go.isSliceType(field.type)) {
// for tsp, arrays are wrapped by default
xmlInfo.wraps = go.getTypeDeclaration(field.type.elementType);
}

const handleName = (decorator: tcgc.DecoratorInfo): void => {
if (field) {
xmlInfo.name = decorator.arguments['name'];
} else {
// when applied to a model, it means the model's XML element
// node has a different name than the model.
xmlInfo.wrapper = decorator.arguments['name'];
}
};

for (const decorator of decorators) {
switch (decorator.name) {
case 'TypeSpec.@encodedName':
if (decorator.arguments['mimeType'] === 'application/xml') {
handleName(decorator);
}
break;
case 'TypeSpec.Xml.@attribute':
xmlInfo.attribute = true;
break;
case 'TypeSpec.Xml.@name':
handleName(decorator);
break;
case 'TypeSpec.Xml.@unwrapped':
// unwrapped can only be applied fields
if (field) {
if (go.isPrimitiveType(field.type) && field.type.typeName === 'string') {
// an unwrapped string means it's text
xmlInfo.text = true;
} else if (go.isSliceType(field.type)) {
// unwrapped slice. default to using the serialized name
xmlInfo.wraps = undefined;
xmlInfo.name = field.serializedName;
}
}
break;
}
}

return xmlInfo;
}
Loading

0 comments on commit 8976aa6

Please sign in to comment.