json-schema-to-typescript
Advanced tools
Comparing version 7.1.0 to 8.0.0
# Changelog | ||
## 8.0.0 | ||
- e144890 Improve generated output when mixing nulls and unions (#261) | ||
## 7.1.0 | ||
@@ -4,0 +8,0 @@ |
@@ -10,3 +10,3 @@ "use strict"; | ||
if (options === void 0) { options = index_1.DEFAULT_OPTIONS; } | ||
return [ | ||
return ([ | ||
options.bannerComment, | ||
@@ -18,4 +18,3 @@ declareNamedTypes(ast, options, ast.standaloneName), | ||
.filter(Boolean) | ||
.join('\n\n') | ||
+ '\n'; // trailing newline | ||
.join('\n\n') + '\n'); // trailing newline | ||
} | ||
@@ -43,5 +42,3 @@ exports.generate = generate; | ||
case 'INTERFACE': | ||
type = getSuperTypesAndParams(ast).reduce(function (prev, ast) { | ||
return prev + declareEnums(ast, options, processed); | ||
}, ''); | ||
type = getSuperTypesAndParams(ast).reduce(function (prev, ast) { return prev + declareEnums(ast, options, processed); }, ''); | ||
break; | ||
@@ -66,7 +63,12 @@ default: | ||
type = [ | ||
AST_1.hasStandaloneName(ast) && (ast.standaloneName === rootASTName || options.declareExternallyReferenced) && generateStandaloneInterface(ast, options), | ||
getSuperTypesAndParams(ast).map(function (ast) { | ||
return declareNamedInterfaces(ast, options, rootASTName, processed); | ||
}).filter(Boolean).join('\n') | ||
].filter(Boolean).join('\n'); | ||
AST_1.hasStandaloneName(ast) && | ||
(ast.standaloneName === rootASTName || options.declareExternallyReferenced) && | ||
generateStandaloneInterface(ast, options), | ||
getSuperTypesAndParams(ast) | ||
.map(function (ast) { return declareNamedInterfaces(ast, options, rootASTName, processed); }) | ||
.filter(Boolean) | ||
.join('\n') | ||
] | ||
.filter(Boolean) | ||
.join('\n'); | ||
break; | ||
@@ -76,3 +78,6 @@ case 'INTERSECTION': | ||
case 'UNION': | ||
type = ast.params.map(function (_) { return declareNamedInterfaces(_, options, rootASTName, processed); }).filter(Boolean).join('\n'); | ||
type = ast.params | ||
.map(function (_) { return declareNamedInterfaces(_, options, rootASTName, processed); }) | ||
.filter(Boolean) | ||
.join('\n'); | ||
if (ast.type === 'TUPLE' && ast.spreadParam) { | ||
@@ -99,3 +104,5 @@ type += declareNamedInterfaces(ast.spreadParam, options, rootASTName, processed); | ||
AST_1.hasStandaloneName(ast) ? generateStandaloneType(ast, options) : undefined | ||
].filter(Boolean).join('\n'); | ||
] | ||
.filter(Boolean) | ||
.join('\n'); | ||
break; | ||
@@ -106,6 +113,9 @@ case 'ENUM': | ||
case 'INTERFACE': | ||
type = getSuperTypesAndParams(ast).map(function (ast) { | ||
return (ast.standaloneName === rootASTName || options.declareExternallyReferenced) && declareNamedTypes(ast, options, rootASTName, processed); | ||
type = getSuperTypesAndParams(ast) | ||
.map(function (ast) { | ||
return (ast.standaloneName === rootASTName || options.declareExternallyReferenced) && | ||
declareNamedTypes(ast, options, rootASTName, processed); | ||
}) | ||
.filter(Boolean).join('\n'); | ||
.filter(Boolean) | ||
.join('\n'); | ||
break; | ||
@@ -117,5 +127,12 @@ case 'INTERSECTION': | ||
AST_1.hasStandaloneName(ast) ? generateStandaloneType(ast, options) : undefined, | ||
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'); | ||
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'); | ||
break; | ||
@@ -142,85 +159,98 @@ default: | ||
switch (ast.type) { | ||
case 'ANY': return 'any'; | ||
case 'ARRAY': return (function () { | ||
var type = generateType(ast.params, options); | ||
return type.endsWith('"') ? '(' + type + ')[]' : type + '[]'; | ||
})(); | ||
case 'BOOLEAN': return 'boolean'; | ||
case 'INTERFACE': return generateInterface(ast, options); | ||
case 'INTERSECTION': return generateSetOperation(ast, options); | ||
case 'LITERAL': return JSON.stringify(ast.params); | ||
case 'NUMBER': return 'number'; | ||
case 'NULL': return 'null'; | ||
case 'OBJECT': return 'object'; | ||
case 'REFERENCE': return ast.params; | ||
case 'STRING': return 'string'; | ||
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; | ||
case 'ANY': | ||
return 'any'; | ||
case 'ARRAY': | ||
return (function () { | ||
var type = generateType(ast.params, options); | ||
return type.endsWith('"') ? '(' + type + ')[]' : type + '[]'; | ||
})(); | ||
case 'BOOLEAN': | ||
return 'boolean'; | ||
case 'INTERFACE': | ||
return generateInterface(ast, options); | ||
case 'INTERSECTION': | ||
return generateSetOperation(ast, options); | ||
case 'LITERAL': | ||
return JSON.stringify(ast.params); | ||
case 'NUMBER': | ||
return 'number'; | ||
case 'NULL': | ||
return 'null'; | ||
case 'OBJECT': | ||
return 'object'; | ||
case 'REFERENCE': | ||
return ast.params; | ||
case 'STRING': | ||
return 'string'; | ||
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); | ||
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); | ||
function addSpreadParam(params) { | ||
if (spreadParam) { | ||
var spread = '...(' + generateType(spreadParam, options) + ')[]'; | ||
params.push(spread); | ||
} | ||
return params; | ||
} | ||
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)); | ||
function paramsToString(params) { | ||
return '[' + params.join(', ') + ']'; | ||
} | ||
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); | ||
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)); | ||
} | ||
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('|'); | ||
} | ||
return typesToUnion.join('|'); | ||
} | ||
// no max items so only need to return one type | ||
return paramsToString(addSpreadParam(paramsList)); | ||
})(); | ||
case 'UNION': return generateSetOperation(ast, options); | ||
case 'CUSTOM_TYPE': return ast.params; | ||
// no max items so only need to return one type | ||
return paramsToString(addSpreadParam(paramsList)); | ||
})(); | ||
case 'UNION': | ||
return generateSetOperation(ast, options); | ||
case 'CUSTOM_TYPE': | ||
return ast.params; | ||
} | ||
@@ -237,5 +267,5 @@ } | ||
function generateInterface(ast, options) { | ||
return "{" | ||
+ '\n' | ||
+ ast.params | ||
return ("{" + | ||
'\n' + | ||
ast.params | ||
.filter(function (_) { return !_.isPatternProperty && !_.isUnreachableDefinition; }) | ||
@@ -248,45 +278,42 @@ .map(function (_a) { | ||
var isRequired = _a[0], keyName = _a[1], ast = _a[2], type = _a[3]; | ||
return (AST_1.hasComment(ast) && !ast.standaloneName ? generateComment(ast.comment) + '\n' : '') | ||
+ escapeKeyName(keyName) | ||
+ (isRequired ? '' : '?') | ||
+ ': ' | ||
+ (AST_1.hasStandaloneName(ast) ? utils_1.toSafeString(type) : type); | ||
return (AST_1.hasComment(ast) && !ast.standaloneName ? generateComment(ast.comment) + '\n' : '') + | ||
escapeKeyName(keyName) + | ||
(isRequired ? '' : '?') + | ||
': ' + | ||
(AST_1.hasStandaloneName(ast) ? utils_1.toSafeString(type) : type); | ||
}) | ||
.join('\n') | ||
+ '\n' | ||
+ '}'; | ||
.join('\n') + | ||
'\n' + | ||
'}'); | ||
} | ||
function generateComment(comment) { | ||
return [ | ||
'/**' | ||
].concat(comment.split('\n').map(function (_) { return ' * ' + _; }), [ | ||
' */' | ||
]).join('\n'); | ||
return ['/**'].concat(comment.split('\n').map(function (_) { return ' * ' + _; }), [' */']).join('\n'); | ||
} | ||
function generateStandaloneEnum(ast, options) { | ||
return (AST_1.hasComment(ast) ? generateComment(ast.comment) + '\n' : '') | ||
+ 'export ' + (options.enableConstEnums ? 'const ' : '') + ("enum " + utils_1.toSafeString(ast.standaloneName) + " {") | ||
+ '\n' | ||
+ ast.params.map(function (_a) { | ||
return ((AST_1.hasComment(ast) ? generateComment(ast.comment) + '\n' : '') + | ||
'export ' + | ||
(options.enableConstEnums ? 'const ' : '') + | ||
("enum " + utils_1.toSafeString(ast.standaloneName) + " {") + | ||
'\n' + | ||
ast.params.map(function (_a) { | ||
var ast = _a.ast, keyName = _a.keyName; | ||
return keyName + ' = ' + generateType(ast, options); | ||
}) | ||
.join(',\n') | ||
+ '\n' | ||
+ '}'; | ||
}).join(',\n') + | ||
'\n' + | ||
'}'); | ||
} | ||
function generateStandaloneInterface(ast, options) { | ||
return (AST_1.hasComment(ast) ? generateComment(ast.comment) + '\n' : '') | ||
+ ("export interface " + utils_1.toSafeString(ast.standaloneName) + " ") | ||
+ (ast.superTypes.length > 0 ? "extends " + ast.superTypes.map(function (superType) { return utils_1.toSafeString(superType.standaloneName); }).join(', ') + " " : '') | ||
+ generateInterface(ast, options); | ||
return ((AST_1.hasComment(ast) ? generateComment(ast.comment) + '\n' : '') + | ||
("export interface " + utils_1.toSafeString(ast.standaloneName) + " ") + | ||
(ast.superTypes.length > 0 | ||
? "extends " + ast.superTypes.map(function (superType) { return utils_1.toSafeString(superType.standaloneName); }).join(', ') + " " | ||
: '') + | ||
generateInterface(ast, options)); | ||
} | ||
function generateStandaloneType(ast, options) { | ||
return (AST_1.hasComment(ast) ? generateComment(ast.comment) + '\n' : '') | ||
+ ("export type " + utils_1.toSafeString(ast.standaloneName) + " = " + generateType(lodash_1.omit(ast, 'standaloneName') /* TODO */, options)); | ||
return ((AST_1.hasComment(ast) ? generateComment(ast.comment) + '\n' : '') + | ||
("export type " + utils_1.toSafeString(ast.standaloneName) + " = " + generateType(lodash_1.omit(ast, 'standaloneName') /* TODO */, options))); | ||
} | ||
function escapeKeyName(keyName) { | ||
if (keyName.length | ||
&& /[A-Za-z_$]/.test(keyName.charAt(0)) | ||
&& /^[\w$]+$/.test(keyName)) { | ||
if (keyName.length && /[A-Za-z_$]/.test(keyName.charAt(0)) && /^[\w$]+$/.test(keyName)) { | ||
return keyName; | ||
@@ -300,6 +327,4 @@ } | ||
function getSuperTypesAndParams(ast) { | ||
return ast.params | ||
.map(function (param) { return param.ast; }) | ||
.concat(ast.superTypes); | ||
return ast.params.map(function (param) { return param.ast; }).concat(ast.superTypes); | ||
} | ||
//# sourceMappingURL=generator.js.map |
@@ -93,4 +93,8 @@ "use strict"; | ||
if (options === void 0) { options = exports.DEFAULT_OPTIONS; } | ||
var contents = utils_1.Try(function () { return fs_1.readFileSync(filename); }, function () { throw new ReferenceError("Unable to read file \"" + filename + "\""); }); | ||
var schema = utils_1.Try(function () { return JSON.parse(contents.toString()); }, function () { throw new TypeError("Error parsing JSON in file \"" + filename + "\""); }); | ||
var contents = utils_1.Try(function () { return fs_1.readFileSync(filename); }, function () { | ||
throw new ReferenceError("Unable to read file \"" + filename + "\""); | ||
}); | ||
var schema = utils_1.Try(function () { return JSON.parse(contents.toString()); }, function () { | ||
throw new TypeError("Error parsing JSON in file \"" + filename + "\""); | ||
}); | ||
return compile(schema, utils_1.stripExtension(filename), __assign({ cwd: path_1.dirname(filename) }, options)); | ||
@@ -110,3 +114,3 @@ } | ||
errors.forEach(function (_) { return utils_1.error(_); }); | ||
throw new ValidationError; | ||
throw new ValidationError(); | ||
} | ||
@@ -122,4 +126,4 @@ // normalize options | ||
return [4 /*yield*/, resolver_1.dereference(normalizer_1.normalize(schema, name), _options)]; | ||
case 1: return [2 /*return*/, _a.apply(void 0, [_b.apply(void 0, [_c.apply(void 0, [_d.apply(void 0, [_e.sent(), _options])]), | ||
_options]), _options])]; | ||
case 1: return [2 /*return*/, _a.apply(void 0, [_b.apply(void 0, [_c.apply(void 0, [_d.apply(void 0, [_e.sent(), _options])]), _options]), | ||
_options])]; | ||
} | ||
@@ -126,0 +130,0 @@ }); |
@@ -17,2 +17,10 @@ "use strict"; | ||
} | ||
rules.set('Remove `type=["null"]` if `enum=[null]`', function (schema) { | ||
if (Array.isArray(schema.enum) && | ||
schema.enum.some(function (e) { return e === null; }) && | ||
Array.isArray(schema.type) && | ||
schema.type.includes('null')) { | ||
schema.type = schema.type.filter(function (type) { return type !== 'null'; }); | ||
} | ||
}); | ||
rules.set('Destructure unary types', function (schema) { | ||
@@ -35,5 +43,3 @@ if (schema.type && Array.isArray(schema.type) && schema.type.length === 1) { | ||
rules.set('Default additionalProperties to true', function (schema) { | ||
if (!('additionalProperties' in schema) && | ||
isObjectType(schema) && | ||
schema.patternProperties === undefined) { | ||
if (!('additionalProperties' in schema) && isObjectType(schema) && schema.patternProperties === undefined) { | ||
schema.additionalProperties = true; | ||
@@ -40,0 +46,0 @@ } |
@@ -18,5 +18,3 @@ "use strict"; | ||
return Object.assign(ast, { | ||
params: ast.params.map(function (_) { | ||
return Object.assign(_, { ast: optimize(_.ast, processed) }); | ||
}) | ||
params: ast.params.map(function (_) { return Object.assign(_, { ast: optimize(_.ast, processed) }); }) | ||
}); | ||
@@ -31,5 +29,3 @@ case 'INTERSECTION': | ||
// [A, B, B] -> [A, B] | ||
ast.params = lodash_1.uniqBy(ast.params, function (_) { | ||
return _.type + "------" + stringify(_.params); | ||
}); | ||
ast.params = lodash_1.uniqBy(ast.params, function (_) { return _.type + "------" + stringify(_.params); }); | ||
return Object.assign(ast, { | ||
@@ -36,0 +32,0 @@ params: ast.params.map(function (_) { return optimize(_, processed); }) |
@@ -180,3 +180,5 @@ "use strict"; | ||
keyName: keyName, | ||
params: schema.type.map(function (_) { return parse({ type: _ }, options, rootSchema, undefined, true, processed, usedNames); }), | ||
params: schema.type.map(function (_) { | ||
return parse(__assign({}, schema, { type: _ }), options, rootSchema, undefined, true, processed, usedNames); | ||
}), | ||
standaloneName: standaloneName(schema, keyNameFromDefinition, usedNames), | ||
@@ -279,3 +281,3 @@ type: 'UNION' | ||
// value definition, we can validate against that. | ||
singlePatternProperty = (!schema.additionalProperties && Object.keys(schema.patternProperties).length === 1); | ||
singlePatternProperty = !schema.additionalProperties && Object.keys(schema.patternProperties).length === 1; | ||
asts = asts.concat(lodash_1.map(schema.patternProperties, function (value, key) { | ||
@@ -285,3 +287,3 @@ var ast = parse(value, options, rootSchema, key, true, processed, usedNames); | ||
ast.comment = ast.comment ? ast.comment + "\n\n" + comment : comment; | ||
return ({ | ||
return { | ||
ast: ast, | ||
@@ -292,3 +294,3 @@ isPatternProperty: !singlePatternProperty, | ||
keyName: singlePatternProperty ? '[k: string]' : key | ||
}); | ||
}; | ||
})); | ||
@@ -295,0 +297,0 @@ } |
@@ -47,3 +47,3 @@ "use strict"; | ||
utils_1.log(cli_color_1.whiteBright.bgGreen('resolver'), schema, cwd); | ||
parser = new $RefParser; | ||
parser = new $RefParser(); | ||
return [2 /*return*/, parser.dereference(cwd, schema, $refOptions)]; | ||
@@ -50,0 +50,0 @@ }); |
@@ -16,2 +16,6 @@ "use strict"; | ||
return 'ONE_OF'; | ||
if (Array.isArray(schema.type)) | ||
return 'UNION'; | ||
if (schema.type === 'null') | ||
return 'NULL'; | ||
if (schema.items) | ||
@@ -25,9 +29,11 @@ return 'TYPED_ARRAY'; | ||
return 'REFERENCE'; | ||
if (Array.isArray(schema.type)) | ||
return 'UNION'; | ||
switch (schema.type) { | ||
case 'string': return 'STRING'; | ||
case 'number': return 'NUMBER'; | ||
case 'integer': return 'NUMBER'; | ||
case 'boolean': return 'BOOLEAN'; | ||
case 'string': | ||
return 'STRING'; | ||
case 'number': | ||
return 'NUMBER'; | ||
case 'integer': | ||
return 'NUMBER'; | ||
case 'boolean': | ||
return 'BOOLEAN'; | ||
case 'object': | ||
@@ -38,10 +44,14 @@ if (!schema.properties && !lodash_1.isPlainObject(schema)) { | ||
break; | ||
case 'array': return 'UNTYPED_ARRAY'; | ||
case 'null': return 'NULL'; | ||
case 'any': return 'ANY'; | ||
case 'array': | ||
return 'UNTYPED_ARRAY'; | ||
case 'any': | ||
return 'ANY'; | ||
} | ||
switch (typeof schema.default) { | ||
case 'boolean': return 'BOOLEAN'; | ||
case 'number': return 'NUMBER'; | ||
case 'string': return 'STRING'; | ||
case 'boolean': | ||
return 'BOOLEAN'; | ||
case 'number': | ||
return 'NUMBER'; | ||
case 'string': | ||
return 'STRING'; | ||
} | ||
@@ -48,0 +58,0 @@ if (schema.id) |
@@ -133,3 +133,5 @@ "use strict"; | ||
// technically you can put definitions on any key | ||
Object.keys(schema).filter(function (key) { return !BLACKLISTED_KEYS.has(key); }).forEach(function (key) { | ||
Object.keys(schema) | ||
.filter(function (key) { return !BLACKLISTED_KEYS.has(key); }) | ||
.forEach(function (key) { | ||
var child = schema[key]; | ||
@@ -136,0 +138,0 @@ if (child && typeof child === 'object') { |
{ | ||
"name": "json-schema-to-typescript", | ||
"version": "7.1.0", | ||
"version": "8.0.0", | ||
"description": "compile json schema to typescript typings", | ||
@@ -18,6 +18,7 @@ "main": "dist/src/index.js", | ||
"clean": "shx rm -rf dist", | ||
"lint": "tslint -c tslint.json src/*.ts", | ||
"lint": "eslint src/*.ts test/*.ts", | ||
"tdd": "concurrently -r -p '' -k 'npm run watch' 'npm run watch:test'", | ||
"test": "ava --verbose", | ||
"pretest": "npm run build", | ||
"test": "ava --serial --verbose", | ||
"prepublishOnly": "npm test", | ||
"pretest": "npm run build && npm run lint", | ||
"watch": "tsc -w", | ||
@@ -57,3 +58,3 @@ "watch:test": "ava -w" | ||
"mz": "^2.7.0", | ||
"prettier": "^1.18.2", | ||
"prettier": "^1.19.1", | ||
"stdin": "0.0.1" | ||
@@ -66,2 +67,4 @@ }, | ||
"@types/mz": "0.0.32", | ||
"@typescript-eslint/eslint-plugin": "^2.9.0", | ||
"@typescript-eslint/parser": "^2.9.0", | ||
"ava": "^1.2.1", | ||
@@ -71,5 +74,7 @@ "browserify": "^16.2.3", | ||
"concurrently": "^4.1.0", | ||
"eslint": "^6.7.2", | ||
"eslint-config-prettier": "^6.7.0", | ||
"eslint-plugin-prettier": "^3.1.1", | ||
"shx": "^0.3.2", | ||
"tsify": "^4.0.1", | ||
"tslint": "^5.13.1", | ||
"typescript": "^3.3.3333" | ||
@@ -76,0 +81,0 @@ }, |
#!/usr/bin/env node | ||
import { whiteBright } from 'cli-color' | ||
import { JSONSchema4 } from 'json-schema' | ||
import {whiteBright} from 'cli-color' | ||
import {JSONSchema4} from 'json-schema' | ||
import minimist = require('minimist') | ||
import { readFile, writeFile } from 'mz/fs' | ||
import { resolve } from 'path' | ||
import {readFile, writeFile} from 'mz/fs' | ||
import {resolve} from 'path' | ||
import stdin = require('stdin') | ||
import { compile, Options } from './index' | ||
import {compile, Options} from './index' | ||
main(minimist(process.argv.slice(2), { | ||
alias: { | ||
help: ['h'], | ||
input: ['i'], | ||
output: ['o'] | ||
} | ||
})) | ||
main( | ||
minimist(process.argv.slice(2), { | ||
alias: { | ||
help: ['h'], | ||
input: ['i'], | ||
output: ['o'] | ||
} | ||
}) | ||
) | ||
async function main(argv: minimist.ParsedArgs) { | ||
if (argv.help) { | ||
@@ -37,3 +38,2 @@ printHelp() | ||
} | ||
} | ||
@@ -64,3 +64,3 @@ | ||
process.stdout.write( | ||
` | ||
` | ||
${pkg.name} ${pkg.version} | ||
@@ -67,0 +67,0 @@ Usage: json2ts [--input, -i] [IN_FILE] [--output, -o] [OUT_FILE] [OPTIONS] |
@@ -1,6 +0,6 @@ | ||
import { format as prettify } from 'prettier' | ||
import { Options } from './' | ||
import {format as prettify} from 'prettier' | ||
import {Options} from './' | ||
export function format(code: string, options: Options): string { | ||
return prettify(code, { parser: 'typescript', ...options.style }) | ||
return prettify(code, {parser: 'typescript', ...options.style}) | ||
} |
@@ -1,28 +0,33 @@ | ||
import { whiteBright } from 'cli-color' | ||
import { omit } from 'lodash' | ||
import { DEFAULT_OPTIONS, Options } from './index' | ||
import {whiteBright} from 'cli-color' | ||
import {omit} from 'lodash' | ||
import {DEFAULT_OPTIONS, Options} from './index' | ||
import { | ||
AST, ASTWithStandaloneName, hasComment, hasStandaloneName, T_ANY, TArray, TEnum, TInterface, TIntersection, | ||
TNamedInterface, TUnion | ||
AST, | ||
ASTWithStandaloneName, | ||
hasComment, | ||
hasStandaloneName, | ||
T_ANY, | ||
TArray, | ||
TEnum, | ||
TInterface, | ||
TIntersection, | ||
TNamedInterface, | ||
TUnion | ||
} from './types/AST' | ||
import { log, toSafeString } from './utils' | ||
import {log, toSafeString} from './utils' | ||
export function generate(ast: AST, options = DEFAULT_OPTIONS): string { | ||
return [ | ||
options.bannerComment, | ||
declareNamedTypes(ast, options, ast.standaloneName!), | ||
declareNamedInterfaces(ast, options, ast.standaloneName!), | ||
declareEnums(ast, options) | ||
] | ||
.filter(Boolean) | ||
.join('\n\n') | ||
+ '\n' // trailing newline | ||
return ( | ||
[ | ||
options.bannerComment, | ||
declareNamedTypes(ast, options, ast.standaloneName!), | ||
declareNamedInterfaces(ast, options, ast.standaloneName!), | ||
declareEnums(ast, options) | ||
] | ||
.filter(Boolean) | ||
.join('\n\n') + '\n' | ||
) // trailing newline | ||
} | ||
function declareEnums( | ||
ast: AST, | ||
options: Options, | ||
processed = new Set<AST>() | ||
): string { | ||
function declareEnums(ast: AST, options: Options, processed = new Set<AST>()): string { | ||
if (processed.has(ast)) { | ||
@@ -48,5 +53,3 @@ return '' | ||
case 'INTERFACE': | ||
type = getSuperTypesAndParams(ast).reduce((prev, ast) => | ||
prev + declareEnums(ast, options, processed), | ||
'') | ||
type = getSuperTypesAndParams(ast).reduce((prev, ast) => prev + declareEnums(ast, options, processed), '') | ||
break | ||
@@ -60,9 +63,3 @@ default: | ||
function declareNamedInterfaces( | ||
ast: AST, | ||
options: Options, | ||
rootASTName: string, | ||
processed = new Set<AST>() | ||
): string { | ||
function declareNamedInterfaces(ast: AST, options: Options, rootASTName: string, processed = new Set<AST>()): string { | ||
if (processed.has(ast)) { | ||
@@ -81,7 +78,12 @@ return '' | ||
type = [ | ||
hasStandaloneName(ast) && (ast.standaloneName === rootASTName || options.declareExternallyReferenced) && generateStandaloneInterface(ast, options), | ||
getSuperTypesAndParams(ast).map(ast => | ||
declareNamedInterfaces(ast, options, rootASTName, processed) | ||
).filter(Boolean).join('\n') | ||
].filter(Boolean).join('\n') | ||
hasStandaloneName(ast) && | ||
(ast.standaloneName === rootASTName || options.declareExternallyReferenced) && | ||
generateStandaloneInterface(ast, options), | ||
getSuperTypesAndParams(ast) | ||
.map(ast => declareNamedInterfaces(ast, options, rootASTName, processed)) | ||
.filter(Boolean) | ||
.join('\n') | ||
] | ||
.filter(Boolean) | ||
.join('\n') | ||
break | ||
@@ -91,3 +93,6 @@ case 'INTERSECTION': | ||
case 'UNION': | ||
type = ast.params.map(_ => declareNamedInterfaces(_, options, rootASTName, processed)).filter(Boolean).join('\n') | ||
type = ast.params | ||
.map(_ => declareNamedInterfaces(_, options, rootASTName, processed)) | ||
.filter(Boolean) | ||
.join('\n') | ||
if (ast.type === 'TUPLE' && ast.spreadParam) { | ||
@@ -104,9 +109,3 @@ type += declareNamedInterfaces(ast.spreadParam, options, rootASTName, processed) | ||
function declareNamedTypes( | ||
ast: AST, | ||
options: Options, | ||
rootASTName: string, | ||
processed = new Set<AST>() | ||
): string { | ||
function declareNamedTypes(ast: AST, options: Options, rootASTName: string, processed = new Set<AST>()): string { | ||
if (processed.has(ast)) { | ||
@@ -124,3 +123,5 @@ return '' | ||
hasStandaloneName(ast) ? generateStandaloneType(ast, options) : undefined | ||
].filter(Boolean).join('\n') | ||
] | ||
.filter(Boolean) | ||
.join('\n') | ||
break | ||
@@ -131,5 +132,10 @@ case 'ENUM': | ||
case 'INTERFACE': | ||
type = getSuperTypesAndParams(ast).map(ast => | ||
(ast.standaloneName === rootASTName || options.declareExternallyReferenced) && declareNamedTypes(ast, options, rootASTName, processed)) | ||
.filter(Boolean).join('\n') | ||
type = getSuperTypesAndParams(ast) | ||
.map( | ||
ast => | ||
(ast.standaloneName === rootASTName || options.declareExternallyReferenced) && | ||
declareNamedTypes(ast, options, rootASTName, processed) | ||
) | ||
.filter(Boolean) | ||
.join('\n') | ||
break | ||
@@ -141,5 +147,12 @@ case 'INTERSECTION': | ||
hasStandaloneName(ast) ? generateStandaloneType(ast, options) : undefined, | ||
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') | ||
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') | ||
break | ||
@@ -173,54 +186,65 @@ default: | ||
switch (ast.type) { | ||
case 'ANY': return 'any' | ||
case 'ARRAY': return (() => { | ||
const type = generateType(ast.params, options) | ||
return type.endsWith('"') ? '(' + type + ')[]' : type + '[]' | ||
})() | ||
case 'BOOLEAN': return 'boolean' | ||
case 'INTERFACE': return generateInterface(ast, options) | ||
case 'INTERSECTION': return generateSetOperation(ast, options) | ||
case 'LITERAL': return JSON.stringify(ast.params) | ||
case 'NUMBER': return 'number' | ||
case 'NULL': return 'null' | ||
case 'OBJECT': return 'object' | ||
case 'REFERENCE': return ast.params | ||
case 'STRING': return 'string' | ||
case 'TUPLE': return (() => { | ||
const minItems = ast.minItems | ||
const maxItems = ast.maxItems || -1 | ||
case 'ANY': | ||
return 'any' | ||
case 'ARRAY': | ||
return (() => { | ||
const type = generateType(ast.params, options) | ||
return type.endsWith('"') ? '(' + type + ')[]' : type + '[]' | ||
})() | ||
case 'BOOLEAN': | ||
return 'boolean' | ||
case 'INTERFACE': | ||
return generateInterface(ast, options) | ||
case 'INTERSECTION': | ||
return generateSetOperation(ast, options) | ||
case 'LITERAL': | ||
return JSON.stringify(ast.params) | ||
case 'NUMBER': | ||
return 'number' | ||
case 'NULL': | ||
return 'null' | ||
case 'OBJECT': | ||
return 'object' | ||
case 'REFERENCE': | ||
return ast.params | ||
case 'STRING': | ||
return 'string' | ||
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 | ||
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) | ||
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) | ||
function addSpreadParam(params: string[]): string[] { | ||
if (spreadParam) { | ||
const spread = '...(' + generateType(spreadParam, options) + ')[]' | ||
params.push(spread) | ||
} | ||
return params | ||
} | ||
return params | ||
} | ||
function paramsToString(params: string[]): string { | ||
return '[' + params.join(', ') + ']' | ||
} | ||
function paramsToString(params: string[]): string { | ||
return '[' + params.join(', ') + ']' | ||
} | ||
const paramsList = astParams | ||
.map(param => generateType(param, options)) | ||
const paramsList = astParams.map(param => generateType(param, options)) | ||
if (paramsList.length > minItems) { | ||
/* | ||
if (paramsList.length > minItems) { | ||
/* | ||
if there are more items than the min, we return a union of tuples instead of | ||
@@ -238,32 +262,34 @@ using the optional element operator. This is done because it is more typesafe. | ||
const cumulativeParamsList: string[] = paramsList.slice(0, minItems) | ||
const typesToUnion: string[] = [] | ||
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([])) | ||
} | ||
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]) | ||
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) | ||
if (i === paramsList.length - 1) { | ||
// only the last item in the union should have the spread parameter | ||
addSpreadParam(cumulativeParamsList) | ||
} | ||
typesToUnion.push(paramsToString(cumulativeParamsList)) | ||
} | ||
typesToUnion.push(paramsToString(cumulativeParamsList)) | ||
return typesToUnion.join('|') | ||
} | ||
return typesToUnion.join('|') | ||
} | ||
// no max items so only need to return one type | ||
return paramsToString(addSpreadParam(paramsList)) | ||
})() | ||
case 'UNION': return generateSetOperation(ast, options) | ||
case 'CUSTOM_TYPE': return ast.params | ||
// no max items so only need to return one type | ||
return paramsToString(addSpreadParam(paramsList)) | ||
})() | ||
case 'UNION': | ||
return generateSetOperation(ast, options) | ||
case 'CUSTOM_TYPE': | ||
return ast.params | ||
} | ||
@@ -281,61 +307,66 @@ } | ||
function generateInterface( | ||
ast: TInterface, | ||
options: Options | ||
): string { | ||
return `{` | ||
+ '\n' | ||
+ ast.params | ||
function generateInterface(ast: TInterface, options: Options): string { | ||
return ( | ||
`{` + | ||
'\n' + | ||
ast.params | ||
.filter(_ => !_.isPatternProperty && !_.isUnreachableDefinition) | ||
.map(({ isRequired, keyName, ast }) => [isRequired, keyName, ast, generateType(ast, options)] as [boolean, string, AST, string]) | ||
.map(([isRequired, keyName, ast, type]) => | ||
(hasComment(ast) && !ast.standaloneName ? generateComment(ast.comment) + '\n' : '') | ||
+ escapeKeyName(keyName) | ||
+ (isRequired ? '' : '?') | ||
+ ': ' | ||
+ (hasStandaloneName(ast) ? toSafeString(type) : type) | ||
.map( | ||
({isRequired, keyName, ast}) => | ||
[isRequired, keyName, ast, generateType(ast, options)] as [boolean, string, AST, string] | ||
) | ||
.join('\n') | ||
+ '\n' | ||
+ '}' | ||
.map( | ||
([isRequired, keyName, ast, type]) => | ||
(hasComment(ast) && !ast.standaloneName ? generateComment(ast.comment) + '\n' : '') + | ||
escapeKeyName(keyName) + | ||
(isRequired ? '' : '?') + | ||
': ' + | ||
(hasStandaloneName(ast) ? toSafeString(type) : type) | ||
) | ||
.join('\n') + | ||
'\n' + | ||
'}' | ||
) | ||
} | ||
function generateComment(comment: string): string { | ||
return [ | ||
'/**', | ||
...comment.split('\n').map(_ => ' * ' + _), | ||
' */' | ||
].join('\n') | ||
return ['/**', ...comment.split('\n').map(_ => ' * ' + _), ' */'].join('\n') | ||
} | ||
function generateStandaloneEnum(ast: TEnum, options: Options): string { | ||
return (hasComment(ast) ? generateComment(ast.comment) + '\n' : '') | ||
+ 'export ' + (options.enableConstEnums ? 'const ' : '') + `enum ${toSafeString(ast.standaloneName)} {` | ||
+ '\n' | ||
+ ast.params.map(({ ast, keyName }) => | ||
keyName + ' = ' + generateType(ast, options) | ||
) | ||
.join(',\n') | ||
+ '\n' | ||
+ '}' | ||
return ( | ||
(hasComment(ast) ? generateComment(ast.comment) + '\n' : '') + | ||
'export ' + | ||
(options.enableConstEnums ? 'const ' : '') + | ||
`enum ${toSafeString(ast.standaloneName)} {` + | ||
'\n' + | ||
ast.params.map(({ast, keyName}) => keyName + ' = ' + generateType(ast, options)).join(',\n') + | ||
'\n' + | ||
'}' | ||
) | ||
} | ||
function generateStandaloneInterface(ast: TNamedInterface, options: Options): string { | ||
return (hasComment(ast) ? generateComment(ast.comment) + '\n' : '') | ||
+ `export interface ${toSafeString(ast.standaloneName)} ` | ||
+ (ast.superTypes.length > 0 ? `extends ${ast.superTypes.map(superType => toSafeString(superType.standaloneName)).join(', ')} ` : '') | ||
+ generateInterface(ast, options) | ||
return ( | ||
(hasComment(ast) ? generateComment(ast.comment) + '\n' : '') + | ||
`export interface ${toSafeString(ast.standaloneName)} ` + | ||
(ast.superTypes.length > 0 | ||
? `extends ${ast.superTypes.map(superType => toSafeString(superType.standaloneName)).join(', ')} ` | ||
: '') + | ||
generateInterface(ast, options) | ||
) | ||
} | ||
function generateStandaloneType(ast: ASTWithStandaloneName, options: Options): string { | ||
return (hasComment(ast) ? generateComment(ast.comment) + '\n' : '') | ||
+ `export type ${toSafeString(ast.standaloneName)} = ${generateType(omit<AST>(ast, 'standaloneName') as AST /* TODO */, options)}` | ||
return ( | ||
(hasComment(ast) ? generateComment(ast.comment) + '\n' : '') + | ||
`export type ${toSafeString(ast.standaloneName)} = ${generateType( | ||
omit<AST>(ast, 'standaloneName') as AST /* TODO */, | ||
options | ||
)}` | ||
) | ||
} | ||
function escapeKeyName(keyName: string): string { | ||
if ( | ||
keyName.length | ||
&& /[A-Za-z_$]/.test(keyName.charAt(0)) | ||
&& /^[\w$]+$/.test(keyName) | ||
) { | ||
if (keyName.length && /[A-Za-z_$]/.test(keyName.charAt(0)) && /^[\w$]+$/.test(keyName)) { | ||
return keyName | ||
@@ -350,5 +381,3 @@ } | ||
function getSuperTypesAndParams(ast: TInterface): AST[] { | ||
return ast.params | ||
.map(param => param.ast) | ||
.concat(ast.superTypes) | ||
return ast.params.map(param => param.ast).concat(ast.superTypes) | ||
} |
@@ -1,17 +0,17 @@ | ||
import { readFileSync } from 'fs' | ||
import { JSONSchema4 } from 'json-schema' | ||
import { Options as $RefOptions } from 'json-schema-ref-parser' | ||
import { endsWith, merge } from 'lodash' | ||
import { dirname } from 'path' | ||
import { Options as PrettierOptions } from 'prettier' | ||
import { format } from './formatter' | ||
import { generate } from './generator' | ||
import { normalize } from './normalizer' | ||
import { optimize } from './optimizer' | ||
import { parse } from './parser' | ||
import { dereference } from './resolver' | ||
import { error, stripExtension, Try } from './utils' | ||
import { validate } from './validator' | ||
import {readFileSync} from 'fs' | ||
import {JSONSchema4} from 'json-schema' | ||
import {Options as $RefOptions} from 'json-schema-ref-parser' | ||
import {endsWith, merge} from 'lodash' | ||
import {dirname} from 'path' | ||
import {Options as PrettierOptions} from 'prettier' | ||
import {format} from './formatter' | ||
import {generate} from './generator' | ||
import {normalize} from './normalizer' | ||
import {optimize} from './optimizer' | ||
import {parse} from './parser' | ||
import {dereference} from './resolver' | ||
import {error, stripExtension, Try} from './utils' | ||
import {validate} from './validator' | ||
export { EnumJSONSchema, JSONSchema, NamedEnumJSONSchema, CustomTypeJSONSchema } from './types/JSONSchema' | ||
export {EnumJSONSchema, JSONSchema, NamedEnumJSONSchema, CustomTypeJSONSchema} from './types/JSONSchema' | ||
@@ -79,27 +79,19 @@ export interface Options { | ||
export function compileFromFile( | ||
filename: string, | ||
options: Partial<Options> = DEFAULT_OPTIONS | ||
): Promise<string> { | ||
export function compileFromFile(filename: string, options: Partial<Options> = DEFAULT_OPTIONS): Promise<string> { | ||
const contents = Try( | ||
() => readFileSync(filename), | ||
() => { throw new ReferenceError(`Unable to read file "${filename}"`) } | ||
() => { | ||
throw new ReferenceError(`Unable to read file "${filename}"`) | ||
} | ||
) | ||
const schema = Try<JSONSchema4>( | ||
() => JSON.parse(contents.toString()), | ||
() => { throw new TypeError(`Error parsing JSON in file "${filename}"`) } | ||
() => { | ||
throw new TypeError(`Error parsing JSON in file "${filename}"`) | ||
} | ||
) | ||
return compile( | ||
schema, | ||
stripExtension(filename), | ||
{ cwd: dirname(filename), ...options } | ||
) | ||
return compile(schema, stripExtension(filename), {cwd: dirname(filename), ...options}) | ||
} | ||
export async function compile( | ||
schema: JSONSchema4, | ||
name: string, | ||
options: Partial<Options> = {} | ||
): Promise<string> { | ||
export async function compile(schema: JSONSchema4, name: string, options: Partial<Options> = {}): Promise<string> { | ||
const _options = merge({}, DEFAULT_OPTIONS, options) | ||
@@ -110,3 +102,3 @@ | ||
errors.forEach(_ => error(_)) | ||
throw new ValidationError | ||
throw new ValidationError() | ||
} | ||
@@ -119,10 +111,8 @@ | ||
return format(generate( | ||
optimize( | ||
parse(await dereference(normalize(schema, name), _options), _options) | ||
), | ||
return format( | ||
generate(optimize(parse(await dereference(normalize(schema, name), _options), _options)), _options), | ||
_options | ||
), _options) | ||
) | ||
} | ||
export class ValidationError extends Error { } | ||
export class ValidationError extends Error {} |
@@ -1,6 +0,6 @@ | ||
import { whiteBright } from 'cli-color' | ||
import {whiteBright} from 'cli-color' | ||
import stringify = require('json-stringify-safe') | ||
import { cloneDeep } from 'lodash' | ||
import { JSONSchema, JSONSchemaTypeName, NormalizedJSONSchema } from './types/JSONSchema' | ||
import { escapeBlockComment, justName, log, toSafeString, traverse } from './utils' | ||
import {cloneDeep} from 'lodash' | ||
import {JSONSchema, JSONSchemaTypeName, NormalizedJSONSchema} from './types/JSONSchema' | ||
import {escapeBlockComment, justName, log, toSafeString, traverse} from './utils' | ||
@@ -20,2 +20,13 @@ type Rule = (schema: JSONSchema, rootSchema: JSONSchema, fileName?: string) => void | ||
rules.set('Remove `type=["null"]` if `enum=[null]`', schema => { | ||
if ( | ||
Array.isArray(schema.enum) && | ||
schema.enum.some(e => e === null) && | ||
Array.isArray(schema.type) && | ||
schema.type.includes('null') | ||
) { | ||
schema.type = schema.type.filter(type => type !== 'null') | ||
} | ||
}) | ||
rules.set('Destructure unary types', schema => { | ||
@@ -27,3 +38,3 @@ if (schema.type && Array.isArray(schema.type) && schema.type.length === 1) { | ||
rules.set('Add empty `required` property if none is defined', (schema) => { | ||
rules.set('Add empty `required` property if none is defined', schema => { | ||
if (!('required' in schema) && isObjectType(schema)) { | ||
@@ -34,3 +45,3 @@ schema.required = [] | ||
rules.set('Transform `required`=false to `required`=[]', (schema) => { | ||
rules.set('Transform `required`=false to `required`=[]', schema => { | ||
if (schema.required === false) { | ||
@@ -42,6 +53,4 @@ schema.required = [] | ||
// TODO: default to empty schema (as per spec) instead | ||
rules.set('Default additionalProperties to true', (schema) => { | ||
if (!('additionalProperties' in schema) && | ||
isObjectType(schema) && | ||
schema.patternProperties === undefined) { | ||
rules.set('Default additionalProperties to true', schema => { | ||
if (!('additionalProperties' in schema) && isObjectType(schema) && schema.patternProperties === undefined) { | ||
schema.additionalProperties = true | ||
@@ -61,3 +70,3 @@ } | ||
rules.set('Normalise schema.minItems', (schema) => { | ||
rules.set('Normalise schema.minItems', schema => { | ||
// make sure we only add the props onto array types | ||
@@ -99,3 +108,3 @@ if (isArrayType(schema)) { | ||
rules.forEach((rule, key) => { | ||
traverse(_schema, (schema) => rule(schema, _schema, filename)) | ||
traverse(_schema, schema => rule(schema, _schema, filename)) | ||
log(whiteBright.bgYellow('normalizer'), `Applied rule: "${key}"`) | ||
@@ -102,0 +111,0 @@ }) |
@@ -1,9 +0,8 @@ | ||
import { whiteBright } from 'cli-color' | ||
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 {uniqBy} from 'lodash' | ||
import {AST, T_ANY} from './types/AST' | ||
import {log} from './utils' | ||
export function optimize(ast: AST, processed = new Map<AST, AST>()): AST { | ||
log(whiteBright.bgCyan('optimizer'), ast, processed.has(ast) ? '(FROM CACHE)' : '') | ||
@@ -20,9 +19,6 @@ | ||
return Object.assign(ast, { | ||
params: ast.params.map(_ => | ||
Object.assign(_, { ast: optimize(_.ast, processed) }) | ||
) | ||
params: ast.params.map(_ => Object.assign(_, {ast: optimize(_.ast, processed)})) | ||
}) | ||
case 'INTERSECTION': | ||
case 'UNION': | ||
// [A, B, C, Any] -> Any | ||
@@ -35,5 +31,3 @@ if (ast.params.some(_ => _.type === 'ANY')) { | ||
// [A, B, B] -> [A, B] | ||
ast.params = uniqBy(ast.params, _ => | ||
`${_.type}------${stringify((_ as any).params)}` | ||
) | ||
ast.params = uniqBy(ast.params, _ => `${_.type}------${stringify((_ as any).params)}`) | ||
@@ -40,0 +34,0 @@ return Object.assign(ast, { |
@@ -1,10 +0,19 @@ | ||
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, TTuple } from './types/AST' | ||
import { JSONSchema, JSONSchemaWithDefinitions, SchemaSchema } from './types/JSONSchema' | ||
import { generateName, log } from './utils' | ||
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, | ||
TTuple | ||
} from './types/AST' | ||
import {JSONSchema, JSONSchemaWithDefinitions, SchemaSchema} from './types/JSONSchema' | ||
import {generateName, log} from './utils' | ||
@@ -24,3 +33,2 @@ export type Processed = Map<JSONSchema | JSONSchema4Type, AST> | ||
): AST { | ||
// If we've seen this node before, return it. | ||
@@ -37,3 +45,3 @@ if (processed.has(schema)) { | ||
// TODO: Investigate alternative approaches (lazy-computing nodes, etc.) | ||
let ast = {} as AST | ||
const ast = {} as AST | ||
processed.set(schema, ast) | ||
@@ -43,3 +51,12 @@ const set = (_ast: AST) => Object.assign(ast, _ast) | ||
return isSchema | ||
? parseNonLiteral(schema as SchemaSchema, options, rootSchema, keyName, keyNameFromDefinition, set, processed, usedNames) | ||
? parseNonLiteral( | ||
schema as SchemaSchema, | ||
options, | ||
rootSchema, | ||
keyName, | ||
keyNameFromDefinition, | ||
set, | ||
processed, | ||
usedNames | ||
) | ||
: parseLiteral(schema, keyName, keyNameFromDefinition, set) | ||
@@ -72,3 +89,2 @@ } | ||
) { | ||
log(whiteBright.bgBlue('parser'), schema, '<-' + typeOfSchema(schema), processed.has(schema) ? '(FROM CACHE)' : '') | ||
@@ -185,3 +201,11 @@ | ||
} else if (schema.additionalItems) { | ||
arrayType.spreadParam = parse(schema.additionalItems, options, rootSchema, undefined, true, processed, usedNames) | ||
arrayType.spreadParam = parse( | ||
schema.additionalItems, | ||
options, | ||
rootSchema, | ||
undefined, | ||
true, | ||
processed, | ||
usedNames | ||
) | ||
} | ||
@@ -203,3 +227,5 @@ return set(arrayType) | ||
keyName, | ||
params: (schema.type as JSONSchema4TypeName[]).map(_ => parse({ type: _ }, options, rootSchema, undefined, true, processed, usedNames)), | ||
params: (schema.type as JSONSchema4TypeName[]).map(_ => | ||
parse({...schema, type: _}, options, rootSchema, undefined, true, processed, usedNames) | ||
), | ||
standaloneName: standaloneName(schema, keyNameFromDefinition, usedNames), | ||
@@ -217,3 +243,5 @@ type: 'UNION' | ||
case 'UNNAMED_SCHEMA': | ||
return set(newInterface(schema as SchemaSchema, options, rootSchema, processed, usedNames, keyName, keyNameFromDefinition)) | ||
return set( | ||
newInterface(schema as SchemaSchema, options, rootSchema, processed, usedNames, keyName, keyNameFromDefinition) | ||
) | ||
case 'UNTYPED_ARRAY': | ||
@@ -252,8 +280,4 @@ // normalised to not be undefined | ||
*/ | ||
function standaloneName( | ||
schema: JSONSchema, | ||
keyNameFromDefinition: string | undefined, | ||
usedNames: UsedNames | ||
) { | ||
let name = schema.title || schema.id || keyNameFromDefinition | ||
function standaloneName(schema: JSONSchema, keyNameFromDefinition: string | undefined, usedNames: UsedNames) { | ||
const name = schema.title || schema.id || keyNameFromDefinition | ||
if (name) { | ||
@@ -273,3 +297,3 @@ return generateName(name, usedNames) | ||
): TInterface { | ||
let name = standaloneName(schema, keyNameFromDefinition, usedNames)! | ||
const name = standaloneName(schema, keyNameFromDefinition, usedNames)! | ||
return { | ||
@@ -329,3 +353,2 @@ comment: schema.description, | ||
): TInterfaceParam[] { | ||
let asts: TInterfaceParam[] = map(schema.properties, (value, key: string) => ({ | ||
@@ -344,35 +367,37 @@ ast: parse(value, options, rootSchema, key, true, processed, usedNames), | ||
// value definition, we can validate against that. | ||
singlePatternProperty = ( | ||
!schema.additionalProperties && Object.keys(schema.patternProperties).length === 1 | ||
) | ||
singlePatternProperty = !schema.additionalProperties && Object.keys(schema.patternProperties).length === 1 | ||
asts = asts.concat(map(schema.patternProperties, (value, key: string) => { | ||
let ast = parse(value, options, rootSchema, key, true, processed, usedNames) | ||
let comment = `This interface was referenced by \`${parentSchemaName}\`'s JSON-Schema definition | ||
asts = asts.concat( | ||
map(schema.patternProperties, (value, key: string) => { | ||
const ast = parse(value, options, rootSchema, key, true, processed, usedNames) | ||
const comment = `This interface was referenced by \`${parentSchemaName}\`'s JSON-Schema definition | ||
via the \`patternProperty\` "${key}".` | ||
ast.comment = ast.comment ? `${ast.comment}\n\n${comment}` : comment | ||
return ({ | ||
ast, | ||
isPatternProperty: !singlePatternProperty, | ||
isRequired: singlePatternProperty || includes(schema.required || [], key), | ||
isUnreachableDefinition: false, | ||
keyName: singlePatternProperty ? '[k: string]' : key | ||
ast.comment = ast.comment ? `${ast.comment}\n\n${comment}` : comment | ||
return { | ||
ast, | ||
isPatternProperty: !singlePatternProperty, | ||
isRequired: singlePatternProperty || includes(schema.required || [], key), | ||
isUnreachableDefinition: false, | ||
keyName: singlePatternProperty ? '[k: string]' : key | ||
} | ||
}) | ||
})) | ||
) | ||
} | ||
if (options.unreachableDefinitions) { | ||
asts = asts.concat(map(schema.definitions, (value, key: string) => { | ||
let ast = parse(value, options, rootSchema, key, true, processed, usedNames) | ||
let comment = `This interface was referenced by \`${parentSchemaName}\`'s JSON-Schema | ||
asts = asts.concat( | ||
map(schema.definitions, (value, key: string) => { | ||
const ast = parse(value, options, rootSchema, key, true, processed, usedNames) | ||
const comment = `This interface was referenced by \`${parentSchemaName}\`'s JSON-Schema | ||
via the \`definition\` "${key}".` | ||
ast.comment = ast.comment ? `${ast.comment}\n\n${comment}` : comment | ||
return { | ||
ast, | ||
isPatternProperty: false, | ||
isRequired: includes(schema.required || [], key), | ||
isUnreachableDefinition: true, | ||
keyName: key | ||
} | ||
})) | ||
ast.comment = ast.comment ? `${ast.comment}\n\n${comment}` : comment | ||
return { | ||
ast, | ||
isPatternProperty: false, | ||
isRequired: includes(schema.required || [], key), | ||
isUnreachableDefinition: true, | ||
keyName: key | ||
} | ||
}) | ||
) | ||
} | ||
@@ -411,3 +436,3 @@ | ||
type Definitions = { [k: string]: JSONSchema } | ||
type Definitions = {[k: string]: JSONSchema} | ||
@@ -417,7 +442,3 @@ /** | ||
*/ | ||
function getDefinitions( | ||
schema: JSONSchema, | ||
isSchema = true, | ||
processed = new Set<JSONSchema>() | ||
): Definitions { | ||
function getDefinitions(schema: JSONSchema, isSchema = true, processed = new Set<JSONSchema>()): Definitions { | ||
if (processed.has(schema)) { | ||
@@ -428,6 +449,9 @@ return {} | ||
if (Array.isArray(schema)) { | ||
return schema.reduce((prev, cur) => ({ | ||
...prev, | ||
...getDefinitions(cur, false, processed) | ||
}), {}) | ||
return schema.reduce( | ||
(prev, cur) => ({ | ||
...prev, | ||
...getDefinitions(cur, false, processed) | ||
}), | ||
{} | ||
) | ||
} | ||
@@ -437,6 +461,9 @@ if (isPlainObject(schema)) { | ||
...(isSchema && hasDefinitions(schema) ? schema.definitions : {}), | ||
...Object.keys(schema).reduce<Definitions>((prev, cur) => ({ | ||
...prev, | ||
...getDefinitions(schema[cur], false, processed) | ||
}), {}) | ||
...Object.keys(schema).reduce<Definitions>( | ||
(prev, cur) => ({ | ||
...prev, | ||
...getDefinitions(schema[cur], false, processed) | ||
}), | ||
{} | ||
) | ||
} | ||
@@ -443,0 +470,0 @@ } |
@@ -1,10 +0,13 @@ | ||
import { whiteBright } from 'cli-color' | ||
import {whiteBright} from 'cli-color' | ||
import $RefParser = require('json-schema-ref-parser') | ||
import { JSONSchema } from './types/JSONSchema' | ||
import { log } from './utils' | ||
import {JSONSchema} from './types/JSONSchema' | ||
import {log} from './utils' | ||
export async function dereference(schema: JSONSchema, {cwd, $refOptions}: {cwd: string, $refOptions: $RefParser.Options}): Promise<JSONSchema> { | ||
export async function dereference( | ||
schema: JSONSchema, | ||
{cwd, $refOptions}: {cwd: string; $refOptions: $RefParser.Options} | ||
): Promise<JSONSchema> { | ||
log(whiteBright.bgGreen('resolver'), schema, cwd) | ||
const parser = new $RefParser | ||
const parser = new $RefParser() | ||
return parser.dereference(cwd, schema, $refOptions) | ||
} |
@@ -1,3 +0,3 @@ | ||
import { isPlainObject } from 'lodash' | ||
import { JSONSchema, SCHEMA_TYPE } from './types/JSONSchema' | ||
import {isPlainObject} from 'lodash' | ||
import {JSONSchema, SCHEMA_TYPE} from './types/JSONSchema' | ||
@@ -12,2 +12,4 @@ /** | ||
if (schema.oneOf) return 'ONE_OF' | ||
if (Array.isArray(schema.type)) return 'UNION' | ||
if (schema.type === 'null') return 'NULL' | ||
if (schema.items) return 'TYPED_ARRAY' | ||
@@ -17,8 +19,11 @@ if (schema.enum && schema.tsEnumNames) return 'NAMED_ENUM' | ||
if (schema.$ref) return 'REFERENCE' | ||
if (Array.isArray(schema.type)) return 'UNION' | ||
switch (schema.type) { | ||
case 'string': return 'STRING' | ||
case 'number': return 'NUMBER' | ||
case 'integer': return 'NUMBER' | ||
case 'boolean': return 'BOOLEAN' | ||
case 'string': | ||
return 'STRING' | ||
case 'number': | ||
return 'NUMBER' | ||
case 'integer': | ||
return 'NUMBER' | ||
case 'boolean': | ||
return 'BOOLEAN' | ||
case 'object': | ||
@@ -29,11 +34,15 @@ if (!schema.properties && !isPlainObject(schema)) { | ||
break | ||
case 'array': return 'UNTYPED_ARRAY' | ||
case 'null': return 'NULL' | ||
case 'any': return 'ANY' | ||
case 'array': | ||
return 'UNTYPED_ARRAY' | ||
case 'any': | ||
return 'ANY' | ||
} | ||
switch (typeof schema.default) { | ||
case 'boolean': return 'BOOLEAN' | ||
case 'number': return 'NUMBER' | ||
case 'string': return 'STRING' | ||
case 'boolean': | ||
return 'BOOLEAN' | ||
case 'number': | ||
return 'NUMBER' | ||
case 'string': | ||
return 'STRING' | ||
} | ||
@@ -40,0 +49,0 @@ if (schema.id) return 'NAMED_SCHEMA' |
@@ -1,5 +0,5 @@ | ||
import { whiteBright } from 'cli-color' | ||
import { deburr, isPlainObject, mapValues, trim, upperFirst } from 'lodash' | ||
import { basename, extname } from 'path' | ||
import { JSONSchema } from './types/JSONSchema' | ||
import {whiteBright} from 'cli-color' | ||
import {deburr, isPlainObject, mapValues, trim, upperFirst} from 'lodash' | ||
import {basename, extname} from 'path' | ||
import {JSONSchema} from './types/JSONSchema' | ||
@@ -18,4 +18,4 @@ // TODO: pull out into a separate package | ||
*/ | ||
export function dft<T, U>(object: { [k: string]: any }, cb: (value: U, key: string) => T): void { | ||
for (let key in object) { | ||
export function dft<T, U>(object: {[k: string]: any}, cb: (value: U, key: string) => T): void { | ||
for (const key in object) { | ||
if (!object.hasOwnProperty(key)) continue | ||
@@ -27,20 +27,19 @@ if (isPlainObject(object[key])) dft(object[key], cb) | ||
export function mapDeep( | ||
object: object, | ||
fn: (value: object, key?: string) => object, | ||
key?: string | ||
): object { | ||
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) | ||
export function mapDeep(object: object, fn: (value: object, key?: string) => object, key?: string): object { | ||
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 | ||
) | ||
} | ||
@@ -83,6 +82,3 @@ | ||
]) | ||
function traverseObjectKeys( | ||
obj: Record<string, JSONSchema>, | ||
callback: (schema: JSONSchema) => void | ||
) { | ||
function traverseObjectKeys(obj: Record<string, JSONSchema>, callback: (schema: JSONSchema) => void) { | ||
Object.keys(obj).forEach(k => { | ||
@@ -140,8 +136,10 @@ if (obj[k] && typeof obj[k] === 'object' && !Array.isArray(obj[k])) { | ||
// 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) | ||
} | ||
}) | ||
Object.keys(schema) | ||
.filter(key => !BLACKLISTED_KEYS.has(key)) | ||
.forEach(key => { | ||
const child = schema[key] | ||
if (child && typeof child === 'object') { | ||
traverseObjectKeys(child, callback) | ||
} | ||
}) | ||
} | ||
@@ -175,14 +173,15 @@ | ||
deburr(string) | ||
// replace chars which are not valid for typescript identifiers with whitespace | ||
.replace(/(^\s*[^a-zA-Z_$])|([^a-zA-Z_$\d])/g, ' ') | ||
// uppercase leading underscores followed by lowercase | ||
.replace(/^_[a-z]/g, match => match.toUpperCase()) | ||
// remove non-leading underscores followed by lowercase (convert snake_case) | ||
.replace(/_[a-z]/g, match => match.substr(1, match.length).toUpperCase()) | ||
// uppercase letters after digits, dollars | ||
.replace(/([\d$]+[a-zA-Z])/g, match => match.toUpperCase()) | ||
// uppercase first letter after whitespace | ||
.replace(/\s+([a-zA-Z])/g, match => trim(match.toUpperCase())) | ||
// remove remaining whitespace | ||
.replace(/\s/g, '')) | ||
// replace chars which are not valid for typescript identifiers with whitespace | ||
.replace(/(^\s*[^a-zA-Z_$])|([^a-zA-Z_$\d])/g, ' ') | ||
// uppercase leading underscores followed by lowercase | ||
.replace(/^_[a-z]/g, match => match.toUpperCase()) | ||
// remove non-leading underscores followed by lowercase (convert snake_case) | ||
.replace(/_[a-z]/g, match => match.substr(1, match.length).toUpperCase()) | ||
// uppercase letters after digits, dollars | ||
.replace(/([\d$]+[a-zA-Z])/g, match => match.toUpperCase()) | ||
// uppercase first letter after whitespace | ||
.replace(/\s+([a-zA-Z])/g, match => trim(match.toUpperCase())) | ||
// remove remaining whitespace | ||
.replace(/\s/g, '') | ||
) | ||
} | ||
@@ -189,0 +188,0 @@ |
@@ -1,3 +0,3 @@ | ||
import { JSONSchema } from './types/JSONSchema' | ||
import { mapDeep } from './utils' | ||
import {JSONSchema} from './types/JSONSchema' | ||
import {mapDeep} from './utils' | ||
@@ -4,0 +4,0 @@ type Rule = (schema: JSONSchema) => boolean | void |
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
1332962
52
37988
16
Updatedprettier@^1.19.1