Skip to content

Commit

Permalink
switch to using vacuum
Browse files Browse the repository at this point in the history
  • Loading branch information
aabedraba committed Nov 22, 2023
1 parent 16954d7 commit 741f257
Show file tree
Hide file tree
Showing 2 changed files with 131 additions and 54 deletions.
183 changes: 130 additions & 53 deletions apps/api/src/lib/rating.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,13 @@
import {
Rating,
RatingOutput,
SpectralReport,
generateOpenApiRating,
} from "@rate-my-openapi/core";
import spectralCore from "@stoplight/spectral-core";
import SpectralParsers from "@stoplight/spectral-parsers";
import { bundleAndLoadRuleset } from "@stoplight/spectral-ruleset-bundler/with-loader";
import { readFile, unlink } from "fs/promises";
import { load as loadYAML } from "js-yaml";
import * as fs from "node:fs";
import { join } from "node:path";
import { Worker } from "node:worker_threads";
import OpenAI from "openai";
Expand All @@ -21,8 +20,13 @@ import {
getStorageClient,
} from "../services/storage.js";
import { OpenApiFileExtension, assertValidFileExtension } from "./types.js";
import util from "node:util";
import { exec } from "node:child_process";
import { OpenAPIV3, OpenAPIV3_1 } from "openapi-types";

const { Spectral, Document } = spectralCore;
const execAwait = util.promisify(exec);

const { Document } = spectralCore;

export type SimpleReport = Pick<
Rating,
Expand Down Expand Up @@ -154,78 +158,126 @@ export async function generateReportFromLocal({
return reportResult;
}

export async function getReport(options: {
fileContent: string;
async function generateVacuumReport(options: {
reportId: string;
openAPIFilePath: string;
fileExtension: OpenApiFileExtension;
}): Promise<GetReportResult> {
const parser =
options.fileExtension === "json"
? SpectralParsers.Json
: SpectralParsers.Yaml;
}) {
const rulesetPath = join(process.cwd(), "rulesets/rules.vacuum.yaml");

let openApiSpectralDoc: spectralCore.Document;
let vacuumCliReport;
try {
openApiSpectralDoc = new Document(
options.fileContent,
parser as SpectralParsers.IParser,
options.openAPIFilePath,
);
} catch (err) {
const vacuumCommand =
`vacuum spectral-report -r ${rulesetPath} -o ${options.openAPIFilePath}`.replace(
/\n/g,
"",
);
const { stdout, stderr } = await execAwait(vacuumCommand, {

Check warning

Code scanning / CodeQL

Shell command built from environment values Medium

This shell command depends on an uncontrolled
absolute path
.
maxBuffer: undefined,
});

if (stderr) {
throw new ReportGenerationError(
`Vacuum CLI command failed for report ${options.reportId}`,
{ cause: stderr },
);
}

if (!stdout) {
throw new ReportGenerationError(
`Vacuum CLI command succeeded but did not generate a report for report ${options.reportId}`,
);
}

vacuumCliReport = stdout;
} catch (e) {
throw new ReportGenerationError(
`Unable to parse OpenAPI file ${options.openAPIFilePath}`,
{ cause: err },
`Vacuum CLI command failed for report ${options.reportId}`,
{ cause: e },
);
}

const spectral = new Spectral();
const spectralRulesetFilepath = join(
process.cwd(),
"rulesets/.spectral.yaml",
);

try {
const spectralRuleset = await bundleAndLoadRuleset(
spectralRulesetFilepath,
{
fs,
fetch,
},
);
spectral.setRuleset(spectralRuleset);
} catch (err) {
if (!vacuumCliReport) {
throw new ReportGenerationError(
`Unable to set Spectral ruleset for file ${options.openAPIFilePath}`,
{ cause: err },
`Vacuum CLI command succeeded but did not generate a report for report ${options.reportId}`,
);
}

let spectralOutputReport;
let outputReport: SpectralReport;
try {
spectralOutputReport = await spectral.run(openApiSpectralDoc);
outputReport = JSON.parse(vacuumCliReport);
} catch (err) {
throw new ReportGenerationError(
`Unable to run Spectral for file ${options.openAPIFilePath}`,
`Could not parse vacuum report for report ${options.reportId}`,
{ cause: err },
);
}

let output;
// TODO: being fixed by https://github.com/daveshanley/vacuum/issues/372
// remove this check once fixed in the next couple of days
if (vacuumCliReport.toLowerCase().includes("rolodex")) {
throw new ReportGenerationError(`Rolodex issue ${options.reportId}`);
}

return outputReport;
}

async function generateRatingFromReport(options: {
report: SpectralReport;
fileContent: string;
fileExtension: OpenApiFileExtension;
reportId: string;
}) {
let outputContent: OpenAPIV3.Document | OpenAPIV3_1.Document;
try {
const outputContent =
outputContent =
options.fileExtension === "json"
? JSON.parse(options.fileContent)
: loadYAML(options.fileContent, { json: true });

output = generateOpenApiRating(spectralOutputReport, outputContent);
} catch (err) {
throw new ReportGenerationError(err.message, {
throw new ReportGenerationError("Could not parse OpenAPI document", {
cause: err,
});
}

const issueSummary = getReportMinified(output);
let output;
try {
output = generateOpenApiRating(options.report, outputContent);
} catch (err) {
throw new ReportGenerationError(
`Could not generate rating for report ${options.reportId}`,
{ cause: err },
);
}

return output;
}

async function generateSimpleReport(options: {
fileContent: string;
openAPIFilePath: string;
fileExtension: OpenApiFileExtension;
ratingOutput: RatingOutput;
}) {
const parser =
options.fileExtension === "json"
? SpectralParsers.Json
: SpectralParsers.Yaml;

let openApiSpectralDoc: spectralCore.Document;
try {
openApiSpectralDoc = new Document(
options.fileContent,
parser as SpectralParsers.IParser,
options.openAPIFilePath,
);
} catch (err) {
throw new ReportGenerationError(
`Unable to parse OpenAPI file ${options.openAPIFilePath}`,
{ cause: err },
);
}

const issueSummary = getReportMinified(options.ratingOutput);
const [openAiLongSummary, openAiShortSummary] = await Promise.all([
getOpenAiLongSummary(issueSummary),
getOpenAiShortSummary(issueSummary),
Expand All @@ -244,18 +296,43 @@ export async function getReport(options: {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
title: (openApiSpectralDoc.data as any)?.info?.title || "OpenAPI",
fileExtension: options.fileExtension,
docsScore: output.docsScore,
completenessScore: output.completenessScore,
score: output.score,
securityScore: output.securityScore,
sdkGenerationScore: output.sdkGenerationScore,
docsScore: options.ratingOutput.docsScore,
completenessScore: options.ratingOutput.completenessScore,
score: options.ratingOutput.score,
securityScore: options.ratingOutput.securityScore,
sdkGenerationScore: options.ratingOutput.sdkGenerationScore,
shortSummary: openAiShortSummary ?? undefined,
longSummary: openAiLongSummary ?? undefined,
};

return simpleReport;
}

export async function getReport(options: {
fileContent: string;
reportId: string;
openAPIFilePath: string;
fileExtension: OpenApiFileExtension;
}): Promise<GetReportResult> {
const reportOutput = await generateVacuumReport(options);

const ratingOutput = await generateRatingFromReport({
report: reportOutput,
fileContent: options.fileContent,
fileExtension: options.fileExtension,
reportId: options.reportId,
});

const simpleReport = await generateSimpleReport({
fileContent: options.fileContent,
fileExtension: options.fileExtension,
openAPIFilePath: options.openAPIFilePath,
ratingOutput,
});

return {
simpleReport,
fullReport: output,
fullReport: ratingOutput,
};
}

Expand Down
2 changes: 1 addition & 1 deletion apps/api/src/scripts/regenerate-reports.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@ import { DownloadResponse } from "@google-cloud/storage";
);

for (const simpleReportFile of simpleReportFiles) {
console.time("ReportTime");
let downloadedFileContent: DownloadResponse;

try {
Expand Down Expand Up @@ -60,6 +59,7 @@ import { DownloadResponse } from "@google-cloud/storage";

const content = await readFile(tempPath, "utf-8");

console.time("ReportTime");
try {
const reportResult = await getReport({
fileContent: content,
Expand Down

0 comments on commit 741f257

Please sign in to comment.