From ac65ac600a0e1702a82e336916678e0faa83b0ba Mon Sep 17 00:00:00 2001 From: Lars Trieloff Date: Fri, 6 Dec 2019 09:42:13 +0000 Subject: [PATCH] feat(proxy): generate meta information --- lib/formatInfo.js | 183 ++++++++++++++++++------------------ lib/schemaProxy.js | 16 ++-- lib/symbols.js | 12 +++ lib/traverseSchema.js | 24 +++++ test/schemaProxy.test.js | 12 ++- test/traverseSchema.test.js | 86 +++++++++++++++++ 6 files changed, 234 insertions(+), 99 deletions(-) create mode 100644 lib/symbols.js create mode 100644 test/traverseSchema.test.js diff --git a/lib/formatInfo.js b/lib/formatInfo.js index 774683b9..41cf643b 100644 --- a/lib/formatInfo.js +++ b/lib/formatInfo.js @@ -17,115 +17,119 @@ const parse = require('remark-parse'); const inspect = require('unist-util-inspect'); const stringify = require('mdast-util-to-string'); const { formatname } = require('./formatname'); +const s = require('./symbols'); -function formatInfo({ extension }) { - function plaindescription(schema) { - try { - if (schema.path) { - const filename = path.resolve( - path.dirname(schema.path), - `${path.basename(schema.path, extension)}description.md`, - ); - const longdesc = fs.readFileSync(filename); - return longdesc.toString(); - } - } catch {} - return schema.schema.description || ''; - } - function shorten(str) { - return str.split('\n')[0].split('.')[0]; - } +function isabstract(schema) { + return schema.definitions !== undefined && + (!schema.properties || Object.keys(schema.properties).length === 0); +} - const parser = unified() - .use(parse); +function isextensible(schema) { + return schema.definitions !== undefined || schema['meta:extensible'] === true; +} - function parsedescription(str) { - try { - const markdown = parser.parse(str); - return { - longdescription: markdown, - shortdescription: shorten(stringify(markdown)), - description: str, - }; - } catch { - return { - longdescription: {}, - shortdescription: '', - description: shorten(str), - }; - } +function isidentifiable(schema) { + if (!schema.properties) { + return 'undefined'; } - - function isabstract(schema) { - return schema.definitions !== undefined && - (!schema.properties || Object.keys(schema.properties).length === 0); + if (schema.properties['@id'] && schema.properties['@id'].type === 'string' && schema.properties['@id'].format === 'uri') { + return 'true'; + } else { + return 'false'; } +} - function isextensible(schema) { - return schema.definitions !== undefined || schema['meta:extensible'] === true; - } +function iscustom(schema) { + return [...(schema.allOf || [])] + .filter(e => typeof e === 'object') + .filter(e => typeof e.$ref === 'string') + .filter(({ $ref }) => $ref === 'https://ns.adobe.com/xdm/common/extensible.schema.json#/definitions/@context') + .length > 0; +} - function isidentifiable(schema) { - if (!schema.properties) { - return 'undefined'; +function getdefined(schema) { + if (schema[s.parent]) { + return { + text: path.basename(schema[s.filename]) + "*", + link: schema[s.filename] } - if (schema.properties['@id'] && schema.properties['@id'].type === 'string' && schema.properties['@id'].format === 'uri') { - return 'true'; - } else { - return 'false'; + } + if (schema[s.filename]) { + return { + text: path.basename(schema[s.filename]), + link: schema[s.filename] } } + return undefined; +} - function iscustom(schema) { - return [...(schema.allOf || [])] - .filter(e => typeof e === 'object') - .filter(e => typeof e.$ref === 'string') - .filter(({ $ref }) => $ref === 'https://ns.adobe.com/xdm/common/extensible.schema.json#/definitions/@context') - .length > 0; +function gettype(schema) { + if (typeof schema.type === 'string') { + return schema.type; } - - function getdefined(schema) { - if (schema.rootpath) { - return { - text: path.basename(schema.rootpath) + "*", - link: schema.rootpath - } - } - if (schema.path) { - return { - text: path.basename(schema.path), - link: schema.path - } - } - return undefined; + if (Array.isArray(schema.type)) { + return 'multiple'; } + if (isabstract(schema)) { + return gettype(schema.definitions); + } + return undefined; +} - function gettype(schema) { - if (typeof schema.type === 'string') { - return schema.type; - } - if (Array.isArray(schema.type)) { - return 'multiple'; - } - if (isabstract(schema)) { - return gettype(schema.definitions); +function plaindescription(schema) { + try { + if (schema[s.filename] && !schema[s.parent]) { + const filename = path.resolve( + path.dirname(schema[s.filename]), + schema[s.filename].replace(/\..*$/, '.description.md') + ); + const longdesc = fs.readFileSync(filename); + return longdesc.toString(); } - return undefined; - } + } catch {} + return schema.description || ''; +} + +function shorten(str) { + return str.split('\n')[0].split('.')[0]; +} - function formatmeta(schema) { +const parser = unified() + .use(parse); + +function parsedescription(str) { + try { + const markdown = parser.parse(str); + return { + longdescription: markdown, + shortdescription: shorten(stringify(markdown)), + description: str, + }; + } catch { return { - type: gettype(schema.schema), - abstract: isabstract(schema.schema), - extensible: isextensible(schema.schema), - status: schema.schema['meta:status'] || undefined, - identifiable: isidentifiable(schema.schema), - custom: iscustom(schema.schema), - additional: schema.schema.additionalProperties !== false, - definedin: getdefined(schema) + longdescription: {}, + shortdescription: '', + description: shorten(str), }; } +} + +function formatmeta(schema) { + return { + type: gettype(schema), + abstract: isabstract(schema), + extensible: isextensible(schema), + status: schema['meta:status'] || undefined, + identifiable: isidentifiable(schema), + custom: iscustom(schema), + additional: schema.additionalProperties !== false, + definedin: getdefined(schema), + ...parsedescription(plaindescription(schema)) + }; +} + +function formatInfo({ extension }) { return schemas => map(schemas, (schema) => { const newobj = { @@ -139,3 +143,4 @@ function formatInfo({ extension }) { } module.exports = formatInfo; +module.exports.formatmeta = formatmeta; diff --git a/lib/schemaProxy.js b/lib/schemaProxy.js index aad374ef..8fe23550 100644 --- a/lib/schemaProxy.js +++ b/lib/schemaProxy.js @@ -10,15 +10,8 @@ * governing permissions and limitations under the License. */ const ghslugger = require('github-slugger'); - -const symbols = { - pointer: Symbol('pointer'), - filename: Symbol('filename'), - id: Symbol('id'), - titles: Symbol('titles'), - resolve: Symbol('resolve'), - slug: Symbol('slug'), -}; +const formatmeta = require('./formatInfo').formatmeta; +const symbols = require('./symbols'); const myslug = Symbol('myslug'); @@ -27,6 +20,7 @@ const handler = ({ }) => { const meta = {}; + meta[symbols.parent] = () => parent; meta[symbols.pointer] = () => root; meta[symbols.filename] = () => filename; meta[symbols.id] = (target) => { @@ -74,6 +68,10 @@ const handler = ({ return receiver[myslug]; }; + meta[symbols.meta] = (target, prop, receiver) => { + return formatmeta(receiver); + } + return { ownKeys: target => Reflect.ownKeys(target), diff --git a/lib/symbols.js b/lib/symbols.js new file mode 100644 index 00000000..405178d2 --- /dev/null +++ b/lib/symbols.js @@ -0,0 +1,12 @@ +const symbols = { + pointer: Symbol('pointer'), + filename: Symbol('filename'), + id: Symbol('id'), + titles: Symbol('titles'), + resolve: Symbol('resolve'), + slug: Symbol('slug'), + meta: Symbol('meta'), + parent: Symbol('parent'), +}; + +module.exports = symbols; \ No newline at end of file diff --git a/lib/traverseSchema.js b/lib/traverseSchema.js index db1ddc38..41d26ad4 100644 --- a/lib/traverseSchema.js +++ b/lib/traverseSchema.js @@ -138,4 +138,28 @@ function traverse(node) { return allitems; } +function sflat(arr) { + return arr.reduce((p, v) => { + if (Array.isArray(v)) { + return [...p, ...v]; + } + return [...p, v]; + }, []); +} + +/** + * Traverses a (proxied) JSON schema. This is a less opinionated + * and tighter traversal. + * @param {*} schema + */ +function traverseSchema(schema) { + if (Array.isArray(schema)) { + return sflat(schema.map(traverseSchema)); + } else if (typeof schema === 'object') { + return sflat([schema, ...Object.values(schema).map(traverseSchema)]); + } + return []; +} + module.exports = traverse; +module.exports.traverseSchema = traverseSchema; diff --git a/test/schemaProxy.test.js b/test/schemaProxy.test.js index baeef083..81b86d36 100644 --- a/test/schemaProxy.test.js +++ b/test/schemaProxy.test.js @@ -12,8 +12,9 @@ /* eslint-env mocha */ const assert = require('assert'); +const path = require('path'); const { - loader, pointer, filename, id, titles, resolve, slug, + loader, pointer, filename, id, titles, resolve, slug, meta } = require('../lib/schemaProxy'); const example = { @@ -168,4 +169,13 @@ describe('Testing Schema Proxy', () => { assert.equal(proxied2[slug], 'referencing'); // make sure the slug stays stable assert.equal(proxied3[slug], 'anotherreference'); }); + + it('Schema proxy loads actual schemas with meta information', () => { + const myloader = loader(); + + const examplefile = path.resolve(__dirname, '..', 'examples', 'schemas', 'definitions.schema.json'); + const example = myloader(require(examplefile), examplefile); + + assert.equal(example[meta].shortdescription, 'This is an example of using a definitions object within a schema'); + }); }); diff --git a/test/traverseSchema.test.js b/test/traverseSchema.test.js new file mode 100644 index 00000000..cf9c5a0e --- /dev/null +++ b/test/traverseSchema.test.js @@ -0,0 +1,86 @@ +/* + * 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. + */ +/* eslint-env mocha */ + +const assert = require('assert'); +const { + loader, pointer, filename, id, titles, resolve, slug, +} = require('../lib/schemaProxy'); + +const { traverseSchema } = require('../lib/traverseSchema'); + +const example = { + 'meta:license': [ + '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', + ], + $schema: 'http://json-schema.org/draft-06/schema#', + $id: 'https://example.com/schemas/example', + title: 'Example', + type: 'object', + description: + 'This is an example schema with examples. Too many examples? There can never be too many examples!', + properties: { + foo: { + type: 'string', + description: 'A simple string.', + examples: ['bar'], + version: '1.0.0', + testProperty: 'test', + }, + bar: { + type: 'string', + description: 'A simple string.', + examples: ['bar', 'baz'], + version: '1.0.0', + testProperty: 'test', + }, + zip: { + type: 'object', + title: 'An object', + }, + zup: { + type: 'object', + title: 'An object', + }, + baz: { + anyOf: [ + { $ref: '#/properties/foo' }, + { $ref: '#/properties/bar' }, + ], + }, + }, +}; + +const referencing = { + $schema: 'http://json-schema.org/draft-06/schema#', + $id: 'https://example.com/schemas/referencing', + title: 'Referencing', + properties: { + $ref: 'https://example.com/schemas/example#/properties', + zap: { + type: 'boolean', + }, + }, +}; + +describe('Testing Schema Traversal', () => { + it('Schema Traversal generates a list', () => { + const proxied = loader()(example, 'example.schema.json'); + const schemas = traverseSchema(proxied); + + assert.equal(schemas.length, 9); + assert.equal(schemas[8][filename], 'example.schema.json'); + }); +});