UNPKG

12.8 kBJavaScriptView Raw
1#!/usr/bin/env node
2
3// @ts-check
4'use strict';
5
6const fs = require('fs');
7const path = require('path');
8const readfiles = require('node-readfiles');
9const should = require('should/as-function');
10const yaml = require('yaml');
11
12const validator = require('oas-validator');
13const common = require('oas-kit-common');
14const clone = require('reftools/lib/clone.js').circularClone;
15const reref = require('reftools/lib/reref.js').reref;
16
17const swagger2openapi = require('./index.js');
18
19let globalExpectFailure = false;
20
21const baseName = path.basename(process.argv[1]);
22
23const yargs = require('yargs');
24let argv = yargs
25 .usage(baseName+' [options] {path-to-docs}...')
26 .boolean('anchors')
27 .describe('anchors','allow use of YAML anchors/aliases')
28 .string('encoding')
29 .alias('e', 'encoding')
30 .default('encoding', 'utf8')
31 .describe('encoding', 'encoding for input/output files')
32 .string('fail')
33 .describe('fail', 'path to docs expected to fail')
34 .alias('f', 'fail')
35 .string('jsonschema')
36 .alias('j', 'jsonschema')
37 .describe('jsonschema', 'path to alternative JSON schema')
38 .boolean('laxurls')
39 .alias('l', 'laxurls')
40 .describe('laxurls', 'lax checking of empty urls')
41 .boolean('mediatype')
42 .alias('m','mediatype')
43 .describe('mediatype','check media-types against RFC pattern')
44 .boolean('lint')
45 .describe('lint','lint the definition')
46 .boolean('nopatch')
47 .alias('n', 'nopatch')
48 .describe('nopatch', 'do not patch minor errors in the source definition')
49 .string('output')
50 .alias('o', 'output')
51 .describe('output', 'output conversion result')
52 .boolean('prettify')
53 .alias('p','prettify')
54 .describe('prettify','pretty schema validation errors')
55 .boolean('quiet')
56 .alias('q', 'quiet')
57 .describe('quiet', 'do not show test passes on console, for CI')
58 .boolean('resolve')
59 .alias('r', 'resolve')
60 .describe('resolve', 'resolve external references')
61 .boolean('stop')
62 .alias('s', 'stop')
63 .describe('stop', 'stop on first error')
64 .string('validateSchema')
65 .describe('validateSchema','Run schema validation step: first, last* or never')
66 .count('verbose')
67 .alias('v', 'verbose')
68 .describe('verbose', 'increase verbosity')
69 .boolean('warnOnly')
70 .describe('warnOnly','Do not throw on non-patchable errors')
71 .boolean('whatwg')
72 .alias('w', 'whatwg')
73 .describe('whatwg', 'enable WHATWG URL parsing')
74 .boolean('yaml')
75 .default('yaml', true)
76 .alias('y', 'yaml')
77 .describe('yaml', 'skip YAML-safe test')
78 .help('h')
79 .alias('h', 'help')
80 .strict()
81 .demand(1)
82 .version()
83 .argv;
84
85let pass = 0;
86let fail = 0;
87let failures = [];
88let warnings = [];
89
90let genStack = [];
91
92let options = argv;
93options.patch = !argv.nopatch;
94options.fatal = true;
95if (options.verbose) Error.stackTraceLimit = Infinity;
96
97function finalise(err, options) {
98 if (!argv.quiet || err) {
99 console.warn(common.colour.normal + options.file);
100 }
101 if (err) {
102 console.warn(common.colour.red + options.context.pop() + '\n' + err.message);
103 if (err.name.indexOf('ERR_INVALID_URL')>=0) {
104 // nop
105 }
106 else if (err.message.indexOf('schema validation')>=0) {
107 if (options.validateSchema !== 'first') {
108 warnings.push('Schema fallback '+options.file);
109 }
110 }
111 else if (err.stack && err.name !== 'AssertionError' && err.name !== 'CLIError') {
112 console.warn(err.stack);
113 warnings.push(err.name+' '+options.file);
114 }
115 if (options.lintRule && options.lintRule.description !== err.message) {
116 console.warn(options.lintRule.description);
117 }
118 options.valid = (!!options.expectFailure || options.allowFailure);
119 }
120 if (options.warnings) {
121 for (let warning of options.warnings) {
122 warnings.push(options.file + ' ' + warning.message);
123 }
124 }
125
126 let src = options.original;
127 let result = options.valid;
128
129 if (!argv.quiet) {
130 let colour = ((options.expectFailure ? !result : result) ? common.colour.green : common.colour.red);
131 if (src && src.info) {
132 console.warn(colour + ' %s %s', src.info.title, src.info.version);
133 if (src["x-testcase"]) console.warn(' ',src["x-testcase"]);
134 console.warn(' %s', src.swagger ? (src.host ? src.host : 'relative') : (src.servers && src.servers.length ? src.servers[0].url : 'relative'),common.colour.normal);
135 }
136 }
137 if (result) {
138 pass++;
139 if ((options.file.indexOf('swagger.yaml') >= 0) && argv.output) {
140 let outFile = options.file.replace('swagger.yaml', argv.output);
141 let resultStr = yaml.stringify(options.openapi);
142 fs.writeFileSync(outFile, resultStr, argv.encoding);
143 }
144 }
145 else {
146 fail++;
147 if (options.file != 'unknown') failures.push(options.file);
148 if (argv.stop) process.exit(1);
149 }
150 genStackNext();
151}
152
153function handleResult(err, options) {
154 let result = false;
155 if (err) {
156 options = err.options || { file: 'unknown', src: { info: { version: '', title: '' } } }; // src is just enough to provide dummy outputs
157 options.context = [];
158 options.warnings = [];
159 options.expectFailure = globalExpectFailure;
160 finalise(err,options);
161 }
162 else {
163 result = options.openapi;
164 }
165 let resultStr = yaml.stringify(result);
166
167 if (typeof result !== 'boolean') try {
168 if (!options.yaml) {
169 try {
170 resultStr = yaml.stringify(result); // should be representable safely in yaml
171 //let resultStr2 = yaml.stringify(result); // FIXME dropped 'noRefs:true' here
172 should(resultStr).not.be.exactly('{}','Result should not be empty');
173 //should(resultStr).equal(resultStr2,'Result should have no object identity ref_s');
174 }
175 catch (ex) {
176 if (options.debug) {
177 fs.writeFileSync('./debug.yaml',resultStr,'utf8');
178 console.warn('Result dumped to debug.yaml fixed.yaml');
179 let fix = reref(result);
180 fs.writeFileSync('./fixed.yaml',yaml.stringify(fix),'utf8');
181 }
182 should.fail(false,true,'Result cannot be represented safely in YAML');
183 }
184 }
185
186 validator.validate(result, options)
187 .then(function(options){
188 finalise(null,options);
189 })
190 .catch(function(ex){
191 finalise(ex,options);
192 });
193 }
194 catch (ex) {
195 console.warn(common.colour.normal + options.file);
196 console.warn(common.colour.red + (options.context.length ? options.context.pop() : 'No context')+ '\n' + ex.message);
197 if (ex.stack && ex.name !== 'AssertionError' && ex.name !== 'CLIError') {
198 console.warn(ex.stack);
199 }
200 options.valid = !options.expectFailure;
201 finalise(ex, options);
202 }
203}
204
205function genStackNext() {
206 if (!genStack.length) return false;
207 let gen = genStack.shift();
208 gen.next();
209 return true;
210}
211
212function* check(file, force, expectFailure) {
213 let result = false;
214 options.context = [];
215 options.expectFailure = expectFailure;
216 options.file = file;
217 let components = file.split(path.sep);
218 let name = components[components.length - 1];
219 let src;
220
221 if ((name.indexOf('.yaml') >= 0) || (name.indexOf('.yml') >= 0) || (name.indexOf('.json') >= 0) || force) {
222
223 let srcStr;
224 if (!file.startsWith('http')) {
225 srcStr = fs.readFileSync(path.resolve(file), options.encoding);
226 try {
227 src = JSON.parse(srcStr);
228 }
229 catch (ex) {
230 try {
231 src = yaml.parse(srcStr, { schema: 'core', prettyErrors: true });
232 }
233 catch (ex) {
234 let warning = 'Could not parse file ' + file + '\n' + ex.message;
235 console.warn(common.colour.red + warning);
236 if (ex.stack && ex.message.indexOf('stack')>=0) {
237 console.warn(ex.stack);
238 }
239 warnings.push(warning);
240 }
241 }
242
243 if (!src || ((!src.swagger && !src.openapi))) {
244 genStackNext();
245 return true;
246 }
247 }
248
249 options.original = src;
250 options.source = file;
251 options.text = srcStr;
252 options.expectFailure = false;
253 options.allowFailure = false;
254
255 if ((options.source.indexOf('!')>=0) && (options.source.indexOf('swagger.')>=0)) {
256 expectFailure = true;
257 options.expectFailure = true;
258 options.allowFailure = true;
259 }
260 if ((options.source.indexOf('!')>=0) && (options.source.indexOf('openapi.')>=0)) {
261 expectFailure = true;
262 options.expectFailure = false; // because some things are corrected
263 options.allowFailure = true;
264 }
265
266 if (file.startsWith('http')) {
267 swagger2openapi.convertUrl(file, clone(options))
268 .then(function(options){
269 handleResult(null,options);
270 })
271 .catch(function(ex){
272 console.warn(common.colour.red+ex,common.colour.normal);
273 if (expectFailure) {
274 warnings.push('Converter failed ' + options.source);
275 }
276 else {
277 failures.push('Converter failed ' + options.source);
278 fail++;
279 }
280 genStackNext();
281 result = false;
282 });
283 }
284 else {
285 swagger2openapi.convertObj(src, clone(options))
286 .then(function(options){
287 handleResult(null,options);
288 })
289 .catch(function(ex){
290 console.warn(common.colour.red+ex,common.colour.normal);
291 console.warn(ex.stack);
292 if (expectFailure) {
293 warnings.push('Converter failed ' + options.source);
294 }
295 else {
296 failures.push('Converter failed ' + options.source);
297 fail++;
298 }
299 genStackNext();
300 result = false;
301 });
302 }
303 }
304 else {
305 genStackNext();
306 result = true;
307 }
308 return result;
309}
310
311function processPathSpec(pathspec, expectFailure) {
312 globalExpectFailure = expectFailure;
313 if (pathspec.startsWith('@')) {
314 pathspec = pathspec.substr(1, pathspec.length - 1);
315 let list = fs.readFileSync(pathspec, 'utf8').split('\r').join('').split('\n');
316 for (let file of list) {
317 genStack.push(check(file, false, expectFailure));
318 }
319 genStackNext();
320 }
321 else if (pathspec.startsWith('http')) {
322 genStack.push(check(pathspec, true, expectFailure));
323 genStackNext();
324 }
325 else if (fs.statSync(path.resolve(pathspec)).isFile()) {
326 genStack.push(check(pathspec, true, expectFailure));
327 genStackNext();
328 }
329 else {
330 readfiles(pathspec, { readContents: false, filenameFormat: readfiles.FULL_PATH }, function (err) {
331 if (err) console.warn(yaml.stringify(err));
332 })
333 .then(files => {
334 files = files.sort();
335 for (let file of files) {
336 genStack.push(check(file, false, expectFailure));
337 }
338 genStackNext();
339 })
340 .catch(err => {
341 handleResult(err,options);
342 });
343 }
344}
345
346process.exitCode = 1;
347console.warn('Gathering...');
348for (let pathspec of argv._) {
349 processPathSpec(pathspec, false);
350}
351if (argv.fail) {
352 if (!Array.isArray(argv.fail)) argv.fail = [argv.fail];
353 for (let pathspec of argv.fail) {
354 processPathSpec(pathspec, true);
355 }
356}
357
358process.on('unhandledRejection', r => console.warn('UPR',r));
359
360process.on('exit', function () {
361 if (warnings.length) {
362 warnings.sort();
363 console.warn(common.colour.normal + '\nWarnings:' + common.colour.yellow);
364 for (let w in warnings) {
365 console.warn(warnings[w]);
366 }
367 }
368 if (failures.length) {
369 failures.sort();
370 console.warn(common.colour.normal + '\nFailures:' + common.colour.red);
371 for (let f in failures) {
372 console.warn(failures[f]);
373 }
374 }
375 console.warn(common.colour.normal);
376 console.warn('Tests: %s passing, %s failing, %s warnings', pass, fail, warnings.length);
377 process.exitCode = ((fail === 0 || options.fail) && (pass > 0)) ? 0 : 1;
378});