@azure-tools/typespec-autorest 0.43.0-dev.0 → 0.43.0-dev.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,7 @@
1
- import { extractLroStates, getArmResourceIdentifierConfig, getAsEmbeddingVector, getLroMetadata, getPagedResult, getUnionAsEnum, isFixed, } from "@azure-tools/typespec-azure-core";
1
+ import { FinalStateValue, extractLroStates, getArmResourceIdentifierConfig, getAsEmbeddingVector, getLroMetadata, getPagedResult, getUnionAsEnum, } from "@azure-tools/typespec-azure-core";
2
+ import { getArmCommonTypeOpenAPIRef, isArmCommonType, } from "@azure-tools/typespec-azure-resource-manager";
2
3
  import { shouldFlattenProperty } from "@azure-tools/typespec-client-generator-core";
3
- import { NoTarget, SyntaxKind, compilerAssert, createDiagnosticCollector, getAllTags, getDirectoryPath, getDiscriminator, getDoc, getEncode, getFormat, getKnownValues, getMaxItems, getMaxLength, getMaxValue, getMinItems, getMinLength, getMinValue, getPattern, getProjectedName, getProperty, getPropertyType, getRelativePathFromDirectory, getRootLength, getSummary, getVisibility, ignoreDiagnostics, interpolatePath, isArrayModelType, isDeprecated, isErrorModel, isErrorType, isGlobalNamespace, isNeverType, isNullType, isNumericType, isRecordModelType, isSecret, isService, isStringType, isTemplateDeclaration, isTemplateDeclarationOrInstance, isVoidType, navigateTypesInNamespace, resolveEncodedName, resolvePath, stringTemplateToString, } from "@typespec/compiler";
4
+ import { NoTarget, SyntaxKind, compilerAssert, createDiagnosticCollector, explainStringTemplateNotSerializable, getAllTags, getDirectoryPath, getDiscriminator, getDoc, getEncode, getFormat, getKnownValues, getMaxItems, getMaxLength, getMaxValue, getMinItems, getMinLength, getMinValue, getPattern, getProjectedName, getProperty, getPropertyType, getRelativePathFromDirectory, getRootLength, getSummary, getVisibility, ignoreDiagnostics, interpolatePath, isArrayModelType, isDeprecated, isErrorModel, isErrorType, isGlobalNamespace, isNeverType, isNullType, isNumericType, isRecordModelType, isSecret, isService, isStringType, isTemplateDeclaration, isTemplateDeclarationOrInstance, isVoidType, navigateTypesInNamespace, resolveEncodedName, resolvePath, } from "@typespec/compiler";
4
5
  import { TwoLevelMap } from "@typespec/compiler/utils";
5
6
  import { Visibility, createMetadataInfo, getAllHttpServices, getAuthentication, getHeaderFieldOptions, getQueryParamOptions, getServers, getStatusCodeDescription, getVisibilitySuffix, isContentTypeHeader, isSharedRoute, reportIfNoRoutes, resolveRequestVisibility, } from "@typespec/http";
6
7
  import { checkDuplicateTypeName, getExtensions, getExternalDocs, getOpenAPITypeName, getParameterKey, isReadonlyProperty, resolveInfo, shouldInline, } from "@typespec/openapi";
@@ -126,6 +127,7 @@ export async function getOpenAPIForService(context, options) {
126
127
  }
127
128
  })
128
129
  .filter((x) => x),
130
+ outputFile: context.outputFile,
129
131
  };
130
132
  function resolveHost(program, namespace) {
131
133
  const servers = getServers(program, namespace);
@@ -238,6 +240,40 @@ export async function getOpenAPIForService(context, options) {
238
240
  // strip everything from the key including and after the ?
239
241
  return path.replace(/\/?\?.*/, "");
240
242
  }
243
+ function getFinalStateVia(metadata) {
244
+ switch (metadata.finalStateVia) {
245
+ case FinalStateValue.azureAsyncOperation:
246
+ return "azure-async-operation";
247
+ case FinalStateValue.location:
248
+ return "location";
249
+ case FinalStateValue.operationLocation:
250
+ return "operation-location";
251
+ case FinalStateValue.originalUri:
252
+ return "original-uri";
253
+ default:
254
+ return undefined;
255
+ }
256
+ }
257
+ function getFinalStateSchema(metadata) {
258
+ if (metadata.finalResult !== undefined &&
259
+ metadata.finalResult !== "void" &&
260
+ metadata.finalResult.name.length > 0) {
261
+ const model = metadata.finalResult;
262
+ const schemaOrRef = resolveExternalRef(metadata.finalResult);
263
+ if (schemaOrRef !== undefined) {
264
+ const ref = new Ref();
265
+ ref.value = schemaOrRef.$ref;
266
+ return { "final-state-schema": ref };
267
+ }
268
+ const pending = pendingSchemas.getOrAdd(metadata.finalResult, Visibility.Read, () => ({
269
+ type: model,
270
+ visibility: Visibility.Read,
271
+ ref: refs.getOrAdd(model, Visibility.Read, () => new Ref()),
272
+ }));
273
+ return { "final-state-schema": pending.ref };
274
+ }
275
+ return undefined;
276
+ }
241
277
  function emitOperation(operation) {
242
278
  let { path: fullPath, operation: op, verb, parameters } = operation;
243
279
  let pathsObject = root.paths;
@@ -294,6 +330,22 @@ export async function getOpenAPIForService(context, options) {
294
330
  // which does have LRO metadata.
295
331
  if (lroMetadata !== undefined && operation.verb !== "get") {
296
332
  currentEndpoint["x-ms-long-running-operation"] = true;
333
+ if (options.emitLroOptions !== "none") {
334
+ const finalState = getFinalStateVia(lroMetadata);
335
+ if (finalState !== undefined) {
336
+ const finalSchema = getFinalStateSchema(lroMetadata);
337
+ let lroOptions = {
338
+ "final-state-via": finalState,
339
+ };
340
+ if (finalSchema !== undefined && options.emitLroOptions === "all") {
341
+ lroOptions = {
342
+ "final-state-via": finalState,
343
+ ...finalSchema,
344
+ };
345
+ }
346
+ currentEndpoint["x-ms-long-running-operation-options"] = lroOptions;
347
+ }
348
+ }
297
349
  }
298
350
  // Extract paged metadata from Azure.Core.Page
299
351
  extractPagedMetadata(program, operation);
@@ -442,19 +494,27 @@ export async function getOpenAPIForService(context, options) {
442
494
  }
443
495
  }
444
496
  if (body) {
445
- const isBinary = contentTypes.every((t) => isBinaryPayload(body.type, t));
446
- openapiResponse.schema = isBinary
447
- ? { type: "file" }
448
- : getSchemaOrRef(body.type, {
449
- visibility: Visibility.Read,
450
- ignoreMetadataAnnotations: body.isExplicit && body.containsMetadataAnnotations,
451
- });
497
+ openapiResponse.schema = getSchemaForResponseBody(body, contentTypes);
452
498
  }
453
499
  for (const contentType of contentTypes) {
454
500
  currentProduces.add(contentType);
455
501
  }
456
502
  currentEndpoint.responses[statusCode] = openapiResponse;
457
503
  }
504
+ function getSchemaForResponseBody(body, contentTypes) {
505
+ const isBinary = contentTypes.every((t) => isBinaryPayload(body.type, t));
506
+ if (isBinary) {
507
+ return { type: "file" };
508
+ }
509
+ if (body.bodyKind === "multipart") {
510
+ // OpenAPI2 doesn't support multipart responses, so we just return a string schema
511
+ return { type: "string" };
512
+ }
513
+ return getSchemaOrRef(body.type, {
514
+ visibility: Visibility.Read,
515
+ ignoreMetadataAnnotations: body.isExplicit && body.containsMetadataAnnotations,
516
+ });
517
+ }
458
518
  function getResponseHeader(prop) {
459
519
  const header = {};
460
520
  populateParameter(header, prop, "header", {
@@ -466,7 +526,7 @@ export async function getOpenAPIForService(context, options) {
466
526
  delete header.required;
467
527
  return header;
468
528
  }
469
- function resolveRef(ref) {
529
+ function expandRef(ref) {
470
530
  const absoluteRef = interpolatePath(ref, {
471
531
  "arm-types-dir": options.armTypesDir,
472
532
  });
@@ -475,13 +535,31 @@ export async function getOpenAPIForService(context, options) {
475
535
  }
476
536
  return getRelativePathFromDirectory(getDirectoryPath(context.outputFile), absoluteRef, false);
477
537
  }
478
- function getSchemaOrRef(type, schemaContext) {
479
- const refUrl = getRef(program, type, { version: context.version, service: context.service });
538
+ function resolveExternalRef(type) {
539
+ const refUrl = getRef(program, type);
480
540
  if (refUrl) {
481
541
  return {
482
- $ref: resolveRef(refUrl),
542
+ $ref: expandRef(refUrl),
483
543
  };
484
544
  }
545
+ if (isArmCommonType(type) && (type.kind === "Model" || type.kind === "ModelProperty")) {
546
+ const ref = getArmCommonTypeOpenAPIRef(program, type, {
547
+ version: context.version,
548
+ service: context.service,
549
+ });
550
+ if (ref) {
551
+ return {
552
+ $ref: expandRef(ref),
553
+ };
554
+ }
555
+ }
556
+ return undefined;
557
+ }
558
+ function getSchemaOrRef(type, schemaContext) {
559
+ const ref = resolveExternalRef(type);
560
+ if (ref) {
561
+ return ref;
562
+ }
485
563
  if (type.kind === "Scalar" && program.checker.isStdType(type)) {
486
564
  return getSchemaForScalar(type);
487
565
  }
@@ -560,14 +638,9 @@ export async function getOpenAPIForService(context, options) {
560
638
  property = property.sourceProperty;
561
639
  }
562
640
  }
563
- const refUrl = getRef(program, property, {
564
- version: context.version,
565
- service: context.service,
566
- });
567
- if (refUrl) {
568
- return {
569
- $ref: resolveRef(refUrl),
570
- };
641
+ const ref = resolveExternalRef(property);
642
+ if (ref) {
643
+ return ref;
571
644
  }
572
645
  const parameter = params.get(property);
573
646
  if (parameter) {
@@ -611,33 +684,66 @@ export async function getOpenAPIForService(context, options) {
611
684
  currentConsumes.add(consume);
612
685
  }
613
686
  if (methodParams.body && !isVoidType(methodParams.body.type)) {
614
- const isBinary = isBinaryPayload(methodParams.body.type, consumes);
615
- const schemaContext = {
616
- visibility,
617
- ignoreMetadataAnnotations: methodParams.body.isExplicit && methodParams.body.containsMetadataAnnotations,
618
- };
619
- const schema = isBinary
620
- ? { type: "string", format: "binary" }
621
- : getSchemaOrRef(methodParams.body.type, schemaContext);
622
- if (currentConsumes.has("multipart/form-data")) {
623
- const bodyModelType = methodParams.body.type;
624
- // Assert, this should never happen. Rest library guard against that.
625
- compilerAssert(bodyModelType.kind === "Model", "Body should always be a Model.");
626
- if (bodyModelType) {
627
- for (const param of bodyModelType.properties.values()) {
628
- emitParameter(param, "formData", schemaContext, getJsonName(param));
629
- }
687
+ emitBodyParameters(methodParams.body, visibility);
688
+ }
689
+ }
690
+ function emitBodyParameters(body, visibility) {
691
+ switch (body.bodyKind) {
692
+ case "single":
693
+ emitSingleBodyParameters(body, visibility);
694
+ break;
695
+ case "multipart":
696
+ emitMultipartBodyParameters(body, visibility);
697
+ break;
698
+ }
699
+ }
700
+ function emitSingleBodyParameters(body, visibility) {
701
+ const isBinary = isBinaryPayload(body.type, body.contentTypes);
702
+ const schemaContext = {
703
+ visibility,
704
+ ignoreMetadataAnnotations: body.isExplicit && body.containsMetadataAnnotations,
705
+ };
706
+ const schema = isBinary
707
+ ? { type: "string", format: "binary" }
708
+ : getSchemaOrRef(body.type, schemaContext);
709
+ if (currentConsumes.has("multipart/form-data")) {
710
+ const bodyModelType = body.type;
711
+ // Assert, this should never happen. Rest library guard against that.
712
+ compilerAssert(bodyModelType.kind === "Model", "Body should always be a Model.");
713
+ if (bodyModelType) {
714
+ for (const param of bodyModelType.properties.values()) {
715
+ emitParameter(param, "formData", schemaContext, getJsonName(param));
630
716
  }
631
717
  }
632
- else if (methodParams.body.parameter) {
633
- emitParameter(methodParams.body.parameter, "body", { visibility, ignoreMetadataAnnotations: false }, getJsonName(methodParams.body.parameter), schema);
634
- }
635
- else {
718
+ }
719
+ else if (body.property) {
720
+ emitParameter(body.property, "body", { visibility, ignoreMetadataAnnotations: false }, getJsonName(body.property), schema);
721
+ }
722
+ else {
723
+ currentEndpoint.parameters.push({
724
+ name: "body",
725
+ in: "body",
726
+ schema,
727
+ required: true,
728
+ });
729
+ }
730
+ }
731
+ function emitMultipartBodyParameters(body, visibility) {
732
+ for (const [index, part] of body.parts.entries()) {
733
+ const partName = part.name ?? `part${index}`;
734
+ let schema = getFormDataSchema(part.body.type, { visibility, ignoreMetadataAnnotations: false }, partName);
735
+ if (schema) {
736
+ if (part.multi) {
737
+ schema = {
738
+ type: "array",
739
+ items: schema.type === "file" ? { type: "string", format: "binary" } : schema,
740
+ };
741
+ }
636
742
  currentEndpoint.parameters.push({
637
- name: "body",
638
- in: "body",
639
- schema,
640
- required: true,
743
+ name: partName,
744
+ in: "formData",
745
+ required: !part.optional,
746
+ ...schema,
641
747
  });
642
748
  }
643
749
  }
@@ -720,8 +826,8 @@ export async function getOpenAPIForService(context, options) {
720
826
  if (param.name !== ph.name) {
721
827
  ph["x-ms-client-name"] = param.name;
722
828
  }
723
- if (param.default) {
724
- ph.default = getDefaultValue(param.default);
829
+ if (param.defaultValue) {
830
+ ph.default = getDefaultValue(param.defaultValue);
725
831
  }
726
832
  if (ph.in === "body") {
727
833
  compilerAssert(bodySchema, "bodySchema argument is required to populate body parameter");
@@ -848,13 +954,13 @@ export async function getOpenAPIForService(context, options) {
848
954
  }
849
955
  return false;
850
956
  }
851
- function isVersionEnum(program, enumObj) {
852
- const versions = getVersionsForEnum(program, enumObj);
853
- if (versions !== undefined && versions.length > 0) {
854
- return true;
855
- }
856
- return false;
957
+ }
958
+ function isVersionEnum(program, enumObj) {
959
+ const [_, map] = getVersionsForEnum(program, enumObj);
960
+ if (map !== undefined && map.getVersions()[0].enumMember.enum === enumObj) {
961
+ return true;
857
962
  }
963
+ return false;
858
964
  }
859
965
  function emitTags() {
860
966
  for (const tag of tags) {
@@ -903,6 +1009,29 @@ export async function getOpenAPIForService(context, options) {
903
1009
  });
904
1010
  return {};
905
1011
  }
1012
+ /**
1013
+ * Version enum is special so we we just render the current version with modelAsString: true
1014
+ */
1015
+ function getSchemaForVersionEnum(e, currentVersion) {
1016
+ const member = [...e.members.values()].find((x) => (x.value ?? x.name) === currentVersion);
1017
+ compilerAssert(member, `Version enum ${e.name} does not have a member for ${currentVersion}.`, e);
1018
+ return {
1019
+ type: "string",
1020
+ description: getDoc(program, e),
1021
+ enum: [member.value ?? member.name],
1022
+ "x-ms-enum": {
1023
+ name: e.name,
1024
+ modelAsString: true,
1025
+ values: [
1026
+ {
1027
+ name: member.name,
1028
+ value: member.value ?? member.name,
1029
+ description: getDoc(program, member),
1030
+ },
1031
+ ],
1032
+ },
1033
+ };
1034
+ }
906
1035
  function getSchemaForEnum(e) {
907
1036
  const values = [];
908
1037
  if (e.members.size === 0) {
@@ -919,6 +1048,10 @@ export async function getOpenAPIForService(context, options) {
919
1048
  values.push(option.value ?? option.name);
920
1049
  }
921
1050
  }
1051
+ // If we are rendering a specific version and trying to render the version enum we should treat it specially to only include the current version.
1052
+ if (isVersionEnum(program, e) && context.version) {
1053
+ return getSchemaForVersionEnum(e, context.version);
1054
+ }
922
1055
  const schema = { type, description: getDoc(program, e) };
923
1056
  if (values.length > 0) {
924
1057
  schema.enum = values;
@@ -1025,33 +1158,25 @@ export async function getOpenAPIForService(context, options) {
1025
1158
  function getSchemaForUnionVariant(variant, schemaContext) {
1026
1159
  return getSchemaForType(variant.type, schemaContext);
1027
1160
  }
1028
- function getDefaultValue(type) {
1029
- switch (type.kind) {
1030
- case "String":
1031
- return type.value;
1032
- case "Number":
1033
- return type.value;
1034
- case "Boolean":
1035
- return type.value;
1036
- case "Tuple":
1037
- return type.values.map(getDefaultValue);
1038
- case "EnumMember":
1039
- return type.value ?? type.name;
1040
- case "Intrinsic":
1041
- return isNullType(type)
1042
- ? null
1043
- : reportDiagnostic(program, {
1044
- code: "invalid-default",
1045
- format: { type: type.kind },
1046
- target: type,
1047
- });
1048
- case "UnionVariant":
1049
- return getDefaultValue(type.type);
1161
+ function getDefaultValue(defaultType) {
1162
+ switch (defaultType.valueKind) {
1163
+ case "StringValue":
1164
+ return defaultType.value;
1165
+ case "NumericValue":
1166
+ return defaultType.value.asNumber() ?? undefined;
1167
+ case "BooleanValue":
1168
+ return defaultType.value;
1169
+ case "ArrayValue":
1170
+ return defaultType.values.map((x) => getDefaultValue(x));
1171
+ case "NullValue":
1172
+ return null;
1173
+ case "EnumValue":
1174
+ return defaultType.value.value ?? defaultType.value.name;
1050
1175
  default:
1051
1176
  reportDiagnostic(program, {
1052
1177
  code: "invalid-default",
1053
- format: { type: type.kind },
1054
- target: type,
1178
+ format: { type: defaultType.valueKind },
1179
+ target: defaultType,
1055
1180
  });
1056
1181
  }
1057
1182
  }
@@ -1123,6 +1248,7 @@ export async function getOpenAPIForService(context, options) {
1123
1248
  modelSchema.required = [propertyName];
1124
1249
  }
1125
1250
  }
1251
+ applySummary(model, modelSchema);
1126
1252
  applyExternalDocs(model, modelSchema);
1127
1253
  for (const prop of model.properties.values()) {
1128
1254
  if (!metadataInfo.isPayloadProperty(prop, schemaContext.visibility, schemaContext.ignoreMetadataAnnotations)) {
@@ -1158,8 +1284,9 @@ export async function getOpenAPIForService(context, options) {
1158
1284
  if (description) {
1159
1285
  property.description = description;
1160
1286
  }
1161
- if (prop.default && !("$ref" in property)) {
1162
- property.default = getDefaultValue(prop.default);
1287
+ applySummary(prop, property);
1288
+ if (prop.defaultValue && !("$ref" in property)) {
1289
+ property.default = getDefaultValue(prop.defaultValue);
1163
1290
  }
1164
1291
  if (isReadonlyProperty(program, prop)) {
1165
1292
  property.readOnly = true;
@@ -1223,10 +1350,10 @@ export async function getOpenAPIForService(context, options) {
1223
1350
  }
1224
1351
  function resolveProperty(prop, context) {
1225
1352
  let propSchema;
1226
- if (prop.type.kind === "Enum" && prop.default) {
1353
+ if (prop.type.kind === "Enum" && prop.defaultValue) {
1227
1354
  propSchema = getSchemaForEnum(prop.type);
1228
1355
  }
1229
- else if (prop.type.kind === "Union" && prop.default) {
1356
+ else if (prop.type.kind === "Union" && prop.defaultValue) {
1230
1357
  const [asEnum, _] = getUnionAsEnum(prop.type);
1231
1358
  if (asEnum) {
1232
1359
  propSchema = getSchemaForUnionEnum(prop.type, asEnum);
@@ -1283,6 +1410,10 @@ export async function getOpenAPIForService(context, options) {
1283
1410
  if (docStr) {
1284
1411
  newTarget.description = docStr;
1285
1412
  }
1413
+ const title = getSummary(program, typespecType);
1414
+ if (title) {
1415
+ target.title = title;
1416
+ }
1286
1417
  const formatStr = getFormat(program, typespecType);
1287
1418
  if (isString && formatStr) {
1288
1419
  const allowedStringFormats = [
@@ -1402,6 +1533,12 @@ export async function getOpenAPIForService(context, options) {
1402
1533
  return encodeAsFormat ?? encoding;
1403
1534
  }
1404
1535
  }
1536
+ function applySummary(typespecType, target) {
1537
+ const summary = getSummary(program, typespecType);
1538
+ if (summary) {
1539
+ target.title = summary;
1540
+ }
1541
+ }
1405
1542
  function applyExternalDocs(typespecType, target) {
1406
1543
  const externalDocs = getExternalDocs(program, typespecType);
1407
1544
  if (externalDocs) {
@@ -1412,7 +1549,7 @@ export async function getOpenAPIForService(context, options) {
1412
1549
  if (type.node && type.node.parent && type.node.parent.kind === SyntaxKind.ModelStatement) {
1413
1550
  schema["x-ms-enum"] = {
1414
1551
  name: type.node.parent.id.sv,
1415
- modelAsString: true,
1552
+ modelAsString: false,
1416
1553
  };
1417
1554
  }
1418
1555
  else if (type.kind === "String") {
@@ -1423,7 +1560,7 @@ export async function getOpenAPIForService(context, options) {
1423
1560
  else if (type.kind === "Enum") {
1424
1561
  schema["x-ms-enum"] = {
1425
1562
  name: type.name,
1426
- modelAsString: isFixed(program, type) ? false : true,
1563
+ modelAsString: false,
1427
1564
  };
1428
1565
  const values = [];
1429
1566
  let foundCustom = false;
@@ -1445,12 +1582,14 @@ export async function getOpenAPIForService(context, options) {
1445
1582
  return schema;
1446
1583
  }
1447
1584
  function getSchemaForStringTemplate(stringTemplate) {
1448
- const [value, diagnostics] = stringTemplateToString(stringTemplate);
1449
- if (diagnostics.length > 0) {
1450
- program.reportDiagnostics(diagnostics.map((x) => ({ ...x, severity: "warning" })));
1585
+ if (stringTemplate.stringValue === undefined) {
1586
+ program.reportDiagnostics(explainStringTemplateNotSerializable(stringTemplate).map((x) => ({
1587
+ ...x,
1588
+ severity: "warning",
1589
+ })));
1451
1590
  return { type: "string" };
1452
1591
  }
1453
- return { type: "string", enum: [value] };
1592
+ return { type: "string", enum: [stringTemplate.stringValue] };
1454
1593
  }
1455
1594
  function getSchemaForLiterals(typespecType) {
1456
1595
  switch (typespecType.kind) {
@@ -1662,7 +1801,7 @@ class ErrorTypeFoundError extends Error {
1662
1801
  export function sortOpenAPIDocument(doc) {
1663
1802
  // Doing this to make sure the classes with toJSON are resolved.
1664
1803
  const unsorted = JSON.parse(JSON.stringify(doc));
1665
- const sorted = sortWithJsonSchema(unsorted, AutorestOpenAPISchema, "#/$defs/AutorestOpenAPISchema");
1804
+ const sorted = sortWithJsonSchema(unsorted, AutorestOpenAPISchema);
1666
1805
  return sorted;
1667
1806
  }
1668
1807
  async function loadExamples(host, options, version) {