@adobe/jsonschema2md
Version:
Validate and document complex JSON Schemas the easy way.
322 lines (306 loc) • 10.8 kB
JavaScript
/**
* 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
*/
const i18n = require('i18n');
const writeFile = require('./writeFiles');
const prettyMarkdown = require('./prettyMarkdown');
var Promise=require('bluebird');
var path = require('path');
var _ = require('lodash');
var ejs = require('ejs');
const pejs = Promise.promisifyAll(ejs);
var validUrl = require('valid-url');
const { headers } = require('./header');
var GithubSlugger = require('github-slugger');
function createGithubSlugs(names){
var slugger = new GithubSlugger();
slugger.reset();
names = names.sort();
return names.reduce(function(result, item) {
result[item] = slugger.slug(item);
return result;
}, {});
}
function render([ template, context ]) {
return pejs.renderFileAsync(template, context, { debug: false });
}
function build(total, fragment) {
return total + fragment.replace(/\n\n/g, '\n');
}
function assoc(obj, key, value) {
if (obj==null) {
return assoc({}, key, value);
}
obj[key] = value;
return obj;
}
function flatten(dependencies) {
let deps = [];
if (dependencies) {
const key = _.keys(dependencies)[0];
deps = _.toPairs(dependencies[key]).map(([ first, second ]) => {
second.$id = first;
return second;
});
}
return deps;
}
function stringifyExamples(examples) {
if (examples) {
if (typeof examples === 'string') {
examples = [ examples ];
}
//console.log(examples);
return examples.map(example => {
return JSON.stringify(example, null, 2);
});
} else {
return false;
}
}
/**
* Finds a simple, one-line description of the property's type
* @param {object} prop - a JSON Schema property definition
*/
function simpletype(prop) {
const type = prop.type;
if (prop.$ref!==undefined) {
if (prop.$linkVal!==undefined) {
prop.simpletype = prop.$linkVal;
} else {
console.log('unresolved reference: ' + prop.$ref);
prop.simpletype = 'reference';
}
} else if (prop.enum!==undefined) {
prop.simpletype = '`enum`';
if (prop['meta:enum']===undefined) {
prop['meta:enum'] = {};
}
for (let i=0;i<prop.enum.length;i++) {
if (prop['meta:enum'][prop.enum[i]]===undefined) {
//setting an empty description for each unknown enum
prop['meta:enum'][prop.enum[i]] = '';
}
}
} else if (prop.const!==undefined) {
prop.simpletype = '`const`';
} else if (type==='string') {
prop.simpletype = '`string`';
} else if (type==='number') {
prop.simpletype = '`number`';
} else if (type==='boolean') {
prop.simpletype = '`boolean`';
} else if (type==='integer') {
prop.simpletype = '`integer`';
} else if (type==='object') {
prop.simpletype = '`object`';
} else if (type==='null') {
prop.simpletype = '`null`';
} else if (type==='array') {
if (prop.items!==undefined) {
const innertype = simpletype(prop.items);
if (innertype.simpletype==='complex') {
prop.simpletype = '`array`';
} else {
//console.log(prop.title);
prop.simpletype = innertype.simpletype.replace(/(`)$/, '[]$1');
}
} else {
prop.simpletype = '`array`';
}
} else if (Array.isArray(type)) {
function nullfilter(str) {
return str!=='null';
}
var filtered = type.filter(nullfilter);
if (type.length - 1 === filtered.length) {
prop.nullable = true;
}
if (filtered.length===1) {
prop.type = filtered[0];
prop.simpletype = '`' + filtered[0] + '`';
} else {
prop.type = filtered;
prop.simpletype = 'multiple';
}
} else {
prop.simpletype = 'complex';
}
return prop;
}
/**
* Combines the `required` array data structure with the `properties` map data
* structure, so that each property in `properties` that is required, i.e. listed
* as a value in the `required` array will have an additional property `isrequired`
* @param {*} properties
* @param {*} required
*/
function requiredProperties(properties, required) {
if (required) {
for (let i=0;i<required.length;i++) {
if (properties[required[i]]) {
properties[required[i]].isrequired = true;
}
}
}
return _.mapValues(properties, simpletype);
}
function ejsRender(template, ctx) {
let p = pejs.renderFileAsync(path.join(__dirname, '../templates/md/' + template + '.ejs'), ctx, { debug: false });
return p.value();
//return JSON.stringify(obj, null, 2);
}
const generateMarkdown = function(filename, schema, schemaPath, outDir, dependencyMap, docs, consoleArgs) {
var ctx = {
schema: schema,
_: _,
validUrl: validUrl,
dependencyMap:dependencyMap
};
outDir = outDir ? outDir : path.resolve(path.join('.', 'out'));
let i18nPath;
if (consoleArgs !== undefined && consoleArgs.i !== undefined){
i18nPath=consoleArgs.i ;
} else {
i18nPath=__dirname + '/locales';
}
console.log(filename);
i18n.configure({
// setup some locales - other locales default to en silently
locales:[ 'en' ],
// where to store json files - defaults to './locales' relative to modules directory
directory: i18nPath,
defaultLocale: 'en'
});
//console.log(dependencyMap);
// this structure allows us to have separate templates for each element. Instead of having
// one huge template, each block can be built individually
let multi = [
[ 'frontmatter.ejs', { meta: schema.metaElements } ],
[ 'header.ejs', {
i18n: i18n,
schema: schema,
dependencies: flatten(dependencyMap),
table: headers(schema, schemaPath, filename, docs, outDir).render() } ],
//[ 'divider.ejs', null ],
//[ 'topSchema.ejs', ctx ],
[ 'examples.ejs', { i18n: i18n, examples: stringifyExamples(schema.examples), title: schema.title } ]
];
const required = []; //to store required of whole schema, even those in definitions
// Processing schema.definitions before schema.properties to get any required properties present in definitions
if (_.keys(schema.definitions).length > 0) {
const abstract = {};
for (let i=0; i<_.keys(schema.definitions).length;i++) {
if (schema.definitions[_.keys(schema.definitions)[i]].properties!==undefined) {
const definition = schema.definitions[_.keys(schema.definitions)[i]].properties;
let tempRequired = schema.definitions[_.keys(schema.definitions)[i]].required;
let hasRequiredProperties = (tempRequired !== undefined) ? true : false;
for (let j=0; j<_.keys(definition).length;j++) {
const name = _.keys(definition)[j];
const property = definition[_.keys(definition)[j]];
if (hasRequiredProperties && tempRequired.includes(name)){
required.push(name);
}
//console.log('Checking ' + name + ' against ' + _.keys(schema.properties));
if (_.keys(schema.properties).indexOf(name)=== -1) {
property.definitiongroup = _.keys(schema.definitions)[i];
abstract[name] = property;
}
}
}
}
let propertiesSlugs = createGithubSlugs(_.keys(abstract));
if (_.keys(abstract).length>0) {
//console.log('I got definitions!', abstract);
multi.push([ 'definitions.ejs', {
props: requiredProperties(abstract, required),
title: schema.title,
id: schema.$id,
propertiesSlugs:propertiesSlugs,
i18n: i18n
} ]);
for (let i=0; i<_.keys(abstract).length;i++) {
const name = _.keys(abstract).sort()[i];
multi.push( [ 'property.ejs', {
name: name,
required: required.includes(name),
ejs: ejsRender,
examples: stringifyExamples(abstract[name]['examples']),
schema: simpletype(abstract[name]),
nameSlug: propertiesSlugs[name],
i18n: i18n
} ]);
}
}
}
let propertiesSlugs = createGithubSlugs(_.keys(schema.properties));
if (_.keys(schema.properties).length > 0) {
if (schema.required === undefined){schema.required = [];}
schema.required = _.union(schema.required, required);
//table of contents
multi.push([ 'properties.ejs', {
props: requiredProperties(schema.properties, schema.required),
pprops: _.mapValues(schema.patternProperties, simpletype),
title: schema.title,
additional: schema.additionalProperties,
propertiesSlugs: propertiesSlugs,
i18n: i18n
} ]);
//regular properties
for (let i=0; i<_.keys(schema.properties).length;i++) {
const name = _.keys(schema.properties).sort()[i];
multi.push( [ 'property.ejs', {
name: name,
required: schema.required ? schema.required.includes(name) : false,
examples: stringifyExamples(schema.properties[name]['examples']),
ejs: ejsRender,
schema: simpletype(schema.properties[name]),
nameSlug: propertiesSlugs[name],
i18n: i18n
} ]);
}
}
if (_.keys(schema.patternProperties).length > 0) {
//patterns properties
for (let i=0; i<_.keys(schema.patternProperties).length;i++) {
const name = _.keys(schema.patternProperties)[i];
multi.push( [ 'pattern-property.ejs', {
name: name,
examples: stringifyExamples(schema.patternProperties[name]['examples']),
ejs: ejsRender,
schema: simpletype(schema.patternProperties[name]),
i18n: i18n } ]);
}
}
// Handles join-type properties
// If the schema contains a 'oneOf', 'allOf' or 'anyOf'.
const joinTypeKey = Object.keys(schema).find(key => [ 'oneOf', 'allOf', 'anyOf' ].indexOf(key) > -1);
if (!_.isUndefined(joinTypeKey)) {
const joinType = schema[joinTypeKey];
if (joinType.length > 0) {
multi.push( [ 'join-type.ejs', {
ejs: ejsRender,
schemas: simpletype(joinType),
schema: simpletype(schema),
i18n: i18n } ]);
}
}
//find definitions that contain properties that are not part of the main schema
multi = multi.map(([ template, context ]) => {
return [
path.join(__dirname, '../templates/md/' + template),
assoc(assoc(context, '_', _), 'simpletype', simpletype)
];
});
return Promise.reduce(Promise.map(multi, render), build, '').then(str => {
const mdfile = path.basename(filename).slice(0, -5)+ '.md';
return writeFile(path.join(path.join(outDir), path.dirname(filename.substr(schemaPath.length))), mdfile, prettyMarkdown(str));
}).then(out => {
//console.log('markdown written (promise)', out);
return out;
});
};
module.exports = generateMarkdown;