UNPKG

@adobe/jsonschema2md

Version:

Validate and document complex JSON Schemas the easy way.

440 lines (393 loc) 14.3 kB
/** * Copyright 2017 Adobe Systems Incorporated. All rights reserved. * This file is licensed to you under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. You may obtain a copy * of the License at http://www.apache.org/licenses/LICENSE-2.0 */ var path = require('path'); var _ = require('lodash'); var logger = require('winston'); var readdirp = require('readdirp'); var Promise=require('bluebird'); var fs = Promise.promisifyAll(require('fs')); const markdownWriter=require('./markdownWriter'); const schemaWriter=require('./schemaWriter'); const readmeWriter=require('./readmeWriter'); var deff='#/definitions/'; var absUrlRegex = new RegExp('^(?:[a-z]+:)?//', 'i'); var pointer = require('json-pointer'); var smap; //TODO remove global var sPath; var wmap={}; function get$refType(refValue){ var startpart = '', endpart = '', refType = ''; var arr = refValue.split('#'); if (arr.length > 1) {endpart=arr[1];} startpart=arr[0]; //TODO yRelNoDef //relative-- yRelWithDef, yRelNoDef, //absolute-- yAbsWithDef, yAbsFSchema, yAbsWithFragment var refType=''; var deff='/definitions/'; //if( absUrlRegex.test(refVal) ){ if (startpart.length > 1){ if (startpart in smap){ if (endpart.startsWith(deff)){ refType = 'yAbsWithDef'; } else { if (endpart.length === 0) { refType = 'yAbsFSchema'; } else { refType = 'yAbsWithFragment'; } } } } else { if (endpart.startsWith(deff)){ refType = 'yRelWithDef'; } } // } return { startpart, endpart, refType }; } function normaliseLinks(obj, refArr){ let basepath = refArr.startpart ; let $linkVal = '', $linkPath = ''; if (basepath in smap){ let newpath = path.relative(path.dirname(sPath), smap[basepath].filePath).replace(/\\/g, '/'); //to cater windows paths let temp = newpath.slice(0, -5).split('/'); $linkVal = obj['title'] ? obj['title'] : path.basename(newpath).slice(0, -5); $linkPath = temp.join('/')+'.md'; return { $linkVal, $linkPath }; } } var resolve$ref = Promise.method(function(val, base$id){ let obj, link; if (!(base$id in wmap) ) {wmap[base$id] = {};} let refArr = get$refType(val['$ref']); if (refArr.refType === 'yRelWithDef'){ refArr.startpart = base$id; } if (smap[refArr.startpart]){ obj=smap[refArr.startpart].jsonSchema; if (refArr.refType !== 'yRelWithDef'){ link = normaliseLinks(obj, refArr); if (!wmap[base$id][refArr.startpart]){ wmap[base$id][refArr.startpart]=link; } } if (refArr.refType === 'yAbsFSchema'){ val.$linkVal = link.$linkVal; val.$linkPath = link.$linkPath; return val; } if (pointer.has(obj, refArr.endpart)){ var ischema = _.cloneDeep(pointer.get(obj, refArr.endpart)); _.forOwn(val, (v, k) => { if (k !== '$ref'){ ischema[k]=v; } }); return processISchema(ischema, refArr.startpart); } } }); var processFurther = Promise.method(function(val, key, $id){ let base$id =$id; if (val['$ref']){ return resolve$ref(val, base$id); } else { if (val['items'] && val['type'] === 'array'){ if (val['items']['$ref']){ resolve$ref(val['items']).then(s => { _.forOwn(s, (v, k) => { if (k !== '$ref'){ val['items'][k]=v; } }); }); } } //TODO if any other keyword return val; } }); function processISchema() {}; // define w/ function so it gets hoisted and we avoid eslint errors about what is defined first: processISchema or resolve$ref. Both rely on each other! processISchema = Promise.method(function(schema, base$id){ if (!(base$id in wmap) ) {wmap[base$id] = {};} if (schema['anyOf'] || schema['oneOf']){ // var $definitions=[] schema['type'] = schema['anyOf'] ? 'anyOf' : 'oneOf'; let arr = schema['anyOf']? schema['anyOf'] : schema['oneOf']; _.each(arr, function(value, index) { if (value['$ref']){ resolve$ref(value, base$id).then(piSchema => { delete arr[index]; arr[index]=piSchema; }); } else { processISchema(value, base$id).then(piSchema => { delete arr[index]; arr[index]=piSchema; }); } }); // schema["$definitions"] = $definitions; return schema; } if (schema['items'] ){ let val=schema['items']; if (!schema['type']) {schema['type'] = 'array';} if (_.isArray(val)){ //TODO } else { if (val['$ref']){ resolve$ref(val, base$id).then(piSchema => {//check // not sending correct id schema['items']=piSchema; }); } else { //TODO if such a scenario } } } // schema.$definitions = $definitions return schema; }); function processSchema(schema){ return new Promise((resolve, reject) => { if (!schema.properties) {schema.properties={};} var $id = schema['$id'] || schema['id']; var base$id = $id; if (!(base$id in wmap)) {wmap[base$id] = {};} if (schema['allOf']){ _.each(schema['allOf'], function(value) { if (value['$ref']){ let obj, link; var refArr = get$refType(value['$ref']); if (refArr.refType === 'yRelWithDef'){ refArr.startpart = base$id; } if (smap[refArr.startpart]){ obj=smap[refArr.startpart].jsonSchema; if (refArr.refType !== 'yRelWithDef'){ link=normaliseLinks(obj, refArr); if (!wmap[base$id][refArr.startpart]){ wmap[base$id][refArr.startpart]=link; } } if (pointer.has(obj, refArr.endpart)){ var ischema = _.cloneDeep(pointer.get(obj, refArr.endpart)); if (refArr.refType === 'yAbsFSchema'){ processSchema(ischema).then(psSchema => { if ( psSchema['properties'] ){ _.forOwn(psSchema['properties'], (val, key) => { processFurther(val, key, refArr.startpart).then(pfSchema => { if (pfSchema){ schema.properties[key] = pfSchema; schema.properties[key].$oSchema={}; schema.properties[key].$oSchema.$linkVal=link.$linkVal; schema.properties[key].$oSchema.$linkPath=link.$linkPath; if (pfSchema['required']){ if (key in pfSchema['required']){ schema.required.push(key); } } } }); }); } }); } else { if ( ischema['properties'] ){ _.forOwn(ischema['properties'], (val, key) => { processFurther(val, key, refArr.startpart).then(pfSchema => { if (pfSchema){ schema.properties[key] = pfSchema; if (refArr.refType === 'yAbsWithDef'){ schema.properties[key].$oSchema={}; schema.properties[key].$oSchema.$linkVal=link.$linkVal; schema.properties[key].$oSchema.$linkPath=link.$linkPath; } if (ischema['required']){ if (key in ischema['required']) {schema.required.push(key);} } } else { reject('No further schema found'); } }); }); } } } } } else { _.forOwn(value, function(val, key){ schema[key]=val; // }); // TODO add properties if there // behaviour to be decided } }); resolve(schema); } else if (schema['properties']){ _.forOwn(schema['properties'], (val, key) => { processFurther(val, key, base$id).then(pfSchema => { if (pfSchema){ schema.properties[key] = pfSchema; if (pfSchema['required']){ if (key in pfSchema['required']){ schema.required.push(key); } } } }); }); //TODO check if something missing left here resolve(schema); } }); //generic $ref resolve present in top properties } var Schema=function(ajv, schemaMap){ this._ajv = ajv; this._schemaPathMap=schemaMap; }; Schema.resolveRef=function(key, obj, currpath){ if (key === '$ref'){ var refVal = obj[key]; var temp; if ( absUrlRegex.test(refVal) ){ let parsedUrl = refVal.split('#'); let basepath = parsedUrl[0] ; if (basepath in this._schemaPathMap){ let newpath = path.relative(path.dirname(currpath), this._schemaPathMap[basepath].filePath).replace(/\\/g, '/'); //to cater windows paths obj['$ref'] = newpath; temp = newpath.slice(0, -5).split('/'); obj.$linkVal = path.basename(newpath).slice(0, -5); obj.$linkPath = temp.join('/')+'.md'; //TODO display with title or file path name title } else { obj.$linkPath = refVal; temp = refVal.split('/'); obj.$linkVal = temp.pop() || temp.pop(); } } else if (refVal.startsWith(deff)) { obj.$linkVal = refVal.slice(deff.length); obj.$linkPath = '#'+obj.$linkVal.replace(/ /g, '-'); } else if (refVal.endsWith('json')){ temp = refVal.slice(0, -5).split('/'); obj.$linkVal = temp[temp.length - 1]; obj.$linkPath = temp.join('/')+'.md'; } } if (key === 'anyOf' || key === 'oneOf' || key === 'allOf') {obj.$type=key;} return; }; /* The following function does not seem to be used anymore! var traverseSchema = function(object,schemaFilePath){ return new Promise((resolve,reject) => { var recurse=function(curr,key,prev){ if (key){ if (key === 'anyOf' || key === 'oneOf' || key === 'allOf') {prev.$type=key;} } var result; if (Array.isArray(curr)) {curr.map((item,index) => recurse(item,index,curr));} else { (typeof curr === 'object') ? Object.keys(curr).map(key => recurse(curr[key],key,curr)):Schema.resolveRef(key,prev,schemaFilePath); } return object; }; resolve(recurse(object)); }); }; */ Schema.getExamples = function(filePath, schema){ var exampleFileNames=[]; var examples=[]; var dirname=path.dirname(filePath); var filename=path.basename(filePath, path.extname(filePath)); filename=filename.split('.')[0]+'.example.*.json'; return new Promise((resolve, reject) => { readdirp({ root: dirname, fileFilter: filename }) .on('data', entry => exampleFileNames.push(entry.fullPath)) .on('end', () => resolve(exampleFileNames)) .on('error', err => reject(err)); }).then(exampleFileNames => { if (exampleFileNames.length > 0){ var validate=this._ajv.compile(schema); return Promise.map(exampleFileNames, entry => { return fs.readFileAsync(entry).then(example => { var data = JSON.parse(example.toString()); var valid = validate(data); if (valid) {examples.push(data);} else {logger.error(entry+' is an invalid Example');} }); }).then(() => {schema.examples=examples; return schema; } ); } else {return schema;} }); }; Schema.getDescription = function(filePath, schema){ var temp=path.basename(filePath, path.extname(filePath)); //TODO should err be thrown here? temp=temp.split('.')[0]+'.description.md'; return fs.readFileAsync(path.resolve(path.dirname(filePath), temp), 'utf8') .then(description => { schema.description=description; return schema; }) .catch(() => { return schema; }); }; Schema.setAjv=function(ajv){ this._ajv=ajv; }; Schema.setSchemaPathMap=function(schemaMap){ this._schemaPathMap=schemaMap; }; /** * Loads a schema file for processing into a given target directory * @param {*} schemaMap * @param {*} schemaPath * @param {string} docDir - where documentation will be generated * @param {string} schemaDir - where schemas will be generated, if null, `docDir` will be used * @param {map} metaElements - a map of additional YAML frontmatter to be added to the generated Markdown * @param {boolean} readme - generate a README.md directory listing */ Schema.process = function(schemaMap, schemaPath, docDir, schemaDir, metaElements, readme) { schemaDir = schemaDir ? schemaDir : docDir; smap=schemaMap; let keys = Object.keys(schemaMap); return Promise.mapSeries(keys, schemaKey => { var props = Object.keys(wmap); for (var i = 0; i < props.length; i++) { delete wmap[props[i]]; } let schema = schemaMap[schemaKey].jsonSchema; sPath = schemaMap[schemaKey].filePath; return Schema.getExamples(schemaMap[schemaKey].filePath, schema) .then(egsSchema => Schema.getDescription(schemaMap[schemaKey].filePath, egsSchema)) .then(allSchema => { var schemaClone = _.cloneDeep(allSchema); // return Promise.props({ // wSchema:schemaClone, // mSchema:traverseSchema(allSchema,schemaMap[schemaKey].filePath) // }) return processSchema(schemaClone).then(mSchema => { mSchema.metaElements=metaElements; return { mSchema:mSchema, wSchema:allSchema, dep:wmap }; }); }).then(object => { return Promise.all([ markdownWriter(schemaMap[schemaKey].filePath, object.mSchema, schemaPath, docDir, object.dep), schemaWriter(schemaMap[schemaKey].filePath, object.wSchema, schemaPath, schemaDir) ]); }); }).then(result => { if (readme) { console.log('Output processed. Trying to make a README.md now'); const markdowns = result.map(r => { return r[0];}); return readmeWriter(markdowns, schemaMap, docDir, schemaPath); } else { console.log('Output processed.'); } }); }; module.exports = Schema;