UNPKG

dtsgeneratorws

Version:

TypeScript d.ts file generator for JSON Schema file

310 lines (286 loc) 12.9 kB
import Debug from 'debug'; import { isEqual } from 'lodash'; import { tilde } from '../jsonPointer'; import { getSubSchema, JsonSchema, NormalizedSchema, Schema } from './jsonSchema'; import ReferenceResolver from './referenceResolver'; import SchemaConvertor from './schemaConvertor'; import * as utils from './utils'; const debug = Debug('dtsgen'); const typeMarker = Symbol(); export default class DtsGenerator { private currentSchema!: NormalizedSchema; constructor(private resolver: ReferenceResolver, private convertor: SchemaConvertor) { } public async generate(): Promise<string> { debug('generate type definition files.'); await this.resolver.resolve(); const map = this.convertor.buildSchemaMergedMap(this.resolver.getAllRegisteredSchema(), typeMarker); this.convertor.start(); this.generateEnums(); this.walk(map); const result = this.convertor.end(); return result; } private walk(map: any): void { const keys = Object.keys(map).sort(); for (const key of keys) { const value = map[key]; if (value.hasOwnProperty(typeMarker)) { const schema = value[typeMarker] as Schema; debug(` walk doProcess: key=${key} schemaId=${schema.id.getAbsoluteId()}`); this.walkSchema(schema); delete value[typeMarker]; } if (typeof value === 'object' && Object.keys(value).length > 0) { this.convertor.startNest(key); this.walk(value); this.convertor.endNest(); } } } private walkSchema(schema: Schema): void { const normalized = this.normalizeContent(schema); this.currentSchema = normalized; this.convertor.outputComments(normalized); const type = normalized.content.type; switch (type) { case 'any': return this.generateAnyTypeModel(normalized); case 'array': return this.generateTypeCollection(normalized); case 'object': default: return this.generateDeclareType(normalized); } } private normalizeContent(schema: Schema, pointer?: string): NormalizedSchema { if (pointer != null) { schema = getSubSchema(schema, pointer); } let content = schema.content; if (typeof content === 'boolean') { content = content ? {} : { not: {} }; } else { if (content.allOf) { const work = content; for (let sub of content.allOf) { if (typeof sub === 'object' && sub.$ref) { const ref = this.resolver.dereference(sub.$ref); sub = this.normalizeContent(ref).content; } utils.mergeSchema(work, sub); } delete content.allOf; content = work; } if (content.type === undefined && (content.properties || content.additionalProperties)) { content.type = 'object'; } if (content.nullable) { const type = content.type; if (type == null) { content.type = 'null'; } else if (!Array.isArray(type)) { content.type = [type, 'null']; } else { type.push('null'); } } const types = content.type; if (Array.isArray(types)) { const reduced = utils.reduceTypes(types); content.type = reduced.length === 1 ? reduced[0] : reduced; } } return Object.assign({}, schema, { content }); } private generateDeclareType(schema: NormalizedSchema): void { const content = schema.content; if (content.$ref || content.oneOf || content.anyOf || content.enum || 'const' in content || content.type !== 'object') { this.convertor.outputExportType(schema.id); this.generateTypeProperty(schema, true); } else { this.convertor.startInterfaceNest(schema.id); this.generateProperties(schema); this.convertor.endInterfaceNest(); } } private generateEnums(): void { const enums = this.resolver.getAllEnums(); enums.forEach((values, name) => { this.convertor.outputRawValue(`export const enum ${name} {\n`); values.forEach(value => { const enumKey = value[0].toUpperCase() + value.replace(/[\- ]+([a-zA-Z])/g, match => match[match.length - 1].toUpperCase()) .substr(1); this.convertor.outputRawValue(' '.repeat(4)); this.convertor.outputRawValue(`${enumKey} = '${value}',`); this.convertor.outputRawValue('\n'); }); this.convertor.outputRawValue('}\n\n'); }); } private generateAnyTypeModel(schema: NormalizedSchema): void { this.convertor.startInterfaceNest(schema.id); this.convertor.outputRawValue('[name: string]: any; // any', true); this.convertor.endInterfaceNest(); } private generateTypeCollection(schema: NormalizedSchema): void { this.convertor.outputExportType(schema.id); this.generateArrayTypeProperty(schema, true); } private generateProperties(baseSchema: NormalizedSchema): void { const content = baseSchema.content; if (content.additionalProperties) { this.convertor.outputRawValue('[name: string]: '); const schema = this.normalizeContent(baseSchema, '/additionalProperties'); if (content.additionalProperties === true) { this.convertor.outputStringTypeName(schema, 'any', true); } else { this.generateTypeProperty(schema, true); } } if (content.properties) { for (const propertyName of Object.keys(content.properties)) { const schema = this.normalizeContent(baseSchema, '/properties/' + tilde(propertyName)); this.convertor.outputComments(schema); this.convertor.outputPropertyAttribute(schema); this.convertor.outputPropertyName(schema, propertyName, baseSchema.content.required); this.generateTypeProperty(schema); } } } private generateTypeProperty(schema: NormalizedSchema, terminate = true): void { const content = schema.content; if (content.$ref) { const ref = this.resolver.dereference(content.$ref); if (ref.id == null) { throw new Error('target referenced id is nothing: ' + content.$ref); } const refSchema = this.normalizeContent(ref); return this.convertor.outputTypeIdName(refSchema, this.currentSchema, terminate); } if (content.anyOf || content.oneOf) { this.generateArrayedType(schema, content.anyOf, '/anyOf/', terminate); this.generateArrayedType(schema, content.oneOf, '/oneOf/', terminate); return; } if (content.enum) { const enums = this.resolver.getAllEnums(); const contentEnumName = (content as any).enumName; let enumName = ''; if (enums.has(contentEnumName)) { enumName = contentEnumName; } else { enums.forEach((value, key) => { if (enumName) { return ; } if (isEqual(value, content.enum)) { enumName = key; } }); } this.convertor.outputRawValue(`${enumName};\n `); } else if ('const' in content) { const value = content.const; if (content.type === 'integer' || content.type === 'number') { this.convertor.outputStringTypeName(schema, '' + value, terminate); } else { this.convertor.outputStringTypeName(schema, `"${value}"`, terminate); } } else { this.generateType(schema, terminate); } } private generateArrayedType(baseSchema: NormalizedSchema, contents: JsonSchema[] | undefined, path: string, terminate: boolean): void { if (contents) { this.convertor.outputArrayedType(baseSchema, contents, (_content, index) => { const schema = this.normalizeContent(baseSchema, path + index); if (schema.id.isEmpty()) { this.generateTypeProperty(schema, false); } else { this.convertor.outputTypeIdName(schema, this.currentSchema, false); } }, terminate); } } private generateArrayTypeProperty(schema: NormalizedSchema, terminate = true): void { const items = schema.content.items; const minItems = schema.content.minItems; const maxItems = schema.content.maxItems; if (items == null) { this.convertor.outputStringTypeName(schema, 'any[]', terminate); } else if (!Array.isArray(items)) { this.generateTypeProperty(this.normalizeContent(schema, '/items'), false); this.convertor.outputStringTypeName(schema, '[]', terminate); } else if (items.length === 0 && minItems === undefined && maxItems === undefined) { this.convertor.outputStringTypeName(schema, 'any[]', terminate); } else if (minItems != null && maxItems != null && maxItems < minItems) { this.convertor.outputStringTypeName(schema, 'never', terminate); } else { this.convertor.outputRawValue('['); let itemCount = Math.max(minItems || 0, maxItems || 0, items.length); if (maxItems != null) { itemCount = Math.min(itemCount, maxItems); } for (let i = 0; i < itemCount; i++) { if (i > 0) { this.convertor.outputRawValue(', '); } if (i < items.length) { const type = this.normalizeContent(schema, '/items/' + i); if (type.id.isEmpty()) { this.generateTypeProperty(type, false); } else { this.convertor.outputTypeIdName(type, this.currentSchema, false); } } else { this.convertor.outputStringTypeName(schema, 'any', false, false); } if (minItems == null || i >= minItems) { this.convertor.outputRawValue('?'); } } if (maxItems == null) { if (itemCount > 0) { this.convertor.outputRawValue(', '); } this.convertor.outputStringTypeName(schema, '...any[]', false, false); } this.convertor.outputRawValue(']'); this.convertor.outputStringTypeName(schema, '', terminate); } } private generateType(schema: NormalizedSchema, terminate: boolean, outputOptional = true): void { const type = schema.content.type; if (type == null) { this.convertor.outputPrimitiveTypeName(schema, 'any', terminate, outputOptional); } else if (typeof type === 'string') { this.generateTypeName(schema, type, terminate, outputOptional); } else { const types = utils.reduceTypes(type); if (types.length <= 1) { schema.content.type = types[0]; this.generateType(schema, terminate, outputOptional); } else { this.convertor.outputArrayedType(schema, types, (t) => { this.generateTypeName(schema, t, false, false); }, terminate); } } } private generateTypeName(schema: NormalizedSchema, type: string, terminate: boolean, outputOptional = true): void { const tsType = utils.toTSType(type, schema.content); if (tsType) { this.convertor.outputPrimitiveTypeName(schema, tsType, terminate, outputOptional); } else if (type === 'object') { this.convertor.startTypeNest(); this.generateProperties(schema); this.convertor.endTypeNest(terminate); } else if (type === 'array') { this.generateArrayTypeProperty(schema, terminate); } else { throw new Error('unknown type: ' + type); } } }