Skip to content

Commit

Permalink
Add support for additional properties (#423)
Browse files Browse the repository at this point in the history
Add an 'AdditionalProperties' map on applicable types along with a
custom marshaller and unmarshaller.
If a type supports additional properties and also has a field named
'AdditionalProperties' rename the field by appending a '1'.
Note that the current implementation doesn't work for discriminated
types that contain additional properties, or types that use XML.  Will
be fixed in later PRs.
  • Loading branch information
jhendrixMSFT authored Jun 11, 2020
1 parent b493de4 commit 469d158
Show file tree
Hide file tree
Showing 15 changed files with 1,119 additions and 456 deletions.
2 changes: 1 addition & 1 deletion rushScripts/regeneration.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ const exec = require('child_process').exec;
const swaggerDir = 'src/node_modules/@microsoft.azure/autorest.testserver/swagger/';

const goMappings = {
//'additionalpropertiesgroup': 'additionalProperties.json',
'additionalpropsgroup': 'additionalProperties.json',
'arraygroup': 'body-array.json',
'azurereportgroup': 'azure-report.json',
'azurespecialsgroup': 'azure-special-properties.json',
Expand Down
13 changes: 12 additions & 1 deletion src/common/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import { ArraySchema, ObjectSchema, Operation, Parameter, Response, Schema, SchemaResponse } from '@azure-tools/codemodel';
import { ArraySchema, ObjectSchema, Operation, Parameter, Response, Schema, SchemaResponse, SchemaType } from '@azure-tools/codemodel';
import { values } from '@azure-tools/linq';

// aggregates the Parameter in op.parameters and the first request
export function aggregateParameters(op: Operation): Array<Parameter> {
Expand Down Expand Up @@ -55,3 +56,13 @@ export function isLROOperation(op: Operation): boolean {
export function isObjectSchema(obj: Schema): obj is ObjectSchema {
return (obj as ObjectSchema).properties !== undefined;
}

// returns the additional properties schema if the ObjectSchema defines 'additionalProperties'
export function hasAdditionalProperties(obj: ObjectSchema): Schema | undefined {
for (const parent of values(obj.parents?.immediate)) {
if (parent.type === SchemaType.Dictionary) {
return parent;
}
}
return undefined;
}
117 changes: 112 additions & 5 deletions src/generator/models.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,10 @@
*--------------------------------------------------------------------------------------------*/

import { Session } from '@azure-tools/autorest-extension-base';
import { camelCase, comment, pascalCase } from '@azure-tools/codegen';
import { CodeModel, ConstantSchema, GroupProperty, ImplementationLocation, ObjectSchema, Language, Schema, SchemaType, Parameter, Property } from '@azure-tools/codemodel';
import { comment, pascalCase } from '@azure-tools/codegen';
import { CodeModel, ConstantSchema, GroupProperty, ImplementationLocation, ObjectSchema, Language, Schema, SchemaType, Parameter, Property, DictionarySchema } from '@azure-tools/codemodel';
import { values } from '@azure-tools/linq';
import { isArraySchema, isObjectSchema } from '../common/helpers';
import { isArraySchema, isObjectSchema, hasAdditionalProperties } from '../common/helpers';
import { contentPreamble, hasDescription, sortAscending, substituteDiscriminator } from './helpers';
import { ImportManager } from './imports';

Expand Down Expand Up @@ -152,7 +152,8 @@ class StructDef {
let tag = ` \`${this.Language.marshallingFormat}:"${serialization}"\``;
// if this is a response type then omit the tag IFF the marshalling format is
// JSON, it's a header or is the RawResponse field. XML marshalling needs a tag.
if (this.Language.responseType === true && (this.Language.marshallingFormat !== 'xml' || prop.language.go!.name === 'RawResponse')) {
// also omit the tag for additionalProperties
if ((this.Language.responseType === true && (this.Language.marshallingFormat !== 'xml' || prop.language.go!.name === 'RawResponse')) || prop.language.go!.isAdditionalProperties) {
tag = '';
}
let pointer = '*';
Expand Down Expand Up @@ -238,6 +239,7 @@ function generateStructs(objects?: ObjectSchema[]): StructDef[] {
text += '}\n\n';
structDef.Methods.push({ name: 'Error', text: text });
}
// TODO: unify marshalling schemes
if (obj.discriminator) {
// only need to generate interface method and internal marshaller for discriminators (Fish, Salmon, Shark)
generateDiscriminatorMethods(obj, structDef, parentType!);
Expand All @@ -252,11 +254,19 @@ function generateStructs(objects?: ObjectSchema[]): StructDef[] {
} else if (hasPolymorphicField) {
generateDiscriminatedTypeUnmarshaller(obj, structDef, parentType!);
} else if (obj.language.go!.needsDateTimeMarshalling || obj.language.go!.xmlWrapperName) {
// TODO: unify marshalling schemes?
generateMarshaller(structDef);
if (obj.language.go!.needsDateTimeMarshalling) {
generateUnmarshaller(structDef);
}
} else if (obj.language.go!.marshallingFormat === 'json' && (hasAdditionalProperties(obj) || (parentType && hasAdditionalProperties(parentType)))) {
// TODO: support for XML
generateAdditionalPropertiesMarshaller(structDef, parentType);
let schema = hasAdditionalProperties(obj);
if (!schema) {
// must be the parent
schema = hasAdditionalProperties(parentType!);
}
generateAdditionalPropertiesUnmarshaller(structDef, (<DictionarySchema>schema).elementType, parentType);
}
structDef.ComposedOf.sort((a: ObjectSchema, b: ObjectSchema) => { return sortAscending(a.language.go!.name, b.language.go!.name); });
structTypes.push(structDef);
Expand Down Expand Up @@ -382,6 +392,9 @@ function generateDiscriminatedTypeMarshaller(obj: ObjectSchema, structDef: Struc
let marshaller = `func (${receiver} ${typeName}) MarshalJSON() ([]byte, error) {\n`;
marshaller += `\tobjectMap := ${receiver}.${parentType!.language.go!.name}.marshalInternal(${obj.discriminatorValue})\n`;
for (const prop of values(structDef.Properties)) {
if (prop.language.go!.isAdditionalProperties) {
continue;
}
marshaller += `\tif ${receiver}.${prop.language.go!.name} != nil {\n`;
if (prop.schema.language.go!.internalTimeType) {
marshaller += `\t\tobjectMap["${prop.serializedName}"] = (*${prop.schema.language.go!.internalTimeType})(${receiver}.${prop.language.go!.name})\n`;
Expand Down Expand Up @@ -415,6 +428,9 @@ function generateDiscriminatedTypeUnmarshaller(obj: ObjectSchema, structDef: Str
unmarshaller += '\t\tswitch k {\n';
// unmarshal each field one by one
for (const prop of values(structDef.Properties)) {
if (prop.language.go!.isAdditionalProperties) {
continue;
}
unmarshaller += `\t\tcase "${prop.serializedName}":\n`;
unmarshaller += '\t\t\tif v != nil {\n';
if (prop.schema.language.go!.discriminatorInterface) {
Expand Down Expand Up @@ -532,3 +548,94 @@ function generateAliasType(structDef: StructDef, receiver: string, forMarshal: b
text += `\t}\n`;
return text;
}

function generateAdditionalPropertiesMarshaller(structDef: StructDef, parentType?: ObjectSchema) {
imports.add('encoding/json');
const typeName = structDef.Language.name;
const receiver = typeName[0].toLowerCase();
// generate marshaller method
let marshaller = `func (${receiver} ${typeName}) MarshalJSON() ([]byte, error) {\n`;
marshaller += '\tobjectMap := make(map[string]interface{})\n';
const emitMarshaller = function (prop: Property): string {
let text = `\tif ${receiver}.${prop.language.go!.name} != nil {\n`;
if (prop.schema.language.go!.internalTimeType) {
text += `\t\tobjectMap["${prop.serializedName}"] = (*${prop.schema.language.go!.internalTimeType})(${receiver}.${prop.language.go!.name})\n`;
} else {
text += `\t\tobjectMap["${prop.serializedName}"] = ${receiver}.${prop.language.go!.name}\n`;
}
text += `\t}\n`;
return text;
}
// TODO: multiple inheritance
for (const prop of values(parentType?.properties)) {
if (prop.language.go!.isAdditionalProperties) {
continue;
}
marshaller += emitMarshaller(prop);
}
for (const prop of values(structDef.Properties)) {
if (prop.language.go!.isAdditionalProperties) {
continue;
}
marshaller += emitMarshaller(prop);
}
marshaller += `\tif ${receiver}.AdditionalProperties != nil {\n`;
marshaller += `\t\tfor k, v := range *${receiver}.AdditionalProperties {\n`;
marshaller += '\t\t\tobjectMap[k] = v\n';
marshaller += '\t\t}\n';;
marshaller += '\t}\n';
marshaller += '\treturn json.Marshal(objectMap)\n';
marshaller += '}\n\n';
structDef.Methods.push({ name: 'MarshalJSON', text: marshaller });
}

function generateAdditionalPropertiesUnmarshaller(structDef: StructDef, elementType: Schema, parentType?: ObjectSchema) {
imports.add('encoding/json');
const typeName = structDef.Language.name;
const receiver = typeName[0].toLowerCase();
let unmarshaller = `func (${receiver} *${typeName}) UnmarshalJSON(data []byte) error {\n`;
unmarshaller += '\tvar rawMsg map[string]*json.RawMessage\n';
unmarshaller += '\tif err := json.Unmarshal(data, &rawMsg); err != nil {\n';
unmarshaller += '\t\treturn err\n';
unmarshaller += '\t}\n';
unmarshaller += '\tfor k, v := range rawMsg {\n';
unmarshaller += '\t\tvar err error\n';
unmarshaller += '\t\tswitch k {\n';
const emitUnmarshaller = function (prop: Property): string {
let text = `\t\tcase "${prop.serializedName}":\n`;
text += '\t\t\tif v != nil {\n';
text += `\t\t\t\terr = json.Unmarshal(*v, &${receiver}.${prop.language.go!.name})\n`;
text += '\t\t\t}\n';
return text;
}
// TODO: multiple inheritance
for (const prop of values(parentType?.properties)) {
if (prop.language.go!.isAdditionalProperties) {
continue;
}
unmarshaller += emitUnmarshaller(prop);
}
for (const prop of values(structDef.Properties)) {
if (prop.language.go!.isAdditionalProperties) {
continue;
}
unmarshaller += emitUnmarshaller(prop);
}
unmarshaller += '\t\tdefault:\n';
unmarshaller += `\t\t\tif ${receiver}.AdditionalProperties == nil {\n`;
unmarshaller += `\t\t\t\t${receiver}.AdditionalProperties = &map[string]${elementType.language.go!.name}{}\n`;
unmarshaller += '\t\t\t}\n';
unmarshaller += '\t\t\tif v != nil {\n';
unmarshaller += `\t\t\t\tvar aux ${elementType.language.go!.name}\n`;
unmarshaller += '\t\t\t\terr = json.Unmarshal(*v, &aux)\n';
unmarshaller += `\t\t\t\t(*${receiver}.AdditionalProperties)[k] = aux\n`;
unmarshaller += '\t\t\t}\n';
unmarshaller += '\t\t}\n';
unmarshaller += '\t\tif err != nil {\n';
unmarshaller += '\t\t\treturn err\n';
unmarshaller += '\t\t}\n';
unmarshaller += '\t}\n';
unmarshaller += '\treturn nil\n';
unmarshaller += '}\n\n';
structDef.Methods.push({ name: 'UnmarshalJSON', text: unmarshaller });
}
9 changes: 7 additions & 2 deletions src/transform/namer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@
import { pascalCase, camelCase } from '@azure-tools/codegen';
import { Session } from '@azure-tools/autorest-extension-base';
import { CodeModel, Language } from '@azure-tools/codemodel';
import { length, visitor, clone, values } from '@azure-tools/linq';
import { visitor, clone, values } from '@azure-tools/linq';
import { CommonAcronyms, ReservedWords } from './mappings';
import { aggregateParameters, isLROOperation } from '../common/helpers';
import { aggregateParameters, hasAdditionalProperties, isLROOperation } from '../common/helpers';

const requestMethodSuffix = 'CreateRequest';
const responseMethodSuffix = 'HandleResponse';
Expand Down Expand Up @@ -62,6 +62,11 @@ export async function namer(session: Session<CodeModel>) {
for (const prop of values(obj.properties)) {
const details = <Language>prop.language.go;
details.name = getEscapedReservedName(removePrefix(capitalizeAcronyms(pascalCase(details.name)), 'XMS'), 'Field');
if (hasAdditionalProperties(obj) && details.name === 'AdditionalProperties') {
// this is the case where a type contains the generic additional properties
// and also has a field named additionalProperties. we rename the field.
details.name = 'AdditionalProperties1';
}
}
}

Expand Down
15 changes: 14 additions & 1 deletion src/transform/transform.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { camelCase, KnownMediaType, pascalCase, serialize } from '@azure-tools/c
import { Host, startSession, Session } from '@azure-tools/autorest-extension-base';
import { ObjectSchema, ArraySchema, ChoiceValue, codeModelSchema, CodeModel, DateTimeSchema, GroupProperty, HttpHeader, HttpResponse, ImplementationLocation, Language, OperationGroup, SchemaType, NumberSchema, Operation, SchemaResponse, Parameter, Property, Protocols, Response, Schema, DictionarySchema, Protocol, ChoiceSchema, SealedChoiceSchema, ConstantSchema } from '@azure-tools/codemodel';
import { items, values } from '@azure-tools/linq';
import { aggregateParameters, isPageableOperation, isObjectSchema, isSchemaResponse, PagerInfo, isLROOperation, PollerInfo } from '../common/helpers';
import { aggregateParameters, hasAdditionalProperties, isPageableOperation, isObjectSchema, isSchemaResponse, PagerInfo, isLROOperation, PollerInfo } from '../common/helpers';
import { namer, removePrefix } from './namer';

// The transformer adds Go-specific information to the code model.
Expand Down Expand Up @@ -35,6 +35,12 @@ export async function transform(host: Host) {
async function process(session: Session<CodeModel>) {
processOperationRequests(session);
processOperationResponses(session);
// fix up dictionary element types (additional properties)
// this must happen before processing objects as we depend on the
// schema type being an actual Go type.
for (const dictionary of values(session.model.schemas.dictionaries)) {
dictionary.elementType.language.go!.name = schemaTypeToGoType(session.model, dictionary.elementType, false);
}
// fix up struct field types
for (const obj of values(session.model.schemas.objects)) {
if (obj.discriminator) {
Expand Down Expand Up @@ -73,6 +79,13 @@ async function process(session: Session<CodeModel>) {
// the format to JSON as the vast majority of specs use JSON.
obj.language.go!.marshallingFormat = 'json';
}
const addPropsSchema = hasAdditionalProperties(obj);
if (addPropsSchema) {
// add an 'AdditionalProperties' field to the type
const addProps = newProperty('AdditionalProperties', 'Contains additional key/value pairs not defined in the schema.', addPropsSchema);
addProps.language.go!.isAdditionalProperties = true;
obj.properties?.push(addProps);
}
}
// fix up enum types
for (const choice of values(session.model.schemas.choices)) {
Expand Down
Loading

0 comments on commit 469d158

Please sign in to comment.