UNPKG

microschema

Version:

Helper library to create JSON Schemas in a concise way.

369 lines (318 loc) 9.13 kB
'use strict' // Symbols const isRequired = Symbol('required') const chained = Symbol('chained') // Helper library to create JsonSchemas // in a concise way. // // Example: // // microschema.strictObj({ // identity_id: 'string:required', // client_id: 'number', // redirect_uri: 'string:uri', // scope: 'string', // ip_address: 'string', // children: microschema.arrayOf(microschema.strictObj({ // scope: 'string' // })) // }) module.exports = { // Chaining Properties // ------------------- /** * Sets a property as required * @example * ms.obj({id: ms.required.string()}) * -> {type: 'object', properties: {id: {type: 'string'}}, required: ['id']} */ get required () { const self = chain(this) self[isRequired] = true return self }, // Methods // ------- /** * Creates an object schema. * @param {Object} microschema * @param {Object} opts * @param {string} opts.title * @param {string} opts.description * @param {string} opts.dependencies * @param {*} opts.default * @param {string[]} opts.required */ obj (microschema = {}, opts = {}) { const jsonSchema = { type: 'object', properties: {} } if (opts.title) jsonSchema.title = opts.title if (opts.description) jsonSchema.description = opts.description if (opts.strict) jsonSchema.additionalProperties = false if (opts.dependencies) setDependencies(jsonSchema, opts.dependencies) if (opts.default !== undefined) jsonSchema.default = opts.default if (opts.required) { if (!Array.isArray(opts.required)) throw new Error("'required' must be an array") jsonSchema.required = opts.required } for (const propertyName in microschema) { const typeDesc = microschema[propertyName] let propertySchema if (isString(typeDesc)) { propertySchema = parseTypeDescription(jsonSchema, propertyName, typeDesc) } else { propertySchema = typeDesc if (typeDesc[isRequired]) { jsonSchema.required = jsonSchema.required || [] jsonSchema.required.push(propertyName) } } jsonSchema.properties[propertyName] = propertySchema } return decorate(this, jsonSchema) }, /** * Creates an object schema with additionalProperties: false * @param {Object} microschema * @param {Object} opts * @param {?string} opts.title * @param {?string} opts.description * @param {?string} opts.dependencies * @param {*} opts.default * @param {string[]} opts.required */ strictObj (microschema = {}, opts = {}) { opts.strict = true return this.obj(microschema, opts) }, /** * Pass in all possible values as separate parameters. * All values must be of the same type. * * @example microschema.enum('error', 'warn', 'info', 'debug', 'trace') */ enum (...enums) { if (Array.isArray(enums[0])) enums = enums[0] return decorate(this, { type: getJsonType(enums[0]), enum: enums }) }, /** * Declares a const schema */ const (value) { return decorate(this, { const: value }) }, /** * @param schemaOrType * Pass in either a string or an object: * 1. {String} A json schema type. E.g. 'string' * Example: microschema.arrayOf('string') * 2. {Object} JSON Schema * Example: microschema.arrayOf({type: 'object', properties: {...}}) * @param {Object} opts * @param {?number} opts.minItems * @param {?number} opts.maxItems * @param {?boolean} opts.uniqueItems * @param {?string[]} opts.uniqueItemProperties */ arrayOf (schemaOrType, opts) { const itemSchema = strToSchema(schemaOrType) const s = decorate(this, { type: 'array', items: itemSchema }) const {minItems, maxItems, uniqueItems, uniqueItemProperties} = opts || {} if (minItems !== undefined) s.minItems = minItems if (maxItems !== undefined) s.maxItems = maxItems if (uniqueItems !== undefined) s.uniqueItems = uniqueItems if (uniqueItemProperties !== undefined) s.uniqueItemProperties = uniqueItemProperties return s }, /** * Declares a string schema * @param {Object} opts * @param {?RegExp|string} opts.pattern A regex instance or string to validate against * @param {?string} opts.format A json schema string format * @param {?number} opts.minLength The minimum length of the string * @param {?number} opts.maxLength The maximum length of the string */ string (opts) { const {pattern, format, minLength, maxLength} = opts || {} const s = {type: 'string'} if (pattern) { if (pattern instanceof RegExp) { if (pattern.flags) { throw new Error(`JSON schema does not support regexp flags: ${pattern}`) } s.pattern = pattern.source } else { s.pattern = pattern } } if (format) s.format = format if (minLength) s.minLength = minLength if (maxLength) s.maxLength = maxLength return decorate(this, s) }, /** * @param {Object} opts * @param {?number} opts.min * @param {?number} opts.max * @param {?boolean} opts.integer */ number (opts) { const {min, max, integer} = opts || {} const type = integer ? 'integer' : 'number' const s = {type: type} if (min != null) s.minimum = min if (max != null) s.maximum = max return decorate(this, s) }, /** * @param {Object} opts * @param {?number} opts.min * @param {?number} opts.max */ integer (opts) { return this.number({...opts, integer: true}) }, boolean () { return decorate(this, {type: 'boolean'}) }, null () { return decorate(this, {type: 'null'}) }, types (...params) { const result = {} for (let obj of params) { if (isString(obj)) { obj = parseTypeDescription({}, '', obj) } mergeTypes(obj, result) Object.assign(result, obj) } return decorate(this, result) }, definitions (obj) { const self = chain(this) self[chained].definitions = obj return self }, /** * Declares a {$ref: reference} schema. * @param {string} reference JSON Schema reference */ $ref (reference) { return decorate(this, {$ref: reference}) }, /** * Adds a schema $id attribute * @param {string} id * @example * ms.$id('user').strictObj({ * id: 'string:required', * name: 'string:required' * }) * -> { * $id: 'user', * type: 'object', * required: ['id', 'name'], * additionalProperties: false, * properties: { * id: {type: 'string'}, * name: {type: 'string'} * } * } */ $id (id) { const self = chain(this) self[chained].$id = id return self }, anyOf (...args) { const defs = Array.isArray(args[0]) ? args[0] : args return decorate(this, {anyOf: defs.map(strToSchema)}) }, oneOf (...args) { const defs = Array.isArray(args[0]) ? args[0] : args return decorate(this, {oneOf: defs.map(strToSchema)}) }, allOf (...args) { const defs = Array.isArray(args[0]) ? args[0] : args return decorate(this, {allOf: defs}) } } function decorate (self, obj) { if (self[isRequired]) obj[isRequired] = true if (self[chained]) Object.assign(obj, self[chained]) return obj } function mergeTypes (obj, other) { if (obj.type && other.type) { const first = Array.isArray(obj.type) ? obj.type : [obj.type] const second = Array.isArray(other.type) ? other.type : [other.type] second.push(...first) obj.type = second } } function parseTypeDescription (parentSchema, name, typeDesc) { const [type, ...options] = typeDesc.split(':') const propertySchema = {type: type} for (const option of options) { if (option === 'uri') { propertySchema.format = 'uri' } if (option === 'date-time') { propertySchema.format = 'date-time' } if (option === 'required') { parentSchema.required = parentSchema.required || [] parentSchema.required.push(name) } if (Math.floor(option)) { propertySchema.minLength = 1 propertySchema.maxLength = Math.floor(option) } } return propertySchema } function strToSchema (schemaOrType) { return isString(schemaOrType) ? {type: schemaOrType} : schemaOrType } function chain (self) { if (self[chained]) return self self = Object.create(self) self[chained] = {} return self } function getJsonType (obj) { if (isString(obj)) return 'string' if (Array.isArray(obj)) return 'array' if (isNumber(obj)) return 'number' if (obj == null) return 'null' return 'object' } function isString (str) { return typeof str === 'string' } function isNumber (nr) { return typeof nr === 'number' } function setDependencies (jsonSchema, dependencies) { const formattedDeps = {} for (const prop in dependencies) { const value = dependencies[prop] formattedDeps[prop] = isString(value) ? [value] : value } jsonSchema.dependencies = formattedDeps }