From 6d87778187e70e92eb4e37693ad4316668dad8b7 Mon Sep 17 00:00:00 2001 From: Travis Prescott Date: Fri, 6 Jan 2023 11:21:29 -0800 Subject: [PATCH] Implementation of `@typeChangedFrom` decorator (#1408) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Implementation of `@typeChangedFrom`. * Add `@returnTypeChangedFrom` decorator and supporting methods. * Ensure decorators only working on correct types. * Code review feedback. --- ...ersioning-changeType_2023-01-03-22-38.json | 10 ++ ...ersioning-changeType_2023-01-03-22-38.json | 10 ++ packages/compiler/core/projection-members.ts | 21 +++- packages/versioning/lib/decorators.cadl | 1 + packages/versioning/lib/versioning.cadl | 14 +++ packages/versioning/src/versioning.ts | 119 +++++++++++++++++- packages/versioning/test/versioning.test.ts | 60 +++++++++ 7 files changed, 229 insertions(+), 6 deletions(-) create mode 100644 common/changes/@cadl-lang/compiler/versioning-changeType_2023-01-03-22-38.json create mode 100644 common/changes/@cadl-lang/versioning/versioning-changeType_2023-01-03-22-38.json diff --git a/common/changes/@cadl-lang/compiler/versioning-changeType_2023-01-03-22-38.json b/common/changes/@cadl-lang/compiler/versioning-changeType_2023-01-03-22-38.json new file mode 100644 index 000000000..477b05945 --- /dev/null +++ b/common/changes/@cadl-lang/compiler/versioning-changeType_2023-01-03-22-38.json @@ -0,0 +1,10 @@ +{ + "changes": [ + { + "packageName": "@cadl-lang/compiler", + "comment": "Added `changeReturnType` projection method for operations.", + "type": "none" + } + ], + "packageName": "@cadl-lang/compiler" +} \ No newline at end of file diff --git a/common/changes/@cadl-lang/versioning/versioning-changeType_2023-01-03-22-38.json b/common/changes/@cadl-lang/versioning/versioning-changeType_2023-01-03-22-38.json new file mode 100644 index 000000000..5db5fa14a --- /dev/null +++ b/common/changes/@cadl-lang/versioning/versioning-changeType_2023-01-03-22-38.json @@ -0,0 +1,10 @@ +{ + "changes": [ + { + "packageName": "@cadl-lang/versioning", + "comment": "Added `@returnTypeChangedFrom` decorator.", + "type": "none" + } + ], + "packageName": "@cadl-lang/versioning" +} \ No newline at end of file diff --git a/packages/compiler/core/projection-members.ts b/packages/compiler/core/projection-members.ts index 400b0812a..819f9bcaa 100644 --- a/packages/compiler/core/projection-members.ts +++ b/packages/compiler/core/projection-members.ts @@ -65,6 +65,20 @@ export function createProjectionMembers(checker: Checker): { return voidType; }); }, + changePropertyType(base) { + return createFunctionType((nameT: Type, newType: Type) => { + assertType("property name", nameT, "String"); + const propertyName = nameT.value; + + const prop = base.properties.get(propertyName); + if (!prop) { + throw new ProjectionError(`Property ${propertyName} not found`); + } + prop.type = newType; + + return voidType; + }); + }, addProperty(base) { return createFunctionType((nameT: Type, type: Type, defaultT: Type) => { assertType("property", nameT, "String"); @@ -90,7 +104,6 @@ export function createProjectionMembers(checker: Checker): { return voidType; }); }, - deleteProperty(base) { return createFunctionType((nameT: Type) => { assertType("property", nameT, "String"); @@ -213,6 +226,12 @@ export function createProjectionMembers(checker: Checker): { returnType(base) { return base.returnType; }, + changeReturnType(base) { + return createFunctionType((newType: Type) => { + base.returnType = newType; + return voidType; + }); + }, }, Interface: { ...createBaseMembers(), diff --git a/packages/versioning/lib/decorators.cadl b/packages/versioning/lib/decorators.cadl index dfc23b784..3c0bd6c74 100644 --- a/packages/versioning/lib/decorators.cadl +++ b/packages/versioning/lib/decorators.cadl @@ -11,6 +11,7 @@ extern dec added(target: unknown, version: EnumMember); extern dec removed(target: unknown, version: EnumMember); extern dec renamedFrom(target: unknown, version: EnumMember, oldName?: string); extern dec madeOptional(target: unknown, version: EnumMember); +extern dec typeChangedFrom(target: unknown, version: EnumMember, oldType: unknown); extern fn existsAtVersion(target: unknown, version: EnumMember): boolean; extern fn hasDifferentNameAtVersion(target: unknown, version: EnumMember): boolean; diff --git a/packages/versioning/lib/versioning.cadl b/packages/versioning/lib/versioning.cadl index 440e119e9..d4e31a2a4 100644 --- a/packages/versioning/lib/versioning.cadl +++ b/packages/versioning/lib/versioning.cadl @@ -12,6 +12,9 @@ projection op#v { if hasDifferentNameAtVersion(self, version) { self::rename(getNameAtVersion(self, version)); }; + if hasDifferentReturnTypeAtVersion(self, version) { + self::changeReturnType(getReturnTypeBeforeVersion(self, version)); + }; } from(version) { if !existsAtVersion(self, version) { @@ -36,6 +39,9 @@ projection interface#v { if !existsAtVersion(operation, version) { self::deleteOperation(operation::name); }; + if hasDifferentReturnTypeAtVersion(operation, version) { + operation::changeReturnType(getReturnTypeBeforeVersion(operation, version)); + }; }); } from(version) { @@ -115,6 +121,10 @@ projection model#v { if madeOptionalAfter(p, version) { p::setOptional(false); }; + + if hasDifferentTypeAtVersion(p, version) { + self::changePropertyType(p::name, getTypeBeforeVersion(p, version)); + }; }); }; } @@ -138,6 +148,10 @@ projection model#v { if madeOptionalAfter(p, version) { p::setOptional(true); }; + + if hasDifferentTypeAtVersion(p, version) { + self::changePropertyType(p::name, p::type); + }; }); }; } diff --git a/packages/versioning/src/versioning.ts b/packages/versioning/src/versioning.ts index 2e365a625..bcae28abe 100644 --- a/packages/versioning/src/versioning.ts +++ b/packages/versioning/src/versioning.ts @@ -7,6 +7,7 @@ import { ModelProperty, Namespace, ObjectType, + Operation, Program, ProjectionApplication, reportDeprecated, @@ -21,6 +22,8 @@ const versionsKey = createStateSymbol("versions"); const versionDependencyKey = createStateSymbol("versionDependency"); const renamedFromKey = createStateSymbol("renamedFrom"); const madeOptionalKey = createStateSymbol("madeOptional"); +const typeChangedFromKey = createStateSymbol("typeChangedFrom"); +const returnTypeChangedFromKey = createStateSymbol("returnTypeChangedFrom"); export const namespace = "Cadl.Versioning"; @@ -74,6 +77,68 @@ export function $removed(context: DecoratorContext, t: Type, v: EnumMember) { program.stateMap(removedOnKey).set(t, record); } +/** + * Returns the mapping of versions to old type values, if applicable + * @param p Cadl program + * @param t type to query + * @returns Map of versions to old types, if any + */ +export function getTypeChangedFrom(p: Program, t: Type): Map | undefined { + return p.stateMap(typeChangedFromKey).get(t) as Map; +} + +export function $typeChangedFrom( + context: DecoratorContext, + prop: ModelProperty, + v: EnumMember, + oldType: any +) { + const { program } = context; + + const version = checkIsVersion(context.program, v, context.getArgumentTarget(0)!); + if (!version) { + return; + } + + // retrieve statemap to update or create a new one + let record = getTypeChangedFrom(program, prop) ?? new Map(); + record.set(version, oldType); + // ensure the map is sorted by version + record = new Map([...record.entries()].sort((a, b) => a[0].index - b[0].index)); + program.stateMap(typeChangedFromKey).set(prop, record); +} + +/** + * Returns the mapping of versions to old return type values, if applicable + * @param p Cadl program + * @param t type to query + * @returns Map of versions to old types, if any + */ +export function getReturnTypeChangedFrom(p: Program, t: Type): Map | undefined { + return p.stateMap(returnTypeChangedFromKey).get(t) as Map; +} + +export function $returnTypeChangedFrom( + context: DecoratorContext, + op: Operation, + v: EnumMember, + oldReturnType: any +) { + const { program } = context; + + const version = checkIsVersion(context.program, v, context.getArgumentTarget(0)!); + if (!version) { + return; + } + + // retrieve statemap to update or create a new one + let record = getReturnTypeChangedFrom(program, op) ?? new Map(); + record.set(version, oldReturnType); + // ensure the map is sorted by version + record = new Map([...record.entries()].sort((a, b) => a[0].index - b[0].index)); + program.stateMap(returnTypeChangedFromKey).set(op, record); +} + interface RenamedFrom { version: Version; oldName: string; @@ -172,6 +237,38 @@ export function getNameAtVersion(p: Program, t: Type, v: ObjectType): string { return ""; } +/** + * @returns get old type if applicable. + */ +export function getTypeBeforeVersion(p: Program, t: Type, v: ObjectType): Type | undefined { + const target = toVersion(p, t, v); + const map = getTypeChangedFrom(p, t); + if (!map || !target) return undefined; + + for (const [key, val] of map) { + if (target.index < key.index) { + return val; + } + } + return undefined; +} + +/** + * @returns get old type if applicable. + */ +export function getReturnTypeBeforeVersion(p: Program, t: Type, v: ObjectType): any { + const target = toVersion(p, t, v); + const map = getReturnTypeChangedFrom(p, t); + if (!map || !target) return ""; + + for (const [key, val] of map) { + if (target.index < key.index) { + return val; + } + } + return ""; +} + /** * @deprecated since version 0.39.0. * @returns version when the given type was added if applicable. @@ -568,7 +665,7 @@ export function getVersions(p: Program, t: Type): [Namespace, VersionMap] | [] { * @param version * @returns */ -export function addedAfter(p: Program, type: Type, version: ObjectType) { +export function addedAfter(p: Program, type: Type, version: ObjectType): boolean { reportDeprecated(p, "Deprecated: addedAfter is deprecated. Use existsAtVersion instead.", type); const appliesAt = appliesAtVersion(getAddedOn, p, type, version); return appliesAt === null ? false : !appliesAt; @@ -581,7 +678,7 @@ export function addedAfter(p: Program, type: Type, version: ObjectType) { * @param version * @returns */ -export function removedOnOrBefore(p: Program, type: Type, version: ObjectType) { +export function removedOnOrBefore(p: Program, type: Type, version: ObjectType): boolean { reportDeprecated( p, "Deprecated: removedOnOrBefore is deprecated. Use existsAtVersion instead.", @@ -674,7 +771,7 @@ export function existsAtVersion(p: Program, type: Type, versionKey: ObjectType): * @param version * @returns */ -export function renamedAfter(p: Program, type: Type, version: ObjectType) { +export function renamedAfter(p: Program, type: Type, version: ObjectType): boolean { reportDeprecated( p, "Deprecated: renamedAfter is deprecated. Use hasDifferentNameAtVersion instead.", @@ -684,15 +781,27 @@ export function renamedAfter(p: Program, type: Type, version: ObjectType) { return appliesAt === null ? false : !appliesAt; } -export function hasDifferentNameAtVersion(p: Program, type: Type, version: ObjectType) { +export function hasDifferentNameAtVersion(p: Program, type: Type, version: ObjectType): boolean { return getNameAtVersion(p, type, version) !== ""; } -export function madeOptionalAfter(p: Program, type: Type, version: ObjectType) { +export function madeOptionalAfter(p: Program, type: Type, version: ObjectType): boolean { const appliesAt = appliesAtVersion(getMadeOptionalOn, p, type, version); return appliesAt === null ? false : !appliesAt; } +export function hasDifferentTypeAtVersion(p: Program, type: Type, version: ObjectType): boolean { + return getTypeBeforeVersion(p, type, version) !== undefined; +} + +export function hasDifferentReturnTypeAtVersion( + p: Program, + type: Type, + version: ObjectType +): boolean { + return getReturnTypeBeforeVersion(p, type, version) !== ""; +} + export function getVersionForEnumMember(program: Program, member: EnumMember): Version | undefined { const [, versions] = getVersionsForEnum(program, member); return versions?.getVersionForEnumMember(member); diff --git a/packages/versioning/test/versioning.test.ts b/packages/versioning/test/versioning.test.ts index 7ea45a955..4d09e88ee 100644 --- a/packages/versioning/test/versioning.test.ts +++ b/packages/versioning/test/versioning.test.ts @@ -7,6 +7,7 @@ import { Operation, ProjectionApplication, projectProgram, + Scalar, Type, Union, } from "@cadl-lang/compiler"; @@ -279,6 +280,27 @@ describe("compiler: versioning", () => { ); }); + it("can change property types", async () => { + const { + projections: [v1, v2, v3], + } = await versionedModel( + ["v1", "v2", "v3"], + ` + model Test { + @typeChangedFrom(Versions.v2, string) + @typeChangedFrom(Versions.v3, zonedDateTime) + changed: MyDate; + } + + model MyDate {} + ` + ); + + ok((v1.properties.get("changed")!.type as Scalar).name === "string"); + ok((v2.properties.get("changed")!.type as Scalar).name === "zonedDateTime"); + ok((v3.properties.get("changed")!.type as Model).name === "MyDate"); + }); + async function versionedModel(versions: string[], model: string) { const { Test } = (await runner.compile(` @versioned(Versions) @@ -511,6 +533,24 @@ describe("compiler: versioning", () => { assertHasVariants(v2.returnType as Union, ["a", "b"]); }); + it("can change return types", async () => { + const { + projections: [v1, v2, v3], + } = await versionedOperation( + ["v1", "v2", "v3"], + ` + @returnTypeChangedFrom(Versions.v2, string) + @returnTypeChangedFrom(Versions.v3, zonedDateTime) + op Test(): MyDate; + + model MyDate {}; + ` + ); + ok((v1.returnType as Scalar).name === "string"); + ok((v2.returnType as Scalar).name === "zonedDateTime"); + ok((v3.returnType as Model).name === "MyDate"); + }); + async function versionedOperation(versions: string[], operation: string) { const { Test } = (await runner.compile(` @versioned(Versions) @@ -638,6 +678,26 @@ describe("compiler: versioning", () => { ); }); + it("can change return types of members", async () => { + const { + projections: [v1, v2, v3], + } = await versionedInterface( + ["v1", "v2", "v3"], + ` + interface Test { + @returnTypeChangedFrom(Versions.v2, string) + @returnTypeChangedFrom(Versions.v3, zonedDateTime) + op foo(): MyDate; + } + + model MyDate {}; + ` + ); + ok((v1.operations.get("foo")!.returnType as Scalar).name === "string"); + ok((v2.operations.get("foo")!.returnType as Scalar).name === "zonedDateTime"); + ok((v3.operations.get("foo")!.returnType as Model).name === "MyDate"); + }); + it("can version parameters", async () => { const { projections: [v1, v2],