Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

WIP x-ms-identifiers alternatives #2094

Draft
wants to merge 6 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
changeKind: feature
packages:
- "@azure-tools/typespec-autorest"
- "@azure-tools/typespec-azure-resource-manager"
---

Use the @identifiers decorator to identify and utilize identifiers for x-ms-identifiers. Additionally, use the @key decorator to identify identifiers.
56 changes: 44 additions & 12 deletions packages/typespec-autorest/src/openapi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,10 @@
} from "@azure-tools/typespec-azure-core";
import {
getArmCommonTypeOpenAPIRef,
getArmIdentifiers,
getExternalTypeRef,
isArmCommonType,
isArmProviderNamespace,
isAzureResource,
isConditionallyFlattened,
} from "@azure-tools/typespec-azure-resource-manager";
Expand Down Expand Up @@ -963,7 +965,7 @@
}
return undefined;
}
function getSchemaOrRef(type: Type, schemaContext: SchemaContext): any {
function getSchemaOrRef(type: Type, schemaContext: SchemaContext, namespace?: Namespace): any {
let schemaNameOverride: ((name: string, visibility: Visibility) => string) | undefined =
undefined;
const ref = resolveExternalRef(type);
Expand Down Expand Up @@ -1013,7 +1015,7 @@
const name = getOpenAPITypeName(program, type, typeNameOptions);

if (shouldInline(program, type)) {
const schema = getSchemaForInlineType(type, name, schemaContext);
const schema = getSchemaForInlineType(type, name, schemaContext, namespace);

if (schema === undefined && isErrorType(type)) {
// Exit early so that syntax errors are exposed. This error will
Expand All @@ -1040,7 +1042,12 @@
return { $ref: pending.ref };
}
}
function getSchemaForInlineType(type: Type, name: string, context: SchemaContext) {
function getSchemaForInlineType(
type: Type,
name: string,
context: SchemaContext,
namespace?: Namespace,
) {
if (inProgressInlineTypes.has(type)) {
reportDiagnostic(program, {
code: "inline-cycle",
Expand All @@ -1050,7 +1057,7 @@
return {};
}
inProgressInlineTypes.add(type);
const schema = getSchemaForType(type, context);
const schema = getSchemaForType(type, context, namespace);
inProgressInlineTypes.delete(type);
return schema;
}
Expand Down Expand Up @@ -1627,17 +1634,20 @@
}
}

function getSchemaForType(type: Type, schemaContext: SchemaContext): OpenAPI2Schema | undefined {
function getSchemaForType(
type: Type,
schemaContext: SchemaContext,
namespace?: Namespace,
): OpenAPI2Schema | undefined {
const builtinType = getSchemaForLiterals(type);
if (builtinType !== undefined) {
return builtinType;
}

switch (type.kind) {
case "Intrinsic":
return getSchemaForIntrinsicType(type);
case "Model":
return getSchemaForModel(type, schemaContext);
return getSchemaForModel(type, schemaContext, namespace);
case "ModelProperty":
return getSchemaForType(type.type, schemaContext);
case "Scalar":
Expand Down Expand Up @@ -1841,6 +1851,10 @@
);
}

function ifArmIdentifiersDefault(armIdentifiers: string[]) {
return armIdentifiers.every((identifier) => identifier === "id" || identifier === "name");
}

function getSchemaForUnionVariant(
variant: UnionVariant,
schemaContext: SchemaContext,
Expand Down Expand Up @@ -1885,8 +1899,8 @@
return undefined;
}

function getSchemaForModel(model: Model, schemaContext: SchemaContext) {
const array = getArrayType(model, schemaContext);
function getSchemaForModel(model: Model, schemaContext: SchemaContext, namespace?: Namespace) {
const array = getArrayType(model, schemaContext, namespace);
if (array) {
return array;
}
Expand Down Expand Up @@ -2071,7 +2085,7 @@
propSchema = getSchemaOrRef(prop.type, context);
}
} else {
propSchema = getSchemaOrRef(prop.type, context);
propSchema = getSchemaOrRef(prop.type, context, prop.model?.namespace);
}

if (options.armResourceFlattening && isConditionallyFlattened(program, prop)) {
Expand Down Expand Up @@ -2372,7 +2386,11 @@
/**
* If the model is an array model return the OpenAPI2Schema for the array type.
*/
function getArrayType(typespecType: Model, context: SchemaContext): OpenAPI2Schema | undefined {
function getArrayType(
typespecType: Model,
context: SchemaContext,
namespace?: Namespace,
): OpenAPI2Schema | undefined {
if (isArrayModelType(program, typespecType)) {
const array: OpenAPI2Schema = {
type: "array",
Expand All @@ -2381,14 +2399,28 @@
visibility: context.visibility | Visibility.Item,
}),
};
if (!ifArrayItemContainsIdentifier(program, typespecType as any)) {

const indexer = typespecType.indexer.value as Model;

Check warning on line 2403 in packages/typespec-autorest/src/openapi.ts

View workflow job for this annotation

GitHub Actions / Lint

'indexer' is assigned a value but never used. Allowed unused vars must match /^_/u
const armIdentifiers = getArmIdentifiers(program, typespecType);
if (isArmProviderNamespace(program, namespace) && hasValidArmIdentifiers(armIdentifiers)) {
array["x-ms-identifiers"] = armIdentifiers;
} else if (!ifArrayItemContainsIdentifier(program, typespecType as any)) {
array["x-ms-identifiers"] = [];
}

return applyIntrinsicDecorators(typespecType, array);
}
return undefined;
}

function hasValidArmIdentifiers(armIdentifiers: string[] | undefined) {
return (
armIdentifiers !== undefined &&
armIdentifiers.length > 0 &&
!ifArmIdentifiersDefault(armIdentifiers)
);
}

function getSchemaForScalar(scalar: Scalar): OpenAPI2Schema {
let result: OpenAPI2Schema = {};
const isStd = program.checker.isStdType(scalar);
Expand Down
222 changes: 222 additions & 0 deletions packages/typespec-autorest/test/openapi-output.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -823,6 +823,228 @@ describe("typespec-autorest: extension decorator", () => {
});
});

describe("typespec-azure: identifiers decorator", () => {
it("ignores name/id keys for x-ms-identifiers", async () => {
const oapi = await openApiFor(
`
model Pet {
@key
name: string;
@key
id: int32;
}
model PetList {
value: Pet[]
}
@route("/Pets")
@get op list(): PetList;
`,
);
ok(oapi.paths["/Pets"].get);
deepStrictEqual(oapi.definitions.PetList.properties.value["x-ms-identifiers"], undefined);
});
it("uses identifiers decorator for properties", async () => {
const oapi = await openApiFor(
`
@armProviderNamespace
@useDependency(Azure.ResourceManager.Versions.v1_0_Preview_1)
namespace Microsoft.Test;

model Pet {
name: string;
age: int32;
}
model PetList {
@identifiers(["age"])
value: Pet[]
}
@route("/Pets")
@get op list(): PetList;
`,
);
ok(oapi.paths["/Pets"].get);
deepStrictEqual(oapi.definitions.PetList.properties.value["x-ms-identifiers"], ["age"]);
});
it("identifies keys correctly as x-ms-identifiers", async () => {
const oapi = await openApiFor(
`
@armProviderNamespace
@useDependency(Azure.ResourceManager.Versions.v1_0_Preview_1)
namespace Microsoft.Test;

model Pet {
name: string;
@key
age: int32;
}
model PetList {
value: Pet[]
}
@route("/Pets")
@get op list(): PetList;
`,
);
ok(oapi.paths["/Pets"].get);
deepStrictEqual(oapi.definitions.PetList.properties.value["x-ms-identifiers"], ["age"]);
});
it("x-ms-identifiers ignores keys for non armProviderNamespace", async () => {
const oapi = await openApiFor(
`
model Pet {
name: string;
@key
age: int32;
}
model PetList {
value: Pet[]
}
@route("/Pets")
@get op list(): PetList;
`,
);
ok(oapi.paths["/Pets"].get);
deepStrictEqual(oapi.definitions.PetList.properties.value["x-ms-identifiers"], []);
});

it("prioritizes identifiers decorator over keys", async () => {
const oapi = await openApiFor(
`
model Pet {
name: string;
@key
age: int32;
}
model PetList {
@identifiers([])
value: Pet[]
}
@route("/Pets")
@get op list(): PetList;
`,
);
ok(oapi.paths["/Pets"].get);
deepStrictEqual(oapi.definitions.PetList.properties.value["x-ms-identifiers"], []);
});
it("supports multiple identifiers", async () => {
const oapi = await openApiFor(
`
@armProviderNamespace
@useDependency(Azure.ResourceManager.Versions.v1_0_Preview_1)
namespace Microsoft.Test;

model Pet {
name: string;
age: int32;
}
model PetList {
@identifiers(["name", "age"])
value: Pet[]
}
@route("/Pets")
@get op list(): PetList;
`,
);
ok(oapi.paths["/Pets"].get);
deepStrictEqual(oapi.definitions.PetList.properties.value["x-ms-identifiers"], ["name", "age"]);
});
it("supports inner properties in identifiers decorator", async () => {
const oapi = await openApiFor(
`
@armProviderNamespace
@useDependency(Azure.ResourceManager.Versions.v1_0_Preview_1)
namespace Microsoft.Test;

model Pet {
dogs: Dog;
}

model Dog {
breed: string;
}

model PetList {
@identifiers(["dogs/breed"])
pets: Pet[]
}
@route("/Pets")
@get op list(): PetList;
`,
);
ok(oapi.paths["/Pets"].get);
deepStrictEqual(oapi.definitions.PetList.properties.pets["x-ms-identifiers"], ["dogs/breed"]);
});
it("support inner models in different namespace but route models should be on armProviderNamespace", async () => {
const oapi = await openApiFor(
`
@armProviderNamespace
@useDependency(Azure.ResourceManager.Versions.v1_0_Preview_1)
namespace Microsoft.Test
{

@route("/Pets")
@get op list(): PetList;

model PetList {
@identifiers(["age"])
pets: Microsoft.Modeling.Pet[]
}
}

namespace Microsoft.Modeling
{
model Pet {
age: int32;
}
}
`,
);
ok(oapi.paths["/Pets"].get);
deepStrictEqual(oapi.definitions.PetList.properties.pets["x-ms-identifiers"], ["age"]);
});
it("supports inner properties for keys", async () => {
const oapi = await openApiFor(
`
@armProviderNamespace
@useDependency(Azure.ResourceManager.Versions.v1_0_Preview_1)
namespace Microsoft.Test;

model Pet {
dogs: Dog;
cats: Cat;
}

model Dog {
@key
breed: string;
}

model Cat
{
features: Features;
}

model Features {
@key
color:string;
size:int32;
}

model PetList {
pets: Pet[]
}

@route("/Pets")
@get op list(): PetList;
`,
);
ok(oapi.paths["/Pets"].get);
deepStrictEqual(oapi.definitions.PetList.properties.pets["x-ms-identifiers"], [
"dogs/breed",
"cats/features/color",
]);
});
});

describe("typespec-autorest: multipart formData", () => {
it("expands model into formData parameters", async () => {
const oapi = await openApiFor(`
Expand Down
Loading
Loading