1 | #!/usr/bin/env node
|
2 |
|
3 |
|
4 | 'use strict';
|
5 |
|
6 | const fs = require('fs');
|
7 | const path = require('path');
|
8 | const readfiles = require('node-readfiles');
|
9 | const should = require('should/as-function');
|
10 | const yaml = require('yaml');
|
11 |
|
12 | const validator = require('oas-validator');
|
13 | const common = require('oas-kit-common');
|
14 | const clone = require('reftools/lib/clone.js').circularClone;
|
15 | const reref = require('reftools/lib/reref.js').reref;
|
16 |
|
17 | const swagger2openapi = require('./index.js');
|
18 |
|
19 | let globalExpectFailure = false;
|
20 |
|
21 | const baseName = path.basename(process.argv[1]);
|
22 |
|
23 | const yargs = require('yargs');
|
24 | let 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 |
|
85 | let pass = 0;
|
86 | let fail = 0;
|
87 | let failures = [];
|
88 | let warnings = [];
|
89 |
|
90 | let genStack = [];
|
91 |
|
92 | let options = argv;
|
93 | options.patch = !argv.nopatch;
|
94 | options.fatal = true;
|
95 | if (options.verbose) Error.stackTraceLimit = Infinity;
|
96 |
|
97 | function 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 |
|
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 |
|
153 | function handleResult(err, options) {
|
154 | let result = false;
|
155 | if (err) {
|
156 | options = err.options || { file: 'unknown', src: { info: { version: '', title: '' } } };
|
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);
|
171 |
|
172 | should(resultStr).not.be.exactly('{}','Result should not be empty');
|
173 |
|
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 |
|
205 | function genStackNext() {
|
206 | if (!genStack.length) return false;
|
207 | let gen = genStack.shift();
|
208 | gen.next();
|
209 | return true;
|
210 | }
|
211 |
|
212 | function* 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;
|
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 |
|
311 | function 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 |
|
346 | process.exitCode = 1;
|
347 | console.warn('Gathering...');
|
348 | for (let pathspec of argv._) {
|
349 | processPathSpec(pathspec, false);
|
350 | }
|
351 | if (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 |
|
358 | process.on('unhandledRejection', r => console.warn('UPR',r));
|
359 |
|
360 | process.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 | });
|