feat: manually mirror opencoze's code from bytedance
Change-Id: I09a73aadda978ad9511264a756b2ce51f5761adf
This commit is contained in:
213
frontend/infra/idl/idl2ts-generator/src/plugin/adapter-plugin.ts
Normal file
213
frontend/infra/idl/idl2ts-generator/src/plugin/adapter-plugin.ts
Normal file
@@ -0,0 +1,213 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import path from 'path';
|
||||
|
||||
import fs from 'fs-extra';
|
||||
import {
|
||||
type IPlugin,
|
||||
type Program,
|
||||
before,
|
||||
after,
|
||||
} from '@coze-arch/idl2ts-plugin';
|
||||
import {
|
||||
type IParseEntryCtx,
|
||||
isStructDefinition,
|
||||
type FunctionType,
|
||||
isSetType,
|
||||
isListType,
|
||||
SyntaxType,
|
||||
isMapType,
|
||||
type FieldType,
|
||||
createFile,
|
||||
getAnnotation,
|
||||
getOutputName,
|
||||
getTypeFromDynamicJsonAnnotation,
|
||||
removeFileExt,
|
||||
parseFile,
|
||||
genAst,
|
||||
} from '@coze-arch/idl2ts-helper';
|
||||
import * as t from '@babel/types';
|
||||
|
||||
import { type Options } from '../types';
|
||||
import { type Contexts, type ProcessIdlCtxWithSchema, HOOK } from '../context';
|
||||
|
||||
function isInt(fieldType: FieldType | FunctionType) {
|
||||
return [
|
||||
SyntaxType.I8Keyword,
|
||||
SyntaxType.I16Keyword,
|
||||
SyntaxType.I32Keyword,
|
||||
SyntaxType.I64Keyword,
|
||||
].some(i => i === fieldType.type);
|
||||
}
|
||||
|
||||
export class AdapterPlugin implements IPlugin {
|
||||
private patchTypes = new Map<string, Record<string, string[]>>();
|
||||
private options: Options;
|
||||
constructor(options: Options) {
|
||||
this.options = options;
|
||||
}
|
||||
apply(program: Program<Contexts>): void {
|
||||
program.register(
|
||||
before(HOOK.PROCESS_IDL_NODE),
|
||||
this.adaptStruct.bind(this),
|
||||
);
|
||||
program.register(after(HOOK.GEN_FILE_AST), this.genPatchFiles.bind(this));
|
||||
}
|
||||
|
||||
private genPatchFiles(ctx: IParseEntryCtx) {
|
||||
for (const [idlPath, res] of this.patchTypes.entries()) {
|
||||
const targetFile = getOutputName({
|
||||
source: `${removeFileExt(idlPath)}.ts`,
|
||||
idlRoot: this.options.idlRoot,
|
||||
outputDir:
|
||||
this.options.patchTypesOutput ||
|
||||
path.join(this.options.outputDir, '../patch-types'),
|
||||
});
|
||||
let file: t.File;
|
||||
if (!fs.existsSync(targetFile)) {
|
||||
file = createFile('');
|
||||
} else {
|
||||
file = parseFile(targetFile);
|
||||
}
|
||||
Object.keys(res).forEach(structName => {
|
||||
let target = file.program.body.find(
|
||||
i =>
|
||||
t.isExportNamedDeclaration(i) &&
|
||||
// @ts-expect-error fixme
|
||||
i.declaration?.id.name === structName,
|
||||
) as t.ExportNamedDeclaration;
|
||||
if (!target) {
|
||||
target = genAst(
|
||||
`export namespace ${structName} {}`,
|
||||
) as t.ExportNamedDeclaration;
|
||||
file.program.body.push(target);
|
||||
}
|
||||
const declaration = target.declaration as t.TSModuleDeclaration;
|
||||
for (const fieldName of res[structName]) {
|
||||
if (t.isTSModuleBlock(declaration.body)) {
|
||||
if (
|
||||
!declaration.body.body.some(i => {
|
||||
if (t.isExportNamedDeclaration(i)) {
|
||||
if (
|
||||
t.isTSTypeAliasDeclaration(i.declaration) ||
|
||||
t.isInterfaceDeclaration(i.declaration)
|
||||
) {
|
||||
return i.declaration.id.name === fieldName;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
})
|
||||
) {
|
||||
declaration.body.body.push(
|
||||
genAst(
|
||||
`export type ${fieldName}= unknown`,
|
||||
) as t.TSTypeAliasDeclaration,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
ctx.files.set(targetFile, { content: file, type: 'babel' });
|
||||
}
|
||||
return ctx;
|
||||
}
|
||||
private adaptStruct(ctx: ProcessIdlCtxWithSchema) {
|
||||
const { node, ast } = ctx;
|
||||
if (!node) {
|
||||
return ctx;
|
||||
}
|
||||
if (isStructDefinition(node)) {
|
||||
const decodeEncodeFields = [] as string[];
|
||||
// eslint-disable-next-line complexity
|
||||
node.fields = node.fields.map(f => {
|
||||
// req
|
||||
if (
|
||||
getAnnotation(f.annotations, 'api.converter') === 'atoi_comp_empty'
|
||||
) {
|
||||
if (isInt(f.fieldType)) {
|
||||
// 类型转换为 string
|
||||
f.fieldType.type = SyntaxType.StringKeyword;
|
||||
}
|
||||
}
|
||||
// api.converter 对 int 以及 map 类型生效
|
||||
if (getAnnotation(f.annotations, 'api.converter') === 'itoa') {
|
||||
if (isInt(f.fieldType)) {
|
||||
// 类型转换为 string
|
||||
f.fieldType.type = SyntaxType.StringKeyword;
|
||||
}
|
||||
if (isMapType(f.fieldType)) {
|
||||
const { valueType } = f.fieldType;
|
||||
if (isInt(valueType)) {
|
||||
f.fieldType.valueType.type = SyntaxType.StringKeyword;
|
||||
}
|
||||
}
|
||||
}
|
||||
// item_converter 对 list 类型生效
|
||||
if (
|
||||
['atoi_comp_empty', 'itoa'].includes(
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
getAnnotation(f.annotations, 'api.item_converter')!,
|
||||
)
|
||||
) {
|
||||
if (isSetType(f.fieldType) || isListType(f.fieldType)) {
|
||||
f.fieldType.valueType.type = SyntaxType.StringKeyword;
|
||||
}
|
||||
}
|
||||
|
||||
// 收集 decode encode 注解处理
|
||||
if (getTypeFromDynamicJsonAnnotation(f.annotations)) {
|
||||
decodeEncodeFields.push(f.name.value);
|
||||
}
|
||||
// api.json 注解处理
|
||||
const jsonAnnotation = getAnnotation(f.annotations, 'api.json');
|
||||
if (jsonAnnotation) {
|
||||
f.extensionConfig = f.extensionConfig || {};
|
||||
f.extensionConfig.key = jsonAnnotation;
|
||||
}
|
||||
// api.json_string 注解处理
|
||||
const jsonStrAnnotation = getAnnotation(
|
||||
f.annotations,
|
||||
'api.json_string',
|
||||
);
|
||||
if (jsonStrAnnotation) {
|
||||
if (isInt(f.fieldType)) {
|
||||
// 类型转换为 string
|
||||
f.fieldType.type = SyntaxType.StringKeyword;
|
||||
f.extensionConfig = f.extensionConfig || {};
|
||||
f.extensionConfig.key = jsonStrAnnotation;
|
||||
} else {
|
||||
throw new Error(
|
||||
'api.json_string is expected an annotation int type',
|
||||
);
|
||||
}
|
||||
}
|
||||
return f;
|
||||
});
|
||||
if (decodeEncodeFields.length > 0) {
|
||||
const currentAstRes = this.patchTypes.get(ast.idlPath);
|
||||
if (!currentAstRes) {
|
||||
this.patchTypes.set(ast.idlPath, {
|
||||
[node.name.value]: decodeEncodeFields,
|
||||
});
|
||||
} else {
|
||||
currentAstRes[node.name.value] = decodeEncodeFields;
|
||||
}
|
||||
}
|
||||
}
|
||||
return ctx;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { type Program, after, type IPlugin } from '@coze-arch/idl2ts-plugin';
|
||||
import { type IParseEntryCtx, isPbFile } from '@coze-arch/idl2ts-helper';
|
||||
|
||||
import { HOOK } from '../context';
|
||||
|
||||
export class AutoFixDuplicateIncludesPlugin implements IPlugin {
|
||||
apply(p: Program<{ PARSE_ENTRY: any }>) {
|
||||
p.register(after(HOOK.PARSE_ENTRY), (ctx: IParseEntryCtx) => {
|
||||
if (isPbFile(ctx.entries[0])) {
|
||||
return ctx;
|
||||
}
|
||||
ctx.ast = ctx.ast.map(i => {
|
||||
const res: string[] = [];
|
||||
for (const include of i.includes) {
|
||||
if (res.includes(include)) {
|
||||
console.error(
|
||||
`[${include}]` + `has be includes duplicate in file:${i.idlPath}`,
|
||||
);
|
||||
} else {
|
||||
res.push(include);
|
||||
}
|
||||
}
|
||||
i.includes = res;
|
||||
return i;
|
||||
});
|
||||
return ctx;
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { isAbsolute } from 'path';
|
||||
|
||||
import { type Program, after, type IPlugin } from '@coze-arch/idl2ts-plugin';
|
||||
|
||||
function ensureRelative(idlPath: string) {
|
||||
if (isAbsolute(idlPath)) {
|
||||
return idlPath;
|
||||
}
|
||||
if (!idlPath.startsWith('.')) {
|
||||
return `./${idlPath}`;
|
||||
}
|
||||
return idlPath;
|
||||
}
|
||||
|
||||
export class AutoFixPathPlugin implements IPlugin {
|
||||
apply(p: Program<{ PARSE_ENTRY: any }>) {
|
||||
p.register(after('PARSE_ENTRY'), ctx => {
|
||||
ctx.ast = ctx.ast.map(i => {
|
||||
i.includes = i.includes.map(ensureRelative);
|
||||
return i;
|
||||
});
|
||||
return ctx;
|
||||
});
|
||||
}
|
||||
}
|
||||
494
frontend/infra/idl/idl2ts-generator/src/plugin/client-plugin.ts
Normal file
494
frontend/infra/idl/idl2ts-generator/src/plugin/client-plugin.ts
Normal file
@@ -0,0 +1,494 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import path from 'path';
|
||||
|
||||
import {
|
||||
type IPlugin,
|
||||
type Program,
|
||||
on,
|
||||
after,
|
||||
} from '@coze-arch/idl2ts-plugin';
|
||||
import {
|
||||
type EnumDefinition,
|
||||
isStructDefinition,
|
||||
isIdentifier,
|
||||
type StructDefinition,
|
||||
isBaseType,
|
||||
type FieldType,
|
||||
type FunctionType,
|
||||
isEnumDefinition,
|
||||
isListType,
|
||||
isStringLiteral,
|
||||
type TypedefDefinition,
|
||||
type ConstDefinition,
|
||||
type ConstValue,
|
||||
isSetType,
|
||||
isMapType,
|
||||
isBooleanLiteral,
|
||||
isIntConstant,
|
||||
isDoubleConstant,
|
||||
isConstList,
|
||||
isConstMap,
|
||||
type BaseType,
|
||||
isConstDefinition,
|
||||
type ProcessIdlCtx,
|
||||
isTypedefDefinition,
|
||||
isServiceDefinition,
|
||||
type IParseResultItem,
|
||||
type ServiceDefinition,
|
||||
type IGenTemplateCtx,
|
||||
addComment,
|
||||
getAnnotation,
|
||||
parseFiledName,
|
||||
parseIdFiledType,
|
||||
getTypeFromDynamicJsonAnnotation,
|
||||
withExportDeclaration,
|
||||
uniformNs,
|
||||
removeFileExt,
|
||||
genAst,
|
||||
getOutputName,
|
||||
transformFieldId,
|
||||
getRelativePath,
|
||||
getFieldsAlias,
|
||||
SyntaxType,
|
||||
type UnifyStatement,
|
||||
} from '@coze-arch/idl2ts-helper';
|
||||
import * as t from '@babel/types';
|
||||
|
||||
import { type Options } from '../types';
|
||||
import { TypeMapper } from '../type-mapper';
|
||||
import { genFunc, genPublic } from '../template';
|
||||
import { type Contexts, HOOK } from '../context';
|
||||
|
||||
const hasEnumAnnotation = (statement: UnifyStatement) =>
|
||||
statement.annotations?.annotations.some(
|
||||
i => i.name.value === 'ts.enum' && i.value?.value === 'true',
|
||||
);
|
||||
|
||||
const findEnumItemIndex = (enumName: string, statements: UnifyStatement[]) => {
|
||||
const result: number[] = [];
|
||||
for (let i = 0; i < statements.length; i++) {
|
||||
const statement = statements[i];
|
||||
if (
|
||||
statement.type === SyntaxType.ConstDefinition &&
|
||||
statement.fieldType.type === SyntaxType.Identifier &&
|
||||
statement.fieldType.value === enumName
|
||||
) {
|
||||
result.push(i);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
export class ClientPlugin implements IPlugin {
|
||||
private options: Options;
|
||||
private program!: Program<Contexts>;
|
||||
private needPatchTypeFile = new Set<string>();
|
||||
constructor(options: Options) {
|
||||
this.options = options;
|
||||
}
|
||||
apply(program: Program<Contexts>): void {
|
||||
this.program = program;
|
||||
program.register(
|
||||
on(HOOK.PROCESS_IDL_NODE),
|
||||
ctx => {
|
||||
const { node, dts, ast } = ctx;
|
||||
if (!node) {
|
||||
throw new Error('node is undefined');
|
||||
}
|
||||
if (isStructDefinition(node)) {
|
||||
const { nested, struct } = this.processStructNode(node, ctx);
|
||||
dts.program.body.push(struct);
|
||||
if (nested) {
|
||||
dts.program.body.push(nested);
|
||||
}
|
||||
} else if (isEnumDefinition(node)) {
|
||||
dts.program.body.push(this.processEnumNode(node));
|
||||
} else if (isTypedefDefinition(node) && hasEnumAnnotation(node)) {
|
||||
dts.program.body.push(this.processTypedefEnumNode(node, ctx));
|
||||
} else if (isConstDefinition(node)) {
|
||||
dts.program.body.push(this.processConstNode(node));
|
||||
} else if (isTypedefDefinition(node)) {
|
||||
dts.program.body.push(this.processTypeDefNode(node));
|
||||
} else if (isServiceDefinition(node)) {
|
||||
dts.program.body = [
|
||||
...dts.program.body,
|
||||
...this.processServiceDefinition(node, ast, ctx),
|
||||
];
|
||||
}
|
||||
return ctx;
|
||||
},
|
||||
0,
|
||||
);
|
||||
program.register(after(HOOK.PROCESS_IDL_AST), ctx => {
|
||||
const { ast, dts } = ctx;
|
||||
if (ast.isEntry) {
|
||||
dts.program.body = [genPublic(ctx, this.options), ...dts.program.body];
|
||||
}
|
||||
if (ast.includes) {
|
||||
Object.keys(ast.includeMap).forEach(key => {
|
||||
dts.program.body = [
|
||||
...this.processIncludes(key, ast),
|
||||
...dts.program.body,
|
||||
];
|
||||
});
|
||||
}
|
||||
const outputFile = getOutputName({
|
||||
source: `${removeFileExt(ast.idlPath)}.ts`,
|
||||
outputDir: this.options.outputDir,
|
||||
idlRoot: this.options.idlRoot,
|
||||
});
|
||||
if (this.needPatchTypeFile.has(ast.idlPath)) {
|
||||
let pathName = '';
|
||||
if (this.options.patchTypesAliasOutput) {
|
||||
pathName = path.join(
|
||||
this.options.patchTypesAliasOutput,
|
||||
path.relative(
|
||||
this.options.idlRoot,
|
||||
ast.idlPath.replace('.thrift', ''),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
const patchTypeFile = path.join(
|
||||
this.options.patchTypesOutput ||
|
||||
path.join(this.options.outputDir, '../patch-types'),
|
||||
path.relative(this.options.idlRoot, ast.idlPath),
|
||||
);
|
||||
pathName = getRelativePath(outputFile, patchTypeFile);
|
||||
}
|
||||
|
||||
const code = `import type * as Patch from '${pathName}'`;
|
||||
dts.program.body.unshift(genAst<t.ImportDeclaration>(code));
|
||||
}
|
||||
ctx.output.set(outputFile, { type: 'babel', content: dts });
|
||||
return ctx;
|
||||
});
|
||||
this.program.register(
|
||||
on(HOOK.GEN_FUN_TEMPLATE),
|
||||
(ctx: IGenTemplateCtx) => {
|
||||
ctx.template = genFunc(ctx);
|
||||
return ctx;
|
||||
},
|
||||
0,
|
||||
);
|
||||
}
|
||||
private processEnumNode(node: EnumDefinition) {
|
||||
const { members, name, comments } = node;
|
||||
const enumArr = members.map(i => {
|
||||
const { name, comments, initializer } = i;
|
||||
|
||||
return addComment(
|
||||
t.tsEnumMember(
|
||||
t.identifier(name.value),
|
||||
initializer ? this.getExpFromConstValue(initializer) : undefined,
|
||||
),
|
||||
comments,
|
||||
);
|
||||
});
|
||||
const enumAst = t.tsEnumDeclaration(t.identifier(name.value), enumArr);
|
||||
return withExportDeclaration(enumAst, comments);
|
||||
}
|
||||
private processStructNode(node: StructDefinition, ctx: ProcessIdlCtx) {
|
||||
const { fields, name, comments, nested } = node;
|
||||
|
||||
const typeProps: t.ObjectTypeProperty[] = [];
|
||||
const processedFiledName = {} as Record<string, string>;
|
||||
|
||||
fields.forEach(i => {
|
||||
const fieldName = parseFiledName(i);
|
||||
if (processedFiledName[fieldName]) {
|
||||
return;
|
||||
}
|
||||
const { fieldType, requiredness, comments, annotations } = i;
|
||||
const isAnyType = getAnnotation(annotations, 'api.value_type') === 'any';
|
||||
const dynamicType = getTypeFromDynamicJsonAnnotation(annotations);
|
||||
let valueType: t.FlowType = this.processFiledType(fieldType);
|
||||
if (isAnyType) {
|
||||
valueType = t.anyTypeAnnotation();
|
||||
} else if (dynamicType) {
|
||||
valueType = t.genericTypeAnnotation(
|
||||
t.qualifiedTypeIdentifier(
|
||||
t.identifier(i.name.value),
|
||||
t.qualifiedTypeIdentifier(
|
||||
t.identifier(name.value),
|
||||
t.identifier('Patch'),
|
||||
),
|
||||
),
|
||||
);
|
||||
this.needPatchTypeFile.add(ctx.ast.idlPath);
|
||||
}
|
||||
const prop = t.objectTypeProperty(transformFieldId(fieldName), valueType);
|
||||
|
||||
if (requiredness === 'optional') {
|
||||
prop.optional = true;
|
||||
if (this.options.allowNullForOptional) {
|
||||
prop.value = t.unionTypeAnnotation([
|
||||
prop.value,
|
||||
t.nullLiteralTypeAnnotation(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
processedFiledName[fieldName] = getFieldsAlias(i);
|
||||
typeProps.push(addComment(prop, comments));
|
||||
});
|
||||
|
||||
const ast = t.interfaceDeclaration(
|
||||
t.identifier(name.value),
|
||||
null,
|
||||
[],
|
||||
t.objectTypeAnnotation(typeProps),
|
||||
);
|
||||
return {
|
||||
struct: withExportDeclaration(ast, comments),
|
||||
nested: this.processNested(nested, node, ctx),
|
||||
};
|
||||
}
|
||||
private processFiledType(fieldType: FieldType | FunctionType) {
|
||||
if (isBaseType(fieldType)) {
|
||||
return this.getTsTypeFromThriftBaseType(fieldType);
|
||||
} else if (isListType(fieldType) || isSetType(fieldType)) {
|
||||
const { valueType } = fieldType;
|
||||
return t.arrayTypeAnnotation(this.processFiledType(valueType));
|
||||
} else if (isMapType(fieldType)) {
|
||||
const { valueType } = fieldType;
|
||||
const valueFiledType = this.processFiledType(valueType);
|
||||
return t.objectTypeAnnotation(
|
||||
[],
|
||||
[
|
||||
t.objectTypeIndexer(
|
||||
t.identifier('key'),
|
||||
t.unionTypeAnnotation([
|
||||
t.stringTypeAnnotation(),
|
||||
t.numberTypeAnnotation(),
|
||||
]),
|
||||
valueFiledType,
|
||||
),
|
||||
],
|
||||
);
|
||||
} else if (isIdentifier(fieldType)) {
|
||||
const { namespace, refName } = parseIdFiledType(fieldType);
|
||||
|
||||
if (namespace) {
|
||||
return t.genericTypeAnnotation(
|
||||
t.qualifiedTypeIdentifier(
|
||||
t.identifier(refName),
|
||||
t.identifier(
|
||||
fieldType.namespaceValue?.startsWith('root')
|
||||
? namespace
|
||||
: uniformNs(namespace.replace('.', '_')),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
return t.genericTypeAnnotation(t.identifier(refName));
|
||||
}
|
||||
throw new Error(`unknown type:${fieldType.type}`);
|
||||
}
|
||||
|
||||
private processNested(
|
||||
nested: Record<string, StructDefinition | EnumDefinition> | undefined,
|
||||
parent: StructDefinition,
|
||||
ctx: ProcessIdlCtx,
|
||||
) {
|
||||
if (!nested) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const block = t.tsModuleBlock([]);
|
||||
|
||||
Object.keys(nested).forEach(key => {
|
||||
const node = nested[key];
|
||||
if (isStructDefinition(node)) {
|
||||
const { nested, struct } = this.processStructNode(node, ctx);
|
||||
block.body.push(struct);
|
||||
if (nested) {
|
||||
block.body.push(nested);
|
||||
}
|
||||
} else if (isEnumDefinition(node)) {
|
||||
block.body.push(this.processEnumNode(node));
|
||||
}
|
||||
});
|
||||
const namespaceModule = t.tsModuleDeclaration(
|
||||
t.identifier(parent.name.value),
|
||||
block,
|
||||
);
|
||||
return withExportDeclaration(namespaceModule);
|
||||
}
|
||||
|
||||
private processConstNode(node: ConstDefinition) {
|
||||
const { name, comments, initializer } = node;
|
||||
const exp = this.getExpFromConstValue(initializer);
|
||||
const declarator = t.variableDeclarator(t.identifier(name.value), exp);
|
||||
const ast = t.variableDeclaration('const', [declarator]);
|
||||
return withExportDeclaration(ast, comments);
|
||||
}
|
||||
|
||||
private getExpFromConstValue(initializer: ConstValue): t.Expression {
|
||||
let exp = null as t.Expression | null;
|
||||
if (isStringLiteral(initializer)) {
|
||||
exp = t.stringLiteral(initializer.value);
|
||||
} else if (isBooleanLiteral(initializer)) {
|
||||
exp = t.booleanLiteral(initializer.value);
|
||||
} else if (isIntConstant(initializer) || isDoubleConstant(initializer)) {
|
||||
exp = t.numericLiteral(Number(initializer.value.value));
|
||||
} else if (isConstList(initializer)) {
|
||||
const { elements } = initializer;
|
||||
exp = t.arrayExpression(elements.map(i => this.getExpFromConstValue(i)));
|
||||
} else if (isConstMap(initializer)) {
|
||||
exp = t.objectExpression(
|
||||
initializer.properties.map(i =>
|
||||
t.objectProperty(
|
||||
this.getExpFromConstValue(i.name),
|
||||
this.getExpFromConstValue(i.initializer),
|
||||
isIdentifier(i.name),
|
||||
),
|
||||
),
|
||||
);
|
||||
} else if (isIdentifier(initializer)) {
|
||||
exp = t.identifier(initializer.value);
|
||||
}
|
||||
if (!exp) {
|
||||
throw new Error(`Not support const type yet : ${initializer.type}`);
|
||||
}
|
||||
return exp;
|
||||
}
|
||||
|
||||
private processTypeDefNode(node: TypedefDefinition) {
|
||||
const { definitionType, name, comments } = node;
|
||||
// @ts-expect-error no fix
|
||||
if (node.definitionType?.value?.split('.').length > 2) {
|
||||
// @ts-expect-error no fix
|
||||
node.definitionType.value = node.definitionType.namespaceValue;
|
||||
}
|
||||
const ast = t.tsTypeAliasDeclaration(
|
||||
t.identifier(name.value),
|
||||
null,
|
||||
this.getTsTypeFromFiledType(definitionType),
|
||||
);
|
||||
const res = withExportDeclaration(ast, comments);
|
||||
return res;
|
||||
}
|
||||
|
||||
private processTypedefEnumNode(node: TypedefDefinition, ctx: ProcessIdlCtx) {
|
||||
const { name, comments } = node;
|
||||
const enumName = name.value;
|
||||
const { statements } = ctx.ast;
|
||||
const enumItemIndexArray = findEnumItemIndex(enumName, statements);
|
||||
const enumArr = enumItemIndexArray
|
||||
.map(i => statements[i])
|
||||
.map(i => {
|
||||
const { name, comments, initializer } = i as ConstDefinition;
|
||||
|
||||
return addComment(
|
||||
t.tsEnumMember(
|
||||
t.identifier(name.value.replace(new RegExp(`^${enumName}_`), '')),
|
||||
initializer ? this.getExpFromConstValue(initializer) : initializer,
|
||||
),
|
||||
comments,
|
||||
);
|
||||
});
|
||||
const enumAst = t.tsEnumDeclaration(t.identifier(name.value), enumArr);
|
||||
|
||||
// 从后向前删除枚举项,避免索引变化影响
|
||||
enumItemIndexArray
|
||||
.sort((a, b) => b - a)
|
||||
.forEach(index => {
|
||||
statements.splice(index, 1);
|
||||
});
|
||||
return withExportDeclaration(enumAst, comments);
|
||||
}
|
||||
|
||||
private getTsTypeFromFiledType(fieldType: FieldType) {
|
||||
if (isBaseType(fieldType)) {
|
||||
return this.getTsTypeFromThriftBaseType(fieldType, true);
|
||||
} else if (isListType(fieldType) || isSetType(fieldType)) {
|
||||
const { valueType } = fieldType;
|
||||
return t.tsArrayType(this.getTsTypeFromFiledType(valueType));
|
||||
} else if (isMapType(fieldType)) {
|
||||
const { keyType, valueType } = fieldType;
|
||||
return t.tsTypeReference(
|
||||
t.identifier('Record'),
|
||||
t.tsTypeParameterInstantiation([
|
||||
this.getTsTypeFromFiledType(keyType),
|
||||
this.getTsTypeFromFiledType(valueType),
|
||||
]),
|
||||
);
|
||||
} else if (isIdentifier(fieldType)) {
|
||||
return t.tsTypeReference(t.identifier(fieldType.value));
|
||||
}
|
||||
}
|
||||
|
||||
private getTsTypeFromThriftBaseType(fieldType: BaseType, isTsType = false) {
|
||||
const typeStr = TypeMapper.map(fieldType.type as any);
|
||||
if (typeStr === 'number') {
|
||||
return !isTsType ? t.numberTypeAnnotation() : t.tsNumberKeyword();
|
||||
} else if (typeStr === 'string') {
|
||||
return !isTsType ? t.stringTypeAnnotation() : t.tsStringKeyword();
|
||||
} else if (typeStr === 'object') {
|
||||
const id = t.identifier('Blob');
|
||||
return !isTsType ? t.genericTypeAnnotation(id) : t.tsTypeReference(id);
|
||||
}
|
||||
if (typeStr === 'boolean') {
|
||||
return !isTsType ? t.booleanTypeAnnotation() : t.tsBooleanKeyword();
|
||||
}
|
||||
throw new Error(`not support :${typeStr}`);
|
||||
}
|
||||
|
||||
private processIncludes(include: string, ast: IParseResultItem) {
|
||||
const includePath = getRelativePath(ast.idlPath, ast.includeMap[include]);
|
||||
const name = ast.includeRefer[include];
|
||||
let code = `import * as ${name} from '${includePath}';\n`;
|
||||
code += `export { ${name} };\n`;
|
||||
const res = genAst<t.ImportDeclaration[]>(code);
|
||||
// const res = template.ast(code, { plugins }) as ;
|
||||
if (!Array.isArray(res)) {
|
||||
return [res];
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
private processServiceDefinition(
|
||||
node: ServiceDefinition,
|
||||
ast: IParseResultItem,
|
||||
ctx: ProcessIdlCtx,
|
||||
): t.ExportNamedDeclaration[] {
|
||||
const { functions } = node;
|
||||
if (!ast.isEntry) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const result = [] as t.ExportNamedDeclaration[];
|
||||
functions.forEach(i => {
|
||||
const { comments, extensionConfig } = i;
|
||||
if (!extensionConfig?.method) {
|
||||
return;
|
||||
}
|
||||
const metaCtx = {
|
||||
ast,
|
||||
meta: ctx.meta.find(m => m.name === i.name.value),
|
||||
service: node,
|
||||
method: i,
|
||||
template: '',
|
||||
} as IGenTemplateCtx;
|
||||
// this.program.trigger(HOOK.PARSE_FUN_META, ctx);
|
||||
this.program.trigger(HOOK.GEN_FUN_TEMPLATE, metaCtx);
|
||||
result.push(withExportDeclaration(genAst(metaCtx.template), comments));
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { type Program, after, before } from '@coze-arch/idl2ts-plugin';
|
||||
import { isStructDefinition } from '@coze-arch/idl2ts-helper';
|
||||
|
||||
import { type Contexts, HOOK } from '../context';
|
||||
|
||||
const MAGIC_COMMENT_KEY = '\n*@magic-comment';
|
||||
|
||||
// 忽略 struct 中的字段
|
||||
export class CommentFormatPlugin {
|
||||
apply(p: Program<Contexts>) {
|
||||
p.register(after('PARSE_ENTRY'), ctx => {
|
||||
const result = ctx.ast;
|
||||
for (const item of result) {
|
||||
item.statements.forEach(i => {
|
||||
if (isStructDefinition(i)) {
|
||||
const { fields } = i;
|
||||
i.fields = fields.map(i => {
|
||||
const comments = i.comments || [];
|
||||
let value = '';
|
||||
if (comments.length === 1) {
|
||||
if (Array.isArray(comments[0].value)) {
|
||||
if (comments[0].value.length > 1) {
|
||||
return i;
|
||||
}
|
||||
value = comments[0].value[0];
|
||||
} else {
|
||||
value = comments[0].value;
|
||||
}
|
||||
|
||||
comments[0].value = MAGIC_COMMENT_KEY + value;
|
||||
}
|
||||
|
||||
return { ...i, comments };
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
ctx.ast = result;
|
||||
return ctx;
|
||||
});
|
||||
p.register(before(HOOK.WRITE_FILE), ctx => {
|
||||
ctx.content = ctx.content.replaceAll(
|
||||
`
|
||||
*@magic-comment`,
|
||||
'',
|
||||
);
|
||||
return ctx;
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { type Program, after } from '@coze-arch/idl2ts-plugin';
|
||||
import {
|
||||
isStructDefinition,
|
||||
type FieldDefinition,
|
||||
} from '@coze-arch/idl2ts-helper';
|
||||
|
||||
type Filter = (f: FieldDefinition) => boolean;
|
||||
|
||||
interface IPops {
|
||||
filter: Filter;
|
||||
}
|
||||
|
||||
// 忽略 struct 中的字段
|
||||
export class IgnoreStructFiledPlugin {
|
||||
private filter: Filter;
|
||||
constructor({ filter }: IPops) {
|
||||
this.filter = filter;
|
||||
}
|
||||
apply(p: Program<{ PARSE_ENTRY: { ast: any } }>) {
|
||||
p.register(after('PARSE_ENTRY'), ctx => {
|
||||
const result = ctx.ast;
|
||||
for (const item of result) {
|
||||
item.statements.forEach(i => {
|
||||
if (isStructDefinition(i)) {
|
||||
const { fields } = i;
|
||||
i.fields = fields.filter(f => this.filter(f));
|
||||
}
|
||||
});
|
||||
}
|
||||
ctx.ast = result;
|
||||
return ctx;
|
||||
});
|
||||
}
|
||||
}
|
||||
26
frontend/infra/idl/idl2ts-generator/src/plugin/index.ts
Normal file
26
frontend/infra/idl/idl2ts-generator/src/plugin/index.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export * from './adapter-plugin';
|
||||
export * from './client-plugin';
|
||||
export * from './meta-plugin';
|
||||
export * from './mock-transformer';
|
||||
export * from './schema-plugin';
|
||||
export * from './pkg-entry-plugin';
|
||||
export * from './auto-fix-path-plugin';
|
||||
export * from './ignore-struct-field';
|
||||
export * from './auto-fix-duplicate-plugin';
|
||||
export * from './comment-format-plugin';
|
||||
219
frontend/infra/idl/idl2ts-generator/src/plugin/meta-plugin.ts
Normal file
219
frontend/infra/idl/idl2ts-generator/src/plugin/meta-plugin.ts
Normal file
@@ -0,0 +1,219 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { type IPlugin, type Program, on } from '@coze-arch/idl2ts-plugin';
|
||||
import {
|
||||
type Identifier,
|
||||
type FunctionDefinition,
|
||||
type FieldDefinition,
|
||||
type IParseResultItem,
|
||||
isStructDefinition,
|
||||
isIdentifier,
|
||||
type FunctionType,
|
||||
type IMeta,
|
||||
type IHttpRpcMapping,
|
||||
findDefinition,
|
||||
type Position,
|
||||
type ServiceDefinition,
|
||||
getStatementById,
|
||||
parseIdFiledType,
|
||||
getFieldsAlias,
|
||||
parseId,
|
||||
isFullBody,
|
||||
getAstFromNamespace,
|
||||
getSchemaRootByPath,
|
||||
getAnnotation,
|
||||
} from '@coze-arch/idl2ts-helper';
|
||||
|
||||
import { type Contexts, HOOK, type IProcessMetaItemCtx } from '../context';
|
||||
|
||||
interface IOptions {
|
||||
outputDir: string;
|
||||
idlRoot: string;
|
||||
}
|
||||
|
||||
export class MetaPlugin implements IPlugin {
|
||||
options: IOptions;
|
||||
constructor(options: IOptions) {
|
||||
this.options = options;
|
||||
}
|
||||
apply(program: Program<Contexts>): void {
|
||||
program.register(
|
||||
on(HOOK.PARSE_FUN_META),
|
||||
ctx => {
|
||||
const node = ctx.node as ServiceDefinition;
|
||||
node.functions.forEach(fun => {
|
||||
// 过滤非泛化接口
|
||||
if (!fun.extensionConfig?.method) {
|
||||
return;
|
||||
}
|
||||
const { meta } = program.trigger<IProcessMetaItemCtx>(
|
||||
HOOK.PARSE_FUN_META_ITEM,
|
||||
{
|
||||
ast: ctx.ast,
|
||||
service: node,
|
||||
method: fun,
|
||||
} as IProcessMetaItemCtx,
|
||||
);
|
||||
ctx.meta.push(meta);
|
||||
});
|
||||
return ctx;
|
||||
},
|
||||
0,
|
||||
);
|
||||
program.register(on(HOOK.PARSE_FUN_META_ITEM), ctx => {
|
||||
const { ast, service, method } = ctx;
|
||||
const item = this.parseFunAnnotation(method, ast, service.name.value);
|
||||
ctx.meta = item;
|
||||
return ctx;
|
||||
});
|
||||
}
|
||||
parseFunAnnotation(
|
||||
params: FunctionDefinition,
|
||||
ast: IParseResultItem,
|
||||
service: string,
|
||||
) {
|
||||
const { name, returnType, fields, extensionConfig } = params;
|
||||
const reqType = fields[0].fieldType as any;
|
||||
const reqMapping = this.processPayloadFields(
|
||||
reqType,
|
||||
extensionConfig?.method === 'GET' ? 'query' : 'body',
|
||||
ast,
|
||||
);
|
||||
const res = {
|
||||
url: extensionConfig?.uri,
|
||||
method: extensionConfig?.method ?? 'POST',
|
||||
name: name.value,
|
||||
reqType: parseId(reqType.value),
|
||||
reqMapping,
|
||||
resType: parseId(this.processReqResPramsType(returnType, ast)),
|
||||
schemaRoot: getSchemaRootByPath(ast.idlPath, this.options.idlRoot),
|
||||
service,
|
||||
} as IMeta;
|
||||
// 不是 json 时,需要加上 serializer 标识
|
||||
if (extensionConfig?.serializer && extensionConfig?.serializer !== 'json') {
|
||||
res.serializer = extensionConfig?.serializer;
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
private processReqResPramsType(id: FunctionType, ast: IParseResultItem) {
|
||||
if (isIdentifier(id)) {
|
||||
const statement = getStatementById(id, ast);
|
||||
if (isStructDefinition(statement)) {
|
||||
const wholeBody = statement.fields.find(isFullBody);
|
||||
if (wholeBody) {
|
||||
// 处理 api.body="." 以及 api.full_body=''
|
||||
return `${id.value}['${getFieldsAlias(wholeBody)}']`;
|
||||
} else {
|
||||
return id.value;
|
||||
}
|
||||
}
|
||||
throw new Error('params must be identifier');
|
||||
} else {
|
||||
return 'void';
|
||||
}
|
||||
}
|
||||
|
||||
private processPayloadFields(
|
||||
id: Identifier,
|
||||
defaultPosition: 'query' | 'body',
|
||||
entry: IParseResultItem,
|
||||
): IHttpRpcMapping {
|
||||
const { namespace, refName } = parseIdFiledType(id);
|
||||
|
||||
if (namespace) {
|
||||
const ast = getAstFromNamespace(namespace, entry);
|
||||
const struct = findDefinition(ast, refName);
|
||||
if (!struct || !isStructDefinition(struct)) {
|
||||
throw new Error(`can not find Struct: ${refName} `);
|
||||
}
|
||||
return this.createMapping(struct.fields, defaultPosition);
|
||||
}
|
||||
|
||||
const struct = findDefinition(entry.statements, id.value);
|
||||
if (!struct || !isStructDefinition(struct)) {
|
||||
throw new Error(`can not find Struct: ${id.value} `);
|
||||
}
|
||||
|
||||
return this.createMapping(struct.fields, defaultPosition);
|
||||
}
|
||||
|
||||
private createMapping(
|
||||
fields: FieldDefinition[],
|
||||
defaultPosition: 'query' | 'body',
|
||||
): IHttpRpcMapping {
|
||||
const specificPositionFiled = new Set<string>();
|
||||
const mapping = {} as IHttpRpcMapping;
|
||||
fields.forEach(filed => {
|
||||
const jsonAnnotation = getAnnotation(filed.annotations, 'api.json');
|
||||
if (jsonAnnotation) {
|
||||
filed.extensionConfig = filed.extensionConfig || {};
|
||||
filed.extensionConfig.key = jsonAnnotation;
|
||||
}
|
||||
const { extensionConfig } = filed;
|
||||
const alias = getFieldsAlias(filed);
|
||||
if (extensionConfig) {
|
||||
if (isFullBody(filed)) {
|
||||
mapping.entire_body = [alias];
|
||||
return;
|
||||
}
|
||||
Object.keys(extensionConfig).forEach(key => {
|
||||
if (key === 'position' && extensionConfig.position) {
|
||||
const filedMapping = this.processMapping(
|
||||
mapping,
|
||||
extensionConfig.position,
|
||||
alias,
|
||||
);
|
||||
mapping[extensionConfig.position] = filedMapping;
|
||||
specificPositionFiled.add(alias);
|
||||
}
|
||||
});
|
||||
}
|
||||
// 如果没有指定,根据method默认指定为query 或者 body
|
||||
if (!specificPositionFiled.has(alias)) {
|
||||
const filedMapping = mapping[defaultPosition];
|
||||
mapping[defaultPosition] = filedMapping
|
||||
? [...filedMapping, alias]
|
||||
: [alias];
|
||||
}
|
||||
});
|
||||
return mapping;
|
||||
}
|
||||
|
||||
private processMapping(
|
||||
mapping: IHttpRpcMapping,
|
||||
position: Position,
|
||||
filedName: string,
|
||||
): string[] {
|
||||
const mappingKeys = [
|
||||
'path',
|
||||
'query',
|
||||
'status_code',
|
||||
'header',
|
||||
'cookie',
|
||||
'entire_body',
|
||||
'body',
|
||||
];
|
||||
if (mappingKeys.find(i => i === position)) {
|
||||
const data = mapping[position];
|
||||
return data ? [...data, filedName] : [filedName];
|
||||
} else {
|
||||
return mapping[position] || [];
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,469 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { faker } from '@faker-js/faker';
|
||||
import {
|
||||
type IPlugin,
|
||||
type Program,
|
||||
on,
|
||||
after,
|
||||
} from '@coze-arch/idl2ts-plugin';
|
||||
import {
|
||||
type IParseResultItem,
|
||||
type EnumDefinition,
|
||||
isServiceDefinition,
|
||||
isStructDefinition,
|
||||
isIdentifier,
|
||||
type StructDefinition,
|
||||
isBaseType,
|
||||
type FieldType,
|
||||
type FunctionType,
|
||||
isEnumDefinition,
|
||||
isListType,
|
||||
isStringLiteral,
|
||||
type TypedefDefinition,
|
||||
type ConstDefinition,
|
||||
type ConstValue,
|
||||
isSetType,
|
||||
isMapType,
|
||||
isIntConstant,
|
||||
isConstDefinition,
|
||||
type ProcessIdlCtx,
|
||||
isTypedefDefinition,
|
||||
type UnifyStatement,
|
||||
SyntaxType,
|
||||
type ServiceDefinition,
|
||||
getStatementById,
|
||||
parseIdFiledType,
|
||||
uniformNs,
|
||||
hasDynamicJsonAnnotation,
|
||||
getValuesFromEnum,
|
||||
isFullBody,
|
||||
removeFileExt,
|
||||
parseId,
|
||||
getOutputName,
|
||||
getFieldsAlias,
|
||||
getBaseTypeConverts,
|
||||
getRelativePath,
|
||||
} from '@coze-arch/idl2ts-helper';
|
||||
import * as t from '@babel/types';
|
||||
import template from '@babel/template';
|
||||
|
||||
import { type Options } from '../types';
|
||||
import { genMockPublic } from '../template';
|
||||
import { type Contexts, type GenMockFieldCtx, HOOK } from '../context';
|
||||
|
||||
interface ProcessIdlCtxWithMock extends ProcessIdlCtx {
|
||||
mockStatements: t.Statement[];
|
||||
}
|
||||
|
||||
export class MockTransformerPlugin implements IPlugin {
|
||||
private options: Options;
|
||||
private program!: Program<Contexts>;
|
||||
constructor(options: Options) {
|
||||
this.options = options;
|
||||
}
|
||||
apply(program: Program<Contexts>): void {
|
||||
this.program = program;
|
||||
program.register(
|
||||
on(HOOK.PROCESS_IDL_NODE),
|
||||
ctx => {
|
||||
const { node } = ctx;
|
||||
ctx.mockStatements = ctx.mockStatements || [];
|
||||
|
||||
if (!node) {
|
||||
throw new Error('node is undefined');
|
||||
}
|
||||
const statement = this.processIdlNode(node, ctx);
|
||||
if (statement) {
|
||||
ctx.mockStatements.push(statement);
|
||||
}
|
||||
return ctx;
|
||||
},
|
||||
0,
|
||||
);
|
||||
|
||||
program.register(on(HOOK.GEN_MOCK_FILED), ctx => this.genMockField(ctx));
|
||||
|
||||
program.register(after(HOOK.PROCESS_IDL_AST), ctx => {
|
||||
const { ast, mock, mockStatements } = ctx;
|
||||
const exportId = [] as string[];
|
||||
const mockVarOrder = this.getMockVarOrder(mock);
|
||||
const nextOrder = {} as Record<string, number>;
|
||||
mockStatements.forEach((i, index) => {
|
||||
if (t.isVariableDeclaration(i)) {
|
||||
const { name } = i.declarations[0].id as t.Identifier;
|
||||
exportId.push(name);
|
||||
nextOrder[name] = index;
|
||||
}
|
||||
});
|
||||
// 按照 mock 文件中的顺序优先排序
|
||||
const getOrder = (name: string) =>
|
||||
typeof mockVarOrder[name] !== 'undefined'
|
||||
? mockVarOrder[name]
|
||||
: nextOrder[name];
|
||||
const body = mockStatements.sort((a, b) => {
|
||||
if (t.isVariableDeclaration(a) && t.isVariableDeclaration(b)) {
|
||||
const { name: nameA } = a.declarations[0].id as t.Identifier;
|
||||
const { name: nameB } = b.declarations[0].id as t.Identifier;
|
||||
const result = getOrder(nameA) - getOrder(nameB);
|
||||
return result;
|
||||
}
|
||||
return 0;
|
||||
});
|
||||
if (ast.includes) {
|
||||
Object.keys(ast.includeMap).forEach(i => {
|
||||
body.unshift(this.processIncludes(i, ast));
|
||||
});
|
||||
}
|
||||
const temp = template.ast(
|
||||
`module.exports = {${exportId.join(',')}}`,
|
||||
) as t.Statement;
|
||||
body.push(temp);
|
||||
|
||||
mock.program.body = [genMockPublic(ctx, this.options), ...body];
|
||||
ctx.output.set(
|
||||
getOutputName({
|
||||
source: `${removeFileExt(ast.idlPath)}.mock.js`,
|
||||
outputDir: this.options.outputDir,
|
||||
idlRoot: this.options.idlRoot,
|
||||
}),
|
||||
{ type: 'babel', content: mock },
|
||||
);
|
||||
return ctx;
|
||||
});
|
||||
}
|
||||
|
||||
private getMockVarOrder(file: t.File) {
|
||||
const orders = {} as Record<string, number>;
|
||||
file.program.body.map((i, index) => {
|
||||
if (t.isVariableDeclaration(i)) {
|
||||
const identifier = i.declarations[0].id as t.Identifier;
|
||||
orders[identifier.name] = index;
|
||||
}
|
||||
});
|
||||
return orders;
|
||||
}
|
||||
private processIdlNode(
|
||||
statement: UnifyStatement,
|
||||
ctx: ProcessIdlCtxWithMock,
|
||||
) {
|
||||
if (isStructDefinition(statement)) {
|
||||
return this.processStructNode(statement, ctx);
|
||||
} else if (isTypedefDefinition(statement)) {
|
||||
return this.processTypeDefNode(statement, ctx);
|
||||
} else if (isEnumDefinition(statement)) {
|
||||
return this.processEnumDefNode(statement, ctx);
|
||||
} else if (isConstDefinition(statement)) {
|
||||
return this.processConstDefNode(statement, ctx);
|
||||
} else if (isServiceDefinition(statement)) {
|
||||
return this.processServiceDefinition(statement, ctx);
|
||||
}
|
||||
throw new Error(`can not process Node from statement type: ${statement}`);
|
||||
}
|
||||
|
||||
private processIncludes(include: string, ast: IParseResultItem) {
|
||||
const includePath = getRelativePath(ast.idlPath, ast.includeMap[include]);
|
||||
const name = ast.includeRefer[include];
|
||||
const temp = template.ast(
|
||||
`const ${name} = require('${`${includePath}.mock.js`}')`,
|
||||
) as t.ImportDeclaration;
|
||||
return temp;
|
||||
}
|
||||
private processServiceDefinition(
|
||||
srtuct: ServiceDefinition,
|
||||
ctx: ProcessIdlCtxWithMock,
|
||||
) {
|
||||
const { name, functions } = srtuct;
|
||||
if (this.findTarget(name.value, ctx)) {
|
||||
return;
|
||||
}
|
||||
const variableDeclaration = template.ast(
|
||||
`var ${name.value} = {${functions.map(f => {
|
||||
const { name, returnType, fields } = f;
|
||||
const reqType = fields[0].fieldType as any;
|
||||
const resType = this.processReqResPramsType(returnType, ctx.ast);
|
||||
return `${name.value}:{req:${parseId(reqType.value)},res:${parseId(resType)}}`;
|
||||
})}}`,
|
||||
) as t.ExpressionStatement;
|
||||
return variableDeclaration;
|
||||
}
|
||||
private processReqResPramsType(
|
||||
fieldType: FunctionType,
|
||||
ast: IParseResultItem,
|
||||
) {
|
||||
if (isIdentifier(fieldType)) {
|
||||
const statement = getStatementById(fieldType, ast);
|
||||
if (isStructDefinition(statement)) {
|
||||
const wholeBody = statement.fields.find(isFullBody);
|
||||
if (wholeBody) {
|
||||
// 处理 api.body="."
|
||||
const { annotations } = wholeBody;
|
||||
if (hasDynamicJsonAnnotation(annotations)) {
|
||||
return '{}';
|
||||
}
|
||||
return `${fieldType.value}['${getFieldsAlias(wholeBody)}']`;
|
||||
} else {
|
||||
return fieldType.value;
|
||||
}
|
||||
}
|
||||
throw new Error('params must be identifier');
|
||||
} else {
|
||||
return 'void';
|
||||
}
|
||||
}
|
||||
private processStructNode(
|
||||
struct: StructDefinition,
|
||||
ctx: ProcessIdlCtxWithMock,
|
||||
) {
|
||||
const { name, fields } = struct;
|
||||
if (this.findTarget(name.value, ctx)) {
|
||||
return;
|
||||
}
|
||||
const oldOne = this.getVariableDeclarationById(name.value, ctx);
|
||||
const variableDeclaration =
|
||||
oldOne ||
|
||||
(template.ast(
|
||||
`var ${name.value} = createStruct(()=>{ return {} })`,
|
||||
) as t.VariableDeclaration);
|
||||
const init = (variableDeclaration.declarations[0].init as t.CallExpression)
|
||||
.arguments[0] as t.ArrowFunctionExpression;
|
||||
const returnObj = (
|
||||
(init.body as t.BlockStatement).body.find(i =>
|
||||
t.isReturnStatement(i),
|
||||
) as t.ReturnStatement
|
||||
).argument as t.ObjectExpression;
|
||||
if (!returnObj) {
|
||||
throw new Error('struct mock must return obj');
|
||||
}
|
||||
const fieldNames = new Set(fields.map(i => getFieldsAlias(i)));
|
||||
const newPros = [] as t.ObjectProperty[];
|
||||
const includeFieldName = (pro: t.ObjectProperty) =>
|
||||
(t.isStringLiteral(pro.key) && fieldNames.has(pro.key.value)) ||
|
||||
(t.isIdentifier(pro.key) && fieldNames.has(pro.key.name));
|
||||
returnObj.properties.forEach(i => {
|
||||
if (t.isObjectProperty(i)) {
|
||||
if (includeFieldName(i)) {
|
||||
const key = t.isStringLiteral(i.key)
|
||||
? i.key.value
|
||||
: t.isIdentifier(i.key)
|
||||
? i.key.name
|
||||
: '';
|
||||
fieldNames.delete(key);
|
||||
}
|
||||
newPros.push(i);
|
||||
}
|
||||
});
|
||||
fields.forEach(f => {
|
||||
const { fieldType, defaultValue } = f;
|
||||
const fieldName = getFieldsAlias(f);
|
||||
if (!fieldNames.has(fieldName)) {
|
||||
return;
|
||||
}
|
||||
// 没有的,需要重新生成
|
||||
newPros.push(
|
||||
t.objectProperty(
|
||||
fieldName.includes('-')
|
||||
? t.stringLiteral(fieldName)
|
||||
: t.identifier(fieldName),
|
||||
this.processValue(fieldType, defaultValue || undefined, {
|
||||
fieldDefinition: f,
|
||||
struct,
|
||||
ast: ctx.ast,
|
||||
}),
|
||||
),
|
||||
);
|
||||
});
|
||||
returnObj.properties = newPros;
|
||||
// this.processNodes.set(name.value, variableDeclaration);
|
||||
return variableDeclaration;
|
||||
}
|
||||
private processValue(
|
||||
fieldType: FieldType | FunctionType,
|
||||
defaultValue?: ConstValue,
|
||||
context?: GenMockFieldCtx['context'],
|
||||
) {
|
||||
const ctx = this.program.trigger(HOOK.GEN_MOCK_FILED, {
|
||||
fieldType,
|
||||
defaultValue,
|
||||
context,
|
||||
} as GenMockFieldCtx);
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
return ctx.output!;
|
||||
}
|
||||
// eslint-disable-next-line complexity
|
||||
private genMockField(ctx: GenMockFieldCtx) {
|
||||
let { output } = ctx;
|
||||
const { defaultValue, fieldType } = ctx;
|
||||
if (output) {
|
||||
return ctx;
|
||||
}
|
||||
if (isBaseType(fieldType)) {
|
||||
const type = getBaseTypeConverts('number')[fieldType.type];
|
||||
|
||||
if (type === 'string') {
|
||||
let value = faker.word.words();
|
||||
if (defaultValue && defaultValue.type === SyntaxType.StringLiteral) {
|
||||
value = defaultValue.value;
|
||||
}
|
||||
output = t.stringLiteral(value);
|
||||
} else if (type === 'number') {
|
||||
let value = faker.number.int();
|
||||
if (defaultValue && defaultValue.type === SyntaxType.IntConstant) {
|
||||
value = Number(defaultValue.value.value);
|
||||
}
|
||||
output = t.numericLiteral(value);
|
||||
} else if (type === 'boolean') {
|
||||
let value = faker.datatype.boolean();
|
||||
if (defaultValue && defaultValue.type === SyntaxType.BooleanLiteral) {
|
||||
value = defaultValue.value;
|
||||
}
|
||||
output = t.booleanLiteral(value);
|
||||
} else if (type === 'object') {
|
||||
// binary
|
||||
output = t.callExpression(
|
||||
t.memberExpression(t.identifier('Buffer'), t.identifier('from')),
|
||||
[t.stringLiteral(faker.word.words())],
|
||||
);
|
||||
}
|
||||
} else if (isMapType(fieldType)) {
|
||||
const { valueType } = fieldType;
|
||||
output = t.objectExpression([
|
||||
t.objectProperty(
|
||||
t.identifier(faker.word.words()),
|
||||
this.processValue(valueType),
|
||||
),
|
||||
]);
|
||||
} else if (isListType(fieldType)) {
|
||||
const { valueType } = fieldType;
|
||||
output = t.arrayExpression([this.processValue(valueType)]);
|
||||
} else if (isSetType(fieldType)) {
|
||||
// set 处理成array校验
|
||||
const { valueType } = fieldType;
|
||||
output = t.arrayExpression([this.processValue(valueType)]);
|
||||
} else if (isIdentifier(fieldType)) {
|
||||
// 引用类型
|
||||
const { refName, namespace } = parseIdFiledType(fieldType);
|
||||
if (!namespace) {
|
||||
output = t.callExpression(t.identifier(refName), []);
|
||||
} else {
|
||||
output = t.callExpression(
|
||||
t.memberExpression(
|
||||
t.identifier(uniformNs(namespace)),
|
||||
t.identifier(refName),
|
||||
),
|
||||
[],
|
||||
);
|
||||
}
|
||||
}
|
||||
if (output) {
|
||||
return { fieldType, defaultValue, output };
|
||||
}
|
||||
throw new Error(`can not process fieldType : ${fieldType.type}`);
|
||||
}
|
||||
private processConst(constVal: ConstValue) {
|
||||
// 暂时统一处理成0
|
||||
if (isStringLiteral(constVal)) {
|
||||
return t.stringLiteral(constVal.value);
|
||||
}
|
||||
if (isIntConstant(constVal)) {
|
||||
return t.stringLiteral(constVal.value.value);
|
||||
}
|
||||
return t.numericLiteral(0);
|
||||
}
|
||||
|
||||
private processTypeDefNode(
|
||||
typeDef: TypedefDefinition,
|
||||
ctx: ProcessIdlCtxWithMock,
|
||||
) {
|
||||
const { definitionType, name } = typeDef;
|
||||
if (this.findTarget(name.value, ctx)) {
|
||||
return;
|
||||
}
|
||||
const builder = template(`var ${name.value}= () => %%value%% `);
|
||||
const variableDeclaration = builder({
|
||||
value: this.processValue(definitionType),
|
||||
}) as t.VariableDeclaration;
|
||||
return variableDeclaration;
|
||||
}
|
||||
private processEnumDefNode(def: EnumDefinition, ctx: ProcessIdlCtxWithMock) {
|
||||
const { name, members } = def;
|
||||
const values = getValuesFromEnum(def);
|
||||
|
||||
const commentValues = values.map((value, index) => {
|
||||
const { name } = members[index];
|
||||
return ` ${name.value}: ${value}`;
|
||||
});
|
||||
const comment = { type: 'CommentLine', value: commentValues } as any;
|
||||
const target = this.findTarget(name.value, ctx);
|
||||
if (target) {
|
||||
// 需要更新注释
|
||||
// target.trailingComments = [comment];
|
||||
return;
|
||||
}
|
||||
// 枚举类型统一处理成常量
|
||||
const builder = template(`var ${name.value}= () => %%value%% `);
|
||||
const node = builder({
|
||||
value: t.numericLiteral(values[0] || 0),
|
||||
}) as t.VariableDeclaration;
|
||||
const variableDeclaration = t.addComments(node, 'trailing', [comment]);
|
||||
return variableDeclaration;
|
||||
}
|
||||
private processConstDefNode(
|
||||
constDef: ConstDefinition,
|
||||
ctx: ProcessIdlCtxWithMock,
|
||||
) {
|
||||
const { name, initializer } = constDef;
|
||||
if (this.findTarget(name.value, ctx)) {
|
||||
return;
|
||||
}
|
||||
const builder = template(`var ${name.value}= () => %%value%% `);
|
||||
const node = builder({
|
||||
value: this.processConst(initializer),
|
||||
}) as t.VariableDeclaration;
|
||||
// const variableDeclaration = t.addComment(
|
||||
// ,
|
||||
// 'leading',
|
||||
// '暂时对const默认处理为0,如有需要请自行重新赋值'
|
||||
// );
|
||||
return node;
|
||||
}
|
||||
private getVariableDeclarationById(id: string, ctx: ProcessIdlCtxWithMock) {
|
||||
return ctx.mock.program.body.find(i => {
|
||||
if (t.isVariableDeclaration(i)) {
|
||||
const identifier = i.declarations[0].id as t.Identifier;
|
||||
if (identifier.name === id) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}) as t.VariableDeclaration | undefined;
|
||||
}
|
||||
private findTarget(id: string, ctx: ProcessIdlCtxWithMock) {
|
||||
return ctx.mockStatements.find(i => this.getIdName(i) === id);
|
||||
}
|
||||
private getIdName(statement: t.Statement): string {
|
||||
if (
|
||||
t.isVariableDeclaration(statement) &&
|
||||
t.isVariableDeclarator(statement.declarations[0])
|
||||
) {
|
||||
if (t.isIdentifier(statement.declarations[0].id)) {
|
||||
return statement.declarations[0].id.name;
|
||||
}
|
||||
}
|
||||
return '';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,136 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { EOL } from 'os';
|
||||
|
||||
import { type IPlugin, type Program, after } from '@coze-arch/idl2ts-plugin';
|
||||
import {
|
||||
type ProcessIdlCtx,
|
||||
type IParseEntryCtx,
|
||||
getRelativePath,
|
||||
createFile,
|
||||
getOutputName,
|
||||
} from '@coze-arch/idl2ts-helper';
|
||||
|
||||
import { type Options } from '../types';
|
||||
import { type Contexts, HOOK } from '../context';
|
||||
|
||||
/**
|
||||
* 提供统一 api 入口
|
||||
*/
|
||||
export class PkgEntryPlugin implements IPlugin {
|
||||
private options: Options;
|
||||
private funcs: Map<string, string> = new Map();
|
||||
private entryName: string;
|
||||
constructor(options: Options) {
|
||||
this.options = options;
|
||||
this.entryName = `/${this.options.entryName || 'api'}.ts`;
|
||||
}
|
||||
apply(program: Program<Contexts>): void {
|
||||
program.register(after(HOOK.PARSE_FUN_META), this.collectFuncs.bind(this));
|
||||
program.register(after(HOOK.GEN_FILE_AST), this.genPkgEntry.bind(this));
|
||||
program.register(after(HOOK.GEN_FILE_AST), this.genValidateFile.bind(this));
|
||||
}
|
||||
private collectFuncs(ctx: ProcessIdlCtx) {
|
||||
const { ast, meta } = ctx;
|
||||
const relativePath = getRelativePath(
|
||||
this.options.idlRoot + this.entryName,
|
||||
ast.idlPath,
|
||||
);
|
||||
this.funcs.set(
|
||||
relativePath,
|
||||
// 只支持单 service
|
||||
meta[0].service,
|
||||
);
|
||||
return ctx;
|
||||
}
|
||||
private genPkgEntry = (ctx: IParseEntryCtx) => {
|
||||
const targetFile = this.options.outputDir + this.entryName;
|
||||
let source = '';
|
||||
for (const [path, service] of this.funcs.entries()) {
|
||||
source += `export * as ${service} from '${path}';${EOL}`;
|
||||
}
|
||||
const content = createFile(source);
|
||||
ctx.files.set(targetFile, { content, type: 'babel' });
|
||||
return ctx;
|
||||
};
|
||||
private genValidateFile = (ctx: IParseEntryCtx) => {
|
||||
const { ast } = ctx;
|
||||
const targetFile = `${this.options.outputDir}/_schemas.js`;
|
||||
const targetFileTypes = `${this.options.outputDir}/_schemas.d.ts`;
|
||||
const schemaFiles = this.options.genSchema
|
||||
? ast.map(
|
||||
i =>
|
||||
`${getRelativePath(
|
||||
targetFile,
|
||||
getOutputName({
|
||||
source: i.idlPath,
|
||||
...this.options,
|
||||
}),
|
||||
)}.schema.json`,
|
||||
)
|
||||
: [];
|
||||
const code = schemaFiles.map(i => ` require("${i}")`).join(`,${EOL}`);
|
||||
ctx.files.set(targetFile, {
|
||||
content: `module.exports = [${EOL + code + EOL}] `,
|
||||
type: 'text',
|
||||
});
|
||||
ctx.files.set(targetFileTypes, {
|
||||
content: `declare const _schemas: any[];
|
||||
export default _schemas;
|
||||
`,
|
||||
type: 'text',
|
||||
});
|
||||
ctx.files.set(`${this.options.outputDir}/_mock_utils.js`, {
|
||||
content: getMockUtils(),
|
||||
type: 'text',
|
||||
});
|
||||
return ctx;
|
||||
};
|
||||
}
|
||||
|
||||
function getMockUtils() {
|
||||
return `function rawParse(str) {
|
||||
const lines = (str || '').split('\\n');
|
||||
const entries = lines.map((line) => {
|
||||
line = line.trim();
|
||||
const res = line.match(/at (.+) \\((.+)\\)/)||[]
|
||||
return {
|
||||
beforeParse: line,
|
||||
callee: res[1]
|
||||
};
|
||||
});
|
||||
return entries.filter((x) => x.callee !== undefined);
|
||||
}
|
||||
|
||||
function createStruct(fn) {
|
||||
const structFactory = () => {
|
||||
const error = new Error();
|
||||
const items = rawParse(error.stack).filter((i) => i.callee === 'structFactory').map((i) => i.beforeParse);
|
||||
const isCircle = items.length > Array.from(new Set(items)).length;
|
||||
if (isCircle) {
|
||||
return {};
|
||||
}
|
||||
const res = fn();
|
||||
|
||||
return res;
|
||||
};
|
||||
|
||||
return structFactory;
|
||||
}
|
||||
module.exports={ createStruct }
|
||||
`;
|
||||
}
|
||||
322
frontend/infra/idl/idl2ts-generator/src/plugin/schema-plugin.ts
Normal file
322
frontend/infra/idl/idl2ts-generator/src/plugin/schema-plugin.ts
Normal file
@@ -0,0 +1,322 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import {
|
||||
type IPlugin,
|
||||
type Program,
|
||||
on,
|
||||
before,
|
||||
after,
|
||||
} from '@coze-arch/idl2ts-plugin';
|
||||
import {
|
||||
type EnumDefinition,
|
||||
isServiceDefinition,
|
||||
isStructDefinition,
|
||||
isIdentifier,
|
||||
type StructDefinition,
|
||||
isBaseType,
|
||||
type FieldType,
|
||||
type FunctionType,
|
||||
isEnumDefinition,
|
||||
isListType,
|
||||
isStringLiteral,
|
||||
type TypedefDefinition,
|
||||
type ConstDefinition,
|
||||
type ConstValue,
|
||||
isSetType,
|
||||
isMapType,
|
||||
isIntConstant,
|
||||
isConstDefinition,
|
||||
isTypedefDefinition,
|
||||
type UnifyStatement,
|
||||
SyntaxType,
|
||||
type ServiceDefinition,
|
||||
parseIdFiledType,
|
||||
getValuesFromEnum,
|
||||
removeFileExt,
|
||||
parseFiledName,
|
||||
getAnnotation,
|
||||
getTypeFromDynamicJsonAnnotation,
|
||||
getSchemaRootByPath,
|
||||
getOutputName,
|
||||
} from '@coze-arch/idl2ts-helper';
|
||||
|
||||
import {
|
||||
type Contexts,
|
||||
HOOK,
|
||||
type ProcessIdlCtxWithSchema,
|
||||
type AjvType,
|
||||
type ListType,
|
||||
type RefType,
|
||||
type StringType,
|
||||
type NumberType,
|
||||
type BoolType,
|
||||
type EnumType,
|
||||
type ConstType,
|
||||
type StructType,
|
||||
} from '../context';
|
||||
|
||||
interface IOptions {
|
||||
outputDir: string;
|
||||
idlRoot: string;
|
||||
}
|
||||
export class SchemaPlugin implements IPlugin {
|
||||
private options: IOptions;
|
||||
constructor(options: IOptions) {
|
||||
this.options = options;
|
||||
}
|
||||
apply(program: Program<Contexts>): void {
|
||||
program.register(
|
||||
on(HOOK.PROCESS_IDL_NODE),
|
||||
ctx => {
|
||||
const { schema, node } = ctx;
|
||||
// if (!node) {
|
||||
// return ctx;
|
||||
// }
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
const schemas = this.processIdlNode(node!, ctx);
|
||||
Object.keys(schemas).forEach(id => {
|
||||
schema.definitions[id] = schemas[id];
|
||||
});
|
||||
return ctx;
|
||||
},
|
||||
0,
|
||||
);
|
||||
|
||||
program.register(before(HOOK.PROCESS_IDL_AST), ctx => {
|
||||
ctx.schema = {
|
||||
$id: getSchemaRootByPath(ctx.ast.idlPath, this.options.idlRoot),
|
||||
definitions: {},
|
||||
};
|
||||
return ctx;
|
||||
});
|
||||
|
||||
program.register(after(HOOK.PROCESS_IDL_AST), ctx => {
|
||||
const { ast, schema } = ctx;
|
||||
ctx.output.set(
|
||||
getOutputName({
|
||||
source: `${removeFileExt(ast.idlPath)}.schema.json`,
|
||||
outputDir: this.options.outputDir,
|
||||
idlRoot: this.options.idlRoot,
|
||||
}),
|
||||
{ type: 'json', content: schema },
|
||||
);
|
||||
return ctx;
|
||||
});
|
||||
}
|
||||
|
||||
private processIdlNode(
|
||||
statement: UnifyStatement,
|
||||
ctx: ProcessIdlCtxWithSchema,
|
||||
): { [key: string]: AjvType } {
|
||||
if (isStructDefinition(statement)) {
|
||||
return this.processStructNode(statement, ctx);
|
||||
} else if (isTypedefDefinition(statement)) {
|
||||
return this.processTypeDefNode(statement, ctx);
|
||||
} else if (isEnumDefinition(statement)) {
|
||||
return this.processEnumDefNode(statement);
|
||||
} else if (isConstDefinition(statement)) {
|
||||
return this.processConstDefNode(statement);
|
||||
} else if (isServiceDefinition(statement)) {
|
||||
return this.processServiceDefinition(statement, ctx);
|
||||
}
|
||||
throw new Error(`canot process Node from statement type: ${statement}`);
|
||||
}
|
||||
private processServiceDefinition(
|
||||
struct: ServiceDefinition,
|
||||
ctx: ProcessIdlCtxWithSchema,
|
||||
) {
|
||||
const { functions } = struct;
|
||||
const schemas = {} as { [key: string]: AjvType };
|
||||
functions.map(f => {
|
||||
const { name, returnType, fields } = f;
|
||||
const reqSchema = this.processValue(fields[0].fieldType, ctx);
|
||||
const resSchema = this.processValue(returnType, ctx);
|
||||
schemas[`${name.value}.req`] = reqSchema;
|
||||
schemas[`${name.value}.res`] = resSchema;
|
||||
});
|
||||
return schemas;
|
||||
}
|
||||
private processStructNode(
|
||||
srtuct: StructDefinition,
|
||||
ctx: ProcessIdlCtxWithSchema,
|
||||
) {
|
||||
const { name, fields } = srtuct;
|
||||
const schema = {
|
||||
properties: {},
|
||||
required: [],
|
||||
type: 'object',
|
||||
} as StructType;
|
||||
|
||||
fields.forEach(i => {
|
||||
const fieldName = parseFiledName(i);
|
||||
if (schema.properties[fieldName]) {
|
||||
return;
|
||||
}
|
||||
const { requiredness, annotations, fieldType } = i;
|
||||
const isAnyType = getAnnotation(annotations, 'api.value_type') === 'any';
|
||||
const dynamicType = getTypeFromDynamicJsonAnnotation(annotations);
|
||||
const required = requiredness !== 'optional';
|
||||
let valueType: AjvType = this.processValue(fieldType, ctx);
|
||||
if (isAnyType || dynamicType) {
|
||||
valueType = {};
|
||||
}
|
||||
if (required) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
schema.required!.push(fieldName);
|
||||
}
|
||||
|
||||
schema.properties[fieldName] = valueType;
|
||||
});
|
||||
return { [name.value]: schema };
|
||||
}
|
||||
private processValue(
|
||||
fieldType: FieldType | FunctionType,
|
||||
ctx: ProcessIdlCtxWithSchema,
|
||||
) {
|
||||
if (isBaseType(fieldType)) {
|
||||
const type = getBaseTypeConverts('number')[fieldType.type];
|
||||
if (fieldType.type === SyntaxType.I64Keyword) {
|
||||
// i64
|
||||
const schema: NumberType = { type: 'integer' };
|
||||
return schema;
|
||||
}
|
||||
if (type === 'string') {
|
||||
const schema: StringType = { type: 'string' };
|
||||
return schema;
|
||||
} else if (type === 'number') {
|
||||
const schema: NumberType = { type: 'integer' };
|
||||
return schema;
|
||||
} else if (type === 'boolean') {
|
||||
const schema: BoolType = { type: 'boolean' };
|
||||
return schema;
|
||||
} else if (type === 'object') {
|
||||
// todo check binary
|
||||
const schema: StructType = { type: 'object', properties: {} };
|
||||
return schema;
|
||||
}
|
||||
} else if (isMapType(fieldType)) {
|
||||
const { valueType } = fieldType;
|
||||
const schema: StructType = {
|
||||
type: 'object',
|
||||
additionalProperties: this.processValue(valueType, ctx),
|
||||
};
|
||||
return schema;
|
||||
} else if (isListType(fieldType)) {
|
||||
const { valueType } = fieldType;
|
||||
const schema: ListType = {
|
||||
type: 'array',
|
||||
items: this.processValue(valueType, ctx),
|
||||
};
|
||||
return schema;
|
||||
} else if (isSetType(fieldType)) {
|
||||
// set 处理成array校验
|
||||
const { valueType } = fieldType;
|
||||
const schema: ListType = {
|
||||
type: 'array',
|
||||
items: this.processValue(valueType, ctx),
|
||||
};
|
||||
return schema;
|
||||
} else if (isIdentifier(fieldType)) {
|
||||
// 引用类型
|
||||
const { refName, namespace } = parseIdFiledType(fieldType);
|
||||
if (!namespace) {
|
||||
const schema: RefType = { $ref: `#/definitions/${refName}` };
|
||||
return schema;
|
||||
} else {
|
||||
const schema: RefType = {
|
||||
$ref: `${this.getSchemeRootByNamespace(
|
||||
namespace,
|
||||
ctx,
|
||||
)}#/definitions/${refName}`,
|
||||
};
|
||||
return schema;
|
||||
}
|
||||
}
|
||||
throw new Error(`can not process fieldType : ${fieldType.type}`);
|
||||
}
|
||||
private processConst(constVal: ConstValue) {
|
||||
// 暂时统一处理成0
|
||||
const schema = {} as ConstType;
|
||||
if (isStringLiteral(constVal)) {
|
||||
schema.const = constVal.value;
|
||||
}
|
||||
if (isIntConstant(constVal)) {
|
||||
schema.const = constVal.value.value;
|
||||
}
|
||||
return schema;
|
||||
}
|
||||
|
||||
private processTypeDefNode(
|
||||
typeDef: TypedefDefinition,
|
||||
ctx: ProcessIdlCtxWithSchema,
|
||||
) {
|
||||
const { definitionType, name } = typeDef;
|
||||
const def = { [name.value]: this.processValue(definitionType, ctx) };
|
||||
return def;
|
||||
}
|
||||
private processEnumDefNode(def: EnumDefinition) {
|
||||
const { name } = def;
|
||||
const values = getValuesFromEnum(def);
|
||||
const schema = { enum: values.length > 0 ? values : [0] } as EnumType;
|
||||
return { [name.value]: schema };
|
||||
}
|
||||
private processConstDefNode(constDef: ConstDefinition) {
|
||||
const { name, initializer } = constDef;
|
||||
return { [name.value]: this.processConst(initializer) };
|
||||
}
|
||||
|
||||
/**
|
||||
* namespace -> relative-path
|
||||
* @param namespace
|
||||
* @param ctx
|
||||
*/
|
||||
private getSchemeRootByNamespace(
|
||||
namespace: string,
|
||||
ctx: ProcessIdlCtxWithSchema,
|
||||
) {
|
||||
const { ast } = ctx;
|
||||
let target = '';
|
||||
|
||||
Object.keys(ast.deps).forEach(i => {
|
||||
// const pathName = getSchemaRootByPath(i, this.options.idlRoot);
|
||||
if (i.endsWith(`/${namespace}.thrift`)) {
|
||||
target = getSchemaRootByPath(i, this.options.idlRoot);
|
||||
}
|
||||
});
|
||||
return target;
|
||||
}
|
||||
}
|
||||
// import { SyntaxType } from "@coze-arch/idl-parser";
|
||||
|
||||
function getBaseTypeConverts(i64Type: string) {
|
||||
let newType = 'number';
|
||||
if (i64Type === 'string') {
|
||||
newType = 'string';
|
||||
}
|
||||
|
||||
return {
|
||||
[SyntaxType.ByteKeyword]: 'number',
|
||||
[SyntaxType.I8Keyword]: 'number',
|
||||
[SyntaxType.I16Keyword]: 'number',
|
||||
[SyntaxType.I32Keyword]: 'number',
|
||||
[SyntaxType.I64Keyword]: newType,
|
||||
[SyntaxType.DoubleKeyword]: 'number',
|
||||
[SyntaxType.BinaryKeyword]: 'object',
|
||||
[SyntaxType.StringKeyword]: 'string',
|
||||
[SyntaxType.BoolKeyword]: 'boolean',
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user