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:
Родитель
632efd6814
Коммит
6d87778187
|
@ -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],
|
||||
|
|
Загрузка…
Ссылка в новой задаче