├── .editorconfig ├── .eslintignore ├── .eslintrc.js ├── .github └── workflows │ └── node.js.yml ├── .gitignore ├── .husky └── pre-commit ├── .npmignore ├── LICENSE-ISC.txt ├── example.js ├── index.js ├── package-lock.json ├── package.json ├── readme.md └── test.js /.editorconfig: -------------------------------------------------------------------------------- 1 | ; EditorConfig file: https://EditorConfig.org 2 | ; Install the "EditorConfig" plugin into your editor to use 3 | 4 | root = true 5 | 6 | [*] 7 | charset = utf-8 8 | end_of_line = lf 9 | insert_final_newline = true 10 | indent_style = space 11 | indent_size = 2 12 | trim_trailing_whitespace = true 13 | 14 | [*.md] 15 | indent_size = 4 16 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | !*.js 2 | out 3 | node_modules 4 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | module.exports = { 4 | extends: ['eslint:recommended', 'standard'], 5 | env: { 6 | jest: true, 7 | node: true 8 | }, 9 | parserOptions: { 10 | ecmaVersion: 2018 11 | }, 12 | rules: { 13 | 'no-var': ['error'], 14 | 'prefer-destructuring': ['error'], 15 | 'object-shorthand': ['error'], 16 | 'prefer-template': ['error'] 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /.github/workflows/node.js.yml: -------------------------------------------------------------------------------- 1 | # This workflow will do a clean installation of node dependencies, cache/restore them, build the source code and run tests across different versions of node 2 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-nodejs 3 | 4 | name: Node.js CI 5 | 6 | on: 7 | push: 8 | branches: [ "master" ] 9 | pull_request: 10 | branches: [ "master" ] 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: ubuntu-latest 16 | 17 | strategy: 18 | matrix: 19 | node-version: [20.x] 20 | # See supported Node.js release schedule at https://nodejs.org/en/about/releases/ 21 | 22 | steps: 23 | - uses: actions/checkout@v3 24 | - name: Use Node.js ${{ matrix.node-version }} 25 | uses: actions/setup-node@v3 26 | with: 27 | node-version: ${{ matrix.node-version }} 28 | cache: 'npm' 29 | - run: npm ci 30 | - run: npm test 31 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | 3 | # Generated files 4 | out/ 5 | docs.js 6 | coverage 7 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | npx eslint --report-unused-disable-directives --fix . 2 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | coverage 2 | out 3 | .nyc_output 4 | -------------------------------------------------------------------------------- /LICENSE-ISC.txt: -------------------------------------------------------------------------------- 1 | ISC License (ISC) 2 | 3 | Copyright 2017-2020, Francis Nepomuceno 4 | Copyright 2020, Brett Zamir 5 | 6 | Permission to use, copy, modify, and/or distribute this software for any 7 | purpose with or without fee is hereby granted, provided that the above 8 | copyright notice and this permission notice appear in all copies. 9 | 10 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 11 | WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 12 | MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY 13 | SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER 14 | RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, 15 | NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE 16 | USE OR PERFORMANCE OF THIS SOFTWARE. 17 | -------------------------------------------------------------------------------- /example.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const generate = require('./index') 3 | const fs = require('fs') 4 | 5 | const schema = { 6 | id: 'Person', 7 | type: 'object', 8 | properties: { 9 | name: { type: 'string', description: "A person's name" }, 10 | age: { type: 'integer', description: "A person's age" } 11 | }, 12 | required: ['name'] 13 | } 14 | 15 | fs.writeFileSync('docs.js', generate(schema)) 16 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const json = require('json-pointer') 4 | 5 | function getDefaultPropertyType ({ 6 | propertyNameAsType, capitalizeProperty, defaultPropertyType 7 | }, property) { 8 | const fallbackPropertyType = '*' 9 | 10 | if (property !== undefined && propertyNameAsType) { 11 | return capitalizeProperty ? upperFirst(property) : property 12 | } 13 | if (defaultPropertyType === null || defaultPropertyType === '') { 14 | return defaultPropertyType 15 | } 16 | return defaultPropertyType || fallbackPropertyType 17 | } 18 | 19 | function wrapDescription (config, description) { 20 | const { maxLength } = config 21 | if (!maxLength) { 22 | return [description] 23 | } 24 | const result = [] 25 | 26 | while (true) { 27 | const excess = description.length - config.indentMaxDelta 28 | if (excess <= 0) { 29 | if (description) { 30 | result.push(description) 31 | } 32 | break 33 | } 34 | const maxLine = description.slice(0, config.indentMaxDelta) 35 | const wsIndex = maxLine.search(/\s\S*$/) 36 | let safeString 37 | if (wsIndex === -1) { 38 | // With this being all non-whitespace, e.g., a long link, we 39 | // let it go on without wrapping until whitespace is reached 40 | const remainder = description.slice(config.indentMaxDelta).match(/^\S+/) 41 | safeString = maxLine + (remainder || '') 42 | } else { 43 | safeString = maxLine.slice(0, wsIndex) 44 | } 45 | result.push(safeString) 46 | description = description.slice(safeString.length + 1) 47 | } 48 | return result 49 | } 50 | 51 | module.exports = generate 52 | 53 | function generate (schema, options = {}) { 54 | const jsdoc = [] 55 | 56 | if (!schema || Object.keys(schema).length === 0) { 57 | return '' 58 | } 59 | 60 | const config = parseOptions(options) 61 | 62 | jsdoc.push(...writeDescription(schema, config)) 63 | 64 | if (json.has(schema, '/properties')) { 65 | jsdoc.push(...processProperties(schema, schema, null, config)) 66 | } 67 | if (json.has(schema, '/items')) { 68 | jsdoc.push(...processItems(schema, schema, null, config)) 69 | } 70 | 71 | return format(config.outerIndent, jsdoc) 72 | } 73 | 74 | function parseOptions (options) { 75 | const asteriskAndWhitespaceLength = 3 // ' * ' 76 | const outerIndent = (options.indentChar || ' ').repeat(options.indent || 0) 77 | const indentMaxDelta = options.maxLength - outerIndent.length - 78 | asteriskAndWhitespaceLength 79 | 80 | return { 81 | ...options, 82 | outerIndent, 83 | indentMaxDelta 84 | } 85 | } 86 | 87 | function processItems (schema, rootSchema, base, config) { 88 | const items = json.get(schema, '/items') 89 | if (!Array.isArray(items)) { 90 | return [] 91 | } 92 | const result = [] 93 | items.forEach((item, i) => { 94 | const root = base ? `${base}.` : '' 95 | const prefixedProperty = root + i 96 | const defaultValue = item.default 97 | const optional = !schema.minItems || i >= schema.minItems 98 | if (item.type === 'array' && item.items) { 99 | result.push(...writeProperty('array', prefixedProperty, item.description, optional, defaultValue, schema, config)) 100 | result.push(...processItems(item, rootSchema, prefixedProperty, config)) 101 | } else if (item.type === 'object' && item.properties) { 102 | result.push(...writeProperty('object', prefixedProperty, item.description, optional, defaultValue, schema, config)) 103 | result.push(...processProperties(item, rootSchema, prefixedProperty, config)) 104 | } else { 105 | const type = getSchemaType(item, rootSchema) || getDefaultPropertyType(config) 106 | result.push(...writeProperty(type, prefixedProperty, item.description, optional, defaultValue, item, config)) 107 | } 108 | }) 109 | return result 110 | } 111 | 112 | function processProperties (schema, rootSchema, base, config) { 113 | const props = json.get(schema, '/properties') 114 | const required = json.has(schema, '/required') ? json.get(schema, '/required') : [] 115 | const result = [] 116 | 117 | for (const property in props) { 118 | if (Array.isArray(config.ignore) && config.ignore.includes(property)) { 119 | continue 120 | } else { 121 | const prop = props[property] 122 | const root = base ? `${base}.` : '' 123 | const prefixedProperty = root + property 124 | const defaultValue = prop.default 125 | const optional = !required.includes(property) 126 | if (prop.type === 'object' && prop.properties) { 127 | result.push(...writeProperty('object', prefixedProperty, prop.description, optional, defaultValue, schema, config)) 128 | result.push(...processProperties(prop, rootSchema, prefixedProperty, config)) 129 | } else if (prop.type === 'array' && prop.items) { 130 | result.push(...writeProperty('array', prefixedProperty, prop.description, optional, defaultValue, schema, config)) 131 | result.push(...processItems(prop, rootSchema, prefixedProperty, config)) 132 | } else { 133 | const type = getSchemaType(prop, rootSchema) || getDefaultPropertyType(config, property) 134 | result.push(...writeProperty(type, prefixedProperty, prop.description, optional, defaultValue, prop, config)) 135 | } 136 | } 137 | } 138 | return result 139 | } 140 | 141 | function getSchemaType (schema, rootSchema) { 142 | if (schema.$ref) { 143 | const ref = json.get(rootSchema, schema.$ref.slice(1)) 144 | return getSchemaType(ref, rootSchema) 145 | } 146 | 147 | if (schema.enum) { 148 | if (schema.type === 'string') { 149 | return `"${schema.enum.join('"|"')}"` 150 | } 151 | if ( 152 | schema.type === 'number' || schema.type === 'integer' || 153 | schema.type === 'boolean' 154 | ) { 155 | return `${schema.enum.join('|')}` 156 | } 157 | // Enum can represent more complex types such as array or object 158 | // It can also include a mixture of different types 159 | // Currently, these scenarios are not handled 160 | return schema.type === 'null' ? 'null' : 'enum' 161 | } 162 | 163 | if (schema.const !== undefined) { 164 | if (schema.type === 'string') { 165 | return `"${schema.const}"` 166 | } 167 | if ( 168 | schema.type === 'number' || schema.type === 'integer' || 169 | schema.type === 'boolean' 170 | ) { 171 | return `${schema.const}` 172 | } 173 | // Const can also be of more complex types like arrays or objects 174 | // As of now, these cases are not addressed 175 | return schema.type === 'null' ? 'null' : 'const' 176 | } 177 | 178 | if (Array.isArray(schema.type)) { 179 | if (schema.type.includes('null')) { 180 | return `?${schema.type[0]}` 181 | } else { 182 | return schema.type.join('|') 183 | } 184 | } 185 | 186 | return schema.type 187 | } 188 | 189 | function getType (schema, config, type) { 190 | const typeCheck = type || schema.type 191 | let typeMatch 192 | if (schema.format) { 193 | typeMatch = config.formats && config.formats[schema.format] && 194 | config.formats[schema.format][typeCheck] 195 | } 196 | if (typeMatch === undefined || typeMatch === null) { 197 | typeMatch = config.types && config.types[typeCheck] 198 | } 199 | 200 | let typeStr 201 | if (config.types === null || config.formats === null || 202 | (config.formats && ( 203 | (config.formats[schema.format] === null) || 204 | (config.formats[schema.format] && 205 | config.formats[schema.format][typeCheck] === null) 206 | )) || 207 | (typeMatch !== '' && !typeMatch && (type === null || type === '')) 208 | ) { 209 | typeStr = '' 210 | } else { 211 | typeStr = ` {${ 212 | typeMatch === '' 213 | ? '' 214 | : typeMatch || type || getSchemaType(schema, schema) 215 | }}` 216 | } 217 | 218 | return typeStr 219 | } 220 | 221 | function writeDescription (schema, config) { 222 | const result = [] 223 | const { objectTagName = 'typedef' } = config 224 | let { description } = schema 225 | if (description === undefined) { 226 | description = config.autoDescribe ? generateDescription(schema.title, schema.type) : '' 227 | } 228 | 229 | const type = getType(schema, config) 230 | 231 | if (description || config.addDescriptionLineBreak) { 232 | result.push(...wrapDescription(config, description)) 233 | } 234 | 235 | const namepath = schema.title ? ` ${config.capitalizeTitle ? upperFirst(schema.title) : schema.title}` : '' 236 | result.push(`@${objectTagName}${type}${namepath}`) 237 | 238 | return result 239 | } 240 | 241 | function writeProperty (type, field, description = '', optional, defaultValue, schema, config) { 242 | const typeExpression = getType(schema, config, type) 243 | 244 | let fieldTemplate = ' ' 245 | if (optional) { 246 | fieldTemplate += `[${field}${defaultValue === undefined ? '' : `=${JSON.stringify(defaultValue)}`}]` 247 | } else { 248 | fieldTemplate += field 249 | } 250 | 251 | let desc 252 | if (!description && !config.descriptionPlaceholder) { 253 | desc = '' 254 | } else if (config.hyphenatedDescriptions) { 255 | desc = ` - ${description}` 256 | } else { 257 | desc = ` ${description}` 258 | } 259 | return wrapDescription(config, `@property${typeExpression}${fieldTemplate}${desc}`) 260 | } 261 | 262 | function upperFirst (str) { 263 | return str.slice(0, 1).toUpperCase() + str.slice(1) 264 | } 265 | 266 | function generateDescription (title, type) { 267 | const noun = title ? `${title} ${type}` : type 268 | const article = `a${'aeiou'.split('').includes(noun.charAt()) ? 'n' : ''}` 269 | 270 | return `Represents ${article} ${noun}` 271 | } 272 | 273 | function format (outerIndent, lines) { 274 | const result = [`${outerIndent}/**`] 275 | 276 | result.push(...lines.map(line => line ? `${outerIndent} * ${line}` : ' *')) 277 | result.push(`${outerIndent} */\n`) 278 | 279 | return result.join('\n') 280 | } 281 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "json-schema-to-jsdoc", 3 | "version": "1.1.1", 4 | "description": "JSON Schema to JSDoc generator", 5 | "main": "index.js", 6 | "scripts": { 7 | "lint": "eslint --report-unused-disable-directives .", 8 | "test": "jest --coverage", 9 | "example": "node example.js", 10 | "postexample": "jsdoc docs.js" 11 | }, 12 | "nyc": { 13 | "check-coverage": true, 14 | "branches": 100, 15 | "lines": 100, 16 | "functions": 100, 17 | "statements": 100 18 | }, 19 | "keywords": [ 20 | "JSON", 21 | "schema", 22 | "jsdoc", 23 | "jsonschema", 24 | "generator" 25 | ], 26 | "author": "Francis Nepomuceno", 27 | "contributors": [ 28 | "Brett Zamir" 29 | ], 30 | "license": "ISC", 31 | "repository": { 32 | "type": "git", 33 | "url": "git+https://github.com/n3ps/json-schema-to-jsdoc.git" 34 | }, 35 | "bugs": "https://github.com/n3ps/json-schema-to-jsdoc/issues", 36 | "homepage": "https://github.com/n3ps/json-schema-to-jsdoc", 37 | "engines": { 38 | "node": ">=6.0.0" 39 | }, 40 | "dependencies": { 41 | "json-pointer": "^0.6.2" 42 | }, 43 | "devDependencies": { 44 | "eslint": "^8.57.0", 45 | "eslint-config-standard": "^17.1.0", 46 | "eslint-plugin-import": "^2.29.1", 47 | "eslint-plugin-node": "^11.1.0", 48 | "eslint-plugin-promise": "^6.1.1", 49 | "eslint-plugin-standard": "^5.0.0", 50 | "husky": "^8.0.3", 51 | "jest": "^29.7.0", 52 | "jsdoc": "^4.0.2" 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | [![npm version](https://img.shields.io/npm/v/json-schema-to-jsdoc.svg)](https://www.npmjs.com/package/json-schema-to-jsdoc) 2 | [![Build Status](https://github.com/n3ps/json-schema-to-jsdoc/actions/workflows/node.js.yml/badge.svg)](github.com/n3ps/json-schema-to-jsdoc/actions/workflows/node.js.yml/badge.svg) 3 | 4 | [![Known Vulnerabilities](https://snyk.io/test/github/n3ps/json-schema-to-jsdoc/badge.svg)](https://snyk.io/test/github/n3ps/json-schema-to-jsdoc) 5 | [![Total Alerts](https://img.shields.io/lgtm/alerts/g/n3ps/json-schema-to-jsdoc.svg?logo=lgtm&logoWidth=18)](https://lgtm.com/projects/g/n3ps/json-schema-to-jsdoc/alerts) 6 | [![Code Quality: Javascript](https://img.shields.io/lgtm/grade/javascript/g/n3ps/json-schema-to-jsdoc.svg?logo=lgtm&logoWidth=18)](https://lgtm.com/projects/g/n3ps/json-schema-to-jsdoc/context:javascript) 7 | 8 | # JSON Schema to JSDoc 9 | 10 | Useful when you already have a JSON Schema and want to document the types you want to validate. Works with subschema definitions. 11 | 12 | ## Usage 13 | 14 | ```js 15 | const jsdoc = require('json-schema-to-jsdoc') 16 | 17 | const schema = { 18 | "title": "Person", 19 | "type": "object", 20 | "properties": { 21 | "name": {"type": "string", "description": "A person's name"}, 22 | "age": {"type": "integer", "description": "A person's age"} 23 | }, 24 | "required": ["name"] 25 | } 26 | 27 | jsdoc(schema /* , optionsObject */) 28 | ``` 29 | 30 | ### Output 31 | 32 | ```js 33 | /** 34 | * @typedef {object} Person 35 | * @property {string} name A person's name 36 | * @property {integer} [age] A person's age 37 | */ 38 | ``` 39 | 40 | ## Examples 41 | 42 | #### `hyphenatedDescriptions` 43 | 44 | ```js 45 | jsdoc(schema, { 46 | hyphenatedDescriptions: true 47 | }) 48 | ``` 49 | 50 | ```js 51 | /** 52 | * @typedef {object} Person 53 | * @property {string} name - A person's name 54 | * @property {integer} [age] - A person's age 55 | */ 56 | ``` 57 | 58 | #### `autoDescribe` 59 | 60 | ```js 61 | jsdoc(schema, { 62 | autoDescribe: true 63 | }) 64 | ``` 65 | 66 | ```js 67 | /** 68 | * Represents a Person object 69 | * @typedef {object} Person 70 | * @property {string} name A person's name 71 | * @property {integer} [age] A person's age 72 | */ 73 | ``` 74 | 75 | #### `types` 76 | 77 | ```js 78 | jsdoc(schema, { 79 | types: { 80 | object: 'PlainObject' 81 | } 82 | }) 83 | ``` 84 | 85 | ```js 86 | /** 87 | * @typedef {PlainObject} Person 88 | * @property {string} name A person's name 89 | * @property {integer} [age] A person's age 90 | */ 91 | ``` 92 | 93 | #### `formats` 94 | 95 | ```js 96 | const schema = { 97 | title: 'Info', 98 | type: 'object', 99 | properties: { 100 | code: { 101 | type: 'string', format: 'html', description: 'The HTML source' 102 | } 103 | }, 104 | required: ['code'] 105 | } 106 | 107 | jsdoc(schema, { 108 | formats: { 109 | html: { 110 | string: 'HTML' 111 | } 112 | } 113 | }) 114 | ``` 115 | 116 | ```js 117 | /** 118 | * @typedef {object} Info 119 | * @property {HTML} code The HTML source 120 | */ 121 | ``` 122 | 123 | ## Options 124 | 125 | `addDescriptionLineBreak`: boolean 126 | Inserts an empty line when `autoDescribe` is `false` and the schema 127 | `description` is empty. Defaults to `false`. 128 | 129 | `autoDescribe`: boolean 130 | Adds a description (`"Represents a/n [ ]<type>"`) when the 131 | schema has no `description`. Defaults to `false`. 132 | 133 | `capitalizeProperty`: boolean 134 | When `propertyNameAsType` is `true`, capitalizes the property-as-type, 135 | i.e., `MyTitle` in `@property {MyTitle} myTitle`. Defaults to `false.` 136 | 137 | `capitalizeTitle`: boolean 138 | If a schema `title` is present, capitalizes the schema's `title` in the 139 | output of `@typedef {myType} title`. Defaults to `false`. 140 | 141 | `defaultPropertyType`: null | string 142 | Used when no schema type is present. Defaults to `"*"`. 143 | - `string`: If set to a string, that string will be used (e.g., 144 | "any", "JSON", "external:JSON"). Note that jsdoc recommends `*` for 145 | any, while TypeScript uses "any". If one defines one's own "JSON" 146 | type, one could use that to clarify that only JSON types are used. 147 | - `null`: Will avoid any type brackets or type being added. 148 | 149 | `descriptionPlaceholder`: boolean 150 | If `false` and there is no `description` for the object `@property`, 151 | this will avoid a hyphen or even a space for `{description}` within 152 | `@property {name}{description}`. Defaults to `false`. 153 | 154 | `hyphenatedDescriptions`: boolean 155 | Inserts a hyphen + space in the `{description}` portion of 156 | `@property {name}{description}` (will add a space, however, unless 157 | `descriptionPlaceholder` is `false`). Defaults to `false`. 158 | 159 | `ignore`: string[] 160 | Property names to ignore adding to output. Defaults to empty array. 161 | 162 | `indent`: number 163 | How many of `indentChar` to precede each line. Defaults to `0` (no 164 | indent). Note that a single space will be added in addition to the 165 | indent for every line of the document block after the first. 166 | 167 | `indentChar`: string 168 | Character to use when `indent` is set (e.g., a tab or space). 169 | Defaults to a space. 170 | 171 | `maxLength`: number | boolean 172 | - `number`: Enforce a maximum length in `@typedef` and `@property` 173 | descriptions (taking into account `indent`/`indentChar`). 174 | - `false`: Prevent wrapping entirely. Defaults to `false`. 175 | 176 | `objectTagName`: string 177 | Tag name to use for objects. Defaults to `typedef`. 178 | 179 | `propertyNameAsType`: boolean 180 | Indicates that the property name (for objects) should be used as the 181 | type name (optionally capitalized with `capitalizeProperty`). Defaults 182 | to `false`. 183 | 184 | `types`: null | {[schemaType: string]: string} 185 | Used to determine output of curly-bracketed type content within 186 | `@typedef {...}`. 187 | If `null` no curly brackets or type content will be shown with the 188 | `@typedef` at all. If the schema `type` matches a property in the object map, and it maps to the empty string, an empty `{}` will result. Otherwise, if there is a `type` match, that string will be used as the curly bracketed type, or if there is no match, the schema's `type` will be used for the bracketed content. Defaults to an empty object map (will always just use the schema's `type`). This property may be used to change the likes of `@typedef {object}` to `@typedef {PlainObject}`. 189 | -------------------------------------------------------------------------------- /test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const generate = require('./index') 4 | const jsdoc = generate 5 | 6 | const trailingSpace = ' ' 7 | 8 | const schema = { 9 | title: 'Person', 10 | type: 'object', 11 | properties: { 12 | name: { type: 'string', description: "A person's name" }, 13 | age: { type: 'integer', description: "A person's age" } 14 | }, 15 | required: ['name'] 16 | } 17 | 18 | describe('Simple schemas', () => { 19 | it('Guards', function () { 20 | const inputs = [null, {}, undefined] 21 | inputs.forEach(input => { 22 | expect(generate(input)).toEqual('') 23 | }) 24 | }) 25 | 26 | it('Simple string', function () { 27 | const schema = { type: 'string' } 28 | const expected = `/** 29 | * @typedef {string} 30 | */ 31 | ` 32 | expect(generate(schema)).toEqual(expected) 33 | }) 34 | 35 | it('Simple string with description', function () { 36 | const schema = { type: 'string', description: 'String description' } 37 | const expected = `/** 38 | * String description 39 | * @typedef {string} 40 | */ 41 | ` 42 | expect(generate(schema)).toEqual(expected) 43 | }) 44 | 45 | it('Simple object with title', function () { 46 | const schema = { 47 | title: 'special', 48 | type: 'object' 49 | } 50 | const expected = `/** 51 | * @typedef {object} special 52 | */ 53 | ` 54 | expect(generate(schema)).toEqual(expected) 55 | }) 56 | 57 | it('String with enum', function () { 58 | const schema = { 59 | type: 'string', 60 | enum: ['some', 'different', 'types'] 61 | } 62 | const expected = `/** 63 | * @typedef {"some"|"different"|"types"} 64 | */ 65 | ` 66 | expect(generate(schema)).toEqual(expected) 67 | }) 68 | 69 | it('Number with enum', function () { 70 | const schema = { 71 | type: 'number', 72 | enum: [12, 34.5, 6789] 73 | } 74 | const expected = `/** 75 | * @typedef {12|34.5|6789} 76 | */ 77 | ` 78 | expect(generate(schema)).toEqual(expected) 79 | }) 80 | 81 | it('Integer with enum', function () { 82 | const schema = { 83 | type: 'integer', 84 | enum: [12, 345, 6789] 85 | } 86 | const expected = `/** 87 | * @typedef {12|345|6789} 88 | */ 89 | ` 90 | expect(generate(schema)).toEqual(expected) 91 | }) 92 | 93 | it('Boolean with enum', function () { 94 | const schema = { 95 | type: 'boolean', 96 | enum: [false, true, false] 97 | } 98 | const expected = `/** 99 | * @typedef {false|true|false} 100 | */ 101 | ` 102 | expect(generate(schema)).toEqual(expected) 103 | }) 104 | 105 | it('null with enum', function () { 106 | const schema = { 107 | type: 'null', 108 | enum: [null] 109 | } 110 | const expected = `/** 111 | * @typedef {null} 112 | */ 113 | ` 114 | expect(generate(schema)).toEqual(expected) 115 | }) 116 | 117 | it('String with const', function () { 118 | const schema = { 119 | type: 'string', 120 | const: 'value' 121 | } 122 | const expected = `/** 123 | * @typedef {"value"} 124 | */ 125 | ` 126 | expect(generate(schema)).toEqual(expected) 127 | }) 128 | 129 | it('Number with const', function () { 130 | const schema = { 131 | type: 'number', 132 | const: 23.3 133 | } 134 | const expected = `/** 135 | * @typedef {23.3} 136 | */ 137 | ` 138 | expect(generate(schema)).toEqual(expected) 139 | }) 140 | 141 | it('Integer with const', function () { 142 | const schema = { 143 | type: 'integer', 144 | const: 42 145 | } 146 | const expected = `/** 147 | * @typedef {42} 148 | */ 149 | ` 150 | expect(generate(schema)).toEqual(expected) 151 | }) 152 | 153 | it('Boolean with const', function () { 154 | const schema = { 155 | type: 'boolean', 156 | const: true 157 | } 158 | const expected = `/** 159 | * @typedef {true} 160 | */ 161 | ` 162 | expect(generate(schema)).toEqual(expected) 163 | }) 164 | 165 | it('null with const', function () { 166 | const schema = { 167 | type: 'null', 168 | const: null 169 | } 170 | const expected = `/** 171 | * @typedef {null} 172 | */ 173 | ` 174 | expect(generate(schema)).toEqual(expected) 175 | }) 176 | 177 | it('null with const complex type', function () { 178 | const schema = { 179 | type: 'array', 180 | const: ['red', 'green'] 181 | } 182 | const expected = `/** 183 | * @typedef {const} 184 | */ 185 | ` 186 | expect(generate(schema)).toEqual(expected) 187 | }) 188 | 189 | it('Simple array with title', function () { 190 | const schema = { 191 | title: 'special', 192 | type: 'array' 193 | } 194 | const expected = `/** 195 | * @typedef {array} special 196 | */ 197 | ` 198 | expect(generate(schema)).toEqual(expected) 199 | }) 200 | }) 201 | 202 | describe('Schemas with properties', () => { 203 | it('Schema with `$ref` (object)', function () { 204 | const schema = { 205 | $defs: { // New name for `definitions` 206 | definitionType: { 207 | type: 'number' 208 | } 209 | }, 210 | type: 'object', 211 | properties: { 212 | aNumberProp: { 213 | $ref: '#/$defs/definitionType' 214 | } 215 | } 216 | } 217 | const expected = `/** 218 | * @typedef {object} 219 | * @property {number} [aNumberProp] 220 | */ 221 | ` 222 | expect(generate(schema)).toEqual(expected) 223 | }) 224 | 225 | it('Object with properties', function () { 226 | const schema = { 227 | type: 'object', 228 | properties: { 229 | aStringProp: { 230 | type: 'string' 231 | }, 232 | anObjectProp: { 233 | type: 'object', 234 | properties: { 235 | aNestedProp: { 236 | description: 'Boolean desc.', 237 | type: 'boolean' 238 | }, 239 | aNestedArrayProp: { 240 | description: 'Array desc.', 241 | type: 'array', 242 | minItems: 1, 243 | items: [ 244 | { 245 | type: 'number' 246 | } 247 | ] 248 | } 249 | } 250 | }, 251 | nullableType: { 252 | type: ['string', 'null'] 253 | }, 254 | multipleTypes: { 255 | type: ['string', 'number'] 256 | }, 257 | enumProp: { 258 | enum: ['hello', 'world'] 259 | }, 260 | enumStringProp: { 261 | type: 'string', 262 | enum: ['hello', 'there', 'world'] 263 | } 264 | } 265 | } 266 | const expected = `/** 267 | * @typedef {object} 268 | * @property {string} [aStringProp] 269 | * @property {object} [anObjectProp] 270 | * @property {boolean} [anObjectProp.aNestedProp] Boolean desc. 271 | * @property {array} [anObjectProp.aNestedArrayProp] Array desc. 272 | * @property {number} anObjectProp.aNestedArrayProp.0 273 | * @property {?string} [nullableType] 274 | * @property {string|number} [multipleTypes] 275 | * @property {enum} [enumProp] 276 | * @property {"hello"|"there"|"world"} [enumStringProp] 277 | */ 278 | ` 279 | expect(generate(schema)).toEqual(expected) 280 | }) 281 | 282 | it('Object with properties and `required`', function () { 283 | const schema = { 284 | type: 'object', 285 | properties: { 286 | anObjectProp: { 287 | type: 'object', 288 | required: ['aNestedProp'], 289 | properties: { 290 | aNestedProp: { 291 | type: 'boolean' 292 | }, 293 | anotherNestedProp: { 294 | type: 'number' 295 | } 296 | } 297 | }, 298 | propWithDefault: { 299 | type: 'string', 300 | default: 'hello' 301 | } 302 | } 303 | } 304 | const expected = `/** 305 | * @typedef {object} 306 | * @property {object} [anObjectProp] 307 | * @property {boolean} anObjectProp.aNestedProp 308 | * @property {number} [anObjectProp.anotherNestedProp] 309 | * @property {string} [propWithDefault="hello"] 310 | */ 311 | ` 312 | expect(generate(schema)).toEqual(expected) 313 | }) 314 | 315 | it('Required object', function () { 316 | const schema = { 317 | type: 'object', 318 | title: 'NestedType', 319 | properties: { 320 | cfg: { 321 | type: 'object', 322 | properties: { 323 | } 324 | } 325 | }, 326 | required: [ 327 | 'cfg' 328 | ] 329 | } 330 | 331 | const expected = `/** 332 | * @typedef {PlainObject} NestedType 333 | * @property {PlainObject} cfg 334 | */ 335 | ` 336 | 337 | expect(generate(schema, { 338 | types: { 339 | object: 'PlainObject' 340 | } 341 | })).toEqual(expected) 342 | }) 343 | 344 | it('Required array', function () { 345 | const schema = { 346 | type: 'object', 347 | title: 'NestedType', 348 | properties: { 349 | cfg: { 350 | type: 'array', 351 | items: [] 352 | } 353 | }, 354 | required: [ 355 | 'cfg' 356 | ] 357 | } 358 | 359 | const expected = `/** 360 | * @typedef {object} NestedType 361 | * @property {array} cfg 362 | */ 363 | ` 364 | 365 | expect(generate(schema)).toEqual(expected) 366 | }) 367 | 368 | it('Object with untyped property', function () { 369 | const schema = { 370 | type: 'object', 371 | properties: { 372 | anObjectProp: { 373 | type: 'object', 374 | properties: { 375 | aNestedProp: { 376 | }, 377 | anotherNestedProp: { 378 | type: 'number' 379 | } 380 | } 381 | } 382 | } 383 | } 384 | const expected = `/** 385 | * @typedef {object} 386 | * @property {object} [anObjectProp] 387 | * @property {*} [anObjectProp.aNestedProp] 388 | * @property {number} [anObjectProp.anotherNestedProp] 389 | */ 390 | ` 391 | expect(generate(schema)).toEqual(expected) 392 | }) 393 | 394 | it('Object with deep nesting', function () { 395 | const schema = { 396 | type: 'object', 397 | properties: { 398 | anObjectProp: { 399 | type: 'object', 400 | properties: { 401 | aNestedProp: { 402 | type: 'object', 403 | properties: { 404 | aDeeplyNestedProp: { 405 | type: 'number' 406 | } 407 | } 408 | } 409 | } 410 | } 411 | } 412 | } 413 | const expected = `/** 414 | * @typedef {object} 415 | * @property {object} [anObjectProp] 416 | * @property {object} [anObjectProp.aNestedProp] 417 | * @property {number} [anObjectProp.aNestedProp.aDeeplyNestedProp] 418 | */ 419 | ` 420 | expect(generate(schema)).toEqual(expected) 421 | }) 422 | }) 423 | 424 | describe('Schemas with items', function () { 425 | it('Schema with `$ref` (array with items array)', function () { 426 | const schema = { 427 | $defs: { // New name for `definitions` 428 | definitionType: { 429 | type: 'number' 430 | } 431 | }, 432 | type: 'array', 433 | minItems: 1, 434 | items: [{ 435 | $ref: '#/$defs/definitionType' 436 | }] 437 | } 438 | const expected = `/** 439 | * @typedef {array} 440 | * @property {number} 0 441 | */ 442 | ` 443 | expect(generate(schema)).toEqual(expected) 444 | }) 445 | 446 | it('Schema with `$ref` (array with items object)', function () { 447 | const schema = { 448 | $defs: { // New name for `definitions` 449 | definitionType: { 450 | type: 'number' 451 | } 452 | }, 453 | type: 'array', 454 | items: { 455 | $ref: '#/$defs/definitionType' 456 | } 457 | } 458 | const expected = `/** 459 | * @typedef {array} 460 | */ 461 | ` 462 | expect(generate(schema)).toEqual(expected) 463 | }) 464 | 465 | it('Array with items', function () { 466 | const schema = { 467 | type: 'array', 468 | minItems: 3, 469 | items: [ 470 | { 471 | type: 'string' 472 | }, 473 | { 474 | type: 'object', 475 | properties: { 476 | aNestedProp: { 477 | description: 'Boolean desc.', 478 | type: 'boolean' 479 | } 480 | } 481 | }, 482 | { 483 | type: ['string', 'null'] 484 | }, 485 | { 486 | type: ['string', 'number'] 487 | }, 488 | { 489 | enum: ['hello', 'world'] 490 | }, 491 | { 492 | type: 'string', 493 | default: 'hello' 494 | } 495 | ] 496 | } 497 | const expected = `/** 498 | * @typedef {array} 499 | * @property {string} 0 500 | * @property {object} 1 501 | * @property {boolean} [1.aNestedProp] Boolean desc. 502 | * @property {?string} 2 503 | * @property {string|number} [3] 504 | * @property {enum} [4] 505 | * @property {string} [5="hello"] 506 | */ 507 | ` 508 | expect(generate(schema)).toEqual(expected) 509 | }) 510 | 511 | it('Array with untyped property', function () { 512 | const schema = { 513 | type: 'array', 514 | minItems: 1, 515 | items: [ 516 | { 517 | type: 'array', 518 | minItems: 2, 519 | items: [ 520 | { 521 | }, 522 | { 523 | type: 'number' 524 | } 525 | ] 526 | } 527 | ] 528 | } 529 | const expected = `/** 530 | * @typedef {array} 531 | * @property {array} 0 532 | * @property {*} 0.0 533 | * @property {number} 0.1 534 | */ 535 | ` 536 | expect(generate(schema)).toEqual(expected) 537 | }) 538 | 539 | it('Object with deep nesting', function () { 540 | const schema = { 541 | type: 'object', 542 | properties: { 543 | anObjectProp: { 544 | type: 'object', 545 | properties: { 546 | aNestedProp: { 547 | type: 'object', 548 | properties: { 549 | aDeeplyNestedProp: { 550 | type: 'number' 551 | } 552 | } 553 | } 554 | } 555 | } 556 | } 557 | } 558 | const expected = `/** 559 | * @typedef {object} 560 | * @property {object} [anObjectProp] 561 | * @property {object} [anObjectProp.aNestedProp] 562 | * @property {number} [anObjectProp.aNestedProp.aDeeplyNestedProp] 563 | */ 564 | ` 565 | expect(generate(schema)).toEqual(expected) 566 | }) 567 | }) 568 | 569 | describe('option: `autoDescribe`', function () { 570 | it('Simple object with `autoDescribe`: true', function () { 571 | const schema = { 572 | type: 'object' 573 | } 574 | const expected = `/** 575 | * Represents an object 576 | * @typedef {object} 577 | */ 578 | ` 579 | expect(generate(schema, { 580 | autoDescribe: true 581 | })).toEqual(expected) 582 | }) 583 | 584 | it('Object with `title` and `autoDescribe`: true', function () { 585 | const schema = { 586 | type: 'object', 587 | title: 'Title' 588 | } 589 | const expected = `/** 590 | * Represents a Title object 591 | * @typedef {object} Title 592 | */ 593 | ` 594 | expect(generate(schema, { 595 | autoDescribe: true 596 | })).toEqual(expected) 597 | }) 598 | }) 599 | 600 | describe('option: `autoDescriptionLineBreak`', () => { 601 | it('Simple object with `addDescriptionLineBreak`: true', function () { 602 | const schema = { 603 | type: 'object' 604 | } 605 | const expected = `/** 606 | * 607 | * @typedef {object} 608 | */ 609 | ` 610 | expect(generate(schema, { 611 | addDescriptionLineBreak: true 612 | })).toEqual(expected) 613 | }) 614 | }) 615 | 616 | describe('option: `types`', () => { 617 | it('Simple object with `types`: null', function () { 618 | const schema = { 619 | type: 'object' 620 | } 621 | const expected = `/** 622 | * @typedef 623 | */ 624 | ` 625 | expect(generate(schema, { 626 | types: null 627 | })).toEqual(expected) 628 | }) 629 | it('Simple object with empty string `types`', function () { 630 | const schema = { 631 | type: 'object' 632 | } 633 | const expected = `/** 634 | * @typedef {} 635 | */ 636 | ` 637 | expect(generate(schema, { 638 | types: { 639 | object: '' 640 | } 641 | })).toEqual(expected) 642 | }) 643 | it('Simple object with `types`', function () { 644 | const schema = { 645 | type: 'object' 646 | } 647 | const expected = `/** 648 | * @typedef {PlainObject} 649 | */ 650 | ` 651 | expect(generate(schema, { 652 | types: { 653 | object: 'PlainObject' 654 | } 655 | })).toEqual(expected) 656 | }) 657 | }) 658 | 659 | describe('option: `formats`', () => { 660 | it('Simple object with `formats`: null', function () { 661 | const schema = { 662 | type: 'object', 663 | format: 'special' 664 | } 665 | const expected = `/** 666 | * @typedef 667 | */ 668 | ` 669 | expect(generate(schema, { 670 | formats: null 671 | })).toEqual(expected) 672 | }) 673 | 674 | it('Simple object with `formats`: null for format', function () { 675 | const schema = { 676 | type: 'object', 677 | format: 'special' 678 | } 679 | const expected = `/** 680 | * @typedef 681 | */ 682 | ` 683 | expect(generate(schema, { 684 | formats: { 685 | special: null 686 | } 687 | })).toEqual(expected) 688 | }) 689 | 690 | it('Simple object with `formats`: null for type and format', function () { 691 | const schema = { 692 | type: 'object', 693 | format: 'special' 694 | } 695 | const expected = `/** 696 | * @typedef 697 | */ 698 | ` 699 | expect(generate(schema, { 700 | formats: { 701 | special: { 702 | object: null 703 | } 704 | } 705 | })).toEqual(expected) 706 | }) 707 | 708 | it('Simple object with empty string `formats`', function () { 709 | const schema = { 710 | type: 'object', 711 | format: 'special' 712 | } 713 | const expected = `/** 714 | * @typedef {} 715 | */ 716 | ` 717 | expect(generate(schema, { 718 | formats: { 719 | special: { 720 | object: '' 721 | } 722 | } 723 | })).toEqual(expected) 724 | }) 725 | it('Simple object with `formats`', function () { 726 | const schema = { 727 | type: 'object', 728 | format: 'special' 729 | } 730 | const expected = `/** 731 | * @typedef {PlainObject} 732 | */ 733 | ` 734 | expect(generate(schema, { 735 | formats: { 736 | special: { 737 | object: 'PlainObject' 738 | } 739 | } 740 | })).toEqual(expected) 741 | }) 742 | 743 | it('Object with properties using `formats`', function () { 744 | const schema = { 745 | type: 'object', 746 | properties: { 747 | anHTMLProp: { 748 | type: 'string', 749 | format: 'html' 750 | } 751 | } 752 | } 753 | const expected = `/** 754 | * @typedef {object} 755 | * @property {HTML} [anHTMLProp] 756 | */ 757 | ` 758 | expect(generate(schema, { 759 | formats: { 760 | html: { 761 | string: 'HTML' 762 | } 763 | } 764 | })).toEqual(expected) 765 | }) 766 | }) 767 | 768 | describe('option: `propertyNameAsType`', function () { 769 | it('Object with untyped property', function () { 770 | const schema = { 771 | type: 'object', 772 | properties: { 773 | anObjectProp: { 774 | type: 'object', 775 | properties: { 776 | aNestedProp: { 777 | }, 778 | anotherNestedProp: { 779 | type: 'number' 780 | } 781 | } 782 | } 783 | } 784 | } 785 | const expected = `/** 786 | * @typedef {object} 787 | * @property {object} [anObjectProp] 788 | * @property {aNestedProp} [anObjectProp.aNestedProp] 789 | * @property {number} [anObjectProp.anotherNestedProp] 790 | */ 791 | ` 792 | expect(generate(schema, { 793 | propertyNameAsType: true 794 | })).toEqual(expected) 795 | }) 796 | }) 797 | 798 | describe('option: `capitalizeProperty`', function () { 799 | it('Object with untyped property', function () { 800 | const schema = { 801 | type: 'object', 802 | properties: { 803 | anObjectProp: { 804 | type: 'object', 805 | properties: { 806 | aNestedProp: { 807 | }, 808 | anotherNestedProp: { 809 | type: 'number' 810 | } 811 | } 812 | } 813 | } 814 | } 815 | const expected = `/** 816 | * @typedef {object} 817 | * @property {object} [anObjectProp] 818 | * @property {ANestedProp} [anObjectProp.aNestedProp] 819 | * @property {number} [anObjectProp.anotherNestedProp] 820 | */ 821 | ` 822 | expect(generate(schema, { 823 | propertyNameAsType: true, 824 | capitalizeProperty: true 825 | })).toEqual(expected) 826 | }) 827 | }) 828 | 829 | describe('option: `capitalizeTitle`', () => { 830 | it('Simple object with title and `capitalizeTitle`: true', function () { 831 | const schema = { 832 | title: 'special', 833 | type: 'object' 834 | } 835 | const expected = `/** 836 | * @typedef {object} Special 837 | */ 838 | ` 839 | expect(generate(schema, { 840 | capitalizeTitle: true 841 | })).toEqual(expected) 842 | }) 843 | }) 844 | 845 | describe('option: `indent`', () => { 846 | it('Object with properties with space indent', function () { 847 | const schema = { 848 | type: 'object', 849 | properties: { 850 | aStringProp: { 851 | type: 'string' 852 | }, 853 | anObjectProp: { 854 | type: 'object', 855 | properties: { 856 | aNestedProp: { 857 | description: 'Boolean desc.', 858 | type: 'boolean' 859 | } 860 | } 861 | }, 862 | nullableType: { 863 | type: ['string', 'null'] 864 | }, 865 | multipleTypes: { 866 | type: ['string', 'number'] 867 | }, 868 | enumProp: { 869 | enum: ['hello', 'world'] 870 | } 871 | } 872 | } 873 | const spaces = ' ' 874 | const expected = `${spaces}/** 875 | ${spaces} * @typedef {object} 876 | ${spaces} * @property {string} [aStringProp] 877 | ${spaces} * @property {object} [anObjectProp] 878 | ${spaces} * @property {boolean} [anObjectProp.aNestedProp] Boolean desc. 879 | ${spaces} * @property {?string} [nullableType] 880 | ${spaces} * @property {string|number} [multipleTypes] 881 | ${spaces} * @property {enum} [enumProp] 882 | ${spaces} */ 883 | ` 884 | expect(generate(schema, { 885 | indent: 3 886 | })).toEqual(expected) 887 | }) 888 | 889 | it('Object with properties with tab indent', function () { 890 | const schema = { 891 | type: 'object', 892 | properties: { 893 | aStringProp: { 894 | type: 'string' 895 | }, 896 | anObjectProp: { 897 | type: 'object', 898 | properties: { 899 | aNestedProp: { 900 | description: 'Boolean desc.', 901 | type: 'boolean' 902 | } 903 | } 904 | }, 905 | nullableType: { 906 | type: ['string', 'null'] 907 | }, 908 | multipleTypes: { 909 | type: ['string', 'number'] 910 | }, 911 | enumProp: { 912 | enum: ['hello', 'world'] 913 | } 914 | } 915 | } 916 | const tabs = '\t\t\t' 917 | const expected = `${tabs}/** 918 | ${tabs} * @typedef {object} 919 | ${tabs} * @property {string} [aStringProp] 920 | ${tabs} * @property {object} [anObjectProp] 921 | ${tabs} * @property {boolean} [anObjectProp.aNestedProp] Boolean desc. 922 | ${tabs} * @property {?string} [nullableType] 923 | ${tabs} * @property {string|number} [multipleTypes] 924 | ${tabs} * @property {enum} [enumProp] 925 | ${tabs} */ 926 | ` 927 | expect(generate(schema, { 928 | indentChar: '\t', 929 | indent: 3 930 | })).toEqual(expected) 931 | }) 932 | }) 933 | 934 | describe('option: `descriptionPlaceholder`', () => { 935 | it('Object with properties (with true `descriptionPlaceholder`)', function () { 936 | const schema = { 937 | type: 'object', 938 | properties: { 939 | aStringProp: { 940 | type: 'string' 941 | }, 942 | anObjectProp: { 943 | type: 'object', 944 | properties: { 945 | aNestedProp: { 946 | description: 'Boolean desc.', 947 | type: 'boolean' 948 | } 949 | } 950 | }, 951 | nullableType: { 952 | type: ['string', 'null'] 953 | }, 954 | multipleTypes: { 955 | type: ['string', 'number'] 956 | }, 957 | enumProp: { 958 | enum: ['hello', 'world'] 959 | } 960 | } 961 | } 962 | const expected = `/** 963 | * @typedef {object} 964 | * @property {string} [aStringProp]${trailingSpace} 965 | * @property {object} [anObjectProp]${trailingSpace} 966 | * @property {boolean} [anObjectProp.aNestedProp] Boolean desc. 967 | * @property {?string} [nullableType]${trailingSpace} 968 | * @property {string|number} [multipleTypes]${trailingSpace} 969 | * @property {enum} [enumProp]${trailingSpace} 970 | */ 971 | ` 972 | expect(generate(schema, { 973 | descriptionPlaceholder: true 974 | })).toEqual(expected) 975 | }) 976 | }) 977 | 978 | describe('option: `hyphenatedDescriptions`', () => { 979 | it('Object with properties (with true `hyphenatedDescriptions`)', function () { 980 | const schema = { 981 | type: 'object', 982 | properties: { 983 | aStringProp: { 984 | type: 'string' 985 | }, 986 | anObjectProp: { 987 | type: 'object', 988 | properties: { 989 | aNestedProp: { 990 | description: 'Boolean desc.', 991 | type: 'boolean' 992 | } 993 | } 994 | }, 995 | nullableType: { 996 | type: ['string', 'null'] 997 | }, 998 | multipleTypes: { 999 | type: ['string', 'number'] 1000 | }, 1001 | enumProp: { 1002 | enum: ['hello', 'world'] 1003 | } 1004 | } 1005 | } 1006 | const expected = `/** 1007 | * @typedef {object} 1008 | * @property {string} [aStringProp] 1009 | * @property {object} [anObjectProp] 1010 | * @property {boolean} [anObjectProp.aNestedProp] - Boolean desc. 1011 | * @property {?string} [nullableType] 1012 | * @property {string|number} [multipleTypes] 1013 | * @property {enum} [enumProp] 1014 | */ 1015 | ` 1016 | expect(generate(schema, { 1017 | hyphenatedDescriptions: true 1018 | })).toEqual(expected) 1019 | }) 1020 | }) 1021 | 1022 | describe('option: `ignore`', () => { 1023 | it('Object with properties and `ignore` option', function () { 1024 | const schema = { 1025 | type: 'object', 1026 | properties: { 1027 | aStringProp: { 1028 | type: 'string' 1029 | }, 1030 | anObjectProp: { 1031 | type: 'object', 1032 | properties: { 1033 | aNestedProp: { 1034 | type: 'boolean' 1035 | } 1036 | } 1037 | } 1038 | } 1039 | } 1040 | const expected = `/** 1041 | * @typedef {object} 1042 | * @property {string} [aStringProp] 1043 | */ 1044 | ` 1045 | expect(generate(schema, { 1046 | ignore: ['anObjectProp'] 1047 | })).toEqual(expected) 1048 | }) 1049 | }) 1050 | 1051 | describe('option `defaultPropertyType`', function () { 1052 | it('Object with untyped property and "JSON" `defaultPropertyType`', function () { 1053 | const schema = { 1054 | type: 'object', 1055 | properties: { 1056 | anObjectProp: { 1057 | type: 'object', 1058 | properties: { 1059 | aNestedProp: { 1060 | }, 1061 | anotherNestedProp: { 1062 | type: 'number' 1063 | } 1064 | } 1065 | } 1066 | } 1067 | } 1068 | const expected = `/** 1069 | * @typedef {object} 1070 | * @property {object} [anObjectProp] 1071 | * @property {JSON} [anObjectProp.aNestedProp] 1072 | * @property {number} [anObjectProp.anotherNestedProp] 1073 | */ 1074 | ` 1075 | expect(generate(schema, { 1076 | defaultPropertyType: 'JSON' 1077 | })).toEqual(expected) 1078 | }) 1079 | 1080 | it('Object with untyped property and `null` `defaultPropertyType`', function () { 1081 | const schema = { 1082 | type: 'object', 1083 | properties: { 1084 | anObjectProp: { 1085 | type: 'object', 1086 | properties: { 1087 | aNestedProp: { 1088 | }, 1089 | anotherNestedProp: { 1090 | type: 'number' 1091 | } 1092 | } 1093 | } 1094 | } 1095 | } 1096 | const expected = `/** 1097 | * @typedef {object} 1098 | * @property {object} [anObjectProp] 1099 | * @property [anObjectProp.aNestedProp] 1100 | * @property {number} [anObjectProp.anotherNestedProp] 1101 | */ 1102 | ` 1103 | expect(generate(schema, { 1104 | defaultPropertyType: null 1105 | })).toEqual(expected) 1106 | }) 1107 | }) 1108 | 1109 | describe('option: `maxLength`', () => { 1110 | it('Simple object with description and `maxLength`', function () { 1111 | const schema = { 1112 | type: 'object', 1113 | properties: { 1114 | aShortStringProp: { 1115 | description: 'A short description', 1116 | type: 'string' 1117 | }, 1118 | aStringProp: { 1119 | description: 'This is a very, very, very, very, very, very, very, very, very, very, very long description on the property.', 1120 | type: 'string' 1121 | }, 1122 | aNonBreakingStringProp: { 1123 | description: 'https://example.com/a/very/very/very/very/very/very/very/very/long/nonbreaking/string', 1124 | type: 'string' 1125 | }, 1126 | aLongStringBreakingAtEnd: { 1127 | description: 'https://example.com/another/very/very/very/very/very/very/very/lng/string breaking at end', 1128 | type: 'string' 1129 | } 1130 | }, 1131 | description: 'This is a very, very, very, very, very, very, very, very, very, very, very long description.' 1132 | } 1133 | const indent = ' ' 1134 | const expected = `${indent}/** 1135 | ${indent} * This is a very, very, very, very, very, very, very, very, very, very, 1136 | ${indent} * very long description. 1137 | ${indent} * @typedef {object} 1138 | ${indent} * @property {string} [aShortStringProp] A short description 1139 | ${indent} * @property {string} [aStringProp] This is a very, very, very, very, very, 1140 | ${indent} * very, very, very, very, very, very long description on the property. 1141 | ${indent} * @property {string} [aNonBreakingStringProp] 1142 | ${indent} * https://example.com/a/very/very/very/very/very/very/very/very/long/nonbreaking/string 1143 | ${indent} * @property {string} [aLongStringBreakingAtEnd] 1144 | ${indent} * https://example.com/another/very/very/very/very/very/very/very/lng/string 1145 | ${indent} * breaking at end 1146 | ${indent} */ 1147 | ` 1148 | expect(generate(schema, { 1149 | indent: 4, 1150 | maxLength: 80 1151 | })).toEqual(expected) 1152 | 1153 | const expectedNowrapping = `${indent}/** 1154 | ${indent} * This is a very, very, very, very, very, very, very, very, very, very, very long description. 1155 | ${indent} * @typedef {object} 1156 | ${indent} * @property {string} [aShortStringProp] A short description 1157 | ${indent} * @property {string} [aStringProp] This is a very, very, very, very, very, very, very, very, very, very, very long description on the property. 1158 | ${indent} * @property {string} [aNonBreakingStringProp] https://example.com/a/very/very/very/very/very/very/very/very/long/nonbreaking/string 1159 | ${indent} * @property {string} [aLongStringBreakingAtEnd] https://example.com/another/very/very/very/very/very/very/very/lng/string breaking at end 1160 | ${indent} */ 1161 | ` 1162 | 1163 | expect(generate(schema, { 1164 | indent: 4 1165 | })).toEqual(expectedNowrapping) 1166 | }) 1167 | }) 1168 | 1169 | describe('Examples', () => { 1170 | it('No options', () => { 1171 | const expected = `/** 1172 | * @typedef {object} Person 1173 | * @property {string} name A person's name 1174 | * @property {integer} [age] A person's age 1175 | */ 1176 | ` 1177 | const result = jsdoc(schema) 1178 | expect(result).toEqual(expected) 1179 | }) 1180 | 1181 | it('`hyphenatedDescriptions`', () => { 1182 | const expected = `/** 1183 | * @typedef {object} Person 1184 | * @property {string} name - A person's name 1185 | * @property {integer} [age] - A person's age 1186 | */ 1187 | ` 1188 | const result = jsdoc(schema, { 1189 | hyphenatedDescriptions: true 1190 | }) 1191 | expect(result).toEqual(expected) 1192 | }) 1193 | 1194 | it('`autoDescribe`', () => { 1195 | const expected = `/** 1196 | * Represents a Person object 1197 | * @typedef {object} Person 1198 | * @property {string} name A person's name 1199 | * @property {integer} [age] A person's age 1200 | */ 1201 | ` 1202 | const result = jsdoc(schema, { 1203 | autoDescribe: true 1204 | }) 1205 | expect(result).toEqual(expected) 1206 | }) 1207 | 1208 | it('`types`', () => { 1209 | const expected = `/** 1210 | * @typedef {PlainObject} Person 1211 | * @property {string} name A person's name 1212 | * @property {integer} [age] A person's age 1213 | */ 1214 | ` 1215 | const result = jsdoc(schema, { 1216 | types: { 1217 | object: 'PlainObject' 1218 | } 1219 | }) 1220 | expect(result).toEqual(expected) 1221 | }) 1222 | 1223 | it('`formats`', () => { 1224 | const schema = { 1225 | title: 'Info', 1226 | type: 'object', 1227 | properties: { 1228 | code: { 1229 | type: 'string', format: 'html', description: 'The HTML source' 1230 | } 1231 | }, 1232 | required: ['code'] 1233 | } 1234 | 1235 | const expected = `/** 1236 | * @typedef {object} Info 1237 | * @property {HTML} code The HTML source 1238 | */ 1239 | ` 1240 | const result = jsdoc(schema, { 1241 | formats: { 1242 | html: { 1243 | string: 'HTML' 1244 | } 1245 | } 1246 | }) 1247 | expect(result).toEqual(expected) 1248 | }) 1249 | }) 1250 | --------------------------------------------------------------------------------