Implementation of `@typeChangedFrom` decorator (#1408)

* Implementation of `@typeChangedFrom`.

* Add `@returnTypeChangedFrom` decorator and supporting methods.

* Ensure decorators only working on correct types.

* Code review feedback.
This commit is contained in:
Travis Prescott 2023-01-06 11:21:29 -08:00 коммит произвёл GitHub
Родитель 632efd6814
Коммит 6d87778187
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
7 изменённых файлов: 229 добавлений и 6 удалений

Просмотреть файл

@ -0,0 +1,10 @@
{
"changes": [
{
"packageName": "@cadl-lang/compiler",
"comment": "Added `changeReturnType` projection method for operations.",
"type": "none"
}
],
"packageName": "@cadl-lang/compiler"
}

Просмотреть файл

@ -0,0 +1,10 @@
{
"changes": [
{
"packageName": "@cadl-lang/versioning",
"comment": "Added `@returnTypeChangedFrom` decorator.",
"type": "none"
}
],
"packageName": "@cadl-lang/versioning"
}

Просмотреть файл

@ -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(),

Просмотреть файл

@ -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;

Просмотреть файл

@ -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);
};
});
};
}

Просмотреть файл

@ -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<Version, Type> | undefined {
return p.stateMap(typeChangedFromKey).get(t) as Map<Version, Type>;
}
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<Version, any>();
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<Version, Type> | undefined {
return p.stateMap(returnTypeChangedFromKey).get(t) as Map<Version, Type>;
}
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<Version, any>();
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);

Просмотреть файл

@ -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],