diff --git a/index.js b/index.js index e9563cd..37d028a 100644 --- a/index.js +++ b/index.js @@ -1,6 +1,8 @@ 'use strict' const json = require('json-pointer') +const NewBlockIndicator = 1 +let generatedDefs = [] function getDefaultPropertyType ({ propertyNameAsType, capitalizeProperty, defaultPropertyType @@ -59,15 +61,18 @@ function generate (schema, options = {}) { const config = parseOptions(options) - jsdoc.push(...writeDescription(schema, config)) - - if (json.has(schema, '/properties')) { - jsdoc.push(...processProperties(schema, schema, null, config)) - } - if (json.has(schema, '/items')) { - jsdoc.push(...processItems(schema, schema, null, config)) + let defs + if (json.has(schema, '/definitions')) { + defs = json.get(schema, '/definitions') + } else if (json.has(schema, '/$defs')) { + defs = json.get(schema, '/$defs') } + const required = json.has(schema, '/required') ? json.get(schema, '/required') : [] + + jsdoc.push(...writeDescription(schema, schema, config)) + jsdoc.push(...processObject(schema, '', '', required, schema, config, true)) + jsdoc.push(...processDefinitions(defs, schema, null, config)) return format(config.outerIndent, jsdoc) } @@ -96,20 +101,21 @@ function processItems (schema, rootSchema, base, config) { const defaultValue = item.default const optional = !schema.minItems || i >= schema.minItems if (item.type === 'array' && item.items) { - result.push(...writeProperty('array', prefixedProperty, item.description, optional, defaultValue, schema, config)) + result.push(...writeProperty('array', prefixedProperty, item.description, optional, defaultValue, schema, rootSchema, config)) result.push(...processItems(item, rootSchema, prefixedProperty, config)) } else if (item.type === 'object' && item.properties) { - result.push(...writeProperty('object', prefixedProperty, item.description, optional, defaultValue, schema, config)) + result.push(...writeProperty('object', prefixedProperty, item.description, optional, defaultValue, schema, rootSchema, config)) result.push(...processProperties(item, rootSchema, prefixedProperty, config)) } else { const type = getSchemaType(item, rootSchema) || getDefaultPropertyType(config) - result.push(...writeProperty(type, prefixedProperty, item.description, optional, defaultValue, item, config)) + result.push(...writeProperty(type, prefixedProperty, item.description, optional, defaultValue, item, rootSchema, config)) } }) return result } function processProperties (schema, rootSchema, base, config) { + if (!json.has(schema, '/properties')) return [] const props = json.get(schema, '/properties') const required = json.has(schema, '/required') ? json.get(schema, '/required') : [] const result = [] @@ -118,30 +124,126 @@ function processProperties (schema, rootSchema, base, config) { if (Array.isArray(config.ignore) && config.ignore.includes(property)) { continue } else { - const prop = props[property] const root = base ? `${base}.` : '' const prefixedProperty = root + property - const defaultValue = prop.default - const optional = !required.includes(property) - if (prop.type === 'object' && prop.properties) { - result.push(...writeProperty('object', prefixedProperty, prop.description, optional, defaultValue, schema, config)) - result.push(...processProperties(prop, rootSchema, prefixedProperty, config)) - } else if (prop.type === 'array' && prop.items) { - result.push(...writeProperty('array', prefixedProperty, prop.description, optional, defaultValue, schema, config)) - result.push(...processItems(prop, rootSchema, prefixedProperty, config)) + result.push(...processObject(props[property], property, prefixedProperty, required, rootSchema, config)) + } + } + return result +} + +function processObject (obj, objName, prefixedProperty, required, rootSchema, config, rootElement) { + const defaultValue = obj.default + const result = [] + const optional = !required.includes(objName) + + if (obj.allOf || obj.anyOf) { + const refs = [] + const refsIsA = [] + const refsAnyOf = [] + const entries = [] + const container = obj.allOf ? obj.allOf : obj.anyOf + const separator = obj.allOf ? '&' : '|' + + for (const e of container) { + if (json.has(e, '/$ref')) { + if (e.classRelation && e.classRelation === 'is-a') { + refsIsA.push(e) + } else { + refs.push(e) + } } else { - const type = getSchemaType(prop, rootSchema) || getDefaultPropertyType(config, property) - result.push(...writeProperty(type, prefixedProperty, prop.description, optional, defaultValue, prop, config)) + entries.push(e) + } + } + + if (obj.anyOf) { + let count = 0 + for (const e of refs) { + const refElement = json.get(rootSchema, e.$ref.replace(/^#/gm, '')) + if (refElement.title) { + e.title = `${obj.title}_${refElement.title}` + } else { + e.title = `${obj.title}_subtype${count}` + count++ + } + refsAnyOf.push(e) + generatedDefs.push(e) + } + for (const e of entries) { + const type = getSchemaType(e, rootSchema, config) || getDefaultPropertyType(config, objName) + if (type === 'object') { + if (e.title) { + e.title = `${obj.title}_${e.title}` + } else { + e.title = `${obj.title}_subtype${count}` + count++ + } + refsAnyOf.push(e) + generatedDefs.push(e) + } + } + } + + if (refsIsA.length === 0 && refsAnyOf.length === 0) { + if (!rootElement) result.push(...writeProperty('object', prefixedProperty, obj.description, optional, defaultValue, obj, rootSchema, config)) + } else { + const optional = !required.includes(objName) + let type = '' + for (let i = 0; i < refsIsA.length; i++) { + if (type !== '') type += separator + type += getSchemaType(refsIsA[i], rootSchema) + } + for (let i = 0; i < refsAnyOf.length; i++) { + if (type !== '') type += separator + type += refsAnyOf[i].title + } + if (type === '') type = getDefaultPropertyType(config, objName) + if (!rootElement) result.push(...writeProperty(type, prefixedProperty, obj.description, optional, defaultValue, obj, rootSchema, config)) + } + + if (obj.allOf) { + for (const e of refs) { + result.push(...processProperties(json.get(rootSchema, e.$ref.replace(/^#/gm, '')), rootSchema, prefixedProperty, config)) + } + for (const e of entries) { + result.push(...processProperties(e, rootSchema, prefixedProperty, config)) + } + } + } else { + if (obj.$ref) { + if (obj.classRelation && obj.classRelation === 'is-a') { + const type = getSchemaType(obj, rootSchema) || getDefaultPropertyType(config, objName) + if (!rootElement) result.push(...writeProperty(type, prefixedProperty, obj.description, optional, defaultValue, obj, rootSchema, config)) + } else { + if (!rootElement) result.push(...writeProperty('object', prefixedProperty, obj.description, optional, defaultValue, obj, rootSchema, config)) + result.push(...processProperties(json.get(rootSchema, obj.$ref.slice(1)), rootSchema, prefixedProperty, config)) + } + } else { + if (obj.type === 'object' && obj.properties) { + if (!rootElement) result.push(...writeProperty('object', prefixedProperty, obj.description, optional, defaultValue, obj, rootSchema, config)) + result.push(...processProperties(obj, rootSchema, prefixedProperty, config)) + } else if (obj.type === 'array' && obj.items) { + if (!rootElement) result.push(...writeProperty('array', prefixedProperty, obj.description, optional, defaultValue, obj, rootSchema, config)) + result.push(...processItems(obj, rootSchema, prefixedProperty, config)) + } else { + const type = getSchemaType(obj, rootSchema) || getDefaultPropertyType(config, objName) + if (!rootElement) result.push(...writeProperty(type, prefixedProperty, obj.description, optional, defaultValue, obj, rootSchema, config)) } } } + return result } function getSchemaType (schema, rootSchema) { if (schema.$ref) { const ref = json.get(rootSchema, schema.$ref.slice(1)) - return getSchemaType(ref, rootSchema) + if (schema.classRelation && schema.classRelation === 'is-a') { + return ref.title + } else { + return ref.type + } } if (schema.enum) { @@ -168,7 +270,7 @@ function getSchemaType (schema, rootSchema) { return schema.type } -function getType (schema, config, type) { +function getType (schema, rootSchema, config, type) { const typeCheck = type || schema.type let typeMatch if (schema.format) { @@ -193,14 +295,50 @@ function getType (schema, config, type) { typeStr = ` {${ typeMatch === '' ? '' - : typeMatch || type || getSchemaType(schema, schema) + : typeMatch || type || getSchemaType(schema, rootSchema) }}` } return typeStr } -function writeDescription (schema, config) { +function processDefinitions (defs, rootSchema, base, config) { + const result = [] + + for (const def in defs) { + result.push(...writeDefinition(defs[def], rootSchema, config)) + } + for (const def of generatedDefs) { + if (def.$ref) { + const edit = { ...json.get(rootSchema, def.$ref.replace(/^#/gm, '')) } + edit.title = def.title + result.push(...writeDefinition(edit, rootSchema, config)) + } else { + result.push(...writeDefinition(def, rootSchema, config)) + } + } + + generatedDefs = [] + + return result +} + +function writeDefinition (def, rootSchema, config) { + const result = [] + result.push(NewBlockIndicator) + result.push(...writeDescription(def, rootSchema, config)) + if (def.type === 'object') { + if (json.has(def, '/properties')) { + result.push(...processProperties(def, rootSchema, null, config)) + } + if (json.has(def, '/items')) { + result.push(...processItems(def, rootSchema, null, config)) + } + } + return result +} + +function writeDescription (schema, rootSchema, config) { const result = [] const { objectTagName = 'typedef' } = config let { description } = schema @@ -208,7 +346,7 @@ function writeDescription (schema, config) { description = config.autoDescribe ? generateDescription(schema.title, schema.type) : '' } - const type = getType(schema, config) + const type = getType(schema, rootSchema, config) if (description || config.addDescriptionLineBreak) { result.push(...wrapDescription(config, description)) @@ -220,8 +358,8 @@ function writeDescription (schema, config) { return result } -function writeProperty (type, field, description = '', optional, defaultValue, schema, config) { - const typeExpression = getType(schema, config, type) +function writeProperty (type, field, description = '', optional, defaultValue, schema, rootSchema, config) { + const typeExpression = getType(schema, rootSchema, config, type) let fieldTemplate = ' ' if (optional) { @@ -255,7 +393,13 @@ function generateDescription (title, type) { function format (outerIndent, lines) { const result = [`${outerIndent}/**`] - result.push(...lines.map(line => line ? `${outerIndent} * ${line}` : ' *')) + result.push(...lines.map(line => { + if (line === NewBlockIndicator) { + return ' */\n\n/**' + } else { + return line ? `${outerIndent} * ${line}` : ' *' + } + })) result.push(`${outerIndent} */\n`) return result.join('\n') diff --git a/test.js b/test.js index eadb6d5..9e7e952 100644 --- a/test.js +++ b/test.js @@ -128,28 +128,6 @@ describe('Simple schemas', () => { }) describe('Schemas with properties', () => { - it('Schema with `$ref` (object)', function () { - const schema = { - $defs: { // New name for `definitions` - definitionType: { - type: 'number' - } - }, - type: 'object', - properties: { - aNumberProp: { - $ref: '#/$defs/definitionType' - } - } - } - const expected = `/** - * @typedef {object} - * @property {number} [aNumberProp] - */ -` - expect(generate(schema)).toEqual(expected) - }) - it('Object with properties', function () { const schema = { type: 'object', @@ -350,10 +328,11 @@ describe('Schemas with properties', () => { }) describe('Schemas with items', function () { - it('Schema with `$ref` (array with items array)', function () { + it('Schema with `$ref` (array with items array), default', function () { const schema = { $defs: { // New name for `definitions` definitionType: { + title: 'customNumber', type: 'number' } }, @@ -367,14 +346,46 @@ describe('Schemas with items', function () { * @typedef {array} * @property {number} 0 */ + +/** + * @typedef {number} customNumber + */ +` + expect(generate(schema)).toEqual(expected) + }) + + it('Schema with `$ref` (array with items array), is-a', function () { + const schema = { + $defs: { // New name for `definitions` + definitionType: { + title: 'customNumber', + type: 'number' + } + }, + type: 'array', + minItems: 1, + items: [{ + $ref: '#/$defs/definitionType', + classRelation: 'is-a' + }] + } + const expected = `/** + * @typedef {array} + * @property {customNumber} 0 + */ + +/** + * @typedef {number} customNumber + */ ` expect(generate(schema)).toEqual(expected) }) - it('Schema with `$ref` (array with items object)', function () { + it('Schema with `$ref` (array with items object) ,default', function () { const schema = { $defs: { // New name for `definitions` definitionType: { + title: 'customNumber', type: 'number' } }, @@ -386,6 +397,35 @@ describe('Schemas with items', function () { const expected = `/** * @typedef {array} */ + +/** + * @typedef {number} customNumber + */ +` + expect(generate(schema)).toEqual(expected) + }) + + it('Schema with `$ref` (array with items object), is-a', function () { + const schema = { + $defs: { // New name for `definitions` + definitionType: { + title: 'customNumber', + type: 'number' + } + }, + type: 'array', + items: { + $ref: '#/$defs/definitionType', + classRelation: 'is-a' + } + } + const expected = `/** + * @typedef {array} + */ + +/** + * @typedef {number} customNumber + */ ` expect(generate(schema)).toEqual(expected) }) @@ -494,6 +534,574 @@ describe('Schemas with items', function () { }) }) +describe('Nested schemas', () => { + it('Simple Object reference default, first element', () => { + const schema = { + $defs: { + Person: { + title: 'Person', + type: 'object', + properties: { + name: { type: 'string', description: "A person's name" }, + age: { type: 'integer', description: "A person's age" } + } + } + }, + title: 'Laborer', + $ref: '#/$defs/Person' + } + const expected = `/** + * @typedef {object} Laborer + * @property {string} [name] A person's name + * @property {integer} [age] A person's age + */ + +/** + * @typedef {object} Person + * @property {string} [name] A person's name + * @property {integer} [age] A person's age + */ +` + expect(generate(schema)).toEqual(expected) + }) + + it('Simple Object reference default, nested element', () => { + const schema = { + $defs: { + Person: { + title: 'Person', + type: 'object', + properties: { + name: { type: 'string', description: "A person's name" }, + age: { type: 'integer', description: "A person's age" } + } + } + }, + title: 'LaborerList', + type: 'object', + properties: { + Laborer: { + title: 'Laborer', + $ref: '#/$defs/Person' + } + } + } + const expected = `/** + * @typedef {object} LaborerList + * @property {object} [Laborer] + * @property {string} [Laborer.name] A person's name + * @property {integer} [Laborer.age] A person's age + */ + +/** + * @typedef {object} Person + * @property {string} [name] A person's name + * @property {integer} [age] A person's age + */ +` + expect(generate(schema)).toEqual(expected) + }) + + it('Simple Object reference is-a, first element', () => { + const schema = { + $defs: { + Person: { + title: 'Person', + type: 'object', + properties: { + name: { type: 'string', description: "A person's name" }, + age: { type: 'integer', description: "A person's age" } + } + } + }, + title: 'Laborer', + $ref: '#/$defs/Person', + classRelation: 'is-a' + } + const expected = `/** + * @typedef {Person} Laborer + */ + +/** + * @typedef {object} Person + * @property {string} [name] A person's name + * @property {integer} [age] A person's age + */ +` + expect(generate(schema)).toEqual(expected) + }) + + it('Simple Object reference is-a, nested element', () => { + const schema = { + $defs: { + Person: { + title: 'Person', + type: 'object', + properties: { + name: { type: 'string', description: "A person's name" }, + age: { type: 'integer', description: "A person's age" } + } + } + }, + title: 'LaborerList', + type: 'object', + properties: { + Laborer: { + title: 'Laborer', + $ref: '#/$defs/Person', + classRelation: 'is-a' + } + } + } + const expected = `/** + * @typedef {object} LaborerList + * @property {Person} [Laborer] + */ + +/** + * @typedef {object} Person + * @property {string} [name] A person's name + * @property {integer} [age] A person's age + */ +` + expect(generate(schema)).toEqual(expected) + }) + + it('allOf reference default', () => { + const schema = { + $defs: { + Person: { + title: 'Person', + type: 'object', + properties: { + name: { type: 'string', description: "A person's name" }, + age: { type: 'integer', description: "A person's age" } + } + }, + Address: { + title: 'Address', + type: 'object', + properties: { + town: { type: 'string', description: 'The town' }, + street: { type: 'string', description: 'The street' }, + number: { type: 'integer', description: 'The number' } + } + } + }, + title: 'List', + type: 'object', + properties: { + Laborer: { + title: 'Laborer', + allOf: [ + { $ref: '#/$defs/Person' }, + { $ref: '#/$defs/Address' } + ] + } + } + } + const expected = `/** + * @typedef {object} List + * @property {object} [Laborer] + * @property {string} [Laborer.name] A person's name + * @property {integer} [Laborer.age] A person's age + * @property {string} [Laborer.town] The town + * @property {string} [Laborer.street] The street + * @property {integer} [Laborer.number] The number + */ + +/** + * @typedef {object} Person + * @property {string} [name] A person's name + * @property {integer} [age] A person's age + */ + +/** + * @typedef {object} Address + * @property {string} [town] The town + * @property {string} [street] The street + * @property {integer} [number] The number + */ +` + expect(generate(schema)).toEqual(expected) + }) + + it('allOf reference is-a', () => { + const schema = { + $defs: { + Person: { + title: 'Person', + type: 'object', + properties: { + name: { type: 'string', description: "A person's name" }, + age: { type: 'integer', description: "A person's age" } + } + }, + Address: { + title: 'Address', + type: 'object', + properties: { + town: { type: 'string', description: 'The town' }, + street: { type: 'string', description: 'The street' }, + number: { type: 'integer', description: 'The number' } + } + } + }, + title: 'List', + type: 'object', + properties: { + Laborer: { + title: 'Laborer', + allOf: [ + { $ref: '#/$defs/Person', classRelation: 'is-a' }, + { $ref: '#/$defs/Address', classRelation: 'is-a' } + ] + } + } + } + const expected = `/** + * @typedef {object} List + * @property {Person&Address} [Laborer] + */ + +/** + * @typedef {object} Person + * @property {string} [name] A person's name + * @property {integer} [age] A person's age + */ + +/** + * @typedef {object} Address + * @property {string} [town] The town + * @property {string} [street] The street + * @property {integer} [number] The number + */ +` + expect(generate(schema)).toEqual(expected) + }) + + it('allOf reference nested object', () => { + const schema = { + title: 'List', + type: 'object', + properties: { + Laborer: { + title: 'Laborer', + allOf: [ + { + type: 'object', + properties: { + Number: { type: 'number', description: 'A number' }, + String: { type: 'string', description: 'A String' } + } + } + ] + } + } + } + const expected = `/** + * @typedef {object} List + * @property {object} [Laborer] + * @property {number} [Laborer.Number] A number + * @property {string} [Laborer.String] A String + */ +` + expect(generate(schema)).toEqual(expected) + }) + + it('allOf reference is-a, default and nested Object', () => { + const schema = { + $defs: { + Person: { + title: 'Person', + type: 'object', + properties: { + name: { type: 'string', description: "A person's name" }, + age: { type: 'integer', description: "A person's age" } + } + }, + Address: { + title: 'Address', + type: 'object', + properties: { + town: { type: 'string', description: 'The town' }, + street: { type: 'string', description: 'The street' }, + number: { type: 'integer', description: 'The number' } + } + } + }, + title: 'List', + type: 'object', + properties: { + Laborer: { + title: 'Laborer', + allOf: [ + { $ref: '#/$defs/Person' }, + { $ref: '#/$defs/Address', classRelation: 'is-a' }, + { + type: 'object', + properties: { + Number: { type: 'number', description: 'A number' }, + String: { type: 'string', description: 'A String' } + } + } + ] + } + } + } + const expected = `/** + * @typedef {object} List + * @property {Address} [Laborer] + * @property {string} [Laborer.name] A person's name + * @property {integer} [Laborer.age] A person's age + * @property {number} [Laborer.Number] A number + * @property {string} [Laborer.String] A String + */ + +/** + * @typedef {object} Person + * @property {string} [name] A person's name + * @property {integer} [age] A person's age + */ + +/** + * @typedef {object} Address + * @property {string} [town] The town + * @property {string} [street] The street + * @property {integer} [number] The number + */ +` + expect(generate(schema)).toEqual(expected) + }) + + it('anyOf reference default', () => { + const schema = { + $defs: { + Person: { + title: 'Person', + type: 'object', + properties: { + name: { type: 'string', description: "A person's name" }, + age: { type: 'integer', description: "A person's age" } + } + }, + Address: { + title: 'Address', + type: 'object', + properties: { + town: { type: 'string', description: 'The town' }, + street: { type: 'string', description: 'The street' }, + number: { type: 'integer', description: 'The number' } + } + } + }, + title: 'List', + type: 'object', + properties: { + Laborer: { + title: 'Laborer', + anyOf: [ + { $ref: '#/$defs/Person' }, + { $ref: '#/$defs/Address' } + ] + } + } + } + const expected = `/** + * @typedef {object} List + * @property {Laborer_Person|Laborer_Address} [Laborer] + */ + +/** + * @typedef {object} Person + * @property {string} [name] A person's name + * @property {integer} [age] A person's age + */ + +/** + * @typedef {object} Address + * @property {string} [town] The town + * @property {string} [street] The street + * @property {integer} [number] The number + */ + +/** + * @typedef {object} Laborer_Person + * @property {string} [name] A person's name + * @property {integer} [age] A person's age + */ + +/** + * @typedef {object} Laborer_Address + * @property {string} [town] The town + * @property {string} [street] The street + * @property {integer} [number] The number + */ +` + expect(generate(schema)).toEqual(expected) + }) + + it('anyOf reference is-a', () => { + const schema = { + $defs: { + Person: { + title: 'Person', + type: 'object', + properties: { + name: { type: 'string', description: "A person's name" }, + age: { type: 'integer', description: "A person's age" } + } + }, + Address: { + title: 'Address', + type: 'object', + properties: { + town: { type: 'string', description: 'The town' }, + street: { type: 'string', description: 'The street' }, + number: { type: 'integer', description: 'The number' } + } + } + }, + title: 'List', + type: 'object', + properties: { + Laborer: { + title: 'Laborer', + anyOf: [ + { $ref: '#/$defs/Person', classRelation: 'is-a' }, + { $ref: '#/$defs/Address', classRelation: 'is-a' } + ] + } + } + } + const expected = `/** + * @typedef {object} List + * @property {Person|Address} [Laborer] + */ + +/** + * @typedef {object} Person + * @property {string} [name] A person's name + * @property {integer} [age] A person's age + */ + +/** + * @typedef {object} Address + * @property {string} [town] The town + * @property {string} [street] The street + * @property {integer} [number] The number + */ +` + expect(generate(schema)).toEqual(expected) + }) + + it('anyOf reference nested object', () => { + const schema = { + title: 'List', + type: 'object', + properties: { + Laborer: { + title: 'Laborer', + anyOf: [ + { + type: 'object', + properties: { + Number: { type: 'number', description: 'A number' }, + String: { type: 'string', description: 'A String' } + } + } + ] + } + } + } + const expected = `/** + * @typedef {object} List + * @property {Laborer_subtype0} [Laborer] + */ + +/** + * @typedef {object} Laborer_subtype0 + * @property {number} [Number] A number + * @property {string} [String] A String + */ +` + expect(generate(schema)).toEqual(expected) + }) + + it('anyOf reference is-a, default and nested Object', () => { + const schema = { + $defs: { + Person: { + title: 'Person', + type: 'object', + properties: { + name: { type: 'string', description: "A person's name" }, + age: { type: 'integer', description: "A person's age" } + } + }, + Address: { + title: 'Address', + type: 'object', + properties: { + town: { type: 'string', description: 'The town' }, + street: { type: 'string', description: 'The street' }, + number: { type: 'integer', description: 'The number' } + } + } + }, + title: 'List', + type: 'object', + properties: { + Laborer: { + title: 'Laborer', + anyOf: [ + { $ref: '#/$defs/Person' }, + { $ref: '#/$defs/Address', classRelation: 'is-a' }, + { + type: 'object', + properties: { + Number: { type: 'number', description: 'A number' }, + String: { type: 'string', description: 'A String' } + } + } + ] + } + } + } + const expected = `/** + * @typedef {object} List + * @property {Address|Laborer_Person|Laborer_subtype0} [Laborer] + */ + +/** + * @typedef {object} Person + * @property {string} [name] A person's name + * @property {integer} [age] A person's age + */ + +/** + * @typedef {object} Address + * @property {string} [town] The town + * @property {string} [street] The street + * @property {integer} [number] The number + */ + +/** + * @typedef {object} Laborer_Person + * @property {string} [name] A person's name + * @property {integer} [age] A person's age + */ + +/** + * @typedef {object} Laborer_subtype0 + * @property {number} [Number] A number + * @property {string} [String] A String + */ +` + expect(generate(schema)).toEqual(expected) + }) +}) + describe('option: `autoDescribe`', function () { it('Simple object with `autoDescribe`: true', function () { const schema = {