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,7 @@
---
changeKind: fix
packages:
- "@typespec/versioning"
---

[API] Fix running multiple versioning mutators together
21 changes: 19 additions & 2 deletions packages/versioning/src/versioning.ts
Original file line number Diff line number Diff line change
Expand Up @@ -391,13 +391,30 @@ export function getAvailabilityMapInTimeline(
)
return undefined;

added = resolveWhenFirstAdded(added, removed, parentAdded);
// Only keep versioning info related to this timeline
const timelineAdded = added.filter((x) => timeline.getIndex(x) !== -1);
const timelineRemoved = removed.filter((x) => timeline.getIndex(x) !== -1);
const hasTypeChangedInTimeline =
typeChanged !== undefined && [...typeChanged.keys()].some((v) => timeline.getIndex(v) !== -1);
const hasReturnTypeChangedInTimeline =
returnTypeChanged !== undefined &&
[...returnTypeChanged.keys()].some((v) => timeline.getIndex(v) !== -1);

if (
!timelineAdded.length &&
!timelineRemoved.length &&
!hasTypeChangedInTimeline &&
!hasReturnTypeChangedInTimeline
)
return undefined;

added = resolveWhenFirstAdded(timelineAdded, timelineRemoved, 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);
const rem = timelineRemoved.find((x) => timeline.getIndex(x) === index);
if (rem) {
isAvail = false;
avail.set(moment, Availability.Removed);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { Namespace, Scalar, Type } from "@typespec/compiler";
import { unsafe_mutateSubgraphWithNamespace } from "@typespec/compiler/experimental";
import { t } from "@typespec/compiler/testing";
import { strictEqual } from "assert";
import { describe, expect, it } from "vitest";
import { getVersioningMutators } from "../../src/mutator.js";
Expand Down Expand Up @@ -211,3 +212,54 @@ describe("operations in interface", () => {
(decorators) => `interface Test { ${decorators} A(): void; }`,
);
});

describe("apply multiple versioning mutators", () => {
// https://github.com/microsoft/typespec/issues/9927
it("properties with @added from different services are preserved", async () => {
const { ServiceA, ServiceB, program } = await Tester.compile(t.code`
@versioned(VersionsA)
namespace ${t.namespace("ServiceA")} {
enum VersionsA { av1, av2 }
model Foo {
name: string;
@added(VersionsA.av2)
description?: string;
}
}

@versioned(VersionsB)
namespace ${t.namespace("ServiceB")} {
enum VersionsB { bv1, bv2 }
model Bar {
id: int32;
@added(VersionsB.bv2)
value?: string;
}
}
`);

const serviceAMutators = getVersioningMutators(program, ServiceA);
const serviceBMutators = getVersioningMutators(program, ServiceB);

strictEqual(serviceAMutators?.kind, "versioned");
strictEqual(serviceBMutators?.kind, "versioned");

const serviceAV2 = serviceAMutators.snapshots[1].mutator;
const serviceBV2 = serviceBMutators.snapshots[1].mutator;

const globalNs = program.getGlobalNamespaceType();
const result = unsafe_mutateSubgraphWithNamespace(program, [serviceAV2, serviceBV2], globalNs);

const mutatedGlobal = result.type as Namespace;
const serviceA = mutatedGlobal.namespaces.get("ServiceA")!;
const serviceB = mutatedGlobal.namespaces.get("ServiceB")!;

const foo = serviceA.models.get("Foo")!;
expect(foo.properties.has("name")).toBe(true);
expect(foo.properties.has("description")).toBe(true);

const bar = serviceB.models.get("Bar")!;
expect(bar.properties.has("id")).toBe(true);
expect(bar.properties.has("value")).toBe(true);
});
});
Loading