Skip to content

Commit

Permalink
Allow unversioned project to select a specific version of a versioned…
Browse files Browse the repository at this point in the history
… library (#346)
  • Loading branch information
timotheeguerin authored Mar 25, 2022
1 parent 3a96ddd commit cada697
Show file tree
Hide file tree
Showing 10 changed files with 335 additions and 122 deletions.
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({
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

0 comments on commit cada697

Please sign in to comment.