Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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,10 @@
{
"changes": [
{
"packageName": "@cadl-lang/compiler",
"comment": "Add namespace to model expressions",
"type": "minor"
}
],
"packageName": "@cadl-lang/compiler"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"changes": [
{
"packageName": "@cadl-lang/openapi3",
"comment": "Uptake change to versioning library",
"type": "minor"
}
],
"packageName": "@cadl-lang/openapi3"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"changes": [
{
"packageName": "@cadl-lang/versioning",
"comment": "Fix issues with spreading versioned model",
"type": "minor"
}
],
"packageName": "@cadl-lang/versioning"
}
25 changes: 24 additions & 1 deletion packages/compiler/core/checker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -949,9 +949,29 @@ export function createChecker(program: Program): Checker {
| EnumStatementNode
| InterfaceStatementNode
| UnionStatementNode
| ModelExpressionNode
): NamespaceType | undefined {
if (node === globalNamespaceType.node) return undefined;

if (node.kind === SyntaxKind.ModelExpression) {
let parent: Node | undefined = node.parent;
while (parent !== undefined) {
if (
parent.kind === SyntaxKind.ModelStatement ||
parent.kind === SyntaxKind.OperationStatement ||
parent.kind === SyntaxKind.EnumStatement ||
parent.kind === SyntaxKind.InterfaceStatement ||
parent.kind === SyntaxKind.UnionStatement ||
parent.kind === SyntaxKind.ModelExpression
) {
return getParentNamespaceType(parent);
} else {
parent = parent.parent;
}
}
return undefined;
}

// we leave namespaces for interface members as undefined
if (
node.kind === SyntaxKind.OperationStatement &&
Expand Down Expand Up @@ -1525,6 +1545,7 @@ export function createChecker(program: Program): Checker {
name: "",
node: node,
properties,
namespace: getParentNamespaceType(node),
decorators: [],
derivedModels: [],
});
Expand Down Expand Up @@ -2777,7 +2798,9 @@ export function createChecker(program: Program): Checker {
evalContext = createEvalContext(node);
for (const [i, param] of node.parameters.entries()) {
if (!args[i]) {
throw new ProjectionError("need argument for parameter " + node.parameters[i]);
throw new ProjectionError(
"need argument for parameter " + SyntaxKind[node.parameters[i].kind]
);
}

const argVal = args[i];
Expand Down
4 changes: 2 additions & 2 deletions packages/openapi3/src/openapi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ import {
HttpOperationResponse,
OperationDetails,
} from "@cadl-lang/rest";
import { getVersionRecords } from "@cadl-lang/versioning";
import { buildVersionProjections } from "@cadl-lang/versioning";
import { getOneOf, getRef } from "./decorators.js";
import { OpenAPILibrary, reportDiagnostic } from "./lib.js";
import {
Expand Down Expand Up @@ -229,7 +229,7 @@ function createOAPIEmitter(program: Program, options: OpenAPIEmitterOptions) {
if (!serviceNs) {
return;
}
const versions = getVersionRecords(program, serviceNs);
const versions = buildVersionProjections(program, serviceNs);
for (const record of versions) {
if (record.version) {
record.projections.push({
Expand Down
2 changes: 1 addition & 1 deletion packages/openapi3/test/test-openapi-output.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ describe("openapi3: definitions", () => {
x: { type: "integer", format: "int32" },
},
required: ["x"],
"x-cadl-name": "(anonymous model)",
"x-cadl-name": "root.(anonymous model)",
});
});

Expand Down
2 changes: 1 addition & 1 deletion packages/versioning/lib/versioning.cadl
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ projection model#v {
return never;
} else {
self::properties::forEach((p) => {
if addedAfter(p, version, self) {
if addedAfter(p, version) {
self::deleteProperty(p::name);
};

Expand Down
130 changes: 90 additions & 40 deletions packages/versioning/src/versioning.ts
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,7 @@ export function $versioned({ program }: DecoratorContext, t: Type, v: Type) {
program.stateMap(versionsKey).set(t, versions);
}

function getVersion(p: Program, t: Type) {
function getVersion(p: Program, t: Type): string[] {
return p.stateMap(versionsKey).get(t);
}

Expand Down Expand Up @@ -291,20 +291,31 @@ function validateVersionedNamespaceUsage(
}
}

interface VersionRecord {
version: string | undefined;
projections: ProjectionApplication[];
export interface VersionResolution {
/**
* Version for the root namespace. `undefined` if not versioned.
*/
rootVersion: string | undefined;

/**
* Resolved version for all the referenced namespaces.
*/
versions: Map<NamespaceType, string>;
}

export function getVersionRecords(program: Program, rootNs: NamespaceType): VersionRecord[] {
const versions = getVersions(program, rootNs);
const records: VersionRecord[] = [];
/**
* 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: Program, rootNs: NamespaceType): VersionResolution[] {
const [, versions] = getVersions(program, rootNs);
const dependencies = getVersionDependencies(program, rootNs) ?? new Map();

if (!versions || versions.length === 0) {
if (dependencies.size === 0) {
return [{ version: undefined, projections: [] }];
return [{ rootVersion: undefined, versions: new Map() }];
} else {
const map = new Map();
for (const [dependencyNs, version] of dependencies) {
if (typeof version !== "string") {
const rootNsName = program.checker!.getNamespaceString(rootNs);
Expand All @@ -313,23 +324,17 @@ export function getVersionRecords(program: Program, rootNs: NamespaceType): Vers
`Unexpected error: Namespace ${rootNsName} version dependency to ${dependencyNsName} should be a string.`
);
}

records.push({
version: undefined,
projections: [
{
scope: dependencyNs,
projectionName: "v",
arguments: [version],
},
],
});
map.set(dependencyNs, version);
}
return [{ rootVersion: undefined, versions: map }];
}
} else {
for (const version of versions) {
// TODO: find versioned dependencies
const projections = [{ scope: rootNs, projectionName: "v", arguments: [version] }];
return versions.map((version) => {
const resolution: VersionResolution = {
rootVersion: version,
versions: new Map<NamespaceType, string>(),
};
resolution.versions.set(rootNs, version);

for (const [dependencyNs, versionMap] of dependencies) {
if (!(versionMap instanceof Map)) {
Expand All @@ -339,28 +344,58 @@ export function getVersionRecords(program: Program, rootNs: NamespaceType): Vers
`Unexpected error: Namespace ${rootNsName} version dependency to ${dependencyNsName} should be a mapping of version.`
);
}
if (!versionMap.has(version)) continue;
projections.push({
scope: dependencyNs,
projectionName: "v",
arguments: [versionMap.get(version!)],
});
resolution.versions.set(dependencyNs, versionMap.get(version));
}

records.push({ version: version, projections });
return resolution;
});
}
}

/**
* Represent the set of projections used to project to that version.
*/
interface VersionProjections {
version: string | undefined;
projections: ProjectionApplication[];
}

const versionIndex = new Map<string, Map<NamespaceType, string>>();

function indexVersions(resolutions: VersionResolution[]) {
versionIndex.clear();
for (const resolution of resolutions) {
for (const version of resolution.versions.values()) {
versionIndex.set(version, resolution.versions);
}
}
}

return records;
export function buildVersionProjections(
program: Program,
rootNs: NamespaceType
): VersionProjections[] {
const resolutions = resolveVersions(program, rootNs);
indexVersions(resolutions);
return resolutions.map((resolution) => {
const projections = [...resolution.versions.entries()].map(([ns, version]) => {
return {
scope: ns,
projectionName: "v",
arguments: [version],
};
});
return { version: resolution.rootVersion, projections };
});
}

const versionCache = new WeakMap<Type, string[]>();
function cacheVersion(key: Type, versions: string[]) {
const versionCache = new WeakMap<Type, [NamespaceType, string[]] | []>();
function cacheVersion(key: Type, versions: [NamespaceType, string[]] | []) {
versionCache.set(key, versions);
return versions;
}

export function getVersions(p: Program, t: Type): string[] {
export function getVersions(p: Program, t: Type): [NamespaceType, string[]] | [] {
if (versionCache.has(t)) {
return versionCache.get(t)!;
}
Expand All @@ -369,11 +404,11 @@ export function getVersions(p: Program, t: Type): string[] {
const nsVersion = getVersion(p, t);

if (nsVersion !== undefined) {
return cacheVersion(t, nsVersion);
return cacheVersion(t, [t, nsVersion]);
} else if (t.namespace) {
return cacheVersion(t, getVersions(p, t.namespace));
} else {
return cacheVersion(t, []);
return cacheVersion(t, [t, []]);
}
} else if (
t.kind === "Operation" ||
Expand All @@ -389,6 +424,14 @@ export function getVersions(p: Program, t: Type): string[] {
} else {
return cacheVersion(t, []);
}
} else if (t.kind === "ModelProperty") {
if (t.sourceProperty) {
return getVersions(p, t.sourceProperty);
} else if (t.model) {
return getVersions(p, t.model);
} else {
return cacheVersion(t, []);
}
} else {
return cacheVersion(t, []);
}
Expand Down Expand Up @@ -427,24 +470,31 @@ function appliesAtVersion(
version: string,
versionSource?: Type
) {
const versions = getVersions(p, versionSource ?? type);
const [namespace, versions] = getVersions(p, versionSource ?? type);
if (namespace) {
const newVersion = versionIndex.get(version)?.get(namespace);
if (newVersion) {
version = newVersion;
}
}
if (!versions || versions.length === 0) {
return null;
}

const appliedOnVersion = getMetadataFn(p, type);
if (appliedOnVersion === undefined) {
return null;
}
const appliedOnVersionIndex = versions.indexOf(appliedOnVersion);
if (appliedOnVersionIndex === -1) return null;

const testVersionIndex = versions.indexOf(version);
if (testVersionIndex === -1) return null;

return testVersionIndex >= appliedOnVersionIndex;
}

export function versionCompare(p: Program, versionSource: Type, v1: string, v2: string): number {
const versions = getVersions(p, versionSource);
const [, versions] = getVersions(p, versionSource);
if (!versions || versions.length === 0) {
return 0;
}
Expand All @@ -457,7 +507,7 @@ export function versionCompare(p: Program, versionSource: Type, v1: string, v2:
}

export function hasVersion(p: Program, t: Type, v: string) {
const versions = getVersions(p, t);
const [, versions] = getVersions(p, t);
if (!versions) return false;
return versions.includes(v);
}
Loading