Skip to content
This repository was archived by the owner on Jun 22, 2023. It is now read-only.

Commit a89ffe1

Browse files
author
Boris Cherny
committed
Add reasonable max for tuple types (fix bcherny#438)
1 parent 4837ef2 commit a89ffe1

18 files changed

+2098
-828
lines changed

README.md

+1
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,7 @@ See [server demo](example) and [browser demo](https://github.com/bcherny/json-sc
9090
| enableConstEnums | boolean | `true` | Prepend enums with [`const`](https://www.typescriptlang.org/docs/handbook/enums.html#computed-and-constant-members)? |
9191
| format | boolean | `true` | Format code? Set this to `false` to improve performance. |
9292
| ignoreMinAndMaxItems | boolean | `false` | Ignore maxItems and minItems for `array` types, preventing tuples being generated. |
93+
| 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`.
9394
| 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 |
9495
| unknownAny | boolean | `true` | Use `unknown` instead of `any` where possible |
9596
| unreachableDefinitions | boolean | `false` | Generates code for `definitions` that aren't referenced by the schema. |

src/cli.ts

+5
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,11 @@ Boolean values can be set to false using the 'no-' prefix.
161161
Prepend enums with 'const'?
162162
--format
163163
Format code? Set this to false to improve performance.
164+
--maxItems
165+
Maximum number of unioned tuples to emit when representing bounded-size
166+
array types, before falling back to emitting unbounded arrays. Increase
167+
this to improve precision of emitted types, decrease it to improve
168+
performance, or set it to -1 to ignore minItems and maxItems.
164169
--style.XXX=YYY
165170
Prettier configuration
166171
--unknownAny

src/index.ts

+11
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import {error, stripExtension, Try, log} from './utils'
1414
import {validate} from './validator'
1515
import {isDeepStrictEqual} from 'util'
1616
import {link} from './linker'
17+
import {validateOptions} from './optionValidator'
1718

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

@@ -50,6 +51,13 @@ export interface Options {
5051
* Ignore maxItems and minItems for `array` types, preventing tuples being generated.
5152
*/
5253
ignoreMinAndMaxItems: boolean
54+
/**
55+
* Maximum number of unioned tuples to emit when representing bounded-size array types,
56+
* before falling back to emitting unbounded arrays. Increase this to improve precision
57+
* of emitted types, decrease it to improve performance, or set it to `-1` to ignore
58+
* `minItems` and `maxItems`.
59+
*/
60+
maxItems: number
5361
/**
5462
* Append all index signatures with `| undefined` so that they are strictly typed.
5563
*
@@ -84,6 +92,7 @@ export const DEFAULT_OPTIONS: Options = {
8492
enableConstEnums: true,
8593
format: true,
8694
ignoreMinAndMaxItems: false,
95+
maxItems: 20,
8796
strictIndexSignatures: false,
8897
style: {
8998
bracketSpacing: false,
@@ -115,6 +124,8 @@ export function compileFromFile(filename: string, options: Partial<Options> = DE
115124
}
116125

117126
export async function compile(schema: JSONSchema4, name: string, options: Partial<Options> = {}): Promise<string> {
127+
validateOptions(options)
128+
118129
const _options = merge({}, DEFAULT_OPTIONS, options)
119130

120131
const start = Date.now()

src/normalizer.ts

+41-12
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import {JSONSchemaTypeName, LinkedJSONSchema, NormalizedJSONSchema, Parent} from './types/JSONSchema'
2-
import {escapeBlockComment, justName, toSafeString, traverse} from './utils'
2+
import {appendToDescription, escapeBlockComment, justName, toSafeString, traverse} from './utils'
33
import {Options} from './'
44

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

60-
rules.set('Escape closing JSDoc Comment', schema => {
60+
rules.set('Escape closing JSDoc comment', schema => {
6161
escapeBlockComment(schema)
6262
})
6363

64+
rules.set('Add JSDoc comments for minItems and maxItems', schema => {
65+
if (!isArrayType(schema)) {
66+
return
67+
}
68+
const commentsToAppend = [
69+
'minItems' in schema ? `@minItems ${schema.minItems}` : '',
70+
'maxItems' in schema ? `@maxItems ${schema.maxItems}` : ''
71+
].filter(Boolean)
72+
if (commentsToAppend.length) {
73+
schema.description = appendToDescription(schema.description, ...commentsToAppend)
74+
}
75+
})
76+
6477
rules.set('Optionally remove maxItems and minItems', (schema, _fileName, options) => {
65-
if (options.ignoreMinAndMaxItems) {
66-
if ('maxItems' in schema) {
67-
delete schema.maxItems
68-
}
69-
if ('minItems' in schema) {
70-
delete schema.minItems
71-
}
78+
if (!isArrayType(schema)) {
79+
return
80+
}
81+
if ('minItems' in schema && options.ignoreMinAndMaxItems) {
82+
delete schema.minItems
83+
}
84+
if ('maxItems' in schema && (options.ignoreMinAndMaxItems || options.maxItems === -1)) {
85+
delete schema.maxItems
7286
}
7387
})
7488

@@ -77,13 +91,28 @@ rules.set('Normalize schema.minItems', (schema, _fileName, options) => {
7791
return
7892
}
7993
// make sure we only add the props onto array types
80-
if (isArrayType(schema)) {
81-
const {minItems} = schema
82-
schema.minItems = typeof minItems === 'number' ? minItems : 0
94+
if (!isArrayType(schema)) {
95+
return
8396
}
97+
const {minItems} = schema
98+
schema.minItems = typeof minItems === 'number' ? minItems : 0
8499
// cannot normalize maxItems because maxItems = 0 has an actual meaning
85100
})
86101

102+
rules.set('Remove maxItems if it is big enough to likely cause OOMs', (schema, _fileName, options) => {
103+
if (options.ignoreMinAndMaxItems || options.maxItems === -1) {
104+
return
105+
}
106+
if (!isArrayType(schema)) {
107+
return
108+
}
109+
const {maxItems, minItems} = schema
110+
// minItems is guaranteed to be a number after the previous rule runs
111+
if (maxItems !== undefined && maxItems - (minItems as number) > options.maxItems) {
112+
delete schema.maxItems
113+
}
114+
})
115+
87116
rules.set('Normalize schema.items', (schema, _fileName, options) => {
88117
if (options.ignoreMinAndMaxItems) {
89118
return

src/optionValidator.ts

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import {Options} from '.'
2+
3+
export function validateOptions({maxItems}: Partial<Options>): void {
4+
if (maxItems !== undefined && maxItems < -1) {
5+
throw RangeError(`Expected options.maxItems to be >= -1, but was given ${maxItems}.`)
6+
}
7+
}

src/utils.ts

+7
Original file line numberDiff line numberDiff line change
@@ -346,3 +346,10 @@ export function maybeStripNameHints(schema: JSONSchema): JSONSchema {
346346
}
347347
return schema
348348
}
349+
350+
export function appendToDescription(existingDescription: string | undefined, ...values: string[]): string {
351+
if (existingDescription) {
352+
return `${existingDescription}\n\n${values.join('\n')}`
353+
}
354+
return values.join('\n')
355+
}

0 commit comments

Comments
 (0)