Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow unversioned project to select a specific version of a versioned library #346

Merged
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/openapi3",
"comment": "Uptake change to allow versioned dependency with unversioned service",
"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": "Enable ability to pick a specific version for a versioned dependency when service itself isn't versioned",
"type": "minor"
}
],
"packageName": "@cadl-lang/versioning"
}
6 changes: 1 addition & 5 deletions packages/openapi3/src/openapi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -241,18 +241,14 @@ function createOAPIEmitter(program: Program, options: OpenAPIEmitterOptions) {
}
const versions = getVersionRecords(program, serviceNs);
for (const record of versions) {
record.projections.push({
timotheeguerin marked this conversation as resolved.
Show resolved Hide resolved
projectionName: "toTypescript",
arguments: [],
});
if (record.version) {
record.projections.push({
projectionName: "atVersion",
arguments: [record.version],
});
}

if (record.version !== undefined) {
if (record.projections.length > 0) {
program.enableProjections(record.projections);
}

Expand Down
43 changes: 43 additions & 0 deletions packages/samples/test/output/use-versioned-lib/openapi.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
{
"openapi": "3.0.0",
"info": {
"title": "Pet Store Service",
"version": "0000-00-00"
},
"tags": [],
"paths": {
"/": {
"get": {
"operationId": "VersionedApi_read",
"parameters": [],
"responses": {
"200": {
"description": "Ok",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Library.PetToy"
}
}
}
}
}
}
}
},
"components": {
"schemas": {
"Library.PetToy": {
"type": "object",
"properties": {
"name": {
"type": "string"
}
},
"required": [
"name"
]
}
}
}
}
13 changes: 13 additions & 0 deletions packages/samples/use-versioned-lib/library.cadl
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import "@cadl-lang/versioning";

@versioned(VERSIONS)
namespace Library;

alias VERSIONS = "1.0" | "1.1";

model PetToy {
name: string;

@added("1.1")
material: string;
}
11 changes: 11 additions & 0 deletions packages/samples/use-versioned-lib/main.cadl
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import "@cadl-lang/versioning";
import "@cadl-lang/rest";
import "./library.cadl";

// Use version 1.0 of the Library
@serviceTitle("Pet Store Service")
@versionedDependency(Library, "1.0")
namespace VersionedApi;
using Cadl.Http;

op read(): Library.PetToy;
14 changes: 4 additions & 10 deletions packages/versioning/src/lib.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,22 +21,16 @@ const libDef = {
default: `The versioned decorator must be applied to a namespace`,
},
},
"versioned-dependency-not-on-namespace": {
severity: "error",
messages: {
default: `The versionedDependency decorator must be applied to a namespace`,
},
},
"versioned-dependency-not-to-namespace": {
"versioned-dependency-record-not-model": {
severity: "error",
messages: {
default: `The versionedDependency decorator must specify the dependency namespace`,
default: paramMessage`The versionedDependency decorator must provide a model mapping local versions to dependency '${"dependency"}' versions`,
},
},
"versioned-dependency-record-not-model": {
"versioned-dependency-not-string": {
severity: "error",
messages: {
default: `The versionedDependency decorator must provide a model mapping local versions to dependency versions`,
default: paramMessage`The versionedDependency decorator must provide a version of the dependency '${"dependency"}'.`,
},
},
},
Expand Down
151 changes: 103 additions & 48 deletions packages/versioning/src/versioning.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
import {
DecoratorContext,
NamespaceType,
navigateProgram,
Program,
ProjectionApplication,
Type,
validateDecoratorParamType,
validateDecoratorTarget,
} from "@cadl-lang/compiler";
import { reportDiagnostic } from "./lib.js";
const addedOnKey = Symbol();
Expand Down Expand Up @@ -129,89 +132,141 @@ export function $versionedDependency(
{ program }: DecoratorContext,
referenceNamespace: Type,
targetNamespace: Type,
versionRecord: Type
versionRecord: string | Type
) {
if (referenceNamespace.kind !== "Namespace") {
reportDiagnostic(program, {
code: "versioned-dependency-not-on-namespace",
target: referenceNamespace,
});
return;
}

if (targetNamespace.kind !== "Namespace") {
reportDiagnostic(program, {
code: "versioned-dependency-not-to-namespace",
target: referenceNamespace,
});
return;
}

if (versionRecord.kind !== "Model") {
reportDiagnostic(program, {
code: "versioned-dependency-record-not-model",
target: referenceNamespace,
});
if (
!validateDecoratorTarget(program, referenceNamespace, "@versionedDependency", "Namespace") ||
!validateDecoratorParamType(program, referenceNamespace, targetNamespace, "Namespace") ||
!validateDecoratorParamType(program, referenceNamespace, versionRecord, ["Model", "String"])
) {
return;
}

let state = program.stateMap(versionDependencyKey).get(referenceNamespace) as Map<
NamespaceType,
Map<string, string>
string | Map<string, string>
>;

if (!state) {
state = new Map();
program.stateMap(versionDependencyKey).set(referenceNamespace, state);
}

let versionMap = state.get(targetNamespace);
if (!versionMap) {
versionMap = new Map();
state.set(targetNamespace, versionMap);
}
if (typeof versionRecord === "string") {
state.set(targetNamespace, versionRecord);
} else {
let versionMap = state.get(targetNamespace);
if (!versionMap || !(versionMap instanceof Map)) {
versionMap = new Map();
state.set(targetNamespace, versionMap);
}

for (const [name, prop] of versionRecord.properties) {
if (prop.type.kind !== "String") {
continue;
for (const [name, prop] of versionRecord.properties) {
if (prop.type.kind !== "String") {
continue;
}
versionMap.set(name, prop.type.value);
}
versionMap.set(name, prop.type.value);
}
}

export function getVersionDependencies(
p: Program,
namespace: NamespaceType
): Map<NamespaceType, Map<string, string>> | undefined {
): Map<NamespaceType, Map<string, string> | string> | undefined {
return p.stateMap(versionDependencyKey).get(namespace);
}

export function $onValidate(program: Program) {
navigateProgram(program, {
namespace: (namespace) => {
const version = getVersion(program, namespace);
const dependencies = getVersionDependencies(program, namespace);
if (dependencies === undefined) {
return;
}

for (const [dependencyNs, value] of dependencies.entries()) {
if (version && version.length > 0) {
if (!(value instanceof Map)) {
reportDiagnostic(program, {
code: "versioned-dependency-record-not-model",
format: { dependency: program.checker!.getNamespaceString(dependencyNs) },
target: namespace,
});
}
} else {
if (typeof value !== "string") {
reportDiagnostic(program, {
code: "versioned-dependency-not-string",
format: { dependency: program.checker!.getNamespaceString(dependencyNs) },
target: namespace,
});
}
}
}
},
});
}

interface VersionRecord {
version: string | undefined;
projections: ProjectionApplication[];
}

export function getVersionRecords(program: Program, rootNs: NamespaceType): VersionRecord[] {
const versions = getVersions(program, rootNs);
if (!versions || versions.length === 0) {
return [{ version: undefined, projections: [] }];
}
const records: VersionRecord[] = [];
const dependencies = getVersionDependencies(program, rootNs) ?? new Map();
for (const version of versions) {
// TODO: find versioned dependencies
const projections = [{ scope: rootNs, projectionName: "v", arguments: [version] }];

for (const [dependencyNs, versionMap] of dependencies) {
if (!versionMap.has(version)) continue;
projections.push({
scope: dependencyNs,
projectionName: "v",
arguments: [versionMap.get(version!)],
});

if (!versions || versions.length === 0) {
if (dependencies.size === 0) {
return [{ version: undefined, projections: [] }];
} else {
for (const [dependencyNs, version] of dependencies) {
if (typeof version !== "string") {
const rootNsName = program.checker!.getNamespaceString(rootNs);
const dependencyNsName = program.checker!.getNamespaceString(dependencyNs);
throw new Error(
`Unexpected error: Namespace ${rootNsName} version dependency to ${dependencyNsName} should be a string.`
);
}

records.push({
version: undefined,
projections: [
{
scope: dependencyNs,
projectionName: "v",
arguments: [version],
},
],
});
}
}
} else {
for (const version of versions) {
// TODO: find versioned dependencies
const projections = [{ scope: rootNs, projectionName: "v", arguments: [version] }];

for (const [dependencyNs, versionMap] of dependencies) {
if (!(versionMap instanceof Map)) {
const rootNsName = program.checker!.getNamespaceString(rootNs);
const dependencyNsName = program.checker!.getNamespaceString(dependencyNs);
throw new Error(
`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!)],
});
}

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

return records;
Expand Down
Loading