UNPKG

@adobe/jsonschema2md

Version:

Validate and document complex JSON Schemas the easy way.

342 lines (305 loc) 10.8 kB
/* * Copyright 2019 Adobe. 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 * * Unless required by applicable law or agreed to in writing, software distributed under * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS * OF ANY KIND, either express or implied. See the License for the specific language * governing permissions and limitations under the License. */ /* @ts-check */ import yargs from 'yargs'; import fs from 'fs-extra'; import { readdirpPromise } from 'readdirp'; import { iter, pipe, filter, map, obj, } from 'ferrum'; import nodepath from 'path'; import { i18nConfig } from 'es2015-i18n-tag'; import { fileURLToPath } from 'url'; import traverse from './traverseSchema.js'; import build from './markdownBuilder.js'; import { writereadme, writemarkdown } from './writeMarkdown.js'; import readme from './readmeBuilder.js'; import loader from './schemaProxy.js'; import writeSchema from './writeSchema.js'; const __filename = fileURLToPath(import.meta.url); const __dirname = nodepath.dirname(__filename); const { debug, info, error } = console; /** * @typedef {import("../types/api").JsonSchema} JsonSchema */ /** * @typedef {import("../types/api").SchemaList} SchemaList */ /** * @typedef {import("../types/api").SchemaContent} SchemaContent */ /** * @typedef {import("../types/api").SchemaFiles} SchemaFiles */ /** * @typedef {import("../types/api").GeneratedOutput} GeneratedOutput */ /** * Public API for jsonschema2md that can be used to turn JSON Schema files * into readable Markdown documentation. * @param {JsonSchema | SchemaFiles} schema JSON Schema input to get data from * @param {Object} options Additional options for generation * @param {string} [options.schemaPath] - (optional) Path to directory containing all JSON Schemas * or a single JSON Schema file. This will be considered as the baseURL. * @param {string} [options.outDir] - (optional) Path to output directory. Generating files will * be skipped if directory is not specified. * @param {{ [key:string]: string }} [options.metadata] - (optional) Add metadata elements to * .md files. * @param {string} [options.schemaOut] - (optional) Output JSON Schema files including * description and validated examples in the specified folder. * @param {boolean} [options.includeReadme=true] - (optional) Generate a README.md file in the * output directory. * @param {{ [key:string]: string }[]} [options.links] - (optional) Add this file as a link * explaining the specified attribute. * @param {string} [options.i18n="locales/"] - (optional) Path to a locales folder with * JSON files used for internationalization. * @param {"en_US" | "de"} [options.language="en_US"] - (optional) Selected language. * @param {"json" | "yaml"} [options.exampleFormat="json"] - (optional) How to format examples. * @param {string[]} [options.includeProperties=[]] - (optional) Name of custom properties * which should be also in the description of an element. * @param {boolean} [options.header=true] - (optional) Whether or not to include the header * in markdown. * @param {string[]} [options.skipProperties=[]] - (optional) Name of a default property to * skip in markdown. * @returns {GeneratedOutput} List of raw markdown that were generated from input schema. */ export function jsonschema2md(schema, options) { const { schemaPath, outDir, metadata, schemaOut, includeReadme, links, i18n, language, exampleFormat, includeProperties, header, skipProperties, } = options; if (!schema || typeof schema !== 'object') { throw Error('Input is not valid. Provide JSON schema either as Object or Array.'); } const locales = i18n || nodepath.resolve(__dirname, 'locales'); i18nConfig(fs.readJSONSync(nodepath.resolve(locales, `${language || 'en_US'}.json`))); let out = outDir; if (options.out) { // eslint-disable-next-line no-console console.warn("Options 'out' has been deprecated. Please, use 'outDir' instead"); out = options.out; } let meta = metadata; if (options.meta) { // eslint-disable-next-line no-console console.warn("Options 'meta' has been deprecated. Please, use 'metadata' instead"); meta = options.meta; } /** @type {SchemaFiles} */ let normalized; if (Array.isArray(schema)) { normalized = schema; } else { normalized = [{ fileName: 'definition.schema.json', content: schema, }]; } const schemaLoader = loader(); // collect data about the schemas and turn everything into a big object const schemas = pipe( normalized, // Checking if data contains the file path or its contents (JSON schema) map(({ fileName, fullPath, content }) => { if (!content && fullPath) { return schemaLoader(fullPath, fs.readJSONSync(fullPath)); } return schemaLoader(fileName, content); }), traverse, ); /** * @type {GeneratedOutput} */ const output = {}; console.log('preparing schemas...'); output.schema = writeSchema({ schemadir: schemaOut, origindir: schemaPath, })(schemas); if (includeReadme) { console.log('preparing README...'); output.readme = pipe( schemas, // build readme readme({ readme: true, }), writereadme({ out, info, error, debug, meta, }), ); } console.log('preparing documentation...'); output.markdown = pipe( schemas, // generate Markdown ASTs build({ header, links, includeProperties, exampleFormat, skipProperties, rewritelinks: (origin) => { const mddir = out; if (!mddir) { return origin; } const srcdir = schemaPath; const schemadir = schemaOut || schemaPath; const target = nodepath.relative( mddir, nodepath.resolve(schemadir, nodepath.relative(srcdir, origin)), ).split(nodepath.sep).join(nodepath.posix.sep); return target; }, }), // write to files writemarkdown({ out, info, error, debug, meta, }), ); return output; } /** * Main function used in the CLI. * @param {{ [key:string]: unknown }} args CLI arguments from user input * @returns The generated Markdown files to the specified output directory */ export async function main(args) { // parse/process command line arguments const { argv } = yargs(args) .usage('Generate Markdown documentation from JSON Schema.\n\nUsage: $0') .demand('d') .alias('d', 'input') .describe('d', 'path to directory containing all JSON Schemas to process. This will be considered as the baseURL. By default only files ending in .schema.json will be processed, unless the schema-extension is set with the -e flag.') .coerce('d', (d) => { const resolved = d && nodepath.resolve(d); if (fs.existsSync(resolved) && fs.lstatSync(d).isDirectory()) { return resolved; } throw new Error(`Input file "${d}" is not a directory!`); }) .alias('o', 'out') .describe('o', 'path to output directory') .default('o', nodepath.resolve(nodepath.join('.', 'out'))) .coerce('o', (o) => nodepath.resolve(o)) .option('m', { type: 'array', }) .alias('m', 'meta') .describe('m', 'add metadata elements to .md files Eg -m template=reference. Multiple values can be added by repeating the flag Eg: -m template=reference -m hide-nav=true') .coerce('m', (m) => m && pipe( // turn this into an object of key value pairs iter(m), map((i) => i.split('=')), obj, )) .alias('x', 'schema-out') .describe('x', 'output JSON Schema files including description and validated examples in the specified folder, or suppress with -') .default('x', nodepath.resolve(nodepath.join('.', 'out'))) .coerce('x', (x) => (x === '-' ? '-' : nodepath.resolve(x))) .alias('e', 'schema-extension') .describe('e', 'JSON Schema file extension eg. schema.json or json') .default('e', 'schema.json') .alias('n', 'no-readme') .describe('n', 'Do not generate a README.md file in the output directory') .describe('link-*', 'Add this file as a link the explain the * attribute, e.g. --link-abstract=abstract.md') .alias('i', 'i18n') .describe('i', 'path to a locales folder with JSON files') .default('i', nodepath.resolve(__dirname, 'locales')) .coerce('i', (i) => nodepath.resolve(i)) .alias('l', 'language') .describe('l', 'the selected language') .choices('l', ['en_US', 'de', 'nl_NL']) .default('l', 'en_US') .alias('f', 'example-format') .describe('f', 'how to format examples') .choices('f', ['json', 'yaml']) .default('f', 'json') .alias('p', 'properties') .array('p') .describe('p', 'name of a custom property which should be also in the description of an element (may be used multiple times)') .default('p', []) .alias('h', 'header') .boolean('h') .describe('h', 'if the value is false the header will be skipped') .default('h', true) .alias('s', 'skip') .array('s') .describe('s', 'name of a default property to skip in markdown (may be used multiple times), e.g. -s typefact -s proptable') .default('s', []); const links = pipe( iter(argv), filter(([key, _value]) => key.startsWith('link-')), map(([key, value]) => [key.substr(5), value]), obj, ); const schemaPath = argv.d; const outDir = argv.o; const metadata = argv.m; const schemaOut = argv.x !== '-' ? argv.x : null; const includeReadme = !argv.n; const i18n = argv.i; const language = argv.l; const exampleFormat = argv.f; const includeProperties = argv.p; const header = argv.h; const skipProperties = argv.s; const schemaExtension = argv.e; // list all schema files in the specified directory const schemaFiles = await readdirpPromise( schemaPath, { root: schemaPath, fileFilter: (f) => f.path.endsWith(schemaExtension) }, ); console.log(`loading ${schemaFiles.length} schemas`); /** * @type {SchemaList[]} * */ const schemas = schemaFiles.map((schema) => ({ fileName: schema.basename, fullPath: schema.fullPath, })); jsonschema2md(schemas, { schemaPath, outDir, metadata, schemaOut, includeReadme, links, i18n, language, exampleFormat, includeProperties, header, skipProperties, }); return 1; }