UNPKG

@typespec/versioning

Version:

TypeSpec library for declaring and emitting versioned APIs

349 lines 13.7 kB
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