Skip to content

Commit 44b38c6

Browse files
authored
[TypeSpec Validation] Refactor rules into separate classes (#25101)
1 parent baac183 commit 44b38c6

File tree

8 files changed

+177
-68
lines changed

8 files changed

+177
-68
lines changed
Lines changed: 19 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -1,40 +1,8 @@
1-
import { exec } from "child_process";
2-
import debug from "debug";
3-
import { access } from "fs/promises";
41
import { parseArgs, ParseArgsConfig } from "node:util";
5-
import path from "path";
6-
import { simpleGit } from "simple-git";
7-
8-
debug.enable("simple-git");
9-
10-
async function runCmd(cmd: string, cwd: string) {
11-
console.log(`run command:${cmd}`);
12-
const { err, stdout, stderr } = (await new Promise((res) =>
13-
exec(
14-
cmd,
15-
{ encoding: "utf8", maxBuffer: 1024 * 1024 * 64, cwd: cwd },
16-
(err: unknown, stdout: unknown, stderr: unknown) =>
17-
res({ err: err, stdout: stdout, stderr: stderr })
18-
)
19-
)) as any;
20-
let resultString = stderr + stdout;
21-
console.log("Stdout output:");
22-
console.log(stdout);
23-
if (stderr) {
24-
console.log("Stderr output:");
25-
console.log(stderr);
26-
}
27-
if (stderr || err) {
28-
throw new Error(err);
29-
}
30-
return resultString as string;
31-
}
32-
33-
async function checkFileExists(file: string) {
34-
return access(file)
35-
.then(() => true)
36-
.catch(() => false);
37-
}
2+
import { CompileRule } from "./rules/compile.js";
3+
import { FormatRule } from "./rules/format.js";
4+
import { GitDiffRule } from "./rules/git-diff.js";
5+
import { NpmPrefixRule } from "./rules/npm-prefix.js";
386

397
export async function main() {
408
const args = process.argv.slice(2);
@@ -48,39 +16,22 @@ export async function main() {
4816
const folder = parsedArgs.positionals[0];
4917
console.log("Running TypeSpecValidation on folder:", folder);
5018

51-
// Verify all specs are using root level pacakge.json
52-
let expected_npm_prefix = process.cwd();
53-
const actual_npm_prefix = (await runCmd(`npm prefix`, folder)).trim();
54-
if (expected_npm_prefix !== actual_npm_prefix) {
55-
console.log(
56-
"ERROR: TypeSpec folders MUST NOT contain a package.json, and instead MUST rely on the package.json at repo root."
57-
);
58-
throw new Error(
59-
"Expected npm prefix: " + expected_npm_prefix + "\nActual npm prefix: " + actual_npm_prefix
60-
);
61-
}
62-
63-
// Spec compilation check
64-
if (await checkFileExists(path.join(folder, "main.tsp"))) {
65-
await runCmd(`npx --no tsp compile . --warn-as-error`, folder);
19+
let rules = [new NpmPrefixRule(), new CompileRule(), new FormatRule(), new GitDiffRule()];
20+
let success = true;
21+
for (let i = 0; i < rules.length; i++) {
22+
const rule = rules[i];
23+
console.log("\nExecuting rule: " + rule.name);
24+
const result = await rule.execute(folder);
25+
if (result.stdOutput) console.log(result.stdOutput);
26+
if (!result.success) {
27+
success = false;
28+
console.log("Rule " + rule.name + " failed");
29+
if (result.errorOutput) console.log(result.errorOutput);
30+
}
6631
}
67-
if (await checkFileExists(path.join(folder, "client.tsp"))) {
68-
await runCmd(`npx --no tsp compile client.tsp --no-emit --warn-as-error`, folder);
69-
}
70-
71-
// Format parent folder to include shared files
72-
await runCmd(`npx tsp format ../**/*.tsp`, folder);
7332

74-
// Verify generated swagger file is in sync with one on disk
75-
const git = simpleGit();
76-
let gitStatusIsClean = await (await git.status(["--porcelain"])).isClean();
77-
if (!gitStatusIsClean) {
78-
let gitStatus = await git.status();
79-
let gitDiff = await git.diff();
80-
console.log("git status");
81-
console.log(gitStatus);
82-
console.log("git diff");
83-
console.log(gitDiff);
84-
throw new Error("Generated swagger file does not match swagger file on disk");
33+
if (!success) {
34+
return process.exit(1);
8535
}
36+
return;
8637
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
export interface RuleResult {
2+
readonly success: boolean;
3+
readonly stdOutput?: string;
4+
readonly errorOutput?: string;
5+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import { RuleResult } from "./rule-result.js";
2+
3+
export interface Rule {
4+
readonly name: string;
5+
readonly description: string;
6+
execute(folder?: string): Promise<RuleResult>;
7+
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import path from "path";
2+
import { runCmd, checkFileExists } from "../utils.js";
3+
import { Rule } from "../rule.js";
4+
import { RuleResult } from "../rule-result.js";
5+
6+
export class CompileRule implements Rule {
7+
readonly name = "Compile";
8+
readonly description = "Compile TypeSpec";
9+
10+
async execute(folder: string): Promise<RuleResult> {
11+
let success = true;
12+
let stdOutput = "";
13+
let errorOutput: string | undefined;
14+
15+
if (await checkFileExists(path.join(folder, "main.tsp"))) {
16+
let [std, err] = await runCmd(`npx --no tsp compile . --warn-as-error`, folder);
17+
stdOutput += std;
18+
if (err == null) {
19+
success = false;
20+
errorOutput += err;
21+
}
22+
}
23+
if (await checkFileExists(path.join(folder, "client.tsp"))) {
24+
let [std, err] = await runCmd(
25+
`npx --no tsp compile client.tsp --no-emit --warn-as-error`,
26+
folder
27+
);
28+
stdOutput += std;
29+
if (err == null) {
30+
success = false;
31+
errorOutput += err;
32+
}
33+
}
34+
35+
return {
36+
success: success,
37+
stdOutput: stdOutput,
38+
errorOutput: errorOutput,
39+
};
40+
}
41+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import { Rule } from "../rule.js";
2+
import { RuleResult } from "../rule-result.js";
3+
import { runCmd } from "../utils.js";
4+
5+
export class FormatRule implements Rule {
6+
readonly name = "Format";
7+
readonly description = "Format TypeSpec";
8+
9+
async execute(folder: string): Promise<RuleResult> {
10+
// Format parent folder to include shared files
11+
let [stdOutput, errorOutput] = await runCmd(`npx tsp format ../**/*.tsp`, folder);
12+
13+
let success = errorOutput == null ? false : true;
14+
return {
15+
success: success,
16+
stdOutput: stdOutput,
17+
errorOutput: errorOutput,
18+
};
19+
}
20+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import debug from "debug";
2+
import { simpleGit } from "simple-git";
3+
import { Rule } from "../rule.js";
4+
import { RuleResult } from "../rule-result.js";
5+
6+
debug.enable("simple-git");
7+
export class GitDiffRule implements Rule {
8+
readonly name = "GitDiff";
9+
readonly description = "Checks if previous rules resulted in a git diff";
10+
11+
async execute(): Promise<RuleResult> {
12+
const git = simpleGit();
13+
let gitStatusIsClean = await (await git.status(["--porcelain"])).isClean();
14+
15+
let success = true;
16+
let errorOutput: string | undefined;
17+
18+
if (!gitStatusIsClean) {
19+
success = false;
20+
errorOutput = JSON.stringify(await git.status());
21+
errorOutput += await git.diff();
22+
}
23+
24+
return {
25+
success: success,
26+
errorOutput: errorOutput,
27+
};
28+
}
29+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import { runCmd } from "../utils.js";
2+
import { Rule } from "../rule.js";
3+
import { RuleResult } from "../rule-result.js";
4+
5+
export class NpmPrefixRule implements Rule {
6+
readonly name = "NpmPrefix";
7+
readonly description = "Verify spec is using root level package.json";
8+
9+
async execute(folder: string): Promise<RuleResult> {
10+
let expected_npm_prefix = process.cwd();
11+
const actual_npm_prefix = (await runCmd(`npm prefix`, folder))[0].trim();
12+
13+
let success = true;
14+
let stdOutput =
15+
"Expected npm prefix: " +
16+
expected_npm_prefix +
17+
"\n" +
18+
"Actual npm prefix: " +
19+
actual_npm_prefix;
20+
let errorOutput: string | undefined;
21+
22+
if (expected_npm_prefix !== actual_npm_prefix) {
23+
success = false;
24+
errorOutput =
25+
"TypeSpec folders MUST NOT contain a package.json, and instead MUST rely on the package.json at repo root";
26+
}
27+
28+
return {
29+
success: success,
30+
stdOutput: stdOutput,
31+
errorOutput: errorOutput,
32+
};
33+
}
34+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import { access } from "fs/promises";
2+
import { exec } from "child_process";
3+
4+
export async function runCmd(cmd: string, cwd: string) {
5+
console.log(`run command:${cmd}`);
6+
const { err, stdout, stderr } = (await new Promise((res) =>
7+
exec(
8+
cmd,
9+
{ encoding: "utf8", maxBuffer: 1024 * 1024 * 64, cwd: cwd },
10+
(err: unknown, stdout: unknown, stderr: unknown) =>
11+
res({ err: err, stdout: stdout, stderr: stderr })
12+
)
13+
)) as any;
14+
15+
return [stdout, stderr + err?.message] as string[];
16+
}
17+
18+
export async function checkFileExists(file: string) {
19+
return access(file)
20+
.then(() => true)
21+
.catch(() => false);
22+
}

0 commit comments

Comments
 (0)