Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 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 @@
---
# Change versionKind to one of: internal, fix, dependencies, feature, deprecation, breaking
changeKind: feature
packages:
- "@typespec/versioning"
---

Use of `@useDependency` is now optional when referencing types from a versioned library. By default the latest version of the library will be used.
12 changes: 0 additions & 12 deletions packages/versioning/src/lib.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,6 @@ export const $lib = createTypeSpecLibrary({
default: `Versioned dependency mapping must all point to the same namespace but 2 versions have different namespaces '${"namespace1"}' and '${"namespace2"}'.`,
},
},
"versioned-dependency-record-not-mapping": {
severity: "error",
messages: {
default: paramMessage`The versionedDependency decorator must provide a model mapping local versions to dependency '${"dependency"}' versions`,
},
},
"versioned-dependency-not-picked": {
severity: "error",
messages: {
Expand All @@ -45,12 +39,6 @@ export const $lib = createTypeSpecLibrary({
default: paramMessage`Multiple versions from '${"name"}' resolve to the same value. Version enums must resolve to unique values.`,
},
},
"using-versioned-library": {
severity: "error",
messages: {
default: paramMessage`Namespace '${"sourceNs"}' is referencing types from versioned namespace '${"targetNs"}' but didn't specify which versions with @useDependency.`,
},
},
"invalid-renamed-from-value": {
severity: "error",
messages: {
Expand Down
58 changes: 9 additions & 49 deletions packages/versioning/src/validate.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import {
NoTarget,
getNamespaceFullName,
getTypeName,
isTemplateInstance,
Expand Down Expand Up @@ -33,6 +32,14 @@ import {
getVersions,
} from "./versioning.js";

const relationCacheKey = Symbol.for("TypeSpec.Versioning.NamespaceRelationCache");

export function getCachedNamespaceDependencies(
program: Program,
): Map<Namespace | undefined, Set<Namespace>> | undefined {
return (program as any)[relationCacheKey];
}

export function $onValidate(program: Program) {
const namespaceDependencies = new Map<Namespace | undefined, Set<Namespace>>();

Expand All @@ -46,6 +53,7 @@ export function $onValidate(program: Program) {
}
namespaceDependencies.set(source, set);
}
(program as any)[relationCacheKey] = namespaceDependencies;

navigateProgram(
program,
Expand Down Expand Up @@ -149,12 +157,6 @@ export function $onValidate(program: Program) {
code: "incompatible-versioned-namespace-use-dependency",
target: namespace,
});
} else if (!(value instanceof Map)) {
reportDiagnostic(program, {
code: "versioned-dependency-record-not-mapping",
format: { dependency: getNamespaceFullName(dependencyNs) },
target: namespace,
});
}
} else {
if (value instanceof Map) {
Expand Down Expand Up @@ -191,7 +193,6 @@ export function $onValidate(program: Program) {
},
{ includeTemplateDeclaration: true },
);
validateVersionedNamespaceUsage(program, namespaceDependencies);
}

/**
Expand Down Expand Up @@ -403,34 +404,6 @@ function validateVersionEnumValuesUnique(program: Program, namespace: Namespace)
}
}

function validateVersionedNamespaceUsage(
program: Program,
namespaceDependencies: Map<Namespace | undefined, Set<Namespace>>,
) {
for (const [source, targets] of namespaceDependencies.entries()) {
const dependencies = source && getVersionDependencies(program, source);
for (const target of targets) {
const targetVersionedNamespace = findVersionedNamespace(program, target);
const sourceVersionedNamespace = source && findVersionedNamespace(program, source);
if (
targetVersionedNamespace !== undefined &&
!(source && (isSubNamespace(target, source) || isSubNamespace(source, target))) &&
sourceVersionedNamespace !== targetVersionedNamespace &&
dependencies?.get(targetVersionedNamespace) === undefined
) {
reportDiagnostic(program, {
code: "using-versioned-library",
format: {
sourceNs: source ? getNamespaceFullName(source) : "global",
targetNs: getNamespaceFullName(target),
},
target: source ?? NoTarget,
});
}
}
}
}

function validateVersionedPropertyNames(program: Program, source: Type) {
const allVersions = getAllVersions(program, source);
if (allVersions === undefined) return;
Expand Down Expand Up @@ -479,19 +452,6 @@ function validateVersionedPropertyNames(program: Program, source: Type) {
}
}

function isSubNamespace(parent: Namespace, child: Namespace): boolean {
let current: Namespace | undefined = child;

while (current && current.name !== "") {
if (current === parent) {
return true;
}
current = current.namespace;
}

return false;
}

function validateMadeOptional(program: Program, target: Type) {
if (target.kind === "ModelProperty") {
const madeOptionalOn = getMadeOptionalOn(program, target);
Expand Down
33 changes: 20 additions & 13 deletions packages/versioning/src/versioning.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,18 +16,31 @@ import {
type VersionMap,
} from "./decorators.js";
import type { Version, VersionResolution } from "./types.js";
import { getCachedNamespaceDependencies } from "./validate.js";
import { TimelineMoment, VersioningTimeline } from "./versioning-timeline.js";

export function getVersionDependencies(
program: Program,
namespace: Namespace,
): Map<Namespace, Map<Version, Version> | Version> | undefined {
const useDeps = getUseDependencies(program, namespace);
if (useDeps) {
return useDeps;
const explicit = getUseDependencies(program, namespace);
const base = getCachedNamespaceDependencies(program);
const usage = base?.get(namespace);
if (usage === undefined) {
return explicit;
}

return undefined;
const result = new Map<Namespace, Map<Version, Version> | Version>(explicit);

for (const dep of usage) {
if (!explicit?.has(dep)) {
const version = getVersion(program, dep);
if (version) {
const depVersions = version.getVersions();
result.set(dep, depVersions[depVersions.length - 1]);
}
}
}
return result;
}

/**
Expand All @@ -48,14 +61,8 @@ function resolveDependencyVersions(
continue; // Already resolved.
}

if (!(versionMap instanceof Map)) {
const rootNsName = getNamespaceFullName(current);
const dependencyNsName = getNamespaceFullName(dependencyNs);
throw new Error(
`Unexpected error: Namespace ${rootNsName} version dependency to ${dependencyNsName} should be a mapping of version.`,
);
}
const dependencyVersion = versionMap.get(currentVersion);
const dependencyVersion =
versionMap instanceof Map ? versionMap.get(currentVersion) : versionMap;
namespacesToCheck.push([dependencyNs, dependencyVersion]);
resolutions.set(dependencyNs, dependencyVersion);
}
Expand Down
68 changes: 68 additions & 0 deletions packages/versioning/test/resolve-dependencies.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import { expect, it } from "vitest";
import type { VersionResolution } from "../src/index.js";
import { resolveVersions } from "../src/versioning.js";
import { Tester } from "./test-host.js";

async function resolveTimelineMatrix(content: string) {
const { program } = await Tester.compile(content);

const serviceNamespace = program.getGlobalNamespaceType().namespaces.get("Test")!;
const resolutions = resolveVersions(program, serviceNamespace);
return simplify(resolutions);
}

function simplify(resolutions: VersionResolution[]) {
return resolutions.map((x) => ({
root: x.rootVersion?.name,
libs: Object.fromEntries(
[...x.versions.entries()]
.filter((v) => v[0] !== x.rootVersion?.namespace)
.map((v) => [v[0].name, v[1].name]),
),
}));
}

it("automatically resolve latest version of referenced versioned library", async () => {
const resolutions = await resolveTimelineMatrix(`
@versioned(Versions) namespace Test {
enum Versions { v1, v2 }
model Foo {
ref: Lib.LibModel;
}
}

@versioned(Versions) namespace Lib {
enum Versions { l1, l2 }
model LibModel {
prop: string;
}
}
`);

expect(resolutions).toEqual([
{ root: "v1", libs: { Lib: "l2" } },
{ root: "v2", libs: { Lib: "l2" } },
]);
});

it("referencing types from non versioned library is a noop", async () => {
const resolutions = await resolveTimelineMatrix(`
@versioned(Versions) namespace Test {
enum Versions { v1, v2 }
model Foo {
ref: Lib.LibModel;
}
}

namespace Lib {
model LibModel {
prop: string;
}
}
`);

expect(resolutions).toEqual([
{ root: "v1", libs: {} },
{ root: "v2", libs: {} },
]);
});
Loading