in powershell/plugins/plugin-tweak-model.ts [71:383]
export async function tweakModelV2(state: State): Promise<PwshModel> {
const title = pascalCase(fixLeadingNumber(deconstruct(await state.getValue('title', state.model.info.title))));
state.setValue('title', title);
const serviceName = await state.getValue('service-name', titleToAzureServiceName(title));
state.setValue('service-name', serviceName);
const model = state.model;
const schemas = model.schemas;
// xichen: do we need other schema types?
const allSchemas: Array<Schema> = [...schemas.objects ?? [], ...schemas.choices ?? [], ...schemas.sealedChoices ?? []];
model.commands = <any>{
operations: new Dictionary<any>(),
parameters: new Dictionary<any>(),
};
// we're going to create a schema that represents the distinct sum
// of all operation PATH parameters
const universalId = new ObjectSchema(`${serviceName}Identity`, '');
// xichen: Add 'universal-parameter-type' in language.default.uid, so that we can find it later
universalId.language.default.uid = 'universal-parameter-type';
universalId.apiVersions = universalId.apiVersions || [];
state.model.schemas.objects = state.model.schemas.objects || [];
(<any>universalId.language.default).uid = 'universal-parameter-type';
state.model.schemas.objects.push(universalId);
for (const group of values(model.operationGroups)) {
for (const operation of values(group.operations)) {
for (const response of values(operation.responses)) {
// Mark returned object in response
const respSchema: Schema = (<any>response).schema;
if (respSchema?.type === SchemaType.Object) {
respSchema.extensions = respSchema.extensions || {};
respSchema.extensions['is-return-object'] = true;
}
}
for (const param of values(operation.parameters).where(each => each.protocol?.http?.in === ParameterLocation.Path)) {
const name = param.language.default.name;
const hasName = universalId.properties?.find((prop) => prop.language.default.name.toLocaleLowerCase() === name.toLocaleLowerCase());
if (!hasName) {
if (!universalId.properties) {
universalId.properties = [];
}
const newProp = new Property(name, param.language.default.description, param.schema);
newProp.required = false;
newProp.readOnly = false;
newProp.serializedName = param.language.default.serializedName;
universalId.properties.push(newProp);
}
}
}
}
if (await state.getValue('azure', false)) {
const idScheam = new Schema('_identity_type_', 'Resource identity path', SchemaType.String);
const idProp = new Property('id', 'Resource identity path', idScheam);
idProp.readOnly = false;
idProp.required = false;
idProp.language.default.uid = 'universal-parameter:resource identity';
if (!universalId.properties) {
universalId.properties = [];
}
universalId.properties.push(idProp);
}
// xichen: do nothing in m3 logic. Comment it out
// if an operation has a response that has a schema with string/binary we should make the response application/octet-stream
// for (const operationGroups of values(model.operationGroups)) {
// for (const operation of values(operationGroups.operations)) {
// for (const response of values(operation.responses)) {
// if ((response as any).schema) {
// const respSchema = response as any;
// if (respSchema.type === SchemaType.String && respSchema.format === StringFormat.Binary) {
// // WHY WAS THIS HERE?!
// // response.mimeTypes = [KnownMediaType.Stream];
// }
// }
// }
// }
// }
// schemas that have parents and implement properties that are in the parent schemas
// will have the property dropped in the child schema
for (const schema of values(model.schemas.objects)) {
if (length(schema.parents?.immediate) > 0) {
if (!dropDuplicatePropertiesInChildSchemas(schema, state)) {
throw new Error('Schemas are in conflict.');
}
}
}
if (await state.getValue('use-storage-pipeline', false)) {
// we're going to create new models for the reponse headers ?
} else {
// if an operation has a body parameter with string/binary, we should make the request application/octet-stream
// === Header Schemas ===
// go thru the operations, find responses that have header values, and add a property to the schemas that are returned with those values
for (const operationGroups of values(model.operationGroups)) {
for (const operation of values(operationGroups.operations)) {
for (const response of values(operation.responses)) {
// for a given response, find the possible models that can be returned from the service
for (const header of values(response.protocol.http?.headers)) {
if (!(<any>response).schema) {
// no response schema? can we fake one?
// service.message{ Channel: Channel.Debug, Text: `${header.key} is in ${operation.details.default.name} but there is no response model` });
continue;
}
// if the method response has a schema and it's an object, we're going to add our properties to the schema object.
// yes, this means that the reponse model may have properties that are undefined if the server doesn't send back the header
// and other operations might add other headers that are not the same.
// if the method's response is a primitive value (string, boolean, null, number) or an array, we can't modify that type obviously
// in which case, we're going to add a header
// work with schemas that have objects only.
if ((<any>response).schema.type === SchemaType.Object) {
const respSchema = <ObjectSchema>((<any>response).schema);
const curHeader = <any>header;
const headerKey = <string>curHeader.header;
respSchema.language.default.hasHeaders = true;
const property = values(getAllProperties(respSchema)).first((each) => each.language.default.name === headerKey);
if (!property) {
state.message({ Channel: Channel.Debug, Text: `Adding header property '${headerKey}' to model ${respSchema.language.default.name}` });
// create a property for the header value
const newProperty = new Property(headerKey, curHeader.description || '', curHeader.schema);
newProperty.language.default.required = false;
// mark it that it's a header-only property
newProperty.language.default[HeaderProperty] = HeaderPropertyType.Header;
// add it to this model.
if (!respSchema.properties) {
respSchema.properties = [];
}
respSchema.properties.push(newProperty);
} else {
// there is a property with this name already.
// was this previously declared as a header only property?
if (!property.language.default[HeaderProperty]) {
state.message({ Channel: Channel.Debug, Text: `Property ${headerKey} in model ${respSchema.language.default.name} can also come from the header.` });
// no.. There is duplication between header and body property. Probably because of etags.
// tell it to be a header-and-body property.
property.language.default[HeaderProperty] = HeaderPropertyType.HeaderAndBody;
property.language.default.name = headerKey;
}
}
}
}
}
}
}
}
// remove well-known header parameters from operations and add mark the operation has supporting that feature
for (const operationGroups of values(model.operationGroups)) {
for (const operation of values(operationGroups.operations)) {
// if we have an operation with a body, and content-type is a multipart/formdata
// then we should go thru the parameters of the body and look for a string/binary parameters
// and remember to add another parameter for the filename of the string/binary
const request = operation.requests?.[0];
request?.parameters?.filter((param) => param.schema.type !== SchemaType.Object && param.protocol.http?.in === 'body' && param.protocol.http?.style === KnownMediaType.Multipart)
.forEach((param) => {
for (const prop of values(getAllProperties(<ObjectSchema>param.schema))) {
if (prop.schema.type === SchemaType.Binary) {
prop.language.default.isNamedStream = true;
}
}
});
// move well-known hearder parameters into details, and we can process them in the generator how we please.
// operation.details.default.headerparameters = values(operation.parameters).where(p => p.in === ParameterLocation.Header && ['If-Match', 'If-None-Match'].includes(p.name)).toArray();
// remove if-match and if-none-match parameters from the operation itself.
// operation.parameters = values(operation.parameters).where(p => !(p.in === ParameterLocation.Header && ['If-Match', 'If-None-Match'].includes(p.name))).toArray();
}
}
// identify models that are polymorphic in nature
for (const schema of allSchemas) {
if (schema instanceof ObjectSchema) {
const objSchema = <ObjectSchema>schema;
// if this actual type is polymorphic, make sure we know that.
// parent class
if (objSchema.discriminator) {
objSchema.language.default.isPolymorphic = true;
if (objSchema.children) {
objSchema.language.default.polymorphicChildren = objSchema.children?.all;
}
}
// sub class
if (objSchema.discriminatorValue) {
objSchema.language.default.discriminatorValue = objSchema.extensions?.['x-ms-discriminator-value'];
}
}
}
// identify parameters that are constants
for (const group of values(model.operationGroups)) {
for (const operation of values(group.operations)) {
for (const parameter of values(operation.parameters)) {
if (parameter.required) {
if (parameter.schema.type === SchemaType.Choice) {
const choiceSchema = <ChoiceSchema>parameter.schema;
if (choiceSchema.choices.length === 1) {
parameter.language.default.constantValue = choiceSchema.choices[0].value;
}
} else if (parameter.schema.type === SchemaType.Constant) {
const constantSchema = <ConstantSchema>parameter.schema;
parameter.language.default.constantValue = constantSchema.value.value;
} else if (parameter.schema.type === SchemaType.SealedChoice) {
const sealedChoiceSchema = <SealedChoiceSchema>parameter.schema;
if (sealedChoiceSchema.choices.length === 1) {
parameter.language.default.constantValue = sealedChoiceSchema.choices[0].value;
if (sealedChoiceSchema.language.default.skip !== false) {
sealedChoiceSchema.language.default.skip = true;
}
}
}
} else {
if (parameter.schema.type === SchemaType.SealedChoice) {
const sealedChoiceSchema = <SealedChoiceSchema>parameter.schema;
if (sealedChoiceSchema.choices.length === 1) {
sealedChoiceSchema.language.default.skip = false;
}
}
}
}
}
}
// identify properties that are constants
for (const schema of values(schemas.objects)) {
for (const property of values(schema.properties)) {
if (property === undefined) {
continue;
}
if (property.required) {
if (property.schema.type === SchemaType.Choice) {
const choiceSchema = <ChoiceSchema>property.schema;
if (choiceSchema.choices.length === 1) {
// properties with an enum single value are constants
// add the constant value
property.language.default.constantValue = choiceSchema.choices[0].value;
}
} else if (property.schema.type === SchemaType.Constant) {
const constantSchema = <ConstantSchema>property.schema;
property.language.default.constantValue = constantSchema.value.value;
} else if (property.schema.type === SchemaType.SealedChoice) {
const sealedChoiceSchema = <SealedChoiceSchema>property.schema;
if (sealedChoiceSchema.choices.length === 1) {
property.language.default.constantValue = sealedChoiceSchema.choices[0].value;
if (sealedChoiceSchema.language.default.skip !== false) {
sealedChoiceSchema.language.default.skip = true;
}
}
}
} else {
if (property.schema.type === SchemaType.SealedChoice) {
const sealedChoiceSchema = <SealedChoiceSchema>property.schema;
if (sealedChoiceSchema.choices.length === 1) {
sealedChoiceSchema.language.default.skip = false;
}
}
}
}
}
// xichen: Do we need skip?
// const enumsToSkip = new Set<string>();
// // identify properties that are constants
// for (const schema of values(model.schemas)) {
// for (const property of values(schema.properties)) {
// if (property.details.default.required && length(property.schema.enum) === 1) {
// // properties with an enum single value are constants
// // add the constant value
// property.details.default.constantValue = property.schema.enum[0];
// // mark as skip the generation of this model
// enumsToSkip.add(property.schema.details.default.uid);
// // make it a string and keep its name
// property.schema = new Schema(property.schema.details.default.name, { type: property.schema.type });
// } else {
// enumsToSkip.delete(property.schema.details.default.uid);
// }
// }
// }
// // mark enums that shouldn't be generated
// for (const schema of values(model.schemas)) {
// if (enumsToSkip.has(schema.details.default.uid)) {
// schema.details.default.skip = true;
// }
// }
return model;
}