import * as _ from 'lodash'; import type { AstNode } from './compiler'; import { compileTemplate } from './compiler'; import { Surrounding } from '../types/pontConfig'; import { PrimitiveTypeMap } from './primitiveTypeMap'; // primitive type export enum PrimitiveType { number = 'number', string = 'string', boolean = 'boolean' } class Contextable { private context: T; constructor(arg = {}) { _.forEach(arg, (value, key) => { if (value !== undefined) { this[key] = value; } }); } getContext(): T { return this.context; } setContext(context: T) { this.context = context; } getDsName() { const context = this.getContext(); return context?.dataSource?.name ?? ''; } toJSON() { return _.omit(this, 'context'); } } /** deprecated */ class DataType { primitiveType: PrimitiveType; isArr: boolean = false; customType: string = ''; // reference may have generic like Pagination reference: string = ''; enum: Array = []; isTemplateRef = false; } // 兼容性代码,将老的 datatype 转换为新的。 function dateTypeRefs2Ast(refStr: string, originName: string, compileTemplateKeyword?: string) { let ref = refStr.replace(new RegExp(`defs.${originName}.`, 'g'), ''); ref = ref.replace(/defs./g, ''); ref = ref.replace(/= any/g, ''); const PreTemplate = '«'; const EndTemplate = '»'; ref = ref.replace(//g, EndTemplate); const ast = compileTemplate(ref, compileTemplateKeyword); return ast; } // 兼容性代码,将老的 datatype 转换为新的。 function dataType2StandardDataType( dataType: DataType, originName: string, defNames: string[], compileTemplateKeyword?: string ) { let standardDataType = null as StandardDataType; if (dataType.enum && dataType.enum.length) { standardDataType = new StandardDataType([], '', false, -1, dataType, compileTemplateKeyword); standardDataType.setEnum(dataType.enum); } else if (dataType.primitiveType) { standardDataType = new StandardDataType([], dataType.primitiveType, false, -1, dataType, compileTemplateKeyword); } else if (dataType.reference) { const ast = dateTypeRefs2Ast(dataType.reference, originName, compileTemplateKeyword); standardDataType = parseAst2StandardDataType(ast, defNames, []); } if (dataType.isArr) { if (!standardDataType) { standardDataType = new StandardDataType(); } return new StandardDataType([standardDataType], 'Array', false, -1, dataType, compileTemplateKeyword); } if (!standardDataType) { return new StandardDataType(); } return standardDataType; } export class StandardDataType extends Contextable { enum: Array = []; setEnum(enums: Array = []) { this.enum = enums.map((value) => { if (typeof value === 'string') { if (!value.startsWith("'")) { value = "'" + value; } if (!value.endsWith("'")) { value = value + "'"; } } return value; }); } typeProperties: Property[] = []; constructor( public typeArgs = [] as StandardDataType[], /** 例如 number,A(defs.A),Array,Object, '1' | '2' | 'a' 等 */ public typeName = '', public isDefsType = false, /** 指向类的第几个模板,-1 表示没有 */ public templateIndex = -1, /** 其他的任意参数 */ public anyProps: Record = {}, public compileTemplateKeyword = '#/definitions/' ) { super(); if (anyProps) { Object.keys( _.omit(anyProps, ['typeArgs', 'typeName', 'isDefsType', 'templateIndex', 'compileTemplateKeyword']) ).forEach((key) => (this[key] = anyProps[key])); } Reflect.deleteProperty(this, 'anyProps'); } static constructorWithEnum(enums: Array = []) { const dataType = new StandardDataType(); dataType.setEnum(enums); return dataType; } static constructorFromJSON(dataType: StandardDataType, originName: string, defNames: string[]) { if (Object.getOwnPropertyNames(dataType).includes('reference')) { return dataType2StandardDataType(dataType as any, originName, defNames, dataType.compileTemplateKeyword); } const { isDefsType, templateIndex, typeArgs = [], typeName, typeProperties, ...restProps } = dataType; if (typeArgs.length) { const instance: StandardDataType = new StandardDataType( typeArgs.map((arg) => StandardDataType.constructorFromJSON(arg, originName, defNames)), typeName, isDefsType, templateIndex, restProps ); instance.setEnum(dataType.enum); return instance; } const result = new StandardDataType([], typeName, isDefsType, templateIndex, restProps); result.setEnum(dataType.enum); result.typeProperties = (typeProperties || []).map((prop) => new Property(prop)); return result; } setTemplateIndex(classTemplateArgs: StandardDataType[]) { const codes = classTemplateArgs.map((arg) => arg.generateCode()); const index = codes.indexOf(this.generateCode()); this.typeArgs.forEach((arg) => arg.setTemplateIndex(classTemplateArgs)); this.templateIndex = index; } getDefNameWithTemplate() {} generateCodeWithTemplate() {} getDefName(originName) { let name = this.typeName; if (this.isDefsType && this.typeName !== 'ObjectMap') { name = originName ? `defs.${originName}.${this.typeName}` : `defs.${this.typeName}`; } return name; } getEnumType() { return this.enum.join(' | ') || 'string'; } /** 生成 Typescript 类型定义的代码 */ generateCode(originName = '') { if (this.templateIndex !== -1) { return `T${this.templateIndex}`; } if (this.enum.length) { return this.getEnumType(); } const name = this.getDefName(originName); if (this.typeArgs.length) { return `${name}<${this.typeArgs.map((arg) => arg.generateCode(originName)).join(', ')}>`; } if (this.typeProperties.length) { const interfaceCode = `{${this.typeProperties.map((property) => property.toPropertyCode())} }`; if (name) { return `${name}<${interfaceCode}>`; } return interfaceCode; } return name || 'any'; } getInitialValue(usingDef = true) { if (this.typeName === 'Array') { return '[]'; } if (this.isDefsType) { const originName = this.getDsName(); if (!usingDef) { return `new ${this.typeName}()`; } return `new ${this.getDefName(originName)}()`; } if (this.templateIndex > -1) { return 'undefined'; } if (this.typeName === 'string') { return "''"; } if (this.typeName === 'boolean') { return 'false'; } if (this.enum && this.enum.length) { const str = this.enum[0]; if (typeof str === 'string') { return `${str}`; } return str + ''; } return 'undefined'; } /** deprecated */ get initialValue() { return this.getInitialValue(); } } // property both in params and response export class Property extends Contextable { dataType: StandardDataType; description?: string; name: string; required: boolean; in: 'query' | 'body' | 'path' | 'formData' | 'header'; setContext(context) { super.setContext(context); this.dataType.setContext(context); } constructor(prop: Partial) { super(prop); // FIXME: name 可能不合法,这里暂时只判断是否包含 . 。 if (this.name.includes('.')) { this.name = this.name.slice(this.name.lastIndexOf('.') + 1); } } toPropertyCode(surrounding = Surrounding.typeScript, hasRequired = false, optional = false) { let optionalSignal = hasRequired && optional ? '?' : ''; if (hasRequired && !this.required) { optionalSignal = '?'; } let name = this.name; if (!name.match(/^[a-zA-Z_$][a-zA-Z0-9_$]*$/)) { name = `'${name}'`; } const fieldTypeDeclaration = surrounding === Surrounding.javaScript ? '' : `${optionalSignal}: ${this.dataType.generateCode(this.getDsName())}`; return ` /** ${this.description || this.name} */ ${name}${fieldTypeDeclaration};`; } toPropertyCodeWithInitValue(baseName = '') { let typeWithValue = `= ${this.dataType.getInitialValue(false)}`; if (!this.dataType.getInitialValue(false)) { typeWithValue = `: ${this.dataType.generateCode(this.getDsName())}`; } if (this.dataType.typeName === baseName) { typeWithValue = `= {}`; } let name = this.name; if (!name.match(/^[a-zA-Z_$][a-zA-Z0-9_$]*$/)) { name = `'${name}'`; } return ` /** ${this.description || this.name} */ ${name} ${typeWithValue} `; } toBody() { return this.dataType.generateCode(this.getDsName()); } } export class Interface extends Contextable { consumes: string[]; parameters: Property[]; description: string; response: StandardDataType; method: string; name: string; path: string; get responseType() { return this.response.generateCode(this.getDsName()); } getParamsCode(className = 'Params', surrounding = Surrounding.typeScript) { return `class ${className} { ${this.parameters .filter((param) => param.in === 'path' || param.in === 'query') .map((param) => param.toPropertyCode(surrounding, true)) .join('')} } `; } getParamList() { const form = !!this.parameters.find((param) => param.in === 'formData'); const paramList = [ { paramKey: 'params', paramType: 'Params' }, { paramKey: 'form', paramType: form ? 'FormData' : '' }, { paramKey: 'body', paramType: this.getBodyParamsCode() }, { paramKey: 'options', optional: true, paramType: 'any', initialValue: '{}' } ]; return paramList; } getRequestContent() { const paramList = this.getParamList().filter((param) => param.paramType); const method = this.method.toUpperCase(); const hasForm = paramList.map((param) => param.paramKey).includes('form'); const hasBody = paramList.map((param) => param.paramKey).includes('body'); const hasOptions = paramList.map((param) => param.paramKey).includes('options'); return `{ method: "${method}", ${hasForm ? 'body: form,' : ''} ${hasBody ? 'body,' : ''} ${hasOptions ? '...options,' : ''} }`; } getRequestParams(surrounding = Surrounding.typeScript) { const paramList = this.getParamList().filter((param) => param.paramType); if (surrounding === Surrounding.typeScript) { return paramList.map((param) => `${param.paramKey}${param.optional ? '?' : ''}: ${param.paramType}`).join(', '); } return paramList .map((param) => `${param.paramKey}${param.initialValue ? ` = ${param.initialValue}` : ''}`) .join(', '); } getBodyParamsCode() { const bodyParam = this.parameters.find((param) => param.in === 'body'); return (bodyParam && bodyParam.dataType.generateCode(this.getDsName())) || ''; } setContext(context: any) { super.setContext(context); this.parameters.forEach((param) => param.setContext(context)); this.response && this.response.setContext(context); } constructor(inter: Partial) { super(inter); } } export class Mod extends Contextable { description: string; interfaces: Interface[]; name: string; setContext(context: any) { super.setContext(context); this.interfaces.forEach((inter) => inter.setContext({ ...context, mod: this })); } constructor(mod: Partial) { super(mod); this.interfaces = _.orderBy(this.interfaces, 'path'); } } export class BaseClass extends Contextable { name: string; description: string; properties: Property[]; templateArgs: StandardDataType[]; setContext(context: any) { super.setContext(context); this.properties.forEach((prop) => prop.setContext(context)); } constructor(base: Partial) { super(base); this.properties = _.orderBy(this.properties, 'name'); } } export class StandardDataSource { public name: string; public baseClasses: BaseClass[]; public mods: Mod[]; reOrder() { this.baseClasses = _.orderBy(this.baseClasses, 'name'); this.mods = _.orderBy(this.mods, 'name'); } // validate the if the dataSource is valid validate() { const errors = [] as string[]; this.mods.forEach((mod) => { if (!mod.name) { errors.push(`lock 文件不合法,发现没有 name 属性的模块;`); } }); this.baseClasses.forEach((base) => { if (!base.name) { errors.push(`lock 文件不合法,发现没有 name 属性的基类;`); } }); const dupMod = getDuplicateById(this.mods, 'name'); const dupBase = getDuplicateById(this.baseClasses, 'name'); if (dupMod) { errors.push(`模块 ${dupMod.name} 重复了。`); } if (dupBase) { errors.push(`基类 ${dupBase.name} 重复了。`); } if (errors && errors.length) { throw new Error(errors.join('\n')); } return errors; } serialize() { return JSON.stringify( { mods: this.mods, baseClasses: this.baseClasses }, null, 2 ); } setContext() { this.baseClasses.forEach((base) => base.setContext({ dataSource: this })); this.mods.forEach((mod) => mod.setContext({ dataSource: this })); } constructor(standard: { mods: Mod[]; name: string; baseClasses: BaseClass[] }, restProps?: Record) { this.mods = standard.mods; if (standard.name) { this.name = standard.name; } this.baseClasses = standard.baseClasses; if (restProps) { Object.keys(restProps).forEach((key) => (this[key] = restProps[key])); } this.reOrder(); this.setContext(); } static constructorFromLock(localDataObject: StandardDataSource, originName) { const { baseClasses: originalBaseClasses, mods: originalMods, name, ...restProps } = localDataObject; let currentInter: Interface; try { // 兼容性代码,将老的数据结构转换为新的。 const defNames = originalBaseClasses.map((base) => { if (base.name.includes('<')) { return base.name.slice(0, base.name.indexOf('<')); } return base.name; }); const baseClasses = originalBaseClasses.map((base) => { const props = base.properties.map((prop) => { return new Property({ ...prop, dataType: StandardDataType.constructorFromJSON(prop.dataType, originName, defNames) }); }); let templateArgs = base.templateArgs; let name = base.name; if (!templateArgs && base.name.includes('<')) { // 兼容性代码,将老的数据结构转换为新的。 const defNameAst = dateTypeRefs2Ast(base.name, name); const dataType = parseAst2StandardDataType(defNameAst, defNames, []); templateArgs = dataType.typeArgs; name = dataType.typeName; } return new BaseClass({ description: base.description, name, templateArgs, properties: _.unionBy(props, 'name') }); }); const mods = originalMods.map((mod) => { const interfaces = mod.interfaces.map((inter) => { const response = StandardDataType.constructorFromJSON(inter.response, name, defNames); currentInter = inter; const parameters = inter.parameters .map((param) => { const dataType = StandardDataType.constructorFromJSON(param.dataType, name, defNames); return new Property({ ...param, dataType }); }) .filter(_.identity); return new Interface({ ...inter, parameters, response }); }); return new Mod({ description: mod.description, name: mod.name, interfaces }); }); return new StandardDataSource( { baseClasses, mods, name }, restProps ); } catch (e) { const errArray: string[] = []; if (currentInter) { errArray.push(`[interfaces.path]:${currentInter.path}`); } errArray.push(e.toString()); throw new Error(`${errArray.join('\n')}\n请检查api-lock.json文件`); } } } export function getDuplicateById(arr: T[], idKey = 'name'): null | T { if (!arr || !arr.length) { return null; } let result; arr.forEach((item, itemIndex) => { if (arr.slice(0, itemIndex).find((o) => o[idKey] === item[idKey])) { result = item; return; } }); return result; } /** ast 转换为标准类型 */ export function parseAst2StandardDataType( ast: AstNode, defNames: string[], classTemplateArgs: StandardDataType[] = [] ): StandardDataType { const { name, templateArgs } = ast; // 怪异类型兼容 let typeName = PrimitiveTypeMap[name] || name; const isDefsType = defNames.includes(name); const typeArgs = templateArgs.map((arg) => { return parseAst2StandardDataType(arg, defNames, classTemplateArgs); }); const dataType = new StandardDataType(typeArgs, typeName, isDefsType); dataType.setTemplateIndex(classTemplateArgs); return dataType; }