Skip to content

Commit

Permalink
Add reasonable max for tuple types (fix #438)
Browse files Browse the repository at this point in the history
  • Loading branch information
Boris Cherny committed May 22, 2022
1 parent 4837ef2 commit a89ffe1
Show file tree
Hide file tree
Showing 18 changed files with 2,098 additions and 828 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ See [server demo](example) and [browser demo](https://github.com/bcherny/json-sc
| enableConstEnums | boolean | `true` | Prepend enums with [`const`](https://www.typescriptlang.org/docs/handbook/enums.html#computed-and-constant-members)? |
| format | boolean | `true` | Format code? Set this to `false` to improve performance. |
| ignoreMinAndMaxItems | boolean | `false` | Ignore maxItems and minItems for `array` types, preventing tuples being generated. |
| maxItems | number | `20` | Maximum number of unioned tuples to emit when representing bounded-size array types, before falling back to emitting unbounded arrays. Increase this to improve precision of emitted types, decrease it to improve performance, or set it to `-1` to ignore `maxItems`.
| style | object | `{ bracketSpacing: false, printWidth: 120, semi: true, singleQuote: false, tabWidth: 2, trailingComma: 'none', useTabs: false }` | A [Prettier](https://prettier.io/docs/en/options.html) configuration |
| unknownAny | boolean | `true` | Use `unknown` instead of `any` where possible |
| unreachableDefinitions | boolean | `false` | Generates code for `definitions` that aren't referenced by the schema. |
Expand Down
5 changes: 5 additions & 0 deletions src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,11 @@ Boolean values can be set to false using the 'no-' prefix.
Prepend enums with 'const'?
--format
Format code? Set this to false to improve performance.
--maxItems
Maximum number of unioned tuples to emit when representing bounded-size
array types, before falling back to emitting unbounded arrays. Increase
this to improve precision of emitted types, decrease it to improve
performance, or set it to -1 to ignore minItems and maxItems.
--style.XXX=YYY
Prettier configuration
--unknownAny
Expand Down
11 changes: 11 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {error, stripExtension, Try, log} from './utils'
import {validate} from './validator'
import {isDeepStrictEqual} from 'util'
import {link} from './linker'
import {validateOptions} from './optionValidator'

export {EnumJSONSchema, JSONSchema, NamedEnumJSONSchema, CustomTypeJSONSchema} from './types/JSONSchema'

Expand Down Expand Up @@ -50,6 +51,13 @@ export interface Options {
* Ignore maxItems and minItems for `array` types, preventing tuples being generated.
*/
ignoreMinAndMaxItems: boolean
/**
* Maximum number of unioned tuples to emit when representing bounded-size array types,
* before falling back to emitting unbounded arrays. Increase this to improve precision
* of emitted types, decrease it to improve performance, or set it to `-1` to ignore
* `minItems` and `maxItems`.
*/
maxItems: number
/**
* Append all index signatures with `| undefined` so that they are strictly typed.
*
Expand Down Expand Up @@ -84,6 +92,7 @@ export const DEFAULT_OPTIONS: Options = {
enableConstEnums: true,
format: true,
ignoreMinAndMaxItems: false,
maxItems: 20,
strictIndexSignatures: false,
style: {
bracketSpacing: false,
Expand Down Expand Up @@ -115,6 +124,8 @@ export function compileFromFile(filename: string, options: Partial<Options> = DE
}

export async function compile(schema: JSONSchema4, name: string, options: Partial<Options> = {}): Promise<string> {
validateOptions(options)

const _options = merge({}, DEFAULT_OPTIONS, options)

const start = Date.now()
Expand Down
53 changes: 41 additions & 12 deletions src/normalizer.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import {JSONSchemaTypeName, LinkedJSONSchema, NormalizedJSONSchema, Parent} from './types/JSONSchema'
import {escapeBlockComment, justName, toSafeString, traverse} from './utils'
import {appendToDescription, escapeBlockComment, justName, toSafeString, traverse} from './utils'
import {Options} from './'

type Rule = (schema: LinkedJSONSchema, fileName: string, options: Options) => void
Expand Down Expand Up @@ -57,18 +57,32 @@ rules.set('Default top level `id`', (schema, fileName) => {
}
})

rules.set('Escape closing JSDoc Comment', schema => {
rules.set('Escape closing JSDoc comment', schema => {
escapeBlockComment(schema)
})

rules.set('Add JSDoc comments for minItems and maxItems', schema => {
if (!isArrayType(schema)) {
return
}
const commentsToAppend = [
'minItems' in schema ? `@minItems ${schema.minItems}` : '',
'maxItems' in schema ? `@maxItems ${schema.maxItems}` : ''
].filter(Boolean)
if (commentsToAppend.length) {
schema.description = appendToDescription(schema.description, ...commentsToAppend)
}
})

rules.set('Optionally remove maxItems and minItems', (schema, _fileName, options) => {
if (options.ignoreMinAndMaxItems) {
if ('maxItems' in schema) {
delete schema.maxItems
}
if ('minItems' in schema) {
delete schema.minItems
}
if (!isArrayType(schema)) {
return
}
if ('minItems' in schema && options.ignoreMinAndMaxItems) {
delete schema.minItems
}
if ('maxItems' in schema && (options.ignoreMinAndMaxItems || options.maxItems === -1)) {
delete schema.maxItems
}
})

Expand All @@ -77,13 +91,28 @@ rules.set('Normalize schema.minItems', (schema, _fileName, options) => {
return
}
// make sure we only add the props onto array types
if (isArrayType(schema)) {
const {minItems} = schema
schema.minItems = typeof minItems === 'number' ? minItems : 0
if (!isArrayType(schema)) {
return
}
const {minItems} = schema
schema.minItems = typeof minItems === 'number' ? minItems : 0
// cannot normalize maxItems because maxItems = 0 has an actual meaning
})

rules.set('Remove maxItems if it is big enough to likely cause OOMs', (schema, _fileName, options) => {
if (options.ignoreMinAndMaxItems || options.maxItems === -1) {
return
}
if (!isArrayType(schema)) {
return
}
const {maxItems, minItems} = schema
// minItems is guaranteed to be a number after the previous rule runs
if (maxItems !== undefined && maxItems - (minItems as number) > options.maxItems) {
delete schema.maxItems
}
})

rules.set('Normalize schema.items', (schema, _fileName, options) => {
if (options.ignoreMinAndMaxItems) {
return
Expand Down
7 changes: 7 additions & 0 deletions src/optionValidator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import {Options} from '.'

export function validateOptions({maxItems}: Partial<Options>): void {
if (maxItems !== undefined && maxItems < -1) {
throw RangeError(`Expected options.maxItems to be >= -1, but was given ${maxItems}.`)
}
}
7 changes: 7 additions & 0 deletions src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -346,3 +346,10 @@ export function maybeStripNameHints(schema: JSONSchema): JSONSchema {
}
return schema
}

export function appendToDescription(existingDescription: string | undefined, ...values: string[]): string {
if (existingDescription) {
return `${existingDescription}\n\n${values.join('\n')}`
}
return values.join('\n')
}
Loading

0 comments on commit a89ffe1

Please sign in to comment.