Skip to content

Commit 1f2a895

Browse files
OpenAPI stuff
1 parent a592835 commit 1f2a895

32 files changed

Lines changed: 3030 additions & 911 deletions

specs/OpenApiGeneration.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,10 @@
44
55
---
66

7+
CRITICAL: START WITH TESTS THAT VERIFY THAT OpenAPI -> .nap is WORKING. THE OPENAPI -> .nap DETERMINISTIC PART IS F#. ENRICHMENT IS COPILOT ONLY.
8+
9+
---
10+
711
## Vision
812

913
A user points Napper at an OpenAPI 3.x or Swagger 2.x specification and gets a complete test suite: one `.nap` file per operation, organized by tag into subdirectories, with a `.naplist` playlist, a `.napenv` environment file, and meaningful assertions derived from the spec's response schemas.

src/Nap.VsCode/src/cliInstaller.ts

Lines changed: 97 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
// CLI Installer — downloads the correct Napper CLI binary from GitHub releases
22
// Decoupled from vscode SDK — takes config values as parameters
33

4+
import type * as http from "http";
45
import * as https from "https";
56
import * as fs from "fs";
67
import * as path from "path";
@@ -29,19 +30,27 @@ import {
2930
CLI_FILE_MODE_EXECUTABLE,
3031
} from "./constants";
3132

33+
const HTTP_STATUS_OK = 200;
34+
const HTTP_STATUS_REDIRECT_MIN = 300;
35+
const HTTP_STATUS_CLIENT_ERROR_MIN = 400;
36+
37+
const PLATFORM_RID_MAP: ReadonlyMap<string, string> = new Map([
38+
[`${CLI_PLATFORM_DARWIN}-${CLI_ARCH_ARM64}`, CLI_RID_OSX_ARM64],
39+
[`${CLI_PLATFORM_DARWIN}-${CLI_ARCH_X64}`, CLI_RID_OSX_X64],
40+
[`${CLI_PLATFORM_LINUX}-${CLI_ARCH_X64}`, CLI_RID_LINUX_X64],
41+
[`${CLI_PLATFORM_WIN32}-${CLI_ARCH_X64}`, CLI_RID_WIN_X64],
42+
]);
43+
3244
export const platformToRid = (
3345
platform: string,
3446
arch: string
3547
): Result<string, string> => {
36-
if (platform === CLI_PLATFORM_DARWIN && arch === CLI_ARCH_ARM64)
37-
return ok(CLI_RID_OSX_ARM64);
38-
if (platform === CLI_PLATFORM_DARWIN && arch === CLI_ARCH_X64)
39-
return ok(CLI_RID_OSX_X64);
40-
if (platform === CLI_PLATFORM_LINUX && arch === CLI_ARCH_X64)
41-
return ok(CLI_RID_LINUX_X64);
42-
if (platform === CLI_PLATFORM_WIN32 && arch === CLI_ARCH_X64)
43-
return ok(CLI_RID_WIN_X64);
44-
return err(`${CLI_UNSUPPORTED_PLATFORM_MSG}${platform}-${arch}`);
48+
const key = `${platform}-${arch}`;
49+
const rid = PLATFORM_RID_MAP.get(key);
50+
if (rid !== undefined) {
51+
return ok(rid);
52+
}
53+
return err(`${CLI_UNSUPPORTED_PLATFORM_MSG}${key}`);
4554
};
4655

4756
export const assetName = (rid: string): string => {
@@ -62,57 +71,88 @@ export const installedCliPath = (
6271
export const isCliInstalled = (cliPath: string): boolean =>
6372
fs.existsSync(cliPath);
6473

65-
const followRedirect = (
74+
interface RedirectContext {
75+
readonly dest: string;
76+
readonly redirectCount: number;
77+
readonly resolve: (value: Result<void, string>) => void;
78+
}
79+
80+
const handleRedirect = (
81+
response: http.IncomingMessage,
82+
ctx: RedirectContext,
83+
): void => {
84+
const location = response.headers.location;
85+
if (location === undefined || location === "") {
86+
ctx.resolve(err(CLI_REDIRECT_ERROR));
87+
return;
88+
}
89+
response.resume();
90+
followRedirect(location, ctx.dest, ctx.redirectCount + 1)
91+
.then(ctx.resolve)
92+
.catch(() => { ctx.resolve(err(CLI_REDIRECT_ERROR)); });
93+
};
94+
95+
const handleDownload = (
96+
response: http.IncomingMessage,
97+
dest: string,
98+
resolve: (value: Result<void, string>) => void
99+
): void => {
100+
const file = fs.createWriteStream(dest);
101+
response.pipe(file);
102+
file.on("finish", () => {
103+
file.close();
104+
resolve(ok(undefined));
105+
});
106+
file.on("error", (e) => { resolve(err(e.message)); });
107+
};
108+
109+
const buildRequestOptions = (url: string): { hostname: string; path: string; headers: Record<string, string> } => {
110+
const parsedUrl = new URL(url);
111+
return {
112+
hostname: parsedUrl.hostname,
113+
path: parsedUrl.pathname + parsedUrl.search,
114+
headers: { "User-Agent": CLI_BINARY_NAME },
115+
};
116+
};
117+
118+
const isRedirectStatus = (status: number): boolean =>
119+
status >= HTTP_STATUS_REDIRECT_MIN && status < HTTP_STATUS_CLIENT_ERROR_MIN;
120+
121+
const handleResponse = (
122+
response: http.IncomingMessage,
123+
ctx: RedirectContext,
124+
): void => {
125+
const status = response.statusCode ?? 0;
126+
if (isRedirectStatus(status)) {
127+
handleRedirect(response, ctx);
128+
} else if (status !== HTTP_STATUS_OK) {
129+
response.resume();
130+
ctx.resolve(err(`${CLI_DOWNLOAD_ERROR_PREFIX}${status}`));
131+
} else {
132+
handleDownload(response, ctx.dest, ctx.resolve);
133+
}
134+
};
135+
136+
async function followRedirect(
66137
url: string,
67138
dest: string,
68139
redirectCount: number
69-
): Promise<Result<void, string>> => {
140+
): Promise<Result<void, string>> {
70141
if (redirectCount > CLI_MAX_REDIRECTS) {
71-
return Promise.resolve(err(CLI_TOO_MANY_REDIRECTS));
142+
return err(CLI_TOO_MANY_REDIRECTS);
72143
}
73144

74-
return new Promise((resolve) => {
75-
const parsedUrl = new URL(url);
76-
const options = {
77-
hostname: parsedUrl.hostname,
78-
path: parsedUrl.pathname + parsedUrl.search,
79-
headers: { "User-Agent": CLI_BINARY_NAME },
80-
};
145+
const options = buildRequestOptions(url);
81146

147+
return await new Promise((resolve) => {
148+
const ctx: RedirectContext = { dest, redirectCount, resolve };
82149
https
83-
.get(options, (response) => {
84-
const status = response.statusCode ?? 0;
85-
86-
if (status >= 300 && status < 400) {
87-
const location = response.headers.location;
88-
if (!location) {
89-
resolve(err(CLI_REDIRECT_ERROR));
90-
return;
91-
}
92-
response.resume();
93-
resolve(followRedirect(location, dest, redirectCount + 1));
94-
return;
95-
}
96-
97-
if (status !== 200) {
98-
response.resume();
99-
resolve(err(`${CLI_DOWNLOAD_ERROR_PREFIX}${status}`));
100-
return;
101-
}
102-
103-
const file = fs.createWriteStream(dest);
104-
response.pipe(file);
105-
file.on("finish", () => {
106-
file.close();
107-
resolve(ok(undefined));
108-
});
109-
file.on("error", (e) => resolve(err(e.message)));
110-
})
111-
.on("error", (e) => resolve(err(e.message)));
150+
.get(options, (response) => { handleResponse(response, ctx); })
151+
.on("error", (e) => { resolve(err(e.message)); });
112152
});
113-
};
153+
}
114154

115-
export const downloadBinary = (
155+
export const downloadBinary = async (
116156
rid: string,
117157
destPath: string
118158
): Promise<Result<void, string>> => {
@@ -124,7 +164,7 @@ export const downloadBinary = (
124164
fs.mkdirSync(dir, { recursive: true });
125165
}
126166

127-
return followRedirect(url, destPath, 0);
167+
return await followRedirect(url, destPath, 0);
128168
};
129169

130170
export const makeExecutable = (
@@ -146,11 +186,15 @@ export const installCli = async (
146186
arch: string
147187
): Promise<Result<InstallResult, string>> => {
148188
const ridResult = platformToRid(platform, arch);
149-
if (!ridResult.ok) return err(ridResult.error);
189+
if (!ridResult.ok) {
190+
return err(ridResult.error);
191+
}
150192

151193
const destPath = installedCliPath(storageDir, platform);
152194
const downloadResult = await downloadBinary(ridResult.value, destPath);
153-
if (!downloadResult.ok) return err(downloadResult.error);
195+
if (!downloadResult.ok) {
196+
return err(downloadResult.error);
197+
}
154198

155199
makeExecutable(destPath, platform);
156200
return ok({ cliPath: destPath });

0 commit comments

Comments
 (0)