@adobe/jsonschema2md
Version:
Validate and document complex JSON Schemas the easy way.
223 lines (197 loc) • 7.41 kB
JavaScript
/*
* 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.
*/
import GhSlugger from 'github-slugger';
import path from 'path';
import fs from 'fs-extra';
import { URL } from 'url';
import formatmeta from './formatInfo.js';
import symbols from './symbols.js';
import { keyword } from './keywords.js';
const myslug = Symbol('myslug');
/**
* @param {string} file
* @param {number} [num=1]
*/
function loadExamples(file, num = 1) {
const examplefile = path.resolve(path.dirname(file), path.basename(file).replace(/\..*$/, `.example.${num}.json`));
try {
const example = fs.readJSONSync(examplefile);
return [example, ...loadExamples(file, num + 1)];
} catch {
return [];
}
}
const handler = ({
root = '', fullpath = null, filename = '.', schemas, subschemas, parent = null, slugger,
}) => {
const meta = {};
meta[symbols.parent] = () => parent;
meta[symbols.pointer] = () => root;
meta[symbols.filename] = () => filename;
meta[symbols.fullpath] = () => fullpath;
meta[symbols.id] = (target) => {
// if the schema has it's own ID, use it
if (target[keyword`$id`]) {
return target[keyword`$id`];
}
if (parent) {
// if we can determine the parent ID (by walking up the tree, use it)
return parent[symbols.id];
}
return undefined;
};
meta[symbols.titles] = (target) => {
if (parent) {
// if we can determine the parent titles
// then we append our own
return [...parent[symbols.titles], target.title];
}
// otherwise, it's just our own
if (typeof target.title === 'string') {
return [target[keyword`title`]];
}
return [];
};
meta[symbols.resolve] = (target, prop, receiver) => (proppath) => {
// console.log('trying to resolve', proppath, 'in', receiver[symbols.fullpath]);
if (proppath === undefined) {
return receiver;
}
const [head, ...tail] = typeof proppath === 'string' ? proppath.split('/') : proppath;
if ((head === '' || head === undefined) && tail.length === 0) {
return receiver;
} else if (head === '' || head === undefined) {
return receiver[symbols.resolve](tail);
}
return receiver[head] ? receiver[head][symbols.resolve](tail) : undefined;
};
meta[symbols.slug] = (target, prop, receiver) => {
if (!receiver[myslug] && !parent && receiver[symbols.filename]) {
// eslint-disable-next-line no-param-reassign
receiver[myslug] = slugger.slug(receiver[symbols.filename].replace(/\..*$/, ''));
}
if (!receiver[myslug]) {
const parentslug = parent[symbols.slug];
const { title } = receiver;
const name = receiver[symbols.pointer].split('/').pop();
if (typeof title === 'string') {
// eslint-disable-next-line no-param-reassign
receiver[myslug] = slugger.slug(`${parentslug}-${title || name}`);
} else {
// eslint-disable-next-line no-param-reassign
receiver[myslug] = slugger.slug(`${parentslug}-${name}`);
}
}
return receiver[myslug];
};
meta[symbols.meta] = (target, prop, receiver) => formatmeta(receiver);
return {
ownKeys: (target) => Reflect.ownKeys(target),
get: (target, prop, receiver) => {
if (typeof meta[prop] === 'function') {
return meta[prop](target, prop, receiver);
}
const retval = Reflect.get(target, prop, receiver);
if (retval === undefined && receiver[symbols.fullpath] && prop === keyword`examples` && !receiver[symbols.parent]) {
return loadExamples(receiver[symbols.fullpath], 1);
}
if (typeof retval === 'object' && retval !== null) {
if (retval[keyword`$ref`]) {
const [uri, pointer] = retval.$ref.split('#');
// console.log('resolving ref', uri, pointer, 'from', receiver[symbols.fullpath]);
const basedoc = uri || receiver[symbols.id];
let referenced = null;
// $ref is a URI-reference so basedoc might be an id URI or it might be a path
if (schemas.known[basedoc]) {
referenced = schemas.known[basedoc][symbols.resolve](pointer);
} else if (path.parse(basedoc)) {
const basepath = path.dirname(meta[symbols.fullpath]());
let reldoc = uri;
// if uri is a URI then only try to resolve it locally if the scheme is 'file:'
try {
const urlinfo = new URL(uri);
if (urlinfo.protocol === 'file:') {
reldoc = uri.replace(/^file:\/\//, '');
} else {
reldoc = null;
}
} catch (err) {
// console.log('Error parsing URL - ' + uri);
}
if (reldoc) {
const refpath = path.resolve(basepath, reldoc);
if (schemas.files[refpath]) {
referenced = schemas.files[refpath][symbols.resolve](pointer);
}
}
}
if (referenced !== null) {
// inject the referenced schema into the current schema
Object.assign(retval, referenced);
} else {
console.error('cannot resolve', basedoc);
}
} else if (retval[symbols.filename]) {
// console.log('I am in a loop!');
return retval;
}
// console.log('making new proxy from', target, prop, 'receiver', receiver[symbols.id]);
let subschema;
if (subschemas.has(retval)) {
subschema = subschemas.get(retval);
} else {
subschema = new Proxy(retval, handler({
root: `${root}/${prop}`,
parent: receiver,
fullpath,
filename,
schemas,
subschemas,
slugger,
}));
subschemas.set(retval, subschema);
}
if (subschema[keyword`$id`]) {
// stow away the schema for lookup
// eslint-disable-next-line no-param-reassign
schemas.known[subschema[keyword`$id`]] = subschema;
}
return subschema;
}
return retval;
},
};
};
export default function loader() {
const schemas = {
loaded: [],
known: {},
files: {},
};
const subschemas = new Map();
const slugger = new GhSlugger();
return (/** @type {string} */ name, /** @type {any} */ schema) => {
// console.log('loading', name);
const filename = path.basename(name);
const fullpath = name === filename ? undefined : name;
const proxied = new Proxy(schema, handler({
filename, fullpath, schemas, subschemas, slugger,
}));
schemas.loaded.push(proxied);
if (proxied[keyword`$id`]) {
// stow away the schema for lookup
schemas.known[proxied[keyword`$id`]] = proxied;
}
schemas.files[fullpath || filename] = proxied;
return proxied;
};
}