diff --git a/lib/djv.js b/lib/djv.js index ac7b68f..e2a5687 100644 --- a/lib/djv.js +++ b/lib/djv.js @@ -1,4 +1,4 @@ -const { head } = require('./utils'); +const { head } = require('./utils/uri'); const { restore } = require('./utils/template'); const formats = require('./utils/formats'); const { generate, State } = require('./utils/state'); diff --git a/lib/utils/environment.js b/lib/utils/environment.js index 11dc6c0..f382aa0 100644 --- a/lib/utils/environment.js +++ b/lib/utils/environment.js @@ -7,7 +7,8 @@ const properties = require('./properties'); const keywords = require('./keywords'); const validators = require('../validators'); const formats = require('./formats'); -const { keys } = require('./'); +const { keys } = require('./uri'); +const { transformation } = require('./schema'); const contains = require('../validators/contains'); const constant = require('../validators/const'); @@ -15,7 +16,6 @@ const propertyNames = require('../validators/propertyNames'); const environmentConfig = { // TODO remove side effects - // TODO make draft-06 default 'draft-06': () => { Object.assign(properties, { exclusiveMinimum(schema) { @@ -66,7 +66,14 @@ const environmentConfig = { 'uri-template': '!/^(?:(?:[^\\x00-\\x20"\'<>%\\\\^`{|}]|%[0-9a-f]{2})|\\{[+#.\\/;?&=,!@|]?(?:[a-z0-9_]|%[0-9a-f]{2})+(?:\\:[1-9][0-9]{0,3}|\\*)?(?:,(?:[a-z0-9_]|%[0-9a-f]{2})+(?:\\:[1-9][0-9]{0,3}|\\*)?)*\\})*$/i.test(%s)', }); - keys.id = '$id'; + Object.assign(keys, { + id: '$id', + }); + + Object.assign(transformation, { + ANY_SCHEMA: {}, + NOT_ANY_SCHEMA: { not: {} }, + }); }, }; diff --git a/lib/utils/index.js b/lib/utils/index.js index dca4706..0cc077f 100644 --- a/lib/utils/index.js +++ b/lib/utils/index.js @@ -1,119 +1,37 @@ /** * @module utils * @description - * Contains small utilities for djv project + * Basic utilities for djv project */ -const REGEXP_URI = /:\/\//; -const REGEXP_URI_FRAGMENT = /#\/?/; -const REGEXP_URI_PATH = /(^[^:]+:\/\/[^?#]*\/).*/; - -// TODO move to environment -const ANY_SCHEMA = {}; -const NOT_ANY_SCHEMA = { not: {} }; - -function asExpression(fn, schema, tpl) { - if (typeof fn !== 'function') { - return fn; - } - - return fn(schema, tpl); -} - /** - * @name head + * @name asExpression * @type {function} * @description - * Clean an id from its fragment - * @example - * head('http://domain.domain:2020/test/a#test') - * // returns 'http://domain.domain:2020/test/a' - * @param {string} id - * @returns {string} cleaned + * Transform function or string to expression + * @see validators + * @param {function/string} fn + * @param {object} schema + * @param {object} tpl templater instance + * @returns {string} expression */ -function head(uri) { - if (typeof uri !== 'string') { - return uri; +function asExpression(fn, schema, tpl) { + if (typeof fn !== 'function') { + return fn; } - const parts = uri.split(REGEXP_URI_FRAGMENT); - return parts[0]; -} - -function isFullUri(uri) { - return REGEXP_URI.test(uri); + return fn(schema, tpl); } /** - * @name path + * @name hasProperty * @type {function} * @description - * Gets a scheme, domain and a path part from the uri - * @example - * path('http://domain.domain:2020/test/a?test') - * // returns 'http://domain.domain:2020/test/' - * @param {string} uri - * @returns {string} path + * Check if the property exists in a given object + * @param {object} object + * @param {string} property + * @returns {boolean} exists */ -function path(uri) { - return uri.replace(REGEXP_URI_PATH, '$1'); -} - -/** - * @description - * Get the fragment (#...) part of the uri - * @see https://tools.ietf.org/html/rfc3986#section-3 - * @param {string} uri - * @returns {string} fragment - */ -function fragment(uri) { - if (typeof uri !== 'string') { - return uri; - } - - const parts = uri.split(REGEXP_URI_FRAGMENT); - return parts[1]; -} - -/** - * @name makePath - * @type function - * @description - * Concat parts into single uri - * @see https://tools.ietf.org/html/rfc3986#section-3 - * @param {array[string]} parts - * @returns {string} uri - */ -function makePath(parts) { - return parts - .filter(part => typeof part === 'string') - .reduce((uri, id) => { - // if id is full replace uri - if (!uri.length || isFullUri(id)) { - return id; - } - if (!id) { - return uri; - } - - // if fragment found - if (id.indexOf('#') === 0) { - // should replace uri's sharp with id - const sharpUriIndex = uri.indexOf('#'); - if (sharpUriIndex === -1) { - return uri + id; - } - - return uri.slice(0, sharpUriIndex) + id; - } - - // get path part of uri - // and replace the rest with id - const partialUri = path(uri) + id; - return partialUri + (partialUri.indexOf('#') === -1 ? '#' : ''); - }, ''); -} - function hasProperty(object, property) { return ( typeof object === 'object' && @@ -121,99 +39,7 @@ function hasProperty(object, property) { ); } -/** - * @name isSchema - * @type {function} - * @description - * Verify the object could be a schema - * Since draft-06 supports boolean as a schema definition - * @param {object} schema - * @returns {boolean} isSchema - */ -function isSchema(schema) { - return ( - typeof schema === 'object' || - typeof schema === 'boolean' - ); -} - -/** - * @name transformSchema - * @type {function} - * @description - * Transform a schema pseudo presentation - * Since draft-06 supports boolean as a schema definition - * @param {object} schema - * @returns {object} schema - */ -function transformSchema(schema) { - if (schema === true) { - return ANY_SCHEMA; - } else if (schema === false) { - return NOT_ANY_SCHEMA; - } - return schema; -} - -function normalize(uri) { - return decodeURIComponent(uri.replace(/~1/g, '/').replace(/~0/g, '~')); -} - -/** - * @name makeSchema - * @type {function} - * @description - * Generate a simple schema by a given object - * @param {any} instance - * @returns {object} schema - */ -function makeSchema(instance) { - if (typeof instance !== 'object' || instance === null) { - return { enum: [instance] }; - } - - if (Array.isArray(instance)) { - return { - items: instance.map(makeSchema), - // other items should be valid by `false` schema, aka not exist at all - additionalItems: false - }; - } - - const required = Object.keys(instance); - return { - properties: required.reduce((memo, key) => ( - Object.assign({}, memo, { - [key]: makeSchema(instance[key]) - }) - ), {}), - required, - // other properties should be valid by `false` schema, aka not exist at all - // additionalProperties: false, - }; -} - -/** - * @name keys - * @type {object} - * @description - * Keys to access schema attributes - */ -const keys = { - id: 'id', -}; - module.exports = { asExpression, hasProperty, - isSchema, - transformSchema, - makeSchema, - // TODO move to utils/uri - makePath, - isFullUri, - head, - fragment, - normalize, - keys, }; diff --git a/lib/utils/schema.js b/lib/utils/schema.js new file mode 100644 index 0000000..3ad8a34 --- /dev/null +++ b/lib/utils/schema.js @@ -0,0 +1,91 @@ +/** + * @module schema + * @description + * Low-level utilities to check, create and transform schemas + */ + +/** + * @name transformation + * @type {object} + * @description + * Schema values transformation + */ +const transformation = { + ANY_SCHEMA: true, + NOT_ANY_SCHEMA: false, +}; + +/** + * @name is + * @type {function} + * @description + * Verify the object could be a schema + * Since draft-06 supports boolean as a schema definition + * @param {object} schema + * @returns {boolean} isSchema + */ +function is(schema) { + return ( + typeof schema === 'object' || + typeof schema === 'boolean' + ); +} + +/** + * @name transform + * @type {function} + * @description + * Transform a schema pseudo presentation + * Since draft-06 supports boolean as a schema definition + * @param {object} schema + * @returns {object} schema + */ +function transform(schema) { + if (schema === true) { + return transformation.ANY_SCHEMA; + } else if (schema === false) { + return transformation.NOT_ANY_SCHEMA; + } + return schema; +} + +/** + * @name make + * @type {function} + * @description + * Generate a simple schema by a given object + * @param {any} instance + * @returns {object} schema + */ +function make(instance) { + if (typeof instance !== 'object' || instance === null) { + return { enum: [instance] }; + } + + if (Array.isArray(instance)) { + return { + items: instance.map(make), + // other items should be valid by `false` schema, aka not exist at all + additionalItems: false + }; + } + + const required = Object.keys(instance); + return { + properties: required.reduce((memo, key) => ( + Object.assign({}, memo, { + [key]: make(instance[key]) + }) + ), {}), + required, + // other properties should be valid by `false` schema, aka not exist at all + // additionalProperties: false, + }; +} + +module.exports = { + is, + make, + transform, + transformation, +}; diff --git a/lib/utils/state.js b/lib/utils/state.js index be4d85c..afc2870 100644 --- a/lib/utils/state.js +++ b/lib/utils/state.js @@ -1,16 +1,18 @@ const { list: validators } = require('../validators'); const { body, restore, template } = require('./template'); +const { hasProperty } = require('./'); const { normalize, makePath, head, isFullUri, - hasProperty, fragment, - transformSchema, keys, - isSchema, -} = require('./'); +} = require('./uri'); +const { + is: isSchema, + transform: transformSchema, +} = require('./schema'); function State(schema = {}, env) { Object.assign(this, { diff --git a/lib/utils/uri.js b/lib/utils/uri.js new file mode 100644 index 0000000..12b8667 --- /dev/null +++ b/lib/utils/uri.js @@ -0,0 +1,133 @@ +/** + * @module utils + * @description + * Utilities to check and normalize uri + */ +const REGEXP_URI = /:\/\//; +const REGEXP_URI_FRAGMENT = /#\/?/; +const REGEXP_URI_PATH = /(^[^:]+:\/\/[^?#]*\/).*/; + +/** + * @name keys + * @type {object} + * @description + * Keys to apply schema attributes & values + */ +const keys = { + id: 'id', +}; + +/** + * @name head + * @type {function} + * @description + * Clean an id from its fragment + * @example + * head('http://domain.domain:2020/test/a#test') + * // returns 'http://domain.domain:2020/test/a' + * @param {string} id + * @returns {string} cleaned + */ +function head(uri) { + if (typeof uri !== 'string') { + return uri; + } + + const parts = uri.split(REGEXP_URI_FRAGMENT); + return parts[0]; +} + +function isFullUri(uri) { + return REGEXP_URI.test(uri); +} + +/** + * @name path + * @type {function} + * @description + * Gets a scheme, domain and a path part from the uri + * @example + * path('http://domain.domain:2020/test/a?test') + * // returns 'http://domain.domain:2020/test/' + * @param {string} uri + * @returns {string} path + */ +function path(uri) { + return uri.replace(REGEXP_URI_PATH, '$1'); +} + +/** + * @description + * Get the fragment (#...) part of the uri + * @see https://tools.ietf.org/html/rfc3986#section-3 + * @param {string} uri + * @returns {string} fragment + */ +function fragment(uri) { + if (typeof uri !== 'string') { + return uri; + } + + const parts = uri.split(REGEXP_URI_FRAGMENT); + return parts[1]; +} + +/** + * @name makePath + * @type function + * @description + * Concat parts into single uri + * @see https://tools.ietf.org/html/rfc3986#section-3 + * @param {array[string]} parts + * @returns {string} uri + */ +function makePath(parts) { + return parts + .filter(part => typeof part === 'string') + .reduce((uri, id) => { + // if id is full replace uri + if (!uri.length || isFullUri(id)) { + return id; + } + if (!id) { + return uri; + } + + // if fragment found + if (id.indexOf('#') === 0) { + // should replace uri's sharp with id + const sharpUriIndex = uri.indexOf('#'); + if (sharpUriIndex === -1) { + return uri + id; + } + + return uri.slice(0, sharpUriIndex) + id; + } + + // get path part of uri + // and replace the rest with id + const partialUri = path(uri) + id; + return partialUri + (partialUri.indexOf('#') === -1 ? '#' : ''); + }, ''); +} + +/** + * @name normalize + * @type {function} + * @description + * Replace json-pointer special symbols in a given uri. + * @param {string} uri + * @returns {string} normalizedUri + */ +function normalize(uri) { + return decodeURIComponent(uri.replace(/~1/g, '/').replace(/~0/g, '~')); +} + +module.exports = { + makePath, + isFullUri, + head, + fragment, + normalize, + keys, +}; diff --git a/lib/validators/const.js b/lib/validators/const.js index 5ccb943..b88ee52 100644 --- a/lib/validators/const.js +++ b/lib/validators/const.js @@ -1,4 +1,5 @@ -const { hasProperty, makeSchema } = require('../utils'); +const { hasProperty } = require('../utils'); +const { make: makeSchema } = require('../utils/schema'); module.exports = function constant(schema, tpl) { if (!hasProperty(schema, 'const')) { diff --git a/lib/validators/dependencies.js b/lib/validators/dependencies.js index 5717ae0..6be670c 100644 --- a/lib/validators/dependencies.js +++ b/lib/validators/dependencies.js @@ -1,4 +1,5 @@ -const { hasProperty, isSchema } = require('../utils'); +const { hasProperty } = require('../utils'); +const { is: isSchema } = require('../utils/schema'); module.exports = function dependencies(schema, tpl) { if (!hasProperty(schema, 'dependencies')) { diff --git a/test/utils/uri.js b/test/utils/uri.js index 203fcf3..8e8b6b0 100644 --- a/test/utils/uri.js +++ b/test/utils/uri.js @@ -1,5 +1,5 @@ const assert = require('assert'); -const { makePath } = require('../../lib/utils'); +const { makePath } = require('../../lib/utils/uri'); describe('uri', () => { describe('makePath()', () => {