json-schema-to-typescript
Advanced tools
Comparing version 6.1.3 to 7.0.0
# Changelog | ||
## 7.0.0 | ||
- b9c4bcb Add support for `additionalItems` for tuple types | ||
- c5f4f03 Add support for `minItems` and `maxItems` | ||
## 6.1.0 | ||
@@ -4,0 +9,0 @@ |
@@ -39,2 +39,3 @@ #!/usr/bin/env node | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
var cli_color_1 = require("cli-color"); | ||
var minimist = require("minimist"); | ||
@@ -45,3 +46,2 @@ var fs_1 = require("mz/fs"); | ||
var index_1 = require("./index"); | ||
var cli_color_1 = require("cli-color"); | ||
main(minimist(process.argv.slice(2), { | ||
@@ -48,0 +48,0 @@ alias: { |
@@ -35,3 +35,7 @@ "use strict"; | ||
case 'TUPLE': | ||
return ast.params.reduce(function (prev, ast) { return prev + declareEnums(ast, options, processed); }, ''); | ||
type = ast.params.reduce(function (prev, ast) { return prev + declareEnums(ast, options, processed); }, ''); | ||
if (ast.spreadParam) { | ||
type += declareEnums(ast.spreadParam, options, processed); | ||
} | ||
break; | ||
case 'INTERFACE': | ||
@@ -70,2 +74,5 @@ type = getSuperTypesAndParams(ast).reduce(function (prev, ast) { | ||
type = ast.params.map(function (_) { return declareNamedInterfaces(_, options, rootASTName, processed); }).filter(Boolean).join('\n'); | ||
if (ast.type === 'TUPLE' && ast.spreadParam) { | ||
type += declareNamedInterfaces(ast.spreadParam, options, rootASTName, processed); | ||
} | ||
break; | ||
@@ -105,3 +112,4 @@ default: | ||
AST_1.hasStandaloneName(ast) ? generateStandaloneType(ast, options) : undefined, | ||
ast.params.map(function (ast) { return declareNamedTypes(ast, options, rootASTName, processed); }).filter(Boolean).join('\n') | ||
ast.params.map(function (ast) { return declareNamedTypes(ast, options, rootASTName, processed); }).filter(Boolean).join('\n'), | ||
('spreadParam' in ast && ast.spreadParam) ? declareNamedTypes(ast.spreadParam, options, rootASTName, processed) : undefined | ||
].filter(Boolean).join('\n'); | ||
@@ -136,5 +144,69 @@ break; | ||
case 'STRING': return 'string'; | ||
case 'TUPLE': return '[' | ||
+ ast.params.map(function (_) { return generateType(_, options); }).join(', ') | ||
+ ']'; | ||
case 'TUPLE': return (function () { | ||
var minItems = ast.minItems; | ||
var maxItems = ast.maxItems || -1; | ||
var spreadParam = ast.spreadParam; | ||
var astParams = ast.params.slice(); | ||
if (minItems > 0 && minItems > astParams.length && ast.spreadParam === undefined) { | ||
// this is a valid state, and JSONSchema doesn't care about the item type | ||
if (maxItems < 0) { | ||
// no max items and no spread param, so just spread any | ||
spreadParam = AST_1.T_ANY; | ||
} | ||
} | ||
if (maxItems > astParams.length && ast.spreadParam === undefined) { | ||
// this is a valid state, and JSONSchema doesn't care about the item type | ||
// fill the tuple with any elements | ||
for (var i = astParams.length; i < maxItems; i += 1) { | ||
astParams.push(AST_1.T_ANY); | ||
} | ||
} | ||
function addSpreadParam(params) { | ||
if (spreadParam) { | ||
var spread = '...(' + generateType(spreadParam, options) + ')[]'; | ||
params.push(spread); | ||
} | ||
return params; | ||
} | ||
function paramsToString(params) { | ||
return '[' + params.join(', ') + ']'; | ||
} | ||
var paramsList = astParams | ||
.map(function (param) { return generateType(param, options); }); | ||
if (paramsList.length > minItems) { | ||
/* | ||
if there are more items than the min, we return a union of tuples instead of | ||
using the optional element operator. This is done because it is more typesafe. | ||
// optional element operator | ||
type A = [string, string?, string?] | ||
const a: A = ['a', undefined, 'c'] // no error | ||
// union of tuples | ||
type B = [string] | [string, string] | [string, string, string] | ||
const b: B = ['a', undefined, 'c'] // TS error | ||
*/ | ||
var cumulativeParamsList = paramsList.slice(0, minItems); | ||
var typesToUnion = []; | ||
if (cumulativeParamsList.length > 0) { | ||
// actually has minItems, so add the initial state | ||
typesToUnion.push(paramsToString(cumulativeParamsList)); | ||
} | ||
else { | ||
// no minItems means it's acceptable to have an empty tuple type | ||
typesToUnion.push(paramsToString([])); | ||
} | ||
for (var i = minItems; i < paramsList.length; i += 1) { | ||
cumulativeParamsList.push(paramsList[i]); | ||
if (i === paramsList.length - 1) { | ||
// only the last item in the union should have the spread parameter | ||
addSpreadParam(cumulativeParamsList); | ||
} | ||
typesToUnion.push(paramsToString(cumulativeParamsList)); | ||
} | ||
return typesToUnion.join('|'); | ||
} | ||
// no max items so only need to return one type | ||
return paramsToString(addSpreadParam(paramsList)); | ||
})(); | ||
case 'UNION': return generateSetOperation(ast, options); | ||
@@ -141,0 +213,0 @@ case 'CUSTOM_TYPE': return ast.params; |
import { JSONSchema4 } from 'json-schema'; | ||
import { Options as $RefOptions } from 'json-schema-ref-parser'; | ||
import { Options as PrettierOptions } from 'prettier'; | ||
import { Options as $RefOptions } from 'json-schema-ref-parser'; | ||
export { EnumJSONSchema, JSONSchema, NamedEnumJSONSchema, CustomTypeJSONSchema } from './types/JSONSchema'; | ||
@@ -5,0 +5,0 @@ export interface Options { |
@@ -74,2 +74,3 @@ "use strict"; | ||
exports.DEFAULT_OPTIONS = { | ||
$refOptions: {}, | ||
bannerComment: "/* tslint:disable */\n/**\n* This file was automatically generated by json-schema-to-typescript.\n* DO NOT MODIFY IT BY HAND. Instead, modify the source JSONSchema file,\n* and run json-schema-to-typescript to regenerate this file.\n*/", | ||
@@ -88,4 +89,3 @@ cwd: process.cwd(), | ||
}, | ||
unreachableDefinitions: false, | ||
$refOptions: {} | ||
unreachableDefinitions: false | ||
}; | ||
@@ -92,0 +92,0 @@ function compileFromFile(filename, options) { |
"use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
var cli_color_1 = require("cli-color"); | ||
var stringify = require("json-stringify-safe"); | ||
var lodash_1 = require("lodash"); | ||
var utils_1 = require("./utils"); | ||
var stringify = require("json-stringify-safe"); | ||
var rules = new Map(); | ||
function hasType(schema, type) { | ||
return schema.type === type || (Array.isArray(schema.type) && schema.type.includes(type)); | ||
} | ||
function isObjectType(schema) { | ||
return schema.properties !== undefined || hasType(schema, 'object') || hasType(schema, 'any'); | ||
} | ||
function isArrayType(schema) { | ||
return schema.items !== undefined || hasType(schema, 'array') || hasType(schema, 'any'); | ||
} | ||
rules.set('Destructure unary types', function (schema) { | ||
@@ -12,22 +21,20 @@ if (schema.type && Array.isArray(schema.type) && schema.type.length === 1) { | ||
} | ||
return schema; | ||
}); | ||
rules.set('Add empty `required` property if none is defined', function (schema, rootSchema) { | ||
if (stringify(schema) === stringify(rootSchema) && !('required' in schema)) { | ||
rules.set('Add empty `required` property if none is defined', function (schema) { | ||
if (!('required' in schema) && isObjectType(schema)) { | ||
schema.required = []; | ||
} | ||
return schema; | ||
}); | ||
rules.set('Transform `required`=false to `required`=[]', function (schema, rootSchema) { | ||
if (stringify(schema) === stringify(rootSchema) && schema.required === false) { | ||
rules.set('Transform `required`=false to `required`=[]', function (schema) { | ||
if (schema.required === false) { | ||
schema.required = []; | ||
} | ||
return schema; | ||
}); | ||
// TODO: default to empty schema (as per spec) instead | ||
rules.set('Default additionalProperties to true', function (schema, rootSchema) { | ||
if (stringify(schema) === stringify(rootSchema) && !('additionalProperties' in schema)) { | ||
rules.set('Default additionalProperties to true', function (schema) { | ||
if (!('additionalProperties' in schema) && | ||
isObjectType(schema) && | ||
schema.patternProperties === undefined) { | ||
schema.additionalProperties = true; | ||
} | ||
return schema; | ||
}); | ||
@@ -38,6 +45,33 @@ rules.set('Default top level `id`', function (schema, rootSchema, fileName) { | ||
} | ||
return schema; | ||
}); | ||
rules.set('Escape closing JSDoc Comment', function (schema) { | ||
utils_1.escapeBlockComment(schema); | ||
}); | ||
rules.set('Normalise schema.minItems', function (schema) { | ||
// make sure we only add the props onto array types | ||
if (isArrayType(schema)) { | ||
var minItems = schema.minItems; | ||
schema.minItems = typeof minItems === 'number' ? minItems : 0; | ||
} | ||
// cannot normalise maxItems because maxItems = 0 has an actual meaning | ||
}); | ||
rules.set('Normalize schema.items', function (schema) { | ||
var maxItems = schema.maxItems, minItems = schema.minItems; | ||
var hasMaxItems = typeof maxItems === 'number' && maxItems >= 0; | ||
var hasMinItems = typeof minItems === 'number' && minItems > 0; | ||
if (schema.items && !Array.isArray(schema.items) && (hasMaxItems || hasMinItems)) { | ||
var items = schema.items; | ||
// create a tuple of length N | ||
var newItems = Array(maxItems || minItems || 0).fill(items); | ||
if (!hasMaxItems) { | ||
// if there is no maximum, then add a spread item to collect the rest | ||
schema.additionalItems = items; | ||
} | ||
schema.items = newItems; | ||
} | ||
if (Array.isArray(schema.items) && hasMaxItems && maxItems < schema.items.length) { | ||
// it's perfectly valid to provide 5 item defs but require maxItems 1 | ||
// obviously we shouldn't emit a type for items that aren't expected | ||
schema.items = schema.items.slice(0, maxItems); | ||
} | ||
return schema; | ||
@@ -48,3 +82,3 @@ }); | ||
rules.forEach(function (rule, key) { | ||
_schema = utils_1.mapDeep(_schema, function (schema) { return rule(schema, _schema, filename); }); | ||
utils_1.traverse(_schema, function (schema) { return rule(schema, _schema, filename); }); | ||
utils_1.log(cli_color_1.whiteBright.bgYellow('normalizer'), "Applied rule: \"" + key + "\""); | ||
@@ -51,0 +85,0 @@ }); |
"use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
var cli_color_1 = require("cli-color"); | ||
var stringify = require("json-stringify-safe"); | ||
var lodash_1 = require("lodash"); | ||
var AST_1 = require("./types/AST"); | ||
var utils_1 = require("./utils"); | ||
var stringify = require("json-stringify-safe"); | ||
function optimize(ast, processed) { | ||
@@ -9,0 +9,0 @@ if (processed === void 0) { processed = new Map(); } |
@@ -16,6 +16,6 @@ "use strict"; | ||
var lodash_1 = require("lodash"); | ||
var util_1 = require("util"); | ||
var typeOfSchema_1 = require("./typeOfSchema"); | ||
var AST_1 = require("./types/AST"); | ||
var utils_1 = require("./utils"); | ||
var util_1 = require("util"); | ||
function parse(schema, options, rootSchema, keyName, isSchema, processed, usedNames) { | ||
@@ -145,15 +145,30 @@ if (rootSchema === void 0) { rootSchema = schema; } | ||
if (Array.isArray(schema.items)) { | ||
return set({ | ||
// normalised to not be undefined | ||
var minItems_1 = schema.minItems; | ||
var maxItems_1 = schema.maxItems; | ||
var arrayType = { | ||
comment: schema.description, | ||
keyName: keyName, | ||
maxItems: maxItems_1, | ||
minItems: minItems_1, | ||
params: schema.items.map(function (_) { return parse(_, options, rootSchema, undefined, true, processed, usedNames); }), | ||
standaloneName: standaloneName(schema, keyNameFromDefinition, usedNames), | ||
type: 'TUPLE' | ||
}); | ||
}; | ||
if (schema.additionalItems === true) { | ||
arrayType.spreadParam = { | ||
type: 'ANY' | ||
}; | ||
} | ||
else if (schema.additionalItems) { | ||
arrayType.spreadParam = parse(schema.additionalItems, options, rootSchema, undefined, true, processed, usedNames); | ||
} | ||
return set(arrayType); | ||
} | ||
else { | ||
var params_1 = parse(schema.items, options, rootSchema, undefined, true, processed, usedNames); | ||
return set({ | ||
comment: schema.description, | ||
keyName: keyName, | ||
params: parse(schema.items, options, rootSchema, undefined, true, processed, usedNames), | ||
params: params_1, | ||
standaloneName: standaloneName(schema, keyNameFromDefinition, usedNames), | ||
@@ -182,6 +197,24 @@ type: 'ARRAY' | ||
case 'UNTYPED_ARRAY': | ||
// normalised to not be undefined | ||
var minItems = schema.minItems; | ||
var maxItems = typeof schema.maxItems === 'number' ? schema.maxItems : -1; | ||
var params = AST_1.T_ANY; | ||
if (minItems > 0 || maxItems >= 0) { | ||
return set({ | ||
comment: schema.description, | ||
keyName: keyName, | ||
maxItems: schema.maxItems, | ||
minItems: minItems, | ||
// create a tuple of length N | ||
params: Array(Math.max(maxItems, minItems) || 0).fill(params), | ||
// if there is no maximum, then add a spread item to collect the rest | ||
spreadParam: maxItems >= 0 ? undefined : params, | ||
standaloneName: standaloneName(schema, keyNameFromDefinition, usedNames), | ||
type: 'TUPLE' | ||
}); | ||
} | ||
return set({ | ||
comment: schema.description, | ||
keyName: keyName, | ||
params: AST_1.T_ANY, | ||
params: params, | ||
standaloneName: standaloneName(schema, keyNameFromDefinition, usedNames), | ||
@@ -188,0 +221,0 @@ type: 'ARRAY' |
@@ -38,4 +38,4 @@ "use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
var cli_color_1 = require("cli-color"); | ||
var $RefParser = require("json-schema-ref-parser"); | ||
var cli_color_1 = require("cli-color"); | ||
var utils_1 = require("./utils"); | ||
@@ -42,0 +42,0 @@ function dereference(schema, _a) { |
@@ -85,2 +85,5 @@ import { JSONSchema4Type } from 'json-schema'; | ||
params: AST[]; | ||
spreadParam?: AST; | ||
minItems: number; | ||
maxItems?: number; | ||
} | ||
@@ -87,0 +90,0 @@ export interface TUnion extends AbstractAST { |
@@ -1,3 +0,4 @@ | ||
import { JSONSchema4 } from 'json-schema'; | ||
import { JSONSchema4, JSONSchema4TypeName } from 'json-schema'; | ||
export declare type SCHEMA_TYPE = 'ALL_OF' | 'UNNAMED_SCHEMA' | 'ANY' | 'ANY_OF' | 'BOOLEAN' | 'NAMED_ENUM' | 'NAMED_SCHEMA' | 'NULL' | 'NUMBER' | 'STRING' | 'OBJECT' | 'ONE_OF' | 'TYPED_ARRAY' | 'REFERENCE' | 'UNION' | 'UNNAMED_ENUM' | 'UNTYPED_ARRAY' | 'CUSTOM_TYPE'; | ||
export declare type JSONSchemaTypeName = JSONSchema4TypeName; | ||
export interface JSONSchema extends JSONSchema4 { | ||
@@ -4,0 +5,0 @@ /** |
@@ -10,2 +10,3 @@ import { JSONSchema } from './types/JSONSchema'; | ||
export declare function mapDeep(object: object, fn: (value: object, key?: string) => object, key?: string): object; | ||
export declare function traverse(schema: JSONSchema, callback: (schema: JSONSchema) => void): void; | ||
/** | ||
@@ -12,0 +13,0 @@ * Eg. `foo/bar/baz.json` => `baz` |
@@ -31,6 +31,112 @@ "use strict"; | ||
return fn(lodash_1.mapValues(object, function (_, key) { | ||
return lodash_1.isPlainObject(_) ? mapDeep(_, fn, key) : _; | ||
if (lodash_1.isPlainObject(_)) { | ||
return mapDeep(_, fn, key); | ||
} | ||
else if (Array.isArray(_)) { | ||
return _.map(function (item) { | ||
if (lodash_1.isPlainObject(item)) { | ||
return mapDeep(item, fn, key); | ||
} | ||
return item; | ||
}); | ||
} | ||
return _; | ||
}), key); | ||
} | ||
exports.mapDeep = mapDeep; | ||
// keys that shouldn't be traversed by the catchall step | ||
var BLACKLISTED_KEYS = new Set([ | ||
'id', | ||
'$schema', | ||
'title', | ||
'description', | ||
'default', | ||
'multipleOf', | ||
'maximum', | ||
'exclusiveMaximum', | ||
'minimum', | ||
'exclusiveMinimum', | ||
'maxLength', | ||
'minLength', | ||
'pattern', | ||
'additionalItems', | ||
'items', | ||
'maxItems', | ||
'minItems', | ||
'uniqueItems', | ||
'maxProperties', | ||
'minProperties', | ||
'required', | ||
'additionalProperties', | ||
'definitions', | ||
'properties', | ||
'patternProperties', | ||
'dependencies', | ||
'enum', | ||
'type', | ||
'allOf', | ||
'anyOf', | ||
'oneOf', | ||
'not' | ||
]); | ||
function traverseObjectKeys(obj, callback) { | ||
Object.keys(obj).forEach(function (k) { | ||
if (obj[k] && typeof obj[k] === 'object' && !Array.isArray(obj[k])) { | ||
traverse(obj[k], callback); | ||
} | ||
}); | ||
} | ||
function traverseArray(arr, callback) { | ||
arr.forEach(function (i) { return traverse(i, callback); }); | ||
} | ||
function traverse(schema, callback) { | ||
callback(schema); | ||
if (schema.anyOf) { | ||
traverseArray(schema.anyOf, callback); | ||
} | ||
if (schema.allOf) { | ||
traverseArray(schema.allOf, callback); | ||
} | ||
if (schema.oneOf) { | ||
traverseArray(schema.oneOf, callback); | ||
} | ||
if (schema.properties) { | ||
traverseObjectKeys(schema.properties, callback); | ||
} | ||
if (schema.patternProperties) { | ||
traverseObjectKeys(schema.patternProperties, callback); | ||
} | ||
if (schema.additionalProperties && typeof schema.additionalProperties === 'object') { | ||
traverse(schema.additionalProperties, callback); | ||
} | ||
if (schema.items) { | ||
var items = schema.items; | ||
if (Array.isArray(items)) { | ||
traverseArray(items, callback); | ||
} | ||
else { | ||
traverse(items, callback); | ||
} | ||
} | ||
if (schema.additionalItems && typeof schema.additionalItems === 'object') { | ||
traverse(schema.additionalItems, callback); | ||
} | ||
if (schema.dependencies) { | ||
traverseObjectKeys(schema.dependencies, callback); | ||
} | ||
if (schema.definitions) { | ||
traverseObjectKeys(schema.definitions, callback); | ||
} | ||
if (schema.not) { | ||
traverse(schema.not, callback); | ||
} | ||
// technically you can put definitions on any key | ||
Object.keys(schema).filter(function (key) { return !BLACKLISTED_KEYS.has(key); }).forEach(function (key) { | ||
var child = schema[key]; | ||
if (child && typeof child === 'object') { | ||
traverseObjectKeys(child, callback); | ||
} | ||
}); | ||
} | ||
exports.traverse = traverse; | ||
/** | ||
@@ -37,0 +143,0 @@ * Eg. `foo/bar/baz.json` => `baz` |
@@ -15,2 +15,20 @@ "use strict"; | ||
}); | ||
rules.set('When both maxItems and minItems are present, maxItems >= minItems', function (schema) { | ||
var maxItems = schema.maxItems, minItems = schema.minItems; | ||
if (typeof maxItems === 'number' && typeof minItems === 'number') { | ||
return maxItems >= minItems; | ||
} | ||
}); | ||
rules.set('When maxItems exists, maxItems >= 0', function (schema) { | ||
var maxItems = schema.maxItems; | ||
if (typeof maxItems === 'number') { | ||
return maxItems >= 0; | ||
} | ||
}); | ||
rules.set('When minItems exists, minItems >= 0', function (schema) { | ||
var minItems = schema.minItems; | ||
if (typeof minItems === 'number') { | ||
return minItems >= 0; | ||
} | ||
}); | ||
function validate(schema, filename) { | ||
@@ -17,0 +35,0 @@ var errors = []; |
{ | ||
"name": "json-schema-to-typescript", | ||
"version": "6.1.3", | ||
"version": "7.0.0", | ||
"description": "compile json schema to typescript typings", | ||
@@ -48,27 +48,27 @@ "main": "dist/src/index.js", | ||
"dependencies": { | ||
"@types/cli-color": "^0.3.29", | ||
"@types/json-schema": "^7.0.3", | ||
"@types/lodash": "^4.14.121", | ||
"@types/minimist": "^1.2.0", | ||
"@types/mz": "0.0.32", | ||
"@types/node": "^11.10.4", | ||
"@types/prettier": "^1.16.1", | ||
"@types/node": ">=4.5.0", | ||
"@types/prettier": "^1.18.0", | ||
"cli-color": "^1.4.0", | ||
"json-schema-ref-parser": "^6.1.0", | ||
"json-schema-ref-parser": "^7.1.0", | ||
"json-stringify-safe": "^5.0.1", | ||
"lodash": "^4.17.11", | ||
"lodash": "^4.17.15", | ||
"minimist": "^1.2.0", | ||
"mz": "^2.7.0", | ||
"prettier": "^1.16.4", | ||
"prettier": "^1.18.2", | ||
"stdin": "0.0.1" | ||
}, | ||
"devDependencies": { | ||
"ava": "^1.2.1", | ||
"browserify": "^16.2.3", | ||
"@types/cli-color": "^0.3.29", | ||
"@types/lodash": "^4.14.136", | ||
"@types/minimist": "^1.2.0", | ||
"@types/mz": "0.0.32", | ||
"ava": "^2.2.0", | ||
"browserify": "^16.3.0", | ||
"browserify-shim": "^3.8.14", | ||
"concurrently": "^4.1.0", | ||
"concurrently": "^4.1.1", | ||
"shx": "^0.3.2", | ||
"tsify": "^4.0.1", | ||
"tslint": "^5.13.1", | ||
"typescript": "^3.3.3333" | ||
"tslint": "^5.18.0", | ||
"typescript": "^3.5.3" | ||
}, | ||
@@ -75,0 +75,0 @@ "ava": { |
@@ -149,2 +149,4 @@ # json-schema-to-typescript [![Build Status][build]](https://circleci.com/gh/bcherny/json-schema-to-typescript) [![npm]](https://www.npmjs.com/package/json-schema-to-typescript) [![mit]](https://opensource.org/licenses/MIT) | ||
- [x] `oneOf` (treated like `anyOf`) | ||
- [x] `maxItems` ([eg](https://github.com/tdegrunt/jsonschema/blob/67c0e27ce9542efde0bf43dc1b2a95dd87df43c3/examples/all.js#L166)) | ||
- [x] `minItems` ([eg](https://github.com/tdegrunt/jsonschema/blob/67c0e27ce9542efde0bf43dc1b2a95dd87df43c3/examples/all.js#L165)) | ||
- [x] `additionalProperties` of type | ||
@@ -167,4 +169,2 @@ - [x] `patternProperties` (partial support) | ||
- `minimum` ([eg](https://github.com/tdegrunt/jsonschema/blob/67c0e27ce9542efde0bf43dc1b2a95dd87df43c3/examples/all.js#L182)) | ||
- `maxItems` ([eg](https://github.com/tdegrunt/jsonschema/blob/67c0e27ce9542efde0bf43dc1b2a95dd87df43c3/examples/all.js#L166)) | ||
- `minItems` ([eg](https://github.com/tdegrunt/jsonschema/blob/67c0e27ce9542efde0bf43dc1b2a95dd87df43c3/examples/all.js#L165)) | ||
- `maxProperties` ([eg](https://github.com/tdegrunt/jsonschema/blob/67c0e27ce9542efde0bf43dc1b2a95dd87df43c3/examples/all.js#L113)) | ||
@@ -171,0 +171,0 @@ - `minProperties` ([eg](https://github.com/tdegrunt/jsonschema/blob/67c0e27ce9542efde0bf43dc1b2a95dd87df43c3/examples/all.js#L112)) |
#!/usr/bin/env node | ||
import { whiteBright } from 'cli-color' | ||
import { JSONSchema4 } from 'json-schema' | ||
@@ -9,3 +10,2 @@ import minimist = require('minimist') | ||
import { compile, Options } from './index' | ||
import { whiteBright } from 'cli-color' | ||
@@ -12,0 +12,0 @@ main(minimist(process.argv.slice(2), { |
@@ -5,3 +5,3 @@ import { whiteBright } from 'cli-color' | ||
import { | ||
AST, ASTWithStandaloneName, hasComment, hasStandaloneName, TArray, TEnum, TInterface, TIntersection, | ||
AST, ASTWithStandaloneName, hasComment, hasStandaloneName, T_ANY, TArray, TEnum, TInterface, TIntersection, | ||
TNamedInterface, TUnion | ||
@@ -43,3 +43,7 @@ } from './types/AST' | ||
case 'TUPLE': | ||
return ast.params.reduce((prev, ast) => prev + declareEnums(ast, options, processed), '') | ||
type = ast.params.reduce((prev, ast) => prev + declareEnums(ast, options, processed), '') | ||
if (ast.spreadParam) { | ||
type += declareEnums(ast.spreadParam, options, processed) | ||
} | ||
break | ||
case 'INTERFACE': | ||
@@ -87,2 +91,5 @@ type = getSuperTypesAndParams(ast).reduce((prev, ast) => | ||
type = ast.params.map(_ => declareNamedInterfaces(_, options, rootASTName, processed)).filter(Boolean).join('\n') | ||
if (ast.type === 'TUPLE' && ast.spreadParam) { | ||
type += declareNamedInterfaces(ast.spreadParam, options, rootASTName, processed) | ||
} | ||
break | ||
@@ -130,3 +137,4 @@ default: | ||
hasStandaloneName(ast) ? generateStandaloneType(ast, options) : undefined, | ||
ast.params.map(ast => declareNamedTypes(ast, options, rootASTName, processed)).filter(Boolean).join('\n') | ||
ast.params.map(ast => declareNamedTypes(ast, options, rootASTName, processed)).filter(Boolean).join('\n'), | ||
('spreadParam' in ast && ast.spreadParam) ? declareNamedTypes(ast.spreadParam, options, rootASTName, processed) : undefined | ||
].filter(Boolean).join('\n') | ||
@@ -153,3 +161,3 @@ break | ||
case 'ARRAY': return (() => { | ||
let type = generateType(ast.params, options) | ||
const type = generateType(ast.params, options) | ||
return type.endsWith('"') ? '(' + type + ')[]' : type + '[]' | ||
@@ -166,5 +174,80 @@ })() | ||
case 'STRING': return 'string' | ||
case 'TUPLE': return '[' | ||
+ ast.params.map(_ => generateType(_, options)).join(', ') | ||
+ ']' | ||
case 'TUPLE': return (() => { | ||
const minItems = ast.minItems | ||
const maxItems = ast.maxItems || -1 | ||
let spreadParam = ast.spreadParam | ||
const astParams = [...ast.params] | ||
if (minItems > 0 && minItems > astParams.length && ast.spreadParam === undefined) { | ||
// this is a valid state, and JSONSchema doesn't care about the item type | ||
if (maxItems < 0) { | ||
// no max items and no spread param, so just spread any | ||
spreadParam = T_ANY | ||
} | ||
} | ||
if (maxItems > astParams.length && ast.spreadParam === undefined) { | ||
// this is a valid state, and JSONSchema doesn't care about the item type | ||
// fill the tuple with any elements | ||
for (let i = astParams.length; i < maxItems; i += 1) { | ||
astParams.push(T_ANY) | ||
} | ||
} | ||
function addSpreadParam(params: string[]): string[] { | ||
if (spreadParam) { | ||
const spread = '...(' + generateType(spreadParam, options) + ')[]' | ||
params.push(spread) | ||
} | ||
return params | ||
} | ||
function paramsToString(params: string[]): string { | ||
return '[' + params.join(', ') + ']' | ||
} | ||
const paramsList = astParams | ||
.map(param => generateType(param, options)) | ||
if (paramsList.length > minItems) { | ||
/* | ||
if there are more items than the min, we return a union of tuples instead of | ||
using the optional element operator. This is done because it is more typesafe. | ||
// optional element operator | ||
type A = [string, string?, string?] | ||
const a: A = ['a', undefined, 'c'] // no error | ||
// union of tuples | ||
type B = [string] | [string, string] | [string, string, string] | ||
const b: B = ['a', undefined, 'c'] // TS error | ||
*/ | ||
const cumulativeParamsList: string[] = paramsList.slice(0, minItems) | ||
const typesToUnion: string[] = [] | ||
if (cumulativeParamsList.length > 0) { | ||
// actually has minItems, so add the initial state | ||
typesToUnion.push(paramsToString(cumulativeParamsList)) | ||
} else { | ||
// no minItems means it's acceptable to have an empty tuple type | ||
typesToUnion.push(paramsToString([])) | ||
} | ||
for (let i = minItems; i < paramsList.length; i += 1) { | ||
cumulativeParamsList.push(paramsList[i]) | ||
if (i === paramsList.length - 1) { | ||
// only the last item in the union should have the spread parameter | ||
addSpreadParam(cumulativeParamsList) | ||
} | ||
typesToUnion.push(paramsToString(cumulativeParamsList)) | ||
} | ||
return typesToUnion.join('|') | ||
} | ||
// no max items so only need to return one type | ||
return paramsToString(addSpreadParam(paramsList)) | ||
})() | ||
case 'UNION': return generateSetOperation(ast, options) | ||
@@ -171,0 +254,0 @@ case 'CUSTOM_TYPE': return ast.params |
import { readFileSync } from 'fs' | ||
import { JSONSchema4 } from 'json-schema' | ||
import { Options as $RefOptions } from 'json-schema-ref-parser' | ||
import { endsWith, merge } from 'lodash' | ||
@@ -14,3 +15,2 @@ import { dirname } from 'path' | ||
import { validate } from './validator' | ||
import { Options as $RefOptions } from 'json-schema-ref-parser' | ||
@@ -51,2 +51,3 @@ export { EnumJSONSchema, JSONSchema, NamedEnumJSONSchema, CustomTypeJSONSchema } from './types/JSONSchema' | ||
export const DEFAULT_OPTIONS: Options = { | ||
$refOptions: {}, | ||
bannerComment: `/* tslint:disable */ | ||
@@ -70,4 +71,3 @@ /** | ||
}, | ||
unreachableDefinitions: false, | ||
$refOptions: {} | ||
unreachableDefinitions: false | ||
} | ||
@@ -74,0 +74,0 @@ |
import { whiteBright } from 'cli-color' | ||
import stringify = require('json-stringify-safe') | ||
import { cloneDeep } from 'lodash' | ||
import { JSONSchema, NormalizedJSONSchema } from './types/JSONSchema' | ||
import { justName, log, mapDeep, toSafeString, escapeBlockComment } from './utils' | ||
import stringify = require('json-stringify-safe') | ||
import { JSONSchema, JSONSchemaTypeName, NormalizedJSONSchema } from './types/JSONSchema' | ||
import { escapeBlockComment, justName, log, toSafeString, traverse } from './utils' | ||
type Rule = (schema: JSONSchema, rootSchema: JSONSchema, fileName?: string) => JSONSchema | ||
type Rule = (schema: JSONSchema, rootSchema: JSONSchema, fileName?: string) => void | ||
const rules = new Map<string, Rule>() | ||
function hasType(schema: JSONSchema, type: JSONSchemaTypeName) { | ||
return schema.type === type || (Array.isArray(schema.type) && schema.type.includes(type)) | ||
} | ||
function isObjectType(schema: JSONSchema) { | ||
return schema.properties !== undefined || hasType(schema, 'object') || hasType(schema, 'any') | ||
} | ||
function isArrayType(schema: JSONSchema) { | ||
return schema.items !== undefined || hasType(schema, 'array') || hasType(schema, 'any') | ||
} | ||
rules.set('Destructure unary types', schema => { | ||
@@ -14,25 +24,23 @@ if (schema.type && Array.isArray(schema.type) && schema.type.length === 1) { | ||
} | ||
return schema | ||
}) | ||
rules.set('Add empty `required` property if none is defined', (schema, rootSchema) => { | ||
if (stringify(schema) === stringify(rootSchema) && !('required' in schema)) { | ||
rules.set('Add empty `required` property if none is defined', (schema) => { | ||
if (!('required' in schema) && isObjectType(schema)) { | ||
schema.required = [] | ||
} | ||
return schema | ||
}) | ||
rules.set('Transform `required`=false to `required`=[]', (schema, rootSchema) => { | ||
if (stringify(schema) === stringify(rootSchema) && schema.required === false) { | ||
rules.set('Transform `required`=false to `required`=[]', (schema) => { | ||
if (schema.required === false) { | ||
schema.required = [] | ||
} | ||
return schema | ||
}) | ||
// TODO: default to empty schema (as per spec) instead | ||
rules.set('Default additionalProperties to true', (schema, rootSchema) => { | ||
if (stringify(schema) === stringify(rootSchema) && !('additionalProperties' in schema)) { | ||
rules.set('Default additionalProperties to true', (schema) => { | ||
if (!('additionalProperties' in schema) && | ||
isObjectType(schema) && | ||
schema.patternProperties === undefined) { | ||
schema.additionalProperties = true | ||
} | ||
return schema | ||
}) | ||
@@ -44,3 +52,2 @@ | ||
} | ||
return schema | ||
}) | ||
@@ -50,2 +57,35 @@ | ||
escapeBlockComment(schema) | ||
}) | ||
rules.set('Normalise schema.minItems', (schema) => { | ||
// make sure we only add the props onto array types | ||
if (isArrayType(schema)) { | ||
const {minItems} = schema | ||
schema.minItems = typeof minItems === 'number' ? minItems : 0 | ||
} | ||
// cannot normalise maxItems because maxItems = 0 has an actual meaning | ||
}) | ||
rules.set('Normalize schema.items', schema => { | ||
const {maxItems, minItems} = schema | ||
const hasMaxItems = typeof maxItems === 'number' && maxItems >= 0 | ||
const hasMinItems = typeof minItems === 'number' && minItems > 0 | ||
if (schema.items && !Array.isArray(schema.items) && (hasMaxItems || hasMinItems)) { | ||
const items = schema.items | ||
// create a tuple of length N | ||
const newItems = Array(maxItems || minItems || 0).fill(items) | ||
if (!hasMaxItems) { | ||
// if there is no maximum, then add a spread item to collect the rest | ||
schema.additionalItems = items | ||
} | ||
schema.items = newItems | ||
} | ||
if (Array.isArray(schema.items) && hasMaxItems && maxItems! < schema.items.length) { | ||
// it's perfectly valid to provide 5 item defs but require maxItems 1 | ||
// obviously we shouldn't emit a type for items that aren't expected | ||
schema.items = schema.items.slice(0, maxItems) | ||
} | ||
return schema | ||
@@ -55,5 +95,5 @@ }) | ||
export function normalize(schema: JSONSchema, filename?: string): NormalizedJSONSchema { | ||
let _schema = cloneDeep(schema) as NormalizedJSONSchema | ||
const _schema = cloneDeep(schema) as NormalizedJSONSchema | ||
rules.forEach((rule, key) => { | ||
_schema = mapDeep(_schema, schema => rule(schema, _schema, filename)) as NormalizedJSONSchema | ||
traverse(_schema, (schema) => rule(schema, _schema, filename)) | ||
log(whiteBright.bgYellow('normalizer'), `Applied rule: "${key}"`) | ||
@@ -60,0 +100,0 @@ }) |
import { whiteBright } from 'cli-color' | ||
import stringify = require('json-stringify-safe') | ||
import { uniqBy } from 'lodash' | ||
import { AST, T_ANY } from './types/AST' | ||
import { log } from './utils' | ||
import stringify = require('json-stringify-safe') | ||
@@ -7,0 +7,0 @@ export function optimize(ast: AST, processed = new Map<AST, AST>()): AST { |
import { whiteBright } from 'cli-color' | ||
import { JSONSchema4Type, JSONSchema4TypeName } from 'json-schema' | ||
import { findKey, includes, isPlainObject, map } from 'lodash' | ||
import { format } from 'util' | ||
import { Options } from './' | ||
import { typeOfSchema } from './typeOfSchema' | ||
import { AST, hasStandaloneName, T_ANY, T_ANY_ADDITIONAL_PROPERTIES, TInterface, TInterfaceParam, TNamedInterface } from './types/AST' | ||
import { AST, hasStandaloneName, T_ANY, T_ANY_ADDITIONAL_PROPERTIES, TInterface, TInterfaceParam, TNamedInterface, TTuple } from './types/AST' | ||
import { JSONSchema, JSONSchemaWithDefinitions, SchemaSchema } from './types/JSONSchema' | ||
import { generateName, log } from './utils' | ||
import { format } from 'util' | ||
@@ -164,14 +164,28 @@ export type Processed = Map<JSONSchema | JSONSchema4Type, AST> | ||
if (Array.isArray(schema.items)) { | ||
return set({ | ||
// normalised to not be undefined | ||
const minItems = schema.minItems! | ||
const maxItems = schema.maxItems! | ||
const arrayType: TTuple = { | ||
comment: schema.description, | ||
keyName, | ||
maxItems, | ||
minItems, | ||
params: schema.items.map(_ => parse(_, options, rootSchema, undefined, true, processed, usedNames)), | ||
standaloneName: standaloneName(schema, keyNameFromDefinition, usedNames), | ||
type: 'TUPLE' | ||
}) | ||
} | ||
if (schema.additionalItems === true) { | ||
arrayType.spreadParam = { | ||
type: 'ANY' | ||
} | ||
} else if (schema.additionalItems) { | ||
arrayType.spreadParam = parse(schema.additionalItems, options, rootSchema, undefined, true, processed, usedNames) | ||
} | ||
return set(arrayType) | ||
} else { | ||
const params = parse(schema.items!, options, rootSchema, undefined, true, processed, usedNames) | ||
return set({ | ||
comment: schema.description, | ||
keyName, | ||
params: parse(schema.items!, options, rootSchema, undefined, true, processed, usedNames), | ||
params, | ||
standaloneName: standaloneName(schema, keyNameFromDefinition, usedNames), | ||
@@ -200,6 +214,25 @@ type: 'ARRAY' | ||
case 'UNTYPED_ARRAY': | ||
// normalised to not be undefined | ||
const minItems = schema.minItems! | ||
const maxItems = typeof schema.maxItems === 'number' ? schema.maxItems : -1 | ||
const params = T_ANY | ||
if (minItems > 0 || maxItems >= 0) { | ||
return set({ | ||
comment: schema.description, | ||
keyName, | ||
maxItems: schema.maxItems, | ||
minItems, | ||
// create a tuple of length N | ||
params: Array(Math.max(maxItems, minItems) || 0).fill(params), | ||
// if there is no maximum, then add a spread item to collect the rest | ||
spreadParam: maxItems >= 0 ? undefined : params, | ||
standaloneName: standaloneName(schema, keyNameFromDefinition, usedNames), | ||
type: 'TUPLE' | ||
}) | ||
} | ||
return set({ | ||
comment: schema.description, | ||
keyName, | ||
params: T_ANY, | ||
params, | ||
standaloneName: standaloneName(schema, keyNameFromDefinition, usedNames), | ||
@@ -298,3 +331,2 @@ type: 'ARRAY' | ||
let singlePatternProperty = false | ||
@@ -301,0 +333,0 @@ if (schema.patternProperties) { |
@@ -0,3 +1,3 @@ | ||
import { whiteBright } from 'cli-color' | ||
import $RefParser = require('json-schema-ref-parser') | ||
import { whiteBright } from 'cli-color' | ||
import { JSONSchema } from './types/JSONSchema' | ||
@@ -4,0 +4,0 @@ import { log } from './utils' |
@@ -109,2 +109,5 @@ import { JSONSchema4Type } from 'json-schema' | ||
params: AST[] | ||
spreadParam?: AST | ||
minItems: number | ||
maxItems?: number | ||
} | ||
@@ -111,0 +114,0 @@ |
@@ -1,2 +0,2 @@ | ||
import { JSONSchema4 } from 'json-schema' | ||
import { JSONSchema4, JSONSchema4TypeName } from 'json-schema' | ||
@@ -8,2 +8,4 @@ export type SCHEMA_TYPE = 'ALL_OF' | 'UNNAMED_SCHEMA' | 'ANY' | 'ANY_OF' | ||
export type JSONSchemaTypeName = JSONSchema4TypeName | ||
export interface JSONSchema extends JSONSchema4 { | ||
@@ -10,0 +12,0 @@ /** |
115
src/utils.ts
@@ -31,7 +31,116 @@ import { whiteBright } from 'cli-color' | ||
): object { | ||
return fn(mapValues(object, (_, key) => | ||
isPlainObject(_) ? mapDeep(_, fn, key) : _ | ||
), key) | ||
return fn(mapValues(object, (_: unknown, key) => { | ||
if (isPlainObject(_)) { | ||
return mapDeep(_ as object, fn, key) | ||
} else if (Array.isArray(_)) { | ||
return _.map(item => { | ||
if (isPlainObject(item)) { | ||
return mapDeep(item as object, fn, key) | ||
} | ||
return item | ||
}) | ||
} | ||
return _ | ||
}), key) | ||
} | ||
// keys that shouldn't be traversed by the catchall step | ||
const BLACKLISTED_KEYS = new Set([ | ||
'id', | ||
'$schema', | ||
'title', | ||
'description', | ||
'default', | ||
'multipleOf', | ||
'maximum', | ||
'exclusiveMaximum', | ||
'minimum', | ||
'exclusiveMinimum', | ||
'maxLength', | ||
'minLength', | ||
'pattern', | ||
'additionalItems', | ||
'items', | ||
'maxItems', | ||
'minItems', | ||
'uniqueItems', | ||
'maxProperties', | ||
'minProperties', | ||
'required', | ||
'additionalProperties', | ||
'definitions', | ||
'properties', | ||
'patternProperties', | ||
'dependencies', | ||
'enum', | ||
'type', | ||
'allOf', | ||
'anyOf', | ||
'oneOf', | ||
'not' | ||
]) | ||
function traverseObjectKeys( | ||
obj: Record<string, JSONSchema>, | ||
callback: (schema: JSONSchema) => void | ||
) { | ||
Object.keys(obj).forEach(k => { | ||
if (obj[k] && typeof obj[k] === 'object' && !Array.isArray(obj[k])) { | ||
traverse(obj[k], callback) | ||
} | ||
}) | ||
} | ||
function traverseArray(arr: JSONSchema[], callback: (schema: JSONSchema) => void) { | ||
arr.forEach(i => traverse(i, callback)) | ||
} | ||
export function traverse(schema: JSONSchema, callback: (schema: JSONSchema) => void): void { | ||
callback(schema) | ||
if (schema.anyOf) { | ||
traverseArray(schema.anyOf, callback) | ||
} | ||
if (schema.allOf) { | ||
traverseArray(schema.allOf, callback) | ||
} | ||
if (schema.oneOf) { | ||
traverseArray(schema.oneOf, callback) | ||
} | ||
if (schema.properties) { | ||
traverseObjectKeys(schema.properties, callback) | ||
} | ||
if (schema.patternProperties) { | ||
traverseObjectKeys(schema.patternProperties, callback) | ||
} | ||
if (schema.additionalProperties && typeof schema.additionalProperties === 'object') { | ||
traverse(schema.additionalProperties, callback) | ||
} | ||
if (schema.items) { | ||
const {items} = schema | ||
if (Array.isArray(items)) { | ||
traverseArray(items, callback) | ||
} else { | ||
traverse(items, callback) | ||
} | ||
} | ||
if (schema.additionalItems && typeof schema.additionalItems === 'object') { | ||
traverse(schema.additionalItems, callback) | ||
} | ||
if (schema.dependencies) { | ||
traverseObjectKeys(schema.dependencies, callback) | ||
} | ||
if (schema.definitions) { | ||
traverseObjectKeys(schema.definitions, callback) | ||
} | ||
if (schema.not) { | ||
traverse(schema.not, callback) | ||
} | ||
// technically you can put definitions on any key | ||
Object.keys(schema).filter(key => !BLACKLISTED_KEYS.has(key)).forEach(key => { | ||
const child = schema[key] | ||
if (child && typeof child === 'object') { | ||
traverseObjectKeys(child, callback) | ||
} | ||
}) | ||
} | ||
/** | ||
@@ -38,0 +147,0 @@ * Eg. `foo/bar/baz.json` => `baz` |
import { JSONSchema } from './types/JSONSchema' | ||
import { mapDeep } from './utils' | ||
type Rule = (schema: JSONSchema) => false | void | ||
type Rule = (schema: JSONSchema) => boolean | void | ||
const rules = new Map<string, Rule>() | ||
@@ -19,2 +19,23 @@ | ||
rules.set('When both maxItems and minItems are present, maxItems >= minItems', schema => { | ||
const {maxItems, minItems} = schema | ||
if (typeof maxItems === 'number' && typeof minItems === 'number') { | ||
return maxItems >= minItems | ||
} | ||
}) | ||
rules.set('When maxItems exists, maxItems >= 0', schema => { | ||
const {maxItems} = schema | ||
if (typeof maxItems === 'number') { | ||
return maxItems >= 0 | ||
} | ||
}) | ||
rules.set('When minItems exists, minItems >= 0', schema => { | ||
const {minItems} = schema | ||
if (typeof minItems === 'number') { | ||
return minItems >= 0 | ||
} | ||
}) | ||
export function validate(schema: JSONSchema, filename: string): string[] { | ||
@@ -21,0 +42,0 @@ const errors: string[] = [] |
Sorry, the diff of this file is too big to display
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
1337123
11
38000
0
12
+ Added@types/node@22.7.5(transitive)
+ Addedjson-schema-ref-parser@7.1.4(transitive)
+ Addedono@6.0.1(transitive)
+ Addedundici-types@6.19.8(transitive)
- Removed@types/cli-color@^0.3.29
- Removed@types/lodash@^4.14.121
- Removed@types/minimist@^1.2.0
- Removed@types/mz@0.0.32
- Removed@types/cli-color@0.3.30(transitive)
- Removed@types/lodash@4.17.10(transitive)
- Removed@types/minimist@1.2.5(transitive)
- Removed@types/mz@0.0.32(transitive)
- Removed@types/node@11.15.54(transitive)
- Removedformat-util@1.0.5(transitive)
- Removedjson-schema-ref-parser@6.1.0(transitive)
- Removedono@4.0.11(transitive)
Updated@types/node@>=4.5.0
Updated@types/prettier@^1.18.0
Updatedlodash@^4.17.15
Updatedprettier@^1.18.2