@typespec/versioning
Version:
TypeSpec library for declaring and emitting versioned APIs
349 lines • 13.7 kB
JavaScript
import { getNamespaceFullName, } from "@typespec/compiler";
import { getAddedOnVersions, getRemovedOnVersions, getReturnTypeChangedFrom, getTypeChangedFrom, getUseDependencies, getVersion, } from "./decorators.js";
import { getCachedNamespaceDependencies } from "./validate.js";
import { TimelineMoment, VersioningTimeline } from "./versioning-timeline.js";
export function getVersionDependencies(program, namespace) {
const explicit = getUseDependencies(program, namespace);
const base = getCachedNamespaceDependencies(program);
const usage = base?.get(namespace);
if (usage === undefined) {
return explicit;
}
const result = new Map(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;
}
/**
* Resolve the version of dependencies
* @param initialResolutions
*/
function resolveDependencyVersions(program, initialResolutions) {
const resolutions = new Map(initialResolutions);
const namespacesToCheck = [...initialResolutions.entries()];
while (namespacesToCheck.length > 0) {
const [current, currentVersion] = namespacesToCheck.pop();
const dependencies = getVersionDependencies(program, current);
for (const [dependencyNs, versionMap] of dependencies ?? new Map()) {
if (resolutions.has(dependencyNs)) {
continue; // Already resolved.
}
const dependencyVersion = versionMap instanceof Map ? versionMap.get(currentVersion) : versionMap;
namespacesToCheck.push([dependencyNs, dependencyVersion]);
resolutions.set(dependencyNs, dependencyVersion);
}
}
return resolutions;
}
/**
* Resolve the version to use for all namespace for each of the root namespace versions.
* @param program
* @param rootNs Root namespace.
*/
export function resolveVersions(program, namespace) {
const [rootNs, versions] = getVersions(program, namespace);
const dependencies = (rootNs && getVersionDependencies(program, rootNs)) ??
new Map();
if (!versions) {
if (dependencies.size === 0) {
return [{ rootVersion: undefined, versions: new Map() }];
}
else {
const map = new Map();
for (const [dependencyNs, version] of dependencies) {
if (version instanceof Map) {
const rootNsName = getNamespaceFullName(namespace);
const dependencyNsName = getNamespaceFullName(dependencyNs);
throw new Error(`Unexpected error: Namespace ${rootNsName} version dependency to ${dependencyNsName} should be a picked version.`);
}
map.set(dependencyNs, version);
}
return [{ rootVersion: undefined, versions: resolveDependencyVersions(program, map) }];
}
}
else {
return versions.getVersions().map((version) => {
const resolutions = resolveDependencyVersions(program, new Map([[rootNs, version]]));
return {
rootVersion: version,
versions: resolutions,
};
});
}
}
const versionCache = new WeakMap();
function cacheVersion(key, versions) {
versionCache.set(key, versions);
return versions;
}
export function getVersionsForEnum(program, en) {
const namespace = en.namespace;
if (namespace === undefined) {
return [];
}
const nsVersion = getVersion(program, namespace);
if (nsVersion === undefined) {
return [];
}
return [namespace, nsVersion];
}
export function getVersions(p, t) {
const existing = versionCache.get(t);
if (existing) {
return existing;
}
switch (t.kind) {
case "Namespace":
return resolveVersionsForNamespace(p, t);
case "Operation":
case "Interface":
case "Model":
case "Union":
case "Scalar":
case "Enum":
if (t.namespace) {
return cacheVersion(t, getVersions(p, t.namespace) || []);
}
else if (t.kind === "Operation" && t.interface) {
return cacheVersion(t, getVersions(p, t.interface) || []);
}
else {
return cacheVersion(t, []);
}
case "ModelProperty":
if (t.sourceProperty) {
return getVersions(p, t.sourceProperty);
}
else if (t.model) {
return getVersions(p, t.model);
}
else {
return cacheVersion(t, []);
}
case "EnumMember":
return cacheVersion(t, getVersions(p, t.enum) || []);
case "UnionVariant":
return cacheVersion(t, getVersions(p, t.union) || []);
default:
return cacheVersion(t, []);
}
}
function resolveVersionsForNamespace(program, namespace) {
const nsVersion = getVersion(program, namespace);
if (nsVersion !== undefined) {
return cacheVersion(namespace, [namespace, nsVersion]);
}
const parentNamespaceVersion = namespace.namespace && getVersions(program, namespace.namespace)[1];
const hasDependencies = getUseDependencies(program, namespace);
if (parentNamespaceVersion || hasDependencies) {
return cacheVersion(namespace, [namespace, parentNamespaceVersion]);
}
else {
return cacheVersion(namespace, [namespace, undefined]);
}
}
export function getAllVersions(p, t) {
const [namespace, _] = getVersions(p, t);
if (namespace === undefined)
return undefined;
return getVersion(p, namespace)?.getVersions();
}
export var Availability;
(function (Availability) {
Availability["Unavailable"] = "Unavailable";
Availability["Added"] = "Added";
Availability["Available"] = "Available";
Availability["Removed"] = "Removed";
})(Availability || (Availability = {}));
function getParentAddedVersion(program, type, versions) {
let parentMap = undefined;
if (type.kind === "ModelProperty" && type.model !== undefined) {
parentMap = getAvailabilityMap(program, type.model);
}
else if (type.kind === "Operation" && type.interface !== undefined) {
parentMap = getAvailabilityMap(program, type.interface);
}
if (parentMap === undefined)
return undefined;
for (const [key, value] of parentMap.entries()) {
if (value === Availability.Added) {
return versions.find((x) => x.name === key);
}
}
return undefined;
}
function getParentRemovedVersion(program, type, versions) {
let parentMap = undefined;
if (type.kind === "ModelProperty" && type.model !== undefined) {
parentMap = getAvailabilityMap(program, type.model);
}
else if (type.kind === "Operation" && type.interface !== undefined) {
parentMap = getAvailabilityMap(program, type.interface);
}
if (parentMap === undefined)
return undefined;
for (const [key, value] of parentMap.entries()) {
if (value === Availability.Removed) {
return versions.find((x) => x.name === key);
}
}
return undefined;
}
function getParentAddedVersionInTimeline(program, type, timeline) {
let parentMap = undefined;
if (type.kind === "ModelProperty" && type.model !== undefined) {
parentMap = getAvailabilityMapInTimeline(program, type.model, timeline);
}
else if (type.kind === "Operation" && type.interface !== undefined) {
parentMap = getAvailabilityMapInTimeline(program, type.interface, timeline);
}
if (parentMap === undefined)
return undefined;
for (const [moment, availability] of parentMap.entries()) {
if (availability === Availability.Added) {
return moment.versions().next().value;
}
}
return undefined;
}
/**
* Uses the added, removed and parent metadata to resolve any issues with
* implicit versioning and return the added array with this taken into account.
* @param added the array of versions from the `@added` decorator
* @param removed the array of versions from the `@removed` decorator
* @param parentAdded the version when the parent type was added
* @returns the added array, with any implicit versioning taken into consideration.
*/
function resolveWhenFirstAdded(added, removed, parentAdded) {
const implicitlyAvailable = !added.length && !removed.length;
if (implicitlyAvailable) {
// if type has no version info, it inherits from the parent
return [parentAdded];
}
if (added.length) {
const addedFirst = !removed.length || added[0].index < removed[0].index;
if (addedFirst) {
// if the type was added first, then implicitly it wasn't available before
// and thus should NOT inherit from its parent
return added;
}
}
if (removed.length) {
const removedFirst = !added.length || removed[0].index < added[0].index;
if (removedFirst) {
// if the type was removed first the implicitly it was available before
// and thus SHOULD inherit from its parent
return [parentAdded, ...added];
}
}
// we shouldn't get here, but if we do, then make no change to the added array
return added;
}
function resolveRemoved(added, removed, parentRemoved) {
if (removed.length) {
return removed;
}
const implicitlyRemoved = !added.length || (parentRemoved && added[0].index < parentRemoved.index);
if (parentRemoved && implicitlyRemoved) {
return [parentRemoved];
}
return [];
}
export function getAvailabilityMap(program, type) {
const avail = new Map();
const allVersions = getAllVersions(program, type);
// if unversioned then everything exists
if (allVersions === undefined)
return undefined;
const firstVersion = allVersions[0];
const parentAdded = getParentAddedVersion(program, type, allVersions) ?? firstVersion;
const parentRemoved = getParentRemovedVersion(program, type, allVersions);
let added = getAddedOnVersions(program, type) ?? [];
let removed = getRemovedOnVersions(program, type) ?? [];
const typeChanged = getTypeChangedFrom(program, type);
const returnTypeChanged = getReturnTypeChangedFrom(program, type);
// if there's absolutely no versioning information, return undefined
// contextually, this might mean it inherits its versioning info from a parent
// or that it is treated as unversioned
if (!added.length &&
!removed.length &&
typeChanged === undefined &&
returnTypeChanged === undefined)
return undefined;
added = resolveWhenFirstAdded(added, removed, parentAdded);
removed = resolveRemoved(added, removed, parentRemoved);
// something isn't available by default
let isAvail = false;
for (const ver of allVersions) {
const add = added.find((x) => x.index === ver.index);
const rem = removed.find((x) => x.index === ver.index);
if (rem) {
isAvail = false;
avail.set(ver.name, Availability.Removed);
}
else if (add) {
isAvail = true;
avail.set(ver.name, Availability.Added);
}
else if (isAvail) {
avail.set(ver.name, Availability.Available);
}
else {
avail.set(ver.name, Availability.Unavailable);
}
}
return avail;
}
export function getAvailabilityMapInTimeline(program, type, timeline) {
const avail = new Map();
const firstVersion = timeline.first().versions().next().value;
const parentAdded = getParentAddedVersionInTimeline(program, type, timeline) ?? firstVersion;
let added = getAddedOnVersions(program, type) ?? [];
const removed = getRemovedOnVersions(program, type) ?? [];
const typeChanged = getTypeChangedFrom(program, type);
const returnTypeChanged = getReturnTypeChangedFrom(program, type);
// if there's absolutely no versioning information, return undefined
// contextually, this might mean it inherits its versioning info from a parent
// or that it is treated as unversioned
if (!added.length &&
!removed.length &&
typeChanged === undefined &&
returnTypeChanged === undefined)
return undefined;
added = resolveWhenFirstAdded(added, removed, parentAdded);
// something isn't available by default
let isAvail = false;
for (const [index, moment] of timeline.entries()) {
const add = added.find((x) => timeline.getIndex(x) === index);
const rem = removed.find((x) => timeline.getIndex(x) === index);
if (rem) {
isAvail = false;
avail.set(moment, Availability.Removed);
}
else if (add) {
isAvail = true;
avail.set(moment, Availability.Added);
}
else if (isAvail) {
avail.set(moment, Availability.Available);
}
else {
avail.set(moment, Availability.Unavailable);
}
}
return avail;
}
export function getVersionForEnumMember(program, member) {
// Always lookup for the original type. This ensure reference equality when comparing versions.
const parentEnum = member.enum;
const [, versions] = getVersionsForEnum(program, parentEnum);
return versions?.getVersionForEnumMember(member);
}
//# sourceMappingURL=versioning.js.map