UNPKG

@spec2ts/jsonschema

Version:

Utility to convert JSON Schemas to Typescript using TypeScript native compiler

322 lines (321 loc) 12.9 kB
"use strict"; 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