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
1313 *
1414 **************************************************************/
1515import yargs from 'yargs' ;
16- import * as fs from 'node:fs' ;
1716import * as path from 'node:path' ;
18- import { join } from 'path ' ;
17+ import * as fs from 'node:fs ' ;
1918import Parsers from '@stoplight/spectral-parsers' ;
20- import spectralCore from '@stoplight/spectral-core' ;
2119import { importAndCreateRuleInstances , getRuleModules } from './util/ruleUtil.js' ; // Import the helper function
2220import util from 'util' ;
2321import { RapLPCustomSpectral } from './util/RapLPCustomSpectral.js' ;
2422import { DiagnosticReport , RapLPDiagnostic } from './util/RapLPDiagnostic.js' ;
2523import { AggregateError } from './util/RapLPCustomErrorInfo.js' ;
2624import chalk from 'chalk' ;
2725import { 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
2932declare var AggregateError : {
3033 prototype : AggregateError ;
3134 new ( errors : any [ ] , message ?: string ) : AggregateError ;
3235} ;
33- const { Spectral, Document } = spectralCore ;
3436const writeFileAsync = util . promisify ( fs . writeFile ) ;
3537const 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+
271362function 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+
0 commit comments