Skip to content
Closed
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
1 change: 1 addition & 0 deletions .github/workflows/pr-package.yml
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ jobs:
{ "dir": "packages/discord", "name": "@distilled.cloud/discord", "group": "Distilled" },
{ "dir": "packages/expo-eas", "name": "@distilled.cloud/expo-eas", "group": "Distilled" },
{ "dir": "packages/fly-io", "name": "@distilled.cloud/fly-io", "group": "Distilled" },
{ "dir": "packages/forgejo", "name": "@distilled.cloud/forgejo", "group": "Distilled" },
{ "dir": "packages/gcp", "name": "@distilled.cloud/gcp", "group": "Distilled" },
{ "dir": "packages/github", "name": "@distilled.cloud/github", "group": "Distilled" },
{ "dir": "packages/hetzner", "name": "@distilled.cloud/hetzner", "group": "Distilled" },
Expand Down
5 changes: 5 additions & 0 deletions .gitmodules
Original file line number Diff line number Diff line change
Expand Up @@ -84,3 +84,8 @@
path = packages/discord/specs/discord-api-spec
url = https://github.com/discord/discord-api-spec.git
ignore = dirty
[submodule "packages/forgejo/specs/spec-mirror-forgejo"]
path = packages/forgejo/specs/spec-mirror-forgejo
url = https://github.com/distilled-mirror/spec-mirror-forgejo.git
ignore = dirty
shallow = true
130 changes: 130 additions & 0 deletions packages/core/src/codegen/patches.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
import { describe, expect, test } from "bun:test";
import { mkdtempSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import {
FINALIZED_KEY,
danglingTargets,
finalizeConvert,
syncServiceOperations,
} from "./patches.ts";

const model = () => ({
smithy: "2.0",
shapes: {
"ns#Svc": {
type: "service",
operations: [{ target: "ns#AppsList" }, { target: "ns#AppsGet" }],
},
"ns#AppsList": {
type: "operation",
input: { target: "ns#AppsListRequest" },
output: { target: "smithy.api#Unit" },
},
"ns#AppsListRequest": { type: "structure", members: {} },
"ns#AppsGet": {
type: "operation",
input: { target: "smithy.api#Unit" },
output: { target: "smithy.api#Unit" },
},
},
});

const scaffold = (patches?: Record<string, unknown>) => {
const root = mkdtempSync(join(tmpdir(), "finalize-"));
mkdirSync(join(root, ".generated-specs"));
writeFileSync(
join(root, ".generated-specs", "svc.json"),
JSON.stringify(model()),
);
if (patches) {
mkdirSync(join(root, "patches", "svc"), { recursive: true });
writeFileSync(
join(root, "patches", "svc", "a.json"),
JSON.stringify(patches),
);
}
return root;
};

const read = (root: string) =>
JSON.parse(readFileSync(join(root, ".generated-specs", "svc.json"), "utf8"));

describe("finalizeConvert", () => {
test("renames, syncs the service list, stamps the marker", async () => {
const root = scaffold();
await finalizeConvert({ root });
const m = read(root);
expect(Object.keys(m.shapes).sort()).toEqual(
["ns#Svc", "ns#ListApps", "ns#ListAppsRequest", "ns#GetApp"].sort(),
);
expect(m.shapes["ns#Svc"].operations).toEqual([
{ target: "ns#ListApps" },
{ target: "ns#GetApp" },
]);
expect(m.metadata[FINALIZED_KEY]).toBe(true);
});

test("refuses a second pass", async () => {
const root = scaffold();
await finalizeConvert({ root });
await expect(finalizeConvert({ root })).rejects.toThrow(
/already finalized/,
);
});

test("applies Smithy patches and repairs the service list after a move", async () => {
const root = scaffold({
patches: [
{ op: "move", from: "/shapes/ns#AppsGet", path: "/shapes/ns#FetchApp" },
],
});
await finalizeConvert({ root });
const m = read(root);
expect(m.shapes["ns#FetchApp"]).toBeDefined();
expect(m.shapes["ns#Svc"].operations.map((o: any) => o.target)).toEqual([
"ns#ListApps",
"ns#FetchApp",
]);
});

test("fails on a stale patch pointer by default", async () => {
const root = scaffold({
patches: [{ op: "remove", path: "/shapes/ns#Missing" }],
});
await expect(finalizeConvert({ root })).rejects.toThrow(/failed/);
});

test("fails on dangling targets and leaves the model unstamped", async () => {
const root = scaffold({
patches: [
{
op: "replace",
path: "/shapes/ns#AppsList/output/target",
value: "ns#Nope",
},
],
});
await expect(finalizeConvert({ root })).rejects.toThrow(/do not exist/);
expect(read(root).metadata?.[FINALIZED_KEY]).toBeUndefined();
});
});

describe("danglingTargets", () => {
test("ignores prelude ids", () => {
expect(danglingTargets(model())).toEqual([]);
});
});

describe("syncServiceOperations", () => {
test("drops missing and appends unlisted, keeping order", () => {
const m = model();
delete (m.shapes as any)["ns#AppsGet"];
(m.shapes as any)["ns#Zed"] = { type: "operation" };
expect(syncServiceOperations(m)).toBe(1);
expect((m.shapes as any)["ns#Svc"].operations).toEqual([
{ target: "ns#AppsList" },
{ target: "ns#Zed" },
]);
});
});
Loading