From 46530f3c4ea0acd08f087b12ccb7be31e128bfcc Mon Sep 17 00:00:00 2001 From: ppoffice Date: Wed, 25 Dec 2019 00:35:34 -0500 Subject: [PATCH] refactor(schema): add schema default value gen --- include/generator/category.js | 2 +- include/helper/page.js | 3 + include/schema/common/comment.json | 4 +- include/schema/common/head.json | 2 +- include/schema/common/navbar.json | 2 +- include/schema/common/providers.json | 2 +- include/schema/common/search.json | 6 +- include/schema/common/share.json | 6 +- include/schema/common/widgets.json | 19 ++- include/util/schema.js | 222 +++++++++++++++++++++++++++ layout/common/widgets.jsx | 6 +- layout/widget/profile.jsx | 2 +- package.json | 4 +- 13 files changed, 258 insertions(+), 22 deletions(-) create mode 100644 include/util/schema.js diff --git a/include/generator/category.js b/include/generator/category.js index a705ef7..d047cdd 100644 --- a/include/generator/category.js +++ b/include/generator/category.js @@ -9,7 +9,7 @@ module.exports = function(hexo) { function findParent(category) { let parents = []; - if (category && 'parent' in category) { + if (typeof category === 'object' && 'parent' in category) { const parent = locals.categories.filter(cat => cat._id === category.parent).first(); parents = [parent].concat(findParent(parent)); } diff --git a/include/helper/page.js b/include/helper/page.js index 2e117ef..17a0996 100644 --- a/include/helper/page.js +++ b/include/helper/page.js @@ -18,6 +18,9 @@ module.exports = function(hexo) { hexo.extend.helper.register('has_thumbnail', function(post) { const { article } = this.config; + if (typeof post !== 'object') { + return false; + } if (article && article.thumbnail === false) { return false; } diff --git a/include/schema/common/comment.json b/include/schema/common/comment.json index bed0f9f..d01e8da 100644 --- a/include/schema/common/comment.json +++ b/include/schema/common/comment.json @@ -5,10 +5,10 @@ "type": "object", "oneOf": [ { - "$ref": "/comment/changyan.json" + "$ref": "/comment/disqus.json" }, { - "$ref": "/comment/disqus.json" + "$ref": "/comment/changyan.json" }, { "$ref": "/comment/disqusjs.json" diff --git a/include/schema/common/head.json b/include/schema/common/head.json index 3f6d9e6..5d961a9 100644 --- a/include/schema/common/head.json +++ b/include/schema/common/head.json @@ -1,6 +1,6 @@ { "$schema": "http://json-schema.org/draft-07/schema#", - "$id": "/common/footer.json", + "$id": "/common/head.json", "description": "Page metadata configurations", "type": "object", "properties": { diff --git a/include/schema/common/navbar.json b/include/schema/common/navbar.json index c37430b..08db6ad 100644 --- a/include/schema/common/navbar.json +++ b/include/schema/common/navbar.json @@ -1,6 +1,6 @@ { "$schema": "http://json-schema.org/draft-07/schema#", - "$id": "/common/footer.json", + "$id": "/common/navbar.json", "description": "Page top navigation bar configurations", "type": "object", "properties": { diff --git a/include/schema/common/providers.json b/include/schema/common/providers.json index 6b3ee76..f8191cc 100644 --- a/include/schema/common/providers.json +++ b/include/schema/common/providers.json @@ -18,6 +18,6 @@ "type": "string", "description": "Name or URL of the webfont Icon CDN provider", "default": "fontawesome" - }, + } } } \ No newline at end of file diff --git a/include/schema/common/search.json b/include/schema/common/search.json index b619b96..72df43e 100644 --- a/include/schema/common/search.json +++ b/include/schema/common/search.json @@ -4,14 +4,14 @@ "description": "Search plugin configurations", "type": "object", "oneOf": [ + { + "$ref": "/search/insight.json" + }, { "$ref": "/search/baidu.json" }, { "$ref": "/search/google_cse.json" - }, - { - "$ref": "/search/insight.json" } ] } \ No newline at end of file diff --git a/include/schema/common/share.json b/include/schema/common/share.json index 7c4952d..b9314fb 100644 --- a/include/schema/common/share.json +++ b/include/schema/common/share.json @@ -4,6 +4,9 @@ "description": "Share plugin configurations", "type": "object", "oneOf": [ + { + "$ref": "/share/sharethis.json" + }, { "$ref": "/share/addthis.json" }, @@ -15,9 +18,6 @@ }, { "$ref": "/share/sharejs.json" - }, - { - "$ref": "/share/sharethis.json" } ] } \ No newline at end of file diff --git a/include/schema/common/widgets.json b/include/schema/common/widgets.json index 46638fc..f89c3f2 100644 --- a/include/schema/common/widgets.json +++ b/include/schema/common/widgets.json @@ -14,19 +14,28 @@ }, "oneOf": [ { - "$ref": "/widget/alipay.json" + "$ref": "/widget/profile.json" }, { - "$ref": "/widget/buymeacoffee.json" + "$ref": "/widget/toc.json" }, { - "$ref": "/widget/patreon.json" + "$ref": "/widget/links.json" }, { - "$ref": "/widget/paypal.json" + "$ref": "/widget/categories.json" }, { - "$ref": "/widget/wechat.json" + "$ref": "/widget/recent_posts.json" + }, + { + "$ref": "/widget/archives.json" + }, + { + "$ref": "/widget/tags.json" + }, + { + "$ref": "/widget/subscribe_email.json" } ], "required": [ diff --git a/include/util/schema.js b/include/util/schema.js new file mode 100644 index 0000000..1641cb2 --- /dev/null +++ b/include/util/schema.js @@ -0,0 +1,222 @@ +const Ajv = require('ajv'); +const path = require('path'); +const deepmerge = require('deepmerge'); + +const PRIMITIVE_DEFAULTS = { + 'null': null, + 'boolean': false, + 'number': 0, + 'integer': 0, + 'string': '', + 'array': [], + 'object': {} +}; + +class DefaultValue { + constructor(value, description) { + this.value = value; + this.description = description; + } + + merge(source) { + if ('description' in source && source.description) { + this.description = source.description; + } + if ('value' in source && source.value) { + this.value = deepmerge(this.value, source.value); + } + return this; + } + + toString() { + return '[DefaultValue]' + JSON.stringify(value); + } +} + +class Schema { + constructor(loader, def) { + if (!(loader instanceof SchemaLoader)) { + throw new Error('loader must be an instance of SchemaLoader'); + } + if (typeof def !== 'object') { + throw new Error('schema definition must be an object'); + } + this.loader = loader; + this.def = def; + this.compiledSchema = null; + } + + validate(obj) { + if (!this.compiledSchema) { + this.compiledSchema = this.loader.compileValidator(this.def.$id); + } + return this.compiledSchema(obj) ? true : this.compiledSchema.errors; + } + + getArrayDefaultValue(def) { + let value; + if ('items' in def && typeof def.items === 'object') { + const items = Object.assign({}, def.items); + delete items.oneOf; + value = this.getDefaultValue(items); + } + if ('oneOf' in def.items && Array.isArray(def.items.oneOf)) { + value = def.items.oneOf.map(one => { + if (!value) { + return this.getDefaultValue(one); + } + return new DefaultValue(value.value, value.description) + .merge(this.getDefaultValue(one)); + }); + } + if (!Array.isArray(value)) { + value = [value]; + } + return new DefaultValue(value, def.description); + } + + getObjectDefaultValue(def) { + let value = {}; + if ('properties' in def && typeof def.properties === 'object') { + for (let property in def.properties) { + value[property] = this.getDefaultValue(def.properties[property]); + } + } + if ('oneOf' in def && Array.isArray(def.oneOf) && def.oneOf.length) { + value = deepmerge(value, this.getDefaultValue(def.oneOf[0])); + } + return new DefaultValue(value, def.description); + } + + getTypedDefaultValue(def) { + let defaultValue; + const type = Array.isArray(def.type) ? def.type[0] : def.type; + if (type === 'array') { + defaultValue = this.getArrayDefaultValue(def); + } else if (type === 'object') { + defaultValue = this.getObjectDefaultValue(def); + } else if (type in PRIMITIVE_DEFAULTS) { + defaultValue = new DefaultValue(PRIMITIVE_DEFAULTS[type], def.description); + } else { + throw new Error(`Cannot get default value for type ${type}`) + } + // referred default value always get overwritten by its parent default value + if ('$ref' in def && def.$ref) { + defaultValue = this.getReferredDefaultValue(def).merge(defaultValue); + } + return defaultValue; + } + + getReferredDefaultValue(def) { + const schema = this.loader.getSchema(def.$ref); + if (!schema) { + throw new Error(`Schema ${def.$ref} is not loaded`); + } + return this.getDefaultValue(schema.def).merge({ description: def.description }); + } + + getDefaultValue(def = null) { + if (!def) { + def = this.def; + } + if ('const' in def) { + return new DefaultValue(def['const'], def.description); + } + if ('default' in def) { + return new DefaultValue(def['default'], def.description); + } + if ('examples' in def && Array.isArray(def.examples) && def.examples.length) { + return new DefaultValue(def.examples[0], def.description); + } + if ('type' in def && def.type) { + return this.getTypedDefaultValue(def); + } + // $ref only schemas + if ('$ref' in def && def.$ref) { + return this.getReferredDefaultValue(def); + } + } +} + +class SchemaLoader { + constructor() { + this.schemas = {}; + this.ajv = new Ajv(); + } + + getSchema($id) { + return this.schemas[$id]; + } + + addSchema(def) { + if (!Object.prototype.hasOwnProperty.call(def, '$id')) { + throw new Error('The schema definition does not have an $id field'); + } + this.ajv.addSchema(def); + this.schemas[def['$id']] = new Schema(this, def); + } + + removeSchema($id) { + this.ajv.removeSchema(def); + delete this.schemas[$id]; + } + + compileValidator($id) { + return this.ajv.compile(this.schemas[$id].def); + } +} + +function traverseObj(obj, targetKey, handler) { + if (Array.isArray(obj)) { + for (const child of obj) { + traverseObj(child, targetKey, handler); + } + } else if (typeof obj === 'object') { + for (const key in obj) { + if (key === targetKey) { + handler(obj[key]); + } else { + traverseObj(obj[key], targetKey, handler); + } + } + } +} + +SchemaLoader.load = (rootSchemaDef, resolveDirs = []) => { + if (!Array.isArray(resolveDirs)) { + resolveDirs = [resolveDirs]; + } + + const loader = new SchemaLoader(); + loader.addSchema(rootSchemaDef); + + function handler($ref) { + if (typeof $ref !== 'string') { + throw new Error('Invalid schema reference id: ' + JSON.stringify($ref)); + } + if (loader.getSchema($ref)) { + return; + } + for (const dir of resolveDirs) { + let def; + try { + def = require(path.join(dir, $ref)); + } catch (e) { + continue; + } + if (typeof def !== 'object' || def['$id'] !== $ref) { + continue; + } + loader.addSchema(def); + traverseObj(def, '$ref', handler); + return; + } + throw new Error('Cannot find schema definition ' + $ref + '.\n' + + 'Please check if the file exists and its $id is correct'); + } + + traverseObj(rootSchemaDef, '$ref', handler); + return loader; +} + +module.exports = SchemaLoader; \ No newline at end of file diff --git a/layout/common/widgets.jsx b/layout/common/widgets.jsx index 053b8ad..8487f17 100644 --- a/layout/common/widgets.jsx +++ b/layout/common/widgets.jsx @@ -5,7 +5,7 @@ const classname = require('../util/classname'); function formatWidgets(widgets) { const result = {}; if (Array.isArray(widgets)) { - widgets.forEach(widget => { + widgets.filter(widget => typeof widget === 'object').forEach(widget => { if ('position' in widget && (widget.position === 'left' || widget.position === 'right')) { if (!(widget.position in result)) { result[widget.position] = [widget]; @@ -52,7 +52,9 @@ function getColumnOrderClass(position) { } function isColumnSticky(config, position) { - return config.sidebar && position in config.sidebar && config.sidebar[position].sticky === true; + return typeof config.sidebar === 'object' + && position in config.sidebar + && config.sidebar[position].sticky === true; } class Widgets extends Component { diff --git a/layout/widget/profile.jsx b/layout/widget/profile.jsx index 699f560..32c52c7 100644 --- a/layout/widget/profile.jsx +++ b/layout/widget/profile.jsx @@ -8,7 +8,7 @@ class Profile extends Component { return null; } return
- {links.map(link => { + {links.filter(link => typeof link === 'object').map(link => { return {'icon' in link ? : link.name} diff --git a/package.json b/package.json index 9d85541..4b76a08 100644 --- a/package.json +++ b/package.json @@ -32,7 +32,7 @@ "inferno-create-element": "^7.3.3", "moment": "^2.22.2", "ajv": "^6.10.2", - "glob": "^7.1.4", - "js-yaml": "^3.13.1" + "js-yaml": "^3.13.1", + "deepmerge": "^4.2.2" } } \ No newline at end of file