Skip to content

Commit 019c24d

Browse files
feat: added logic for structural and semantic parsing
Signed-off-by: Fredrik Nordlander <fredrik.nordlander@digg.se>
1 parent ac8c9dc commit 019c24d

File tree

9 files changed

+1626
-93
lines changed

9 files changed

+1626
-93
lines changed

package-lock.json

Lines changed: 335 additions & 23 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -40,12 +40,16 @@
4040
"jest": "^30.0.4",
4141
"standard-version": "^9.5.0",
4242
"ts-jest": "^29.4.1",
43-
"typescript": "^5.9.2",
44-
"ts-node": "^10.9.2"
43+
"ts-node": "^10.9.2",
44+
"typescript": "^5.9.2"
4545
},
4646
"dependencies": {
47-
"@stoplight/spectral-core": "^1.19.5",
47+
"@apidevtools/swagger-parser": "^12.1.0",
48+
"@stoplight/spectral-core": "^1.20.0",
4849
"@stoplight/spectral-functions": "^1.7.2",
50+
"@stoplight/spectral-parsers": "^1.0.5",
51+
"@stoplight/spectral-ruleset-bundler": "^1.6.3",
52+
"@stoplight/spectral-rulesets": "^1.22.0",
4953
"@types/express": "^5.0.0",
5054
"adm-zip": "^0.5.16",
5155
"body-parser": "^2.0.0",

src/app.ts

Lines changed: 134 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// SPDX-FileCopyrightText: 2025 Digg - Agency for Digital Government
1+
// SPDX-FileCopyrightText: 2025 diggsweden/rest-api-profil-lint-processor
22
//
33
// SPDX-License-Identifier: EUPL-1.2
44

@@ -13,39 +13,44 @@
1313
*
1414
**************************************************************/
1515
import yargs from 'yargs';
16-
import * as fs from 'node:fs';
1716
import * as path from 'node:path';
18-
import { join } from 'path';
17+
import * as fs from 'node:fs';
1918
import Parsers from '@stoplight/spectral-parsers';
20-
import spectralCore from '@stoplight/spectral-core';
2119
import { importAndCreateRuleInstances, getRuleModules } from './util/ruleUtil.js'; // Import the helper function
2220
import util from 'util';
2321
import { RapLPCustomSpectral } from './util/RapLPCustomSpectral.js';
2422
import { DiagnosticReport, RapLPDiagnostic } from './util/RapLPDiagnostic.js';
2523
import { AggregateError } from './util/RapLPCustomErrorInfo.js';
2624
import chalk from 'chalk';
2725
import { ExcelReportProcessor } from './util/excelReportProcessor.js';
26+
import { parseApiSpecInput,detectSpecFormatPreference,semanticValidate, ParseResult} from './util/validateUtil.js';
27+
import { SpecParseError } from './util/RapLPSpecParseError.js';
28+
import type { IParser } from '@stoplight/spectral-parsers';
29+
import { Document as SpectralDocument } from '@stoplight/spectral-core';
30+
import { Issue } from './util/RapLPIssueHelpers.js';
2831

2932
declare var AggregateError: {
3033
prototype: AggregateError;
3134
new (errors: any[], message?: string): AggregateError;
3235
};
33-
const { Spectral, Document } = spectralCore;
3436
const writeFileAsync = util.promisify(fs.writeFile);
3537
const appendFileAsync = util.promisify(fs.appendFile);
3638

37-
try {
38-
// Parse command-line arguments using yargs
39-
const argv = await yargs(process.argv.slice(2))
40-
.version('1.0.0')
41-
.option('file', {
42-
alias: 'f',
43-
describe: 'Sökväg till OpenAPI specifikation(yaml,json)',
44-
demandOption: true,
45-
type: 'string',
46-
coerce: (file: string) => path.resolve(file), // convert to absolute path
47-
})
48-
.option('categories', {
39+
40+
41+
async function main(): Promise<void> {
42+
try {
43+
// Parse command-line arguments using yargs
44+
const argv = await yargs(process.argv.slice(2))
45+
.version('1.0.0')
46+
.option('file', {
47+
alias: 'f',
48+
describe: 'Sökväg till OpenAPI specifikation(yaml,json)',
49+
demandOption: true,
50+
type: 'string',
51+
coerce: (file: string) => path.resolve(file),
52+
})
53+
.option('categories', {
4954
alias: 'c',
5055
describe: `Regelkategorier separerade med kommatecken.Tillgängliga kategorier: ${getRuleModules().join(',')}`,
5156
type: 'string',
@@ -73,28 +78,112 @@ try {
7378
describe:
7479
'Sökväg till fil för diagnostiseringsinformation från RAP-LP. Om en specificerad, så kommer diagnostiseringsinformationen att skrivas ut till angiven fil i Excel format.',
7580
type: 'string',
81+
}).option('strict', {
82+
describe:
83+
'Aktivera strict mode för validering av semantik och struktur.',
84+
type: 'boolean',
85+
default: false,
7686
}).argv;
77-
// Extract arguments from yargs
78-
const apiSpecFileName = (argv.file as string) || '';
79-
const ruleCategories = argv.categories ? (argv.categories as string).split(',') : undefined;
80-
const logErrorFilePath = argv.logError as string | undefined;
81-
const logDiagnosticFilePath = argv.logDiagnostic as string | undefined;
87+
88+
// Extract arguments from yargs
89+
const apiSpecFileName = (argv.file as string) || '';
90+
const ruleCategories = argv.categories ? (argv.categories as string).split(',') : undefined;
91+
const logErrorFilePath = argv.logError as string | undefined;
92+
const logDiagnosticFilePath = argv.logDiagnostic as string | undefined;
93+
const disableSanity = argv.disableSanity as boolean ?? false;
94+
const strict = argv.strict as boolean ?? false;
95+
96+
// Schemevalidation and Spectral Document creation ----------
97+
let apiSpecDocument: SpectralDocument;
98+
let parseResult: any;
99+
try {
100+
// NOTE: use filePath (camelCase)
101+
const prefer = detectSpecFormatPreference(apiSpecFileName,undefined,'auto');
102+
parseResult = await parseApiSpecInput(
103+
{filePath: apiSpecFileName},{
104+
strict: strict,
105+
preferJsonError: prefer
106+
}
107+
);
108+
if (parseResult.strictIssues && parseResult.strictIssues.length > 0) {
109+
console.error('Strict validation reported issues:');
110+
parseResult.strictIssues.forEach((iss: Issue) =>
111+
console.error(chalk.yellow(`- ${iss.type} at ${iss.path} : ${iss.message} ${iss.line ? `(line ${iss.line})` : ''}`)),
112+
//console.error(`- ${iss.type} at ${iss.path} : ${iss.message} ${iss.line ? `(line ${iss.line})` : ''}`),
113+
);
114+
process.exitCode = 2;
115+
return;
116+
}
117+
} catch (err: any) {
118+
// Hantering av parse-fel (behåll din logik men använd return; nu innanför main())
119+
if (err instanceof SpecParseError) {
120+
const formattedDate = new Date().toISOString();
121+
const logData = {
122+
timeStamp: formattedDate,
123+
message: 'Fel vid parsing av API-specifikationen.',
124+
error: err.toJSON ? err.toJSON() : { message: String(err) },
125+
};
126+
127+
if (logErrorFilePath) {
128+
try {
129+
let existingLogs: any[] = [];
130+
if (argv.append && fs.existsSync(logErrorFilePath)) {
131+
const fileContent = await fs.promises.readFile(logErrorFilePath, 'utf8');
132+
try {
133+
existingLogs = JSON.parse(fileContent);
134+
if (!Array.isArray(existingLogs)) existingLogs = [existingLogs];
135+
} catch {
136+
existingLogs = [];
137+
}
138+
}
139+
existingLogs.push(logData);
140+
const updatedContent = JSON.stringify(existingLogs, null, 2);
141+
await writeFileAsync(logErrorFilePath, Buffer.from(updatedContent, 'utf8'));
142+
console.log(chalk.green(`Parserfel loggat till ${logErrorFilePath}`));
143+
} catch (fileErr: any) {
144+
console.error(chalk.red('Misslyckades att skriva parserfel till loggfilen:'), fileErr.message);
145+
}
146+
} else {
147+
// No log file specified - write to stdout
148+
console.error(chalk.red('<<< Parserfel i API-specifikationen >>>'));
149+
console.error(chalk.red(`Fel: ${err.message}`));
150+
if (err.line || err.column) {
151+
console.error(chalk.yellow(`Rad: ${err.line ?? '-'}, Kolumn: ${err.column ?? '-'}`));
152+
}
153+
if (err.snippet && !err.message.includes(err.snippet)) {
154+
console.error(chalk.gray('--- snippet ---'));
155+
console.error(chalk.gray(err.snippet));
156+
console.error(chalk.gray('---------------'));
157+
}
158+
}
159+
160+
process.exitCode = 1;
161+
return; // terminate main gracefully
162+
}
163+
164+
// Övrigt oväntat fel
165+
logErrorToFile(err);
166+
console.error(chalk.red('Ett fel uppstod vid inläsning/parsing av spec-filen. Se felloggen för mer information.'));
167+
process.exitCode = 1;
168+
return;
169+
}
170+
82171
try {
83172
// Import and create rule instances in RAP-LP
84173
const enabledRulesAndCategorys = await importAndCreateRuleInstances(ruleCategories);
85174
// Load API specification into a Document object
86-
const apiSpecDocument = new Document(
87-
fs.readFileSync(join(apiSpecFileName), 'utf-8').trim(),
88-
Parsers.Yaml,
89-
apiSpecFileName,
90-
);
91175
try {
92176
/**
93177
* CustomSpectral
94178
*/
95179
const customSpectral = new RapLPCustomSpectral();
96180
customSpectral.setCategorys(enabledRulesAndCategorys.instanceCategoryMap);
97181
customSpectral.setRuleset(enabledRulesAndCategorys.rules);
182+
//Use previous parseResult
183+
const parser: IParser<any> = (parseResult.format === 'json' ? Parsers.Json : Parsers.Yaml) as unknown as IParser<any>;
184+
apiSpecDocument = new SpectralDocument(parseResult.raw, parser, apiSpecFileName);
185+
186+
// Run ruleengine
98187
const result = await customSpectral.run(apiSpecDocument);
99188

100189
const customDiagnostic = new RapLPDiagnostic();
@@ -127,20 +216,12 @@ try {
127216
}
128217
};
129218
const formatLintingResult = (result: any) => {
130-
return `allvarlighetsgrad: ${colorizeSeverity(result.allvarlighetsgrad)} \nid: ${result.id} \nkrav: ${
131-
result.krav
132-
} \nområde: ${result.område} \nsökväg:[${result.sökväg}] \nomfattning:${JSON.stringify(
133-
result.omfattning,
134-
null,
135-
2,
136-
)} `;
219+
return `allvarlighetsgrad: ${colorizeSeverity(result.allvarlighetsgrad)} \nid: ${result.id} \nkrav: ${result.krav} \nområde: ${result.område} \nsökväg:[${result.sökväg}] \nomfattning:${JSON.stringify(result.omfattning, null, 2)} `;
137220
};
138221
//Check specified option from yargs input
139222

140223
const currentDate = new Date(); //.toISOString(); // Get current date and time in ISO format
141-
const formattedDate = `${currentDate.getFullYear()}-${padZero(currentDate.getMonth() + 1)}-${padZero(
142-
currentDate.getDate(),
143-
)} ${padZero(currentDate.getHours())}:${padZero(currentDate.getMinutes())}:${padZero(currentDate.getSeconds())}`;
224+
const formattedDate = `${currentDate.getFullYear()}-${padZero(currentDate.getMonth() + 1)}-${padZero(currentDate.getDate())} ${padZero(currentDate.getHours())}:${padZero(currentDate.getMinutes())}:${padZero(currentDate.getSeconds())}`;
144225

145226
function padZero(num: number): string {
146227
return num < 10 ? `0${num}` : `${num}`;
@@ -261,13 +342,23 @@ try {
261342
'Ett fel uppstod vid inläsning av moduler och skapande av regelklasser! Undersök felloggen för RAP-LP för mer information om felet',
262343
),
263344
);
345+
}
346+
} catch (error: any) {
347+
logErrorToFile(error);
348+
console.error(
349+
chalk.red('Ett oväntat fel uppstod! Undersök felloggen för RAP-LP för mer information om felet', error.message),
350+
);
351+
process.exitCode = 1;
264352
}
265-
} catch (error: any) {
266-
logErrorToFile(error);
267-
console.error(
268-
chalk.red('Ett oväntat fel uppstod! Undersök felloggen för RAP-LP för mer information om felet', error.message),
269-
);
270353
}
354+
// Kör main och fånga oväntade promise-rejections
355+
main().catch((err) => {
356+
logErrorToFile(err);
357+
console.error(chalk.red('Oväntat fel i main:'), err);
358+
process.exitCode = 1;
359+
});
360+
361+
271362
function logErrorToFile(error: any) {
272363
const errorMessage = `${new Date().toISOString()} - ${error.stack}\n`;
273364
fs.appendFileSync('rap-lp-error.log', errorMessage);
@@ -282,3 +373,4 @@ function logErrorToFile(error: any) {
282373
});
283374
}
284375
}
376+

src/util/RapLPCustomSpectral.ts

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,10 @@
33
// SPDX-License-Identifier: EUPL-1.2
44

55
import * as SpectralCore from '@stoplight/spectral-core';
6-
import { ruleExecutionStatus, RuleExecutionLog, ruleExecutionLogDictionary } from './RuleExecutionStatusModule.js';
76
import { ISpectralDiagnostic } from '@stoplight/spectral-core';
87
import spectralCore from '@stoplight/spectral-core';
98
const { Spectral, Document } = spectralCore;
10-
import { RapLPCustomSpectralDiagnostic } from './RapLPCustomSpectralDiagnostic.js';
9+
import { RapLPCustomSpectralDiagnostic} from './RapLPCustomSpectralDiagnostic.js';
1110

1211
class RapLPCustomSpectral {
1312
private spectral: SpectralCore.Spectral;
@@ -30,9 +29,11 @@ class RapLPCustomSpectral {
3029
}
3130
async run(document: any): Promise<RapLPCustomSpectralDiagnostic[]> {
3231
const spectralResults = await this.spectral.run(document);
33-
const modifiedResults = this.modifyResults(spectralResults);
3432
return this.modifyResults(spectralResults);
3533
}
34+
async runSemanticValidation(document: any): Promise<ISpectralDiagnostic[]> {
35+
return await this.spectral.run(document);
36+
}
3637

3738
private modifyRuleset(enabledRules: EnabledRules): Record<string, any> {
3839
return enabledRules.rules;
@@ -65,8 +66,8 @@ class RapLPCustomSpectral {
6566
private mapResultToCustom(result: ISpectralDiagnostic): RapLPCustomSpectralDiagnostic {
6667
// Map properties from result ISpectralDiagnostic to CustomSpectralDiagnostic
6768
const { message, code, severity, path, source, range, ...rest } = result;
68-
6969
// Map severity to corresponding string value for allvarlighetsgrad
70+
console.log("Code is:" + code);
7071
let allvarlighetsgrad: string;
7172
switch (severity) {
7273
case 0:
@@ -93,6 +94,7 @@ class RapLPCustomSpectral {
9394
omfattning: range,
9495
};
9596
}
97+
9698
}
9799
export { RapLPCustomSpectral };
98100
/**
@@ -102,3 +104,5 @@ export { RapLPCustomSpectral };
102104
interface EnabledRules {
103105
rules: Record<string, any>;
104106
}
107+
108+

src/util/RapLPCustomSpectralDiagnostic.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,3 +17,6 @@ export interface RapLPCustomSpectralDiagnostic
1717
sökväg?: any;
1818
omfattning?: any;
1919
}
20+
21+
22+

0 commit comments

Comments
 (0)