@spec2ts/jsonschema
Version:
Utility to convert JSON Schemas to Typescript using TypeScript native compiler
322 lines (321 loc) • 12.9 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.pascalCase = exports.isNullable = exports.getAnyType = exports.getSchemaName = exports.createRefContext = exports.createContext = exports.isReference = exports.resolveReferenceDetails = exports.resolveReference = exports.getReferenceType = exports.parseReference = exports.parseDefinitions = exports.getTypeFromProperties = exports.getBaseTypeFromSchema = exports.getTypeFromSchema = void 0;
const path = require("path");
const ts = require("typescript");
const json_schema_ref_parser_1 = require("@apidevtools/json-schema-ref-parser");
const core = require("@spec2ts/core");
//#endregion
//#region Core
/**
* Creates a type node from a given schema.
* Delegates to getBaseTypeFromSchema internally and optionally adds a union with null.
*/
function getTypeFromSchema(schema, context) {
const type = getBaseTypeFromSchema(schema, context);
return isNullable(schema) ? ts.factory.createUnionTypeNode([type, core.keywordType.null]) : type;
}
exports.getTypeFromSchema = getTypeFromSchema;
/** This is the very core of the Schema to TS conversion - it takes a schema and returns the appropriate type. */
function getBaseTypeFromSchema(schema, context) {
var _a, _b;
if (!schema) {
return getAnyType(context);
}
if (isReference(schema)) {
return getReferenceType(schema, context);
}
if (schema.oneOf) {
// oneOf -> union
return ts.factory.createUnionTypeNode(getTypeFromDefinitions(schema.oneOf, context));
}
if (schema.anyOf) {
// anyOf -> union
return ts.factory.createUnionTypeNode(getTypeFromDefinitions(schema.anyOf, context));
}
if (schema.allOf) {
// allOf -> intersection
return ts.factory.createIntersectionTypeNode(getTypeFromDefinitions(schema.allOf, context));
}
if (schema.prefixItems) {
// prefixItems -> tuple
const types = getTypeFromDefinitions(schema.prefixItems, context);
if ((_a = schema.items) !== null && _a !== void 0 ? _a : true) {
types.push(ts.factory.createRestTypeNode(getBaseTypeFromSchema({ items: (_b = schema.items) !== null && _b !== void 0 ? _b : true }, context)));
}
return ts.factory.createTupleTypeNode(types);
}
if (schema.items) {
// items -> array
if (Array.isArray(schema.items)) {
return ts.factory.createArrayTypeNode(ts.factory.createUnionTypeNode(getTypeFromDefinitions(schema.items, context)));
}
if (typeof schema.items === "boolean") {
return ts.factory.createArrayTypeNode(getAnyType(context));
}
return ts.factory.createArrayTypeNode(getTypeFromSchema(schema.items, context));
}
if (schema.properties || schema.additionalProperties) {
// properties -> literal type
return getTypeFromProperties(schema.properties || {}, schema.required || [], schema.additionalProperties, context);
}
// (could be boolean -> strong checking required)
if (typeof schema.const !== "undefined") {
// const -> literal type
return ts.factory.createLiteralTypeNode(((s) => {
switch (typeof s) {
case "string":
return ts.factory.createStringLiteral(s);
case "number":
return ts.factory.createNumericLiteral(s.toString());
case "boolean":
return s ? ts.factory.createTrue() : ts.factory.createFalse();
default:
return ts.factory.createStringLiteral(String(s));
}
})(schema.const));
}
if (schema.enum) {
// enum -> union of literal types
return ts.factory.createUnionTypeNode(schema.enum.map((s) => ts.factory.createLiteralTypeNode(typeof s === "string"
? ts.factory.createStringLiteral(s)
: typeof s === "number"
? ts.factory.createNumericLiteral(s.toString())
: ts.factory.createStringLiteral(String(s)))));
}
if (schema.format == "binary") {
return ts.factory.createTypeReferenceNode("Blob", []);
}
if (context.options.enableDate && (schema.format === "date" || schema.format === "date-time")) {
if (context.options.enableDate === true || context.options.enableDate === "strict") {
return ts.factory.createTypeReferenceNode("Date");
}
else {
return ts.factory.createUnionTypeNode([
core.keywordType.string,
ts.factory.createTypeReferenceNode("Date"),
]);
}
}
if (schema.type) {
return getTypeFromStandardTypes(schema.type, context);
}
return getAnyType(context);
}
exports.getBaseTypeFromSchema = getBaseTypeFromSchema;
/** Recursively creates a type literal with the given props. */
function getTypeFromProperties(props, required, additionalProperties, context) {
const members = Object.keys(props).map((name) => {
const schema = props[name];
const isRequired = required && required.includes(name);
return core.createPropertySignature({
questionToken: !isRequired,
name,
type: getTypeFromDefinition(schema, context),
});
});
if (additionalProperties) {
const type = additionalProperties === true ? core.keywordType.any : getTypeFromSchema(additionalProperties, context);
members.push(core.createIndexSignature(type));
}
return ts.factory.createTypeLiteralNode(members);
}
exports.getTypeFromProperties = getTypeFromProperties;
/** Creates types from definitions. */
function parseDefinitions(schema, context) {
if (!schema || isReference(schema) || !schema.definitions) {
return;
}
return Object.keys(schema.definitions).forEach((refName) => parseReference({ $ref: `#/definitions/${refName}` }, context));
}
exports.parseDefinitions = parseDefinitions;
/** Extract types from a Type Definition array. */
function getTypeFromDefinitions(definitions, context) {
return definitions.map((s) => getTypeFromDefinition(s, context));
}
/** Get a type from a schema definition. */
function getTypeFromDefinition(schema, context) {
if (typeof schema === "boolean") {
return getAnyType(context);
}
return getTypeFromSchema(schema, context);
}
/** Get a type from standard types (eg: string, number, boolean...). */
function getTypeFromStandardTypes(type, context) {
if (!type) {
return getAnyType(context);
}
if (Array.isArray(type)) {
return ts.factory.createUnionTypeNode(type.map((t) => getTypeFromStandardTypes(t, context)));
}
// string, boolean, null, number
if (core.isKeywordTypeName(type))
return core.keywordType[type];
if (type === "integer")
return core.keywordType.number;
return getAnyType(context);
}
//#endregion
//#region Reference
function parseReference(obj, context) {
const $ref = applyRefPrefix(obj.$ref, context);
const { id, schema, path } = resolveReferenceDetails({ $ref }, context);
let ref = context.refs[id];
if (!ref) {
const name = getSchemaName(schema, $ref);
ref = context.refs[id] = {
$ref,
name,
schema,
path,
node: ts.factory.createTypeReferenceNode(name, undefined),
isRemote: $ref.startsWith("http"),
isLocal: $ref.startsWith("#/"),
};
const doParseReference = context.options.parseReference || defaultParseReference;
doParseReference(ref, context);
}
return ref;
}
exports.parseReference = parseReference;
function defaultParseReference(ref, context) {
if (ref.isRemote || ref.isLocal) {
const type = getTypeFromSchema(ref.schema, createRefContext(ref, context));
context.aliases.push(core.createTypeOrInterfaceDeclaration({
modifiers: [core.modifier.export],
name: ref.name,
type,
}));
}
else {
const importPath = getImportFromRef(ref.$ref);
if (importPath) {
addOrUpdateImport(importPath, ref, context);
}
}
}
function getReferenceType(obj, context) {
const { node } = parseReference(obj, context);
return node;
}
exports.getReferenceType = getReferenceType;
function resolveReference(obj, context) {
if (!isReference(obj)) {
return obj;
}
return context.$refs.get(obj.$ref);
}
exports.resolveReference = resolveReference;
function resolveReferenceDetails(obj, context) {
const pointer = context.$refs._resolve(obj.$ref, "");
const cwd = context.options.cwd || process.cwd();
return {
id: pointer.path,
path: path.relative(cwd, pointer.$ref.path),
schema: pointer.value,
};
}
exports.resolveReferenceDetails = resolveReferenceDetails;
function isReference(obj) {
return typeof obj === "object" && !!obj && "$ref" in obj;
}
exports.isReference = isReference;
//#endregion
//#region Utils
async function createContext(schema, options) {
const sanitize = (cwd) => (cwd.endsWith("/") ? cwd : cwd + "/");
const cwd = sanitize(options.cwd || process.cwd());
const $refs = await json_schema_ref_parser_1.default.resolve(cwd, schema, {});
return {
$refs,
schema,
refs: {},
parseReference: defaultParseReference,
imports: [],
aliases: [],
options,
names: {},
};
}
exports.createContext = createContext;
function createRefContext(ref, context) {
if (!ref.path)
return context;
const refPrefix = !context.refPrefix || context.refPrefix === ref.path ? ref.path : path.join(path.dirname(context.refPrefix), ref.path);
return { ...context, refPrefix };
}
exports.createRefContext = createRefContext;
function getSchemaName(schema, path) {
if (schema.title) {
return pascalCase(schema.title);
}
if (schema.$id) {
return pascalCase(schema.$id.replace(/.+\//, "").replace(/\.(json)|(ya?ml)$/, ""));
}
if (!path) {
throw new Error("Can't determine Schema name");
}
return pascalCase(path
.replace(/.+\//, "")
.replace(/\.(json)|(ya?ml)$/, "")
.replace(/#$/, ""));
}
exports.getSchemaName = getSchemaName;
function getAnyType(context) {
return context.options.avoidAny ? core.keywordType.unknown : core.keywordType.any;
}
exports.getAnyType = getAnyType;
function isNullable(schema) {
return !!(schema && schema.nullable);
}
exports.isNullable = isNullable;
function pascalCase(name) {
var _a, _b;
return ((_b = (_a = name
.match(/[a-z0-9]+/gi)) === null || _a === void 0 ? void 0 : _a.map((word) => word.charAt(0).toUpperCase() + word.substr(1)).join("")) !== null && _b !== void 0 ? _b : "");
}
exports.pascalCase = pascalCase;
function addOrUpdateImport(importPath, ref, context) {
var _a;
const importNamedBinding = getSchemaName(ref.schema, ref.$ref);
const importDeclaration = context.imports.find((i) => core.getString(i.moduleSpecifier) === importPath);
if (importDeclaration &&
((_a = importDeclaration.importClause) === null || _a === void 0 ? void 0 : _a.namedBindings) &&
ts.isNamedImports(importDeclaration.importClause.namedBindings)) {
const elements = importDeclaration.importClause.namedBindings.elements;
const hasName = elements.some((e) => e.name.text === importNamedBinding);
if (hasName)
return;
const newImportDeclaration = ts.factory.updateImportDeclaration(importDeclaration, importDeclaration.modifiers, ts.factory.updateImportClause(importDeclaration.importClause, importDeclaration.importClause.isTypeOnly, importDeclaration.importClause.name, ts.factory.updateNamedImports(importDeclaration.importClause.namedBindings, ts.factory.createNodeArray([
...elements,
core.createImportSpecifier(importNamedBinding)
]))), importDeclaration.moduleSpecifier, undefined);
context.imports.splice(context.imports.indexOf(importDeclaration), 1, newImportDeclaration);
}
else {
context.imports.push(core.createNamedImportDeclaration({
moduleSpecifier: importPath,
bindings: [importNamedBinding],
}));
}
}
function getImportFromRef(ref) {
let [importPath] = ref.split("#");
if (!importPath)
return;
importPath = importPath.replace(/(\.json)|(\.ya?ml)$/, "");
if (!importPath.startsWith("./")) {
importPath = "./" + importPath;
}
return importPath;
}
function applyRefPrefix(ref, context) {
if (!context.refPrefix)
return ref;
if (ref.startsWith("http"))
return ref;
if (ref.startsWith("#"))
return context.refPrefix + ref;
return path.join(path.dirname(context.refPrefix), ref);
}
//#endregion