feat: manually mirror opencoze's code from bytedance

Change-Id: I09a73aadda978ad9511264a756b2ce51f5761adf
This commit is contained in:
fanlv
2025-07-20 17:36:12 +08:00
commit 890153324f
14811 changed files with 1923430 additions and 0 deletions

View File

@@ -0,0 +1,68 @@
# @coze-arch/idl-parser
idl parser
## Overview
This package is part of the Coze Studio monorepo and provides architecture functionality. It serves as a core component in the Coze ecosystem.
## Getting Started
### Installation
Add this package to your `package.json`:
```json
{
"dependencies": {
"@coze-arch/idl-parser": "workspace:*"
}
}
```
Then run:
```bash
rush update
```
### Usage
```typescript
import { /* exported functions/components */ } from '@coze-arch/idl-parser';
// Example usage
// TODO: Add specific usage examples
```
## Features
- Core functionality for Coze Studio
- TypeScript support
- Modern ES modules
## API Reference
### Exports
- `*`
For detailed API documentation, please refer to the TypeScript definitions.
## Development
This package is built with:
- TypeScript
- Modern JavaScript
- Vitest for testing
- ESLint for code quality
## Contributing
This package is part of the Coze Studio monorepo. Please follow the monorepo contribution guidelines.
## License
Apache-2.0

View File

@@ -0,0 +1,24 @@
/*
* 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 function filterKeys(obj: Record<string, any>, keys: string[]) {
const newObj: Record<string, any> = {};
for (const key of keys) {
newObj[key] = obj[key];
}
return newObj;
}

View File

@@ -0,0 +1,36 @@
/*
* 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.
*/
/* eslint-disable */
import * as t from '../src/proto';
const content = `
syntax = 'proto3';
// c1
message Foo { // c2
// c3
int32 code = 1; // c4
// c5
string content = 2;
// c6
string message = 3; // c7
}
`;
const document = t.parse(content);
console.log(JSON.stringify(document, null, 2));

View File

@@ -0,0 +1,42 @@
/*
* 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.
*/
/* eslint-disable */
import * as t from '../src/thrift';
import * as path from 'path';
const idl = `
/*
*/
struct UserDeleteDataMap {
1: required UserDeleteData DeleteData
2: string k2 (go.tag = 'json:\\"-\\"')
}
/*
We
*/
enum AvatarMetaType {
UNKNOWN = 0, // 没有数据, 错误数据或者系统错误降级
RANDOM = 1, // 在修改 or 创建时,用户未指定 name 或者选中推荐的文字时,程序随机选择的头像
}
`;
const document = t.parse(idl);
var c = path.join('a/b.thrift', './c.thrift');
console.log(JSON.stringify(document, null, 2));

View 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.
*/
/* eslint-disable */
import * as t from '../src/unify/index';
import * as path from 'path';
// const root = 'test/idl';
// const idl = path.resolve(process.cwd(), root, 'dep/common.thrift');
// const document = t.parse(idl, {
// root,
// namespaceRefer: true,
// cache: false,
// });
// // document.body[4]
// console.log('#gg', document);
const indexThriftContent = `
namespace java com.unify_idx
include 'unify_dependent1.thrift'
typedef unify_dependent1.Foo TFoo
enum Gender {
// male
Male // male tail
// female
Female // female tail
// mix
Mix
}
// const map<Gender, string> genderMap = {
// Gender.Male: '男性',
// Gender.Female: '女性',
// }
union FuncRequest {
1: unify_dependent1.Foo r_key1
2: TFoo list (go.tag = "json:\\"-\\"")
}
`;
const dep1ThriftContent = `
namespace js unify_dep1
typedef Foo Foo1
struct Foo {
1: string f_key1
}
`;
// const fileContentMap = {
// 'unify_index.thrift': indexThriftContent,
// 'unify_dependent1.thrift': dep1ThriftContent,
// };
const indexProtoContent = `
syntax = "proto3";
import "unify.dependent1.proto";
package a.b.c;
message Request {
repeated string key1 = 1[(api.key) = 'f'];
a.b.Foo key3 = 3;
// message Sub {
// enum Num {
// ONE = 1;
// }
// // string k1 = 1;
// Num k2 = 2;
// }
// Sub key2 = 2;
}
`;
const dep1ProtoContent = `
syntax = "proto3";
package a.b;
message Foo {
string f_key1 = 1;
message SubF {}
SubF f_key2 = 2;
}
`;
const fileContentMap = {
'unify_index.proto': indexProtoContent,
'unify.dependent1.proto': dep1ProtoContent,
};
// const document = t.parse(
// 'unify_index.proto',
// {
// root: '.',
// // namespaceRefer: true,
// },
// fileContentMap
// );
// // document.body[4]
// console.log(document);
const baseContent = `
syntax = "proto3";
package a.b;
message Bar {
message BarSub {
enum NumBar {
ONE = 1;
}
}
}
`;
const extraContent = `
syntax = "proto3";
package a.b;
message Extra {}
`;
const indexContent = `
syntax = "proto3";
package a.b;
import 'base.proto';
import 'extra.proto';
message Foo {
// message FooSub {
// enum NumFoo {
// TWO = 2;
// }
// }
// Foo.FooSub.NumFoo k1 = 1;
// FooSub.NumFoo k2 = 2;
// FooSub k3 = 3;
// repeated FooSub k4 = 4;
// map<string, FooSub.NumFoo> k5 = 5;
// Bar.BarSub.NumBar k10 = 10;
Bar.BarSub k11 = 11;
// repeated Bar.BarSub.NumBar k12 = 12;
// map<string, Bar.BarSub> k13 = 13;
}
// message Bar {
// message BarSub {
// enum NumBar {
// ONE = 1;
// }
// }
// }
`;
const document = t.parse(
'index.proto',
{ cache: false },
{
'index.proto': indexContent,
'base.proto': baseContent,
'extra.proto': extraContent,
}
);
const statement = document.statements[0] as t.InterfaceWithFields;
console.log(statement);
// const baseContent = `
// syntax = "proto3";
// package a.b;
// message Common {
// }
// `;
// const indexContent = `
// syntax = "proto3";
// message Foo {
// google.protobuf.Any k1 = 1;
// }
// `;
// const document = t.parse(
// 'index.proto',
// { cache: false },
// {
// 'index.proto': indexContent,
// // 'base.proto': baseContent,
// }
// );
// const { functions } = document.statements[0] as t.ServiceDefinition;
// console.log(functions);

View File

@@ -0,0 +1,3 @@
syntax = "proto3";
message Base {}

View File

@@ -0,0 +1 @@
struct Base {}

View File

@@ -0,0 +1,3 @@
syntax = "proto3";
message Basee {}

View File

@@ -0,0 +1 @@
struct Basee {}

View File

@@ -0,0 +1,7 @@
syntax = "proto3";
package common;
import "base.proto";
import "basee.proto";
message Common {}

View File

@@ -0,0 +1,6 @@
namespace go common
include 'base.thrift'
include 'basee.thrift'
struct Common {}

View File

@@ -0,0 +1,4 @@
syntax = "proto3";
message Foo {
string k1 = 1;,
}

View File

@@ -0,0 +1,3 @@
struct Foo {
1: string k1,,
}

View File

@@ -0,0 +1,5 @@
syntax = "proto3";
service Foo {
option (api.uri_prefix) = "//example.com";
}

View File

@@ -0,0 +1,5 @@
service Foo {
} (
api.uri_prefix = 'https://example.com'
)

View File

@@ -0,0 +1,11 @@
syntax = "proto3";
import "unify_dependent2.proto";
import "./dep/common.proto";
package unify_dep1;
message Foo {
string f_key1 = 1;
common.Common f_key2 = 2;
}

View File

@@ -0,0 +1,12 @@
include 'unify_dependent2.thrift'
include './dep/common.thrift'
namespace js unify_dep1
typedef Foo Foo1
struct Foo {
1: string f_key1
2: common.Common f_key2
}

View File

@@ -0,0 +1,9 @@
syntax = "proto3";
import "./unify_dependent1.proto";
package unify_idx;
enum Number {
ONE = 1;
}

View File

@@ -0,0 +1,5 @@
include './unify_dependent1.thrift'
enum Number {
ONE = 1,
}

View File

@@ -0,0 +1,2 @@
include "./unify_base.thrift"
include "./unify_base1.thrift"

View File

@@ -0,0 +1,29 @@
syntax = "proto3";
import "./unify_dependent1.proto";
import "unify_dependent2.proto";
package unify_idx;
// c0
enum Gender {
// c1
MALE = 1; // c2
// c3
FEMAL = 2; // c4
}
/* cm1 */
message Request {
// cm2
// repeated string key1 = 1[(api.key) = 'f'];
// unify_dep1.Foo key2 = 2;
Number key3 = 3 [(api.position) = 'query'];
}
service Example {
option (api.uri_prefix) = "//example.com";
rpc Biz1(Request) returns (Number) {
option (api.uri) = '/api/biz1';
}
}

View File

@@ -0,0 +1,21 @@
namespace go unify_idx
include './unify_dependent1.thrift'
include 'unify_dependent2.thrift'
typedef unify_dependent1.Foo TFoo
union FuncRequest {
1: unify_dependent1.Foo r_key1
2: TFoo r_key2
}
struct FuncResponse {
1: unify_dependent2.Number key2
}
service Example {
FuncResponse Func(1: FuncRequest req)
} (
api.uri_prefix = 'https://example.com'
)

View File

@@ -0,0 +1,7 @@
syntax = "proto3";
import "base.proto";
message Foo {
Base key1 = 1;
}

View File

@@ -0,0 +1,5 @@
include 'base.thrift'
struct Foo {
1: base.Base key1
}

View File

@@ -0,0 +1,12 @@
syntax = "proto3";
message Common {}
service Example {
rpc Func1 (Common) returns (Common) {
option (google.api.http) = {
get: "/ezo/web/v1/user_camp_result"
body: "*"
};
}
}

View File

@@ -0,0 +1,140 @@
/*
* 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 * as t from '../src/proto';
describe('ferry-parser', () => {
describe('proto field', () => {
it('should convert message field extenstions', () => {
const idl = `
syntax = "proto3";
enum Numbers {
ONE = 1;
}
message Foo {
string k1 = 1 [(api.position) = "query"];
string k2 = 2 [(api.position) = 'body'];
string k3 = 3 [(api.position) = 'path'];
string k4 = 4 [(api.position) = 'header'];
string k5 = 5 [(api.position) = 'entire_body'];
string k6 = 6 [(api.position) = 'raw_body', (aapi.position) = 'raw_body'];
string k7 = 7 [(api.position) = 'status_code', (api.positionn) = 'raw_body'];
string k10 = 10 [(api.key) = 'key10'];
string k11 = 11 [(api.key) = 'k11'];
bytes k12 = 12 [(api.web_type) = 'File'];
int32 k21 = 21 [(api.query) = 'k21'];
int32 k22 = 22 [(api.body) = 'k22'];
int32 k23 = 23 [(api.path) = 'k23'];
int32 k24 = 24 [(api.header) = 'k24'];
int32 k25 = 25 [(api.entire_body) = 'key25'];
int32 k26 = 26 [(api.raw_body) = 'key_26'];
int32 k27 = 27 [(api.status_code) = 'key-27'];
int32 k31 = 31 [(api.query) = 'key31', (api.web_type) = 'number', (api.position) = ''];
int32 k32 = 32 [(api.position) = 'body', (api.key)='key32', (api.value_type) = 'any'];
int32 k33 = 33 [(api.method) = 'POST', (api.position) = 'QUERY'];
int32 k34 = 34 ;
Numbers k35 = 35 [(api.position) = 'path'];
}
`;
const expected = [
{ position: 'query' },
{ position: 'body' },
{ position: 'path' },
{ position: 'header' },
{ position: 'entire_body' },
{ position: 'raw_body' },
{ position: 'status_code' },
{ key: 'key10' },
{},
{ web_type: 'File' },
{ position: 'query' },
{ position: 'body' },
{ position: 'path' },
{ position: 'header' },
{ position: 'entire_body', key: 'key25' },
{ position: 'raw_body', key: 'key_26' },
{ position: 'status_code', key: 'key-27' },
{ position: 'query', key: 'key31', web_type: 'number' },
{ position: 'body', key: 'key32', value_type: 'any' },
{},
undefined,
{ position: 'path' },
];
const document = t.parse(idl);
const Foo = (document.root.nested || {}).Foo as t.MessageDefinition;
const extensionConfigs = Object.values(Foo.fields).map(
field => field.extensionConfig,
);
return expect(extensionConfigs).to.eql(expected);
});
it('should convert message field extenstions using old rules', () => {
const idl = `
syntax = "proto3";
message Foo {
int32 k1 = 1 [(api_req).query = 'k1'];
int32 k2 = 2 [(api_req).body = 'k2'];
int32 k3 = 3 [(api_req).path = 'k3'];
int32 k4 = 4 [(api_req).header = 'k4'];
int32 k6 = 5 [(api_req).raw_body = 'key5'];
int32 k5 = 6 [(api_resp).header = 'key6'];
int32 k7 = 7 [(api_resp).http_code = 'key7'];
string k8 = 8 [(api_resp).body = 'k8'];
}
`;
const expected = [
{ position: 'query' },
{ position: 'body' },
{ position: 'path' },
{ position: 'header' },
{ position: 'raw_body', key: 'key5' },
{ position: 'header', key: 'key6' },
{},
{ position: 'body' },
];
const document = t.parse(idl);
const Foo = (document.root.nested || {}).Foo as t.MessageDefinition;
const extensionConfigs = Object.values(Foo.fields).map(
field => field.extensionConfig,
);
return expect(extensionConfigs).to.eql(expected);
});
it('should throw an error when using invalid type for a path parameter', () => {
const idl = `
syntax = "proto3";
message Foo {
bool k1 = 1 [(api.position) = "path"];
}
`;
try {
t.parse(idl);
} catch (err) {
const { message } = err;
const expected =
"the type of path parameter 'k1' in 'Foo' should be string or integer";
return expect(message).to.equal(expected);
}
return expect(true).to.equal(false);
});
});
});

View File

@@ -0,0 +1,77 @@
/*
* 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 * as path from 'path';
import * as t from '../src/proto';
describe('ferry-parser', () => {
describe('proto index', () => {
it('should convert the file content', () => {
const idl = path.resolve(__dirname, 'idl/index.proto');
const expected = { uri_prefix: '//example.com' };
const document = t.parse(idl);
const Foo = (document.root.nested || {}).Foo as t.ServiceDefinition;
return expect(Foo.extensionConfig).to.eql(expected);
});
it('should throw an error due to invalid file path', () => {
const idl = path.resolve(__dirname, 'idl/indexx.proto');
try {
t.parse(idl);
} catch (err) {
const { message } = err;
return expect(message).to.includes('no such file:');
}
return expect(true).to.equal(false);
});
it('should throw an syntax error', () => {
const idl = `
syntax = "proto3";
message Foo {
string k1 = 1;,
}
`;
const expected = "illegal token ','(source:4:0)";
try {
t.parse(idl);
} catch (err) {
const { message } = err;
return expect(message).to.equal(expected);
}
return expect(true).to.equal(false);
});
it('should throw an syntax error in the file content', () => {
const idl = path.resolve(__dirname, 'idl/error.proto');
const expected = '__tests__/idl/error.proto:3:0)';
try {
t.parse(idl);
} catch (err) {
const { message } = err;
return expect(message).to.includes(expected);
}
return expect(true).to.equal(false);
});
});
});

View File

@@ -0,0 +1,108 @@
/*
* 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 * as t from '../src/proto';
describe('ferry-parser', () => {
describe('proto method', () => {
it('should convert method extenstions', () => {
const idl = `
syntax = 'proto3';
message BizRequest {}
message BizResponse {}
service Foo {
rpc Biz1(BizRequest) returns (BizResponse) {
option (api.uri) = '/api/biz1';
}
rpc Biz2(BizRequest) returns (BizResponse) {
option (api.method) = "POST";
option (api.uri) = "/api/biz2";
option (api.serializer) = "json";
option (api.group) = 'user';
}
rpc Biz3(BizRequest) returns (BizResponse) {
option (api.get) ='/api/biz3';
option (api.serializer) ='form';
}
rpc Biz4(BizRequest) returns (BizResponse) {
option (api.post) ='/api/biz4';
option (api.serializer) ='urlencoded';
}
rpc Biz5(BizRequest) returns (BizResponse) {
option (api.put) ='/api/biz5';
}
rpc Biz6(BizRequest) returns (BizResponse) {
option (api.delete) ='/api/biz6';
}
rpc Biz7(BizRequest) returns (BizResponse);
}
`;
const expected = [
{ uri: '/api/biz1' },
{
method: 'POST',
uri: '/api/biz2',
serializer: 'json',
group: 'user',
},
{ method: 'GET', uri: '/api/biz3', serializer: 'form' },
{ method: 'POST', uri: '/api/biz4', serializer: 'urlencoded' },
{ method: 'PUT', uri: '/api/biz5' },
{ method: 'DELETE', uri: '/api/biz6' },
undefined,
];
const document = t.parse(idl);
const Foo = (document.root.nested || {}).Foo as t.ServiceDefinition;
const extensionConfigs = Object.values(Foo.methods).map(
func => func.extensionConfig,
);
return expect(extensionConfigs).to.eql(expected);
});
it('should convert method extenstions using old rules', () => {
const idl = `
syntax = 'proto3';
message BizRequest {}
message BizResponse {}
service Foo {
rpc Biz1(BizRequest) returns (BizResponse) {
option (api_method).get = "/api/biz1";
option (api_method).serializer = "json";
}
rpc Biz2(BizRequest) returns (BizResponse) {
option (pb_idl.api_method).post = "/api/biz2";
option (pb_idl.api_method).serializer = "form";
}
}
`;
const expected = [
{ method: 'GET', uri: '/api/biz1', serializer: 'json' },
{ method: 'POST', uri: '/api/biz2', serializer: 'form' },
];
const document = t.parse(idl);
const Foo = (document.root.nested || {}).Foo as t.ServiceDefinition;
const extensionConfigs = Object.values(Foo.methods).map(
func => func.extensionConfig,
);
return expect(extensionConfigs).to.eql(expected);
});
});
});

View File

@@ -0,0 +1,51 @@
/*
* 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 * as t from '../src/proto';
describe('ferry-parser', () => {
describe('proto service', () => {
it('should convert service extenstions', () => {
const idl = `
syntax = "proto3";
service Foo {
option (api.uri_prefix) = "//example.com";
}
`;
const expected = { uri_prefix: '//example.com' };
const document = t.parse(idl);
const Foo = (document.root.nested || {}).Foo as t.ServiceDefinition;
return expect(Foo.extensionConfig).to.eql(expected);
});
it('should convert service extenstions with package', () => {
const idl = `
syntax = "proto3";
package example;
service Foo {
option (api.uri_prefix) = "//example.com";
}
`;
const expected = { uri_prefix: '//example.com' };
const document = t.parse(idl);
const Foo = ((document.root.nested || {}).example.nested || {})
.Foo as t.ServiceDefinition;
return expect(Foo.extensionConfig).to.eql(expected);
});
});
});

View File

@@ -0,0 +1,55 @@
/*
* 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 * as t from '../src/thrift';
describe('ferry-parser', () => {
describe('thrift enum', () => {
it('should convert enum member comments', () => {
const idl = `
enum Bar {
// c1
ONE = 1, // c2
/* c3 */
TWO = 2, /* c4 */
// c5
/* c6 */
THTEE = 3, // c7
/* c8
c9 */
FOUR = 4
// c10
FIVE = 5; /* c11 */
}
`;
const expected = [
['c1', 'c2'],
[['c3'], ['c4']],
['c5', ['c6'], 'c7'],
[['c8', ' c9']],
['c10', ['c11']],
];
const document = t.parse(idl);
const { members } = document.body[0] as t.EnumDefinition;
const comments = members.map(member =>
member.comments.map(comment => comment.value),
);
return expect(comments).to.eql(expected);
});
});
});

View File

@@ -0,0 +1,226 @@
/*
* 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 * as t from '../src/thrift';
describe('ferry-parser', () => {
describe('thrift field', () => {
it('should convert struct field extenstions', () => {
const idl = `
enum Numbers {
ONE = 1
}
struct Foo {
1: string k1 (api.position = "query")
2: string k2 (api.position = 'body', aapi.position = 'query')
3: string k3 (api.position = 'path', api.positionn = 'query')
4: string k4 (api.position = 'header')
5: string k5 (api.position = 'entire_body')
6: string k6 (api.position = 'raw_body')
7: string k7 (api.position = 'status_code')
10: string k10 (api.key = 'key10')
11: string k11 (api.key = 'k11')
12: binary k12 (api.web_type = 'File')
13: string k13 (api.value_type = 'any')
14: list<string> k14 (api.value_type = 'any')
21: i32 k21 (api.query = 'k21[]')
22: i32 k22 (api.body = 'k22')
23: i32 k23 (api.path = 'k23')
24: i32 k24 (api.header = 'k24')
25: i32 k25 (api.entire_body = 'key25')
26: i32 k26 (api.raw_body = 'key_26')
27: i32 k27 (api.status_code = 'key-27')
31: i32 k31 (api.query = 'key31', api.web_type = 'number', api.position = '')
32: i32 k32 (api.position = 'body', api.key='key32', api.value_type = 'any')
33: i64 k33 (api.body="kk33, omitempty")
34: Numbers k34 (api.position = 'path')
}
`;
const expected = [
{ position: 'query' },
{ position: 'body' },
{ position: 'path' },
{ position: 'header' },
{ position: 'entire_body' },
{ position: 'raw_body' },
{ position: 'status_code' },
{ key: 'key10' },
{},
{ web_type: 'File' },
{ value_type: 'any' },
{ value_type: 'any' },
{ position: 'query' },
{ position: 'body' },
{ position: 'path' },
{ position: 'header' },
{ position: 'entire_body', key: 'key25' },
{ position: 'raw_body', key: 'key_26' },
{ position: 'status_code', key: 'key-27' },
{ position: 'query', key: 'key31', web_type: 'number' },
{ position: 'body', key: 'key32', value_type: 'any' },
{ position: 'body', key: 'kk33', tag: 'omitempty' },
{ position: 'path' },
];
const document = t.parse(idl);
const { fields } = document.body[1] as t.InterfaceWithFields;
const extensionConfigs = fields.map(field => field.extensionConfig);
return expect(extensionConfigs).to.eql(expected);
});
it('should convert union field extenstions', () => {
const idl = `
union Foo {
1: string k1 (api.position = "query")
}
`;
const expected = [{ position: 'query' }];
const document = t.parse(idl, { reviseTailComment: false });
const { fields } = document.body[0] as t.InterfaceWithFields;
const extensionConfigs = fields.map(field => field.extensionConfig);
return expect(extensionConfigs).to.eql(expected);
});
it('should convert struct field extenstions using agw specification', () => {
const idl = `
struct Foo {
1: string k1 (agw.source = 'query')
2: string k2 (agw.source = 'body')
3: string k3 (agw.source = 'path')
4: string k4 (agw.source = 'header')
5: string k5 (agw.source = 'raw_body')
6: string k6 (agw.target = 'header')
7: string k7 (agw.target = 'body')
7: string k7 (agw.target = 'http_code')
10: string k10 (agw.key = 'key10')
}
`;
const expected = [
{ position: 'query' },
{ position: 'body' },
{ position: 'path' },
{ position: 'header' },
{ position: 'raw_body' },
{ position: 'header' },
{ position: 'body' },
{ position: 'status_code' },
{ key: 'key10' },
];
const document = t.parse(idl);
const { fields } = document.body[0] as t.InterfaceWithFields;
const extensionConfigs = fields.map(field => field.extensionConfig);
return expect(extensionConfigs).to.eql(expected);
});
it('should convert struct field extenstions using golang tag', () => {
const idl = `
struct Foo {
1: string k1 (go.tag = "json:\\"key1\\"")
2: string k2 (go.tag = 'json:"key2,omitempty"')
3: string k3 (go.tag = 'jsonn:"key2,omitempty"')
}
`;
const expected = [{ key: 'key1' }, { key: 'key2', tag: 'omitempty' }, {}];
const document = t.parse(idl);
const { fields } = document.body[0] as t.InterfaceWithFields;
const extensionConfigs = fields.map(field => field.extensionConfig);
return expect(extensionConfigs).to.eql(expected);
});
it('should throw an error when using invalid type for a path parameter', () => {
const idl = `
struct Foo {
1: bool k1 (api.position = 'path')
}
`;
try {
t.parse(idl);
} catch (err) {
const { message } = err;
const expected =
"the type of path parameter 'k1' in 'Foo' should be string or integer";
return expect(message).to.equal(expected);
}
return expect(true).to.equal(false);
});
it('should revise field comments', () => {
const idl = `
struct Foo {
// c1
1: string k1 // c2
/* c3 */
2: string k2 /* c4 */
// c5
/* c6 */
3: string k3 // c7
/* c8
c9 */
4: string k4
// c10
5: string k5; /* c11 */
}
`;
const expected = [
['c1', 'c2'],
[['c3'], ['c4']],
['c5', ['c6'], 'c7'],
[['c8', ' c9']],
['c10', ['c11']],
];
const document = t.parse(idl);
const { fields } = document.body[0] as t.InterfaceWithFields;
const comments = fields.map(field =>
field.comments.map(comment => comment.value),
);
return expect(comments).to.eql(expected);
});
it('should revise empty field comments', () => {
const idl = `
/*
*/
struct Foo {
/**/
1: string k1
/* */
2: string k2
/** */
3: string k3
}
`;
const expected = [[['']], [['']], [['']]];
const document = t.parse(idl);
const { fields } = document.body[0] as t.InterfaceWithFields;
const comments = fields.map(field =>
field.comments.map(comment => comment.value),
);
return expect(comments).to.eql(expected);
});
});
});

View File

@@ -0,0 +1,116 @@
/*
* 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 * as t from '../src/thrift';
describe('ferry-parser', () => {
describe('thrift function', () => {
it('should convert function extenstions', () => {
const idl = `
service Foo {
BizResponse Biz1(1: BizRequest req) (api.uri = '/api/biz1')
BizResponse Biz2(1: BizRequest req) (
api.uri = '/api/biz2',
api.serializer = 'json',
api.method = 'POST',
api.group="user"
)
BizResponse Biz3(1: BizRequest req) (api.get = '/api/biz3', api.serializer='form')
BizResponse Biz4(1: BizRequest req) (api.post = '/api/biz4', api.serializer='urlencoded')
BizResponse Biz5(1: BizRequest req) (api.put = '/api/biz5', api.method = 'post')
BizResponse Biz6(1: BizRequest req) (api.delete = '/api/biz6', api.serializer='wow')
BizResponse Biz7(1: BizRequest req)
}
`;
const expected = [
{ uri: '/api/biz1' },
{
uri: '/api/biz2',
serializer: 'json',
method: 'POST',
group: 'user',
},
{ method: 'GET', uri: '/api/biz3', serializer: 'form' },
{ method: 'POST', uri: '/api/biz4', serializer: 'urlencoded' },
{ method: 'PUT', uri: '/api/biz5' },
{ method: 'DELETE', uri: '/api/biz6' },
undefined,
];
const document = t.parse(idl);
const { functions } = document.body[0] as t.ServiceDefinition;
const extensionConfigs = functions.map(func => func.extensionConfig);
return expect(extensionConfigs).to.eql(expected);
});
it('should convert function extenstions using agw specification', () => {
const idl = `
service Foo {
BizResponse Biz1(1: BizRequest req) (agw.uri = '/api/biz1')
BizResponse Biz2(1: BizRequest req) (
agw.uri = '/api/biz2',
agw.method = 'POST',
)
}
`;
const expected = [
{ uri: '/api/biz1' },
{ uri: '/api/biz2', method: 'POST' },
];
const document = t.parse(idl, { reviseTailComment: false });
const { functions } = document.body[0] as t.ServiceDefinition;
const extensionConfigs = functions.map(func => func.extensionConfig);
return expect(extensionConfigs).to.eql(expected);
});
it('should revise function comments', () => {
const idl = `
service Foo {
// c1
BizResponse Biz1(1: BizRequest req) // c2
/* c3 */
BizResponse Biz2(1: BizRequest req) /* c4 */
// c5
/* c6 */
BizResponse Biz3(1: BizRequest req) // c7
/* c8
c9 */
BizResponse Biz4(1: BizRequest req)
// c10
BizResponse Biz5(1: BizRequest req); /* c11 */
}
`;
const expected = [
['c1', 'c2'],
[['c3'], ['c4']],
['c5', ['c6'], 'c7'],
[['c8', ' c9']],
['c10', ['c11']],
];
const document = t.parse(idl);
const { functions } = document.body[0] as t.ServiceDefinition;
const comments = functions.map(func =>
func.comments.map(comment => comment.value),
);
return expect(comments).to.eql(expected);
});
});
});

View File

@@ -0,0 +1,79 @@
/*
* 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 * as path from 'path';
import * as t from '../src/thrift';
describe('ferry-parser', () => {
describe('thrift index', () => {
it('should convert the file content', () => {
const idl = path.resolve(__dirname, 'idl/index.thrift');
const expected = { uri_prefix: 'https://example.com' };
const document = t.parse(idl);
const { extensionConfig } = document.body[0] as t.ServiceDefinition;
return expect(extensionConfig).to.eql(expected);
});
it('should throw an error due to invalid file path', () => {
const idl = path.resolve(__dirname, 'idl/indexx.thrift');
try {
t.parse(idl);
} catch (err) {
const { message } = err;
return expect(message).to.includes('no such file:');
}
return expect(true).to.equal(false);
});
it('should throw an syntax error', () => {
const idl = `
struct Foo {
1: string k1,,
}
`;
const expected = 'FieldType expected but found: CommaToken(source:3:';
try {
t.parse(idl);
} catch (err) {
const { message } = err;
return expect(message).to.include(expected);
}
return expect(true).to.equal(false);
});
it('should throw an syntax error in the file content', () => {
const idl = path.resolve(__dirname, 'idl/error.thrift');
const expected = '__tests__/idl/error.thrift:2:16)';
try {
t.parse(idl);
} catch (err) {
const { message } = err;
return expect(message).includes(expected);
}
return expect(true).equal(false);
});
});
});

View File

@@ -0,0 +1,33 @@
/*
* 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 * as t from '../src/thrift';
describe('ferry-parser', () => {
describe('thrift service', () => {
it('should convert service extenstions', () => {
const idl = `
service Foo {
} (api.uri_prefix = 'https://example.com')
`;
const expected = { uri_prefix: 'https://example.com' };
const document = t.parse(idl);
const { extensionConfig } = document.body[0] as t.ServiceDefinition;
return expect(extensionConfig).to.eql(expected);
});
});
});

View File

@@ -0,0 +1,7 @@
{
"compilerOptions": {
"esModuleInterop": true,
"allowSyntheticDefaultImports": true
},
"include": ["."]
}

View File

@@ -0,0 +1,174 @@
/*
* 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 * as t from '../src';
import { filterKeys } from './common';
describe('unify-parser', () => {
describe('thrift enum', () => {
it('should convert enum', () => {
const content = `
enum Number {
ONE = 1,
TWO,
}
`;
const document = t.parse(
'index.thrift',
{ cache: false },
{ 'index.thrift': content },
);
const { members } = document.statements[0] as t.EnumDefinition;
return expect(members.length).to.eql(2);
});
it('should resolve enum name', () => {
const content = `
enum Number {
ONE = 1,
TWO,
}
`;
const document = t.parse(
'index.thrift',
{ cache: false },
{ 'index.thrift': content },
);
const { name } = document.statements[0] as t.EnumDefinition;
expect(filterKeys(name, ['value', 'namespaceValue'])).to.eql({
value: 'Number',
namespaceValue: 'root.Number',
});
});
it('should not resolve enum name', () => {
const content = `
enum Number {
ONE = 1,
TWO,
}
`;
const document = t.parse(
'index.thrift',
{ namespaceRefer: false, cache: false },
{ 'index.thrift': content },
);
const { name } = document.statements[0] as t.EnumDefinition;
expect(filterKeys(name, ['value', 'namespaceValue'])).to.eql({
value: 'Number',
namespaceValue: undefined,
});
});
it('should revise enum comments', () => {
const content = `
enum Number {
// c1
ONE = 1, // c2
/* c3 */
TWO,/* c4 */
// c5
/* c6 */
FOUR = 4; // c7
/* c8
c9 */
FIVE;
SIX,
}
`;
const expected = [
['c1', 'c2'],
[['c3'], ['c4']],
['c5', ['c6'], 'c7'],
[['c8', ' c9']],
[],
];
const document = t.parse(
'index.thrift',
{ cache: false },
{ 'index.thrift': content },
);
const { members } = document.statements[0] as t.EnumDefinition;
const comments = members.map(field =>
field.comments.map(comment => comment.value),
);
return expect(comments).to.eql(expected);
});
it('should revise enum comments without dot', () => {
const content = `
enum Number {
// c1
ONE = 1 // c2
/* c3 */
TWO/* c4 */
// c5
/* c6 */
FOUR = 4 // c7
/* c8
c9 */
FIVE
SIX
}
`;
const expected = [
['c1', 'c2'],
[['c3'], ['c4']],
['c5', ['c6'], 'c7'],
[['c8', ' c9']],
[],
];
const document = t.parse(
'index.thrift',
{ cache: false },
{ 'index.thrift': content },
);
const { members } = document.statements[0] as t.EnumDefinition;
const comments = members.map(field =>
field.comments.map(comment => comment.value),
);
return expect(comments).to.eql(expected);
});
});
describe('proto enum', () => {
it('should convert enum', () => {
const content = `
enum Number {
ONE = 1;
TWO = 2;
}
`;
const document = t.parse(
'index.proto',
{ cache: false },
{ 'index.proto': content },
);
const { members } = document.statements[0] as t.EnumDefinition;
return expect(members.length).to.eql(2);
});
});
});

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,343 @@
/*
* 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 * as t from '../src';
import { filterKeys } from './common';
describe('unify-parser', () => {
describe('thrift function', () => {
it('should convert function extenstions', () => {
const content = `
service Foo {
BizResponse Biz1(1: BizRequest req) (api.uri = '/api/biz1')
BizResponse Biz2(1: BizRequest req) (
api.uri = '/api/biz2',
api.serializer = 'json',
api.method = 'POST',
api.group="user"
)
BizResponse Biz3(1: BizRequest req) (api.get = '/api/biz3', api.serializer='form')
BizResponse Biz4(1: BizRequest req) (api.post = '/api/biz4', api.serializer='urlencoded')
BizResponse Biz5(1: BizRequest req) (api.put = '/api/biz5', api.method = 'post', api.version='1')
BizResponse Biz6(1: BizRequest req) (api.delete = '/api/biz6', api.serializer='wow', api.custom = '{"priority":1}')
BizResponse Biz7(1: BizRequest req)
}
`;
const expected = [
{ uri: '/api/biz1' },
{
uri: '/api/biz2',
serializer: 'json',
method: 'POST',
group: 'user',
},
{ method: 'GET', uri: '/api/biz3', serializer: 'form' },
{ method: 'POST', uri: '/api/biz4', serializer: 'urlencoded' },
{ method: 'PUT', uri: '/api/biz5', version: '1' },
{ method: 'DELETE', uri: '/api/biz6', custom: '{"priority":1}' },
{},
];
const document = t.parse(
'index.thrift',
{ cache: false },
{ 'index.thrift': content },
);
const { functions } = document.statements[0] as t.ServiceDefinition;
const extensionConfigs = functions.map(func => func.extensionConfig);
return expect(extensionConfigs).to.eql(expected);
});
it('should convert function extenstions using agw specification', () => {
const content = `
service Foo {
BizResponse Biz1(1: BizRequest req) (agw.uri = '/api/biz1')
BizResponse Biz2(1: BizRequest req) (
agw.uri = '/api/biz2',
agw.method = 'POST',
)
}
`;
const expected = [
{ uri: '/api/biz1' },
{ uri: '/api/biz2', method: 'POST' },
];
const document = t.parse(
'index.thrift',
{ cache: false },
{ 'index.thrift': content },
);
const { functions } = document.statements[0] as t.ServiceDefinition;
const extensionConfigs = functions.map(func => func.extensionConfig);
return expect(extensionConfigs).to.eql(expected);
});
it('should revise function comments', () => {
const content = `
service Foo {
// c1
BizResponse Biz1(1: BizRequest req) // c2
/* c3 */
BizResponse Biz2(1: BizRequest req) /* c4 */
// c5
/* c6 */
BizResponse Biz3(1: BizRequest req) // c7
/* c8
c9 */
BizResponse Biz4(1: BizRequest req)
BizResponse Biz5(1: BizRequest req)
}
`;
const expected = [
['c1', 'c2'],
[['c3'], ['c4']],
['c5', ['c6'], 'c7'],
[['c8', ' c9']],
[],
];
const document = t.parse(
'index.thrift',
{ cache: false },
{ 'index.thrift': content },
);
const { functions } = document.statements[0] as t.ServiceDefinition;
const comments = functions.map(func =>
func.comments.map(comment => comment.value),
);
return expect(comments).to.eql(expected);
});
it('should resolve function name', () => {
const content = `
service Foo {
BizResponse Biz1(1: BizRequest req) // c2
}
`;
const document = t.parse(
'index.thrift',
{ cache: false },
{ 'index.thrift': content },
);
const { functions } = document.statements[0] as t.ServiceDefinition;
const { name } = functions[0];
return expect(filterKeys(name, ['value', 'namespaceValue'])).to.eql({
value: 'Biz1',
namespaceValue: 'root.Biz1',
});
});
it('should resolve func type', () => {
const baseContent = `
namespace go test_base
struct Response {}
`;
const funcContent = `
include "./base.thrift"
service Foo {
base.Response Biz1(1: BizRequest req) // c2
}
`;
const document = t.parse(
'index.thrift',
{ cache: false },
{
'base.thrift': baseContent,
'index.thrift': funcContent,
},
);
const { functions } = document.statements[0] as t.ServiceDefinition;
const identifier = functions[0].returnType as t.Identifier;
return expect(filterKeys(identifier, ['value', 'namespaceValue'])).to.eql(
{
value: 'base.Response',
namespaceValue: 'test_base.Response',
},
);
});
});
describe('proto method', () => {
it('should convert method extenstions', () => {
const content = `
syntax = 'proto3';
message BizRequest {}
message BizResponse {}
service Foo {
rpc Biz1(BizRequest) returns (BizResponse) {
option (api.uri) = '/api/biz1';
}
rpc Biz2(BizRequest) returns (BizResponse) {
option (api.method) = "POST";
option (api.uri) = "/api/biz2";
option (api.serializer) = "json";
option (api.group) = 'user';
}
rpc Biz3(BizRequest) returns (BizResponse) {
option (api.get) ='/api/biz3';
option (api.serializer) ='form';
}
rpc Biz4(BizRequest) returns (BizResponse) {
option (api.post) ='/api/biz4';
option (api.serializer) ='urlencoded';
}
rpc Biz5(BizRequest) returns (BizResponse) {
option (api.put) ='/api/biz5';
}
rpc Biz6(BizRequest) returns (BizResponse) {
option (api.delete) ='/api/biz6';
}
rpc Biz7(BizRequest) returns (BizResponse);
}
`;
const expected = [
{ uri: '/api/biz1' },
{
method: 'POST',
uri: '/api/biz2',
serializer: 'json',
group: 'user',
},
{ method: 'GET', uri: '/api/biz3', serializer: 'form' },
{ method: 'POST', uri: '/api/biz4', serializer: 'urlencoded' },
{ method: 'PUT', uri: '/api/biz5' },
{ method: 'DELETE', uri: '/api/biz6' },
{},
];
const document = t.parse(
'index.proto',
{ cache: false },
{ 'index.proto': content },
);
const { functions } = document.statements[2] as t.ServiceDefinition;
const extensionConfigs = functions.map(func => func.extensionConfig);
return expect(extensionConfigs).to.eql(expected);
});
it('should resolve method name', () => {
const content = `
syntax = 'proto3';
message BizRequest {}
message BizResponse {}
service Foo {
rpc Biz1(BizRequest) returns (BizResponse) {
option (api.uri) = '/api/biz1';
}
}
`;
const document = t.parse(
'index.proto',
{ cache: false },
{ 'index.proto': content },
);
const { functions } = document.statements[2] as t.ServiceDefinition;
const { name } = functions[0];
return expect(filterKeys(name, ['value', 'namespaceValue'])).to.eql({
value: 'Biz1',
namespaceValue: 'root.Foo.Biz1',
});
});
it('should resolve response type', () => {
const baseContent = `
syntax = 'proto3';
package test_base;
message Response {}
`;
const funcContent = `
import "base.proto";
syntax = 'proto3';
message BizRequest {}
service Foo {
rpc Biz1(BizRequest) returns (test_base.Response) {
option (api.uri) = '/api/biz1';
}
}
`;
const document = t.parse(
'index.proto',
{ cache: false },
{
'base.proto': baseContent,
'index.proto': funcContent,
},
);
const { functions } = document.statements[1] as t.ServiceDefinition;
const identifier = functions[0].returnType as t.Identifier;
return expect(filterKeys(identifier, ['value', 'namespaceValue'])).to.eql(
{
value: 'base.Response',
namespaceValue: 'test_base.Response',
},
);
});
it('should resolve response type within the same namespace', () => {
const baseContent = `
syntax = 'proto3';
package same;
message Response {}
message Request {}
`;
const funcContent = `
import "base.proto";
syntax = 'proto3';
package same;
service Foo {
rpc Biz1(Request) returns (same.Response) {
option (api.uri) = '/api/biz1';
}
}
`;
const document = t.parse(
'index.proto',
{ cache: false },
{
'base.proto': baseContent,
'index.proto': funcContent,
},
);
const { functions } = document.statements[0] as t.ServiceDefinition;
const returnType = functions[0].returnType as t.Identifier;
const requestType = functions[0].fields[0].fieldType as t.Identifier;
expect(filterKeys(requestType, ['value', 'namespaceValue'])).to.eql({
value: 'base.Request',
namespaceValue: 'same.Request',
});
expect(filterKeys(returnType, ['value', 'namespaceValue'])).to.eql({
value: 'base.Response',
namespaceValue: 'same.Response',
});
});
});
});

View File

@@ -0,0 +1,750 @@
/*
* 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 * as path from 'path';
import * as t from '../src/unify';
describe('unify-parser', () => {
describe('thrift index', () => {
it('should parse a simple file', () => {
const idl = path.resolve(__dirname, 'idl/index.thrift');
const expected = { uri_prefix: 'https://example.com' };
const document = t.parse(idl, { cache: false });
const { extensionConfig } = document.statements[0] as t.ServiceDefinition;
return expect(extensionConfig).to.eql(expected);
});
it('should parse a complicate file', () => {
const expected = {
namespace: 'unify_idx',
unifyNamespace: 'unify_idx',
include: ['./unify_dependent1.thrift', 'unify_dependent2.thrift'],
};
const document = t.parse('unify_index.thrift', {
root: path.resolve(__dirname, './idl'),
namespaceRefer: false,
cache: false,
});
const target = {
namespace: document.namespace,
unifyNamespace: document.unifyNamespace,
include: document.includes,
};
return expect(target).to.eql(expected);
});
it('should parse a complicate file with relative path', () => {
const expected = {
include: ['base.thrift', 'basee.thrift'],
};
const document = t.parse('dep/common.thrift', {
root: path.resolve(__dirname, './idl'),
namespaceRefer: false,
cache: false,
});
const target = {
include: document.includes,
};
return expect(target).to.eql(expected);
});
it('should parse files from fileContentMap', () => {
const indexContent = `
include './unify_dependent.thrift'
typedef unify_dependent.Foo TFoo
union FuncRequest {
1: unify_dependent.Foo r_key1
2: TFoo r_key2
}
service Example {
unify_dependent.FuncResponse Func(1: FuncRequest req)
} (
)
`;
const dependentContent = `
typedef Foo Foo1
struct Foo {
1: string f_key1
}
struct FuncResponse {
}
`;
const fileContentMap = {
'unify_index.thrift': indexContent,
'unify_dependent.thrift': dependentContent,
};
const expected = {
namespace: '',
unifyNamespace: 'root',
include: ['./unify_dependent.thrift'],
};
const document = t.parse(
'unify_index.thrift',
{ cache: false },
fileContentMap,
);
const target = {
namespace: document.namespace,
unifyNamespace: document.unifyNamespace,
include: document.includes,
};
return expect(target).to.eql(expected);
});
it('should parse files from fileContentMap with relative path', () => {
const indexContent = `
include 'unify_dependent.thrift'
typedef unify_dependent.Foo TFoo
union FuncRequest {
1: unify_dependent.Foo r_key1
2: TFoo r_key2
}
service Example {
unify_dependent.FuncResponse Func(1: FuncRequest req)
} (
)
`;
const dependentContent = `
typedef Foo Foo1
struct Foo {
1: string f_key1
}
struct FuncResponse {
}
`;
const fileContentMap = {
'relative/unify_index.thrift': indexContent,
'relative/unify_dependent.thrift': dependentContent,
};
const expected = {
namespace: '',
unifyNamespace: 'root',
include: ['unify_dependent.thrift'],
};
const document = t.parse(
'relative/unify_index.thrift',
{ cache: false },
fileContentMap,
);
const target = {
namespace: document.namespace,
unifyNamespace: document.unifyNamespace,
include: document.includes,
};
return expect(target).to.eql(expected);
});
it('should parse files from fileContentMap and Java namespace', () => {
const indexContent = `
namespace java com.ferry.index
union FuncRequest {
}
struct FuncResponse {}
service Example {
FuncResponse Func(1: FuncRequest req)
} (
)
`;
const fileContentMap = {
'unify_index.thrift': indexContent,
};
const expected = {
namespace: 'index',
unifyNamespace: 'index',
include: [],
};
const document = t.parse(
'unify_index.thrift',
{ cache: false },
fileContentMap,
);
const target = {
namespace: document.namespace,
unifyNamespace: document.unifyNamespace,
include: document.includes,
};
return expect(target).to.eql(expected);
});
it('should parse files with cache', () => {
const indexContent = `
struct Foo {}
`;
const indexxContent = `
`;
const rootContent = `
include "unify_index_cache.thrift"
`;
t.parse(
'unify_index_cache.thrift',
{
cache: true,
},
{
'unify_index_cache.thrift': indexContent,
},
);
t.parse(
'unify_root.thrift',
{ cache: false },
{
'unify_root.thrift': rootContent,
},
);
const document = t.parse(
'unify_index_cache.thrift',
{ cache: true },
{
'unify_index_cache.thrift': indexxContent,
},
);
return expect(document.statements[0].name.value).to.eql('Foo');
});
it('should parse files with ignoreTag', () => {
const indexContent = `
struct Foo {
1: string k1 (go.tag = "json:\\"key1\\"")
}
`;
const document = t.parse(
'unify_index.thrift',
{
ignoreGoTag: true,
},
{
'unify_index.thrift': indexContent,
},
);
const { fields } = document.statements[0] as t.InterfaceWithFields;
return expect(fields[0].extensionConfig).to.eql({});
});
it('should parse files with goTagDash', () => {
const indexContent = `
struct Foo {
1: string k1 (go.tag = "json:\\"-\\"")
}
`;
const document = t.parse(
'unify_index.thrift',
{},
{
'unify_index.thrift': indexContent,
},
);
const { fields } = document.statements[0] as t.InterfaceWithFields;
return expect(fields.length).to.eql(0);
});
it('should parse files with ignoreTagDash', () => {
const indexContent = `
struct Foo {
1: string k1 (go.tag = "json:\\"-\\"")
}
`;
const document = t.parse(
'unify_index.thrift',
{
ignoreGoTagDash: true,
},
{
'unify_index.thrift': indexContent,
},
);
const { fields } = document.statements[0] as t.InterfaceWithFields;
return expect(fields.length).to.eql(1);
});
it('should search files from searchPaths', () => {
const idl = path.resolve(__dirname, 'idl/unify_search.thrift');
const depDir = path.resolve(__dirname, 'idl/dep');
const document = t.parse(idl, {
cache: false,
searchPaths: [depDir],
});
const struct = document.statements[0] as t.InterfaceWithFields;
return expect(struct.type).to.eql(t.SyntaxType.StructDefinition);
});
it('should search files from relative searchPaths', () => {
const document = t.parse('unify_search.thrift', {
root: path.resolve(__dirname, 'idl'),
cache: false,
searchPaths: ['./dep'],
});
const struct = document.statements[0] as t.InterfaceWithFields;
return expect(struct.type).to.eql(t.SyntaxType.StructDefinition);
});
it('should parse files with a syntax error', () => {
const indexContent = `
struct
`;
try {
t.parse(
'error.thrift',
{ cache: false },
{
'error.thrift': indexContent,
},
);
} catch (err) {
return expect(err.message).to.eql(
'Struct-like must have an identifier',
);
}
return expect(0).to.eql(1);
});
it('should parse files with a dependent file error within fileContentMap', () => {
const indexContent = `
include "./dependent.thrift"
`;
try {
t.parse(
'error.thrift',
{ cache: false },
{
'error.thrift': indexContent,
},
);
} catch (err) {
return expect(err.message).to.eql(
'file dependent.thrift does not exist in fileContentMap',
);
}
return expect(0).to.eql(1);
});
it('should parse files with a entry file error within fileContentMap', () => {
try {
t.parse('specify_error.thrift', { cache: false }, {});
} catch (err) {
return expect(err.message).to.equal(
'file "specify_error.thrift" does not exist in fileContentMap',
);
}
return expect(0).to.eql(1);
});
it('should parse files with a entry file error', () => {
const idl = path.resolve(__dirname, 'idl/special_error.thrift');
try {
t.parse(idl, { cache: false });
} catch (err) {
return expect(err.message).to.contain('no such file:');
}
return expect(0).to.eql(1);
});
it('should parse files with a dependent file error', () => {
const idl = path.resolve(__dirname, 'idl/unify_error.thrift');
try {
t.parse(idl, { cache: false });
} catch (err) {
return expect(err.message).to.contain('does not exist');
}
return expect(0).to.eql(1);
});
it('should parse files with a namespace error', () => {
const indexContent = `
namespace java1 com.index
`;
try {
t.parse(
'error.thrift',
{ cache: false },
{
'error.thrift': indexContent,
},
);
} catch (err) {
return expect(err.message).to.eql('a js namespace should be specifed');
}
return expect(0).to.eql(1);
});
});
describe('proto index', () => {
it('should a simple file', () => {
const idl = path.resolve(__dirname, 'idl/index.proto');
const expected = { uri_prefix: '//example.com' };
const document = t.parse(idl, { cache: false });
const { extensionConfig } = document.statements[0] as t.ServiceDefinition;
return expect(extensionConfig).to.eql(expected);
});
it('should parse a complicate file', () => {
const expected = {
namespace: 'unify_idx',
unifyNamespace: 'unify_idx',
include: ['./unify_dependent1.proto', 'unify_dependent2.proto'],
};
const document = t.parse('unify_index.proto', {
root: path.resolve(__dirname, './idl'),
namespaceRefer: false,
cache: false,
});
const target = {
namespace: document.namespace,
unifyNamespace: document.unifyNamespace,
include: document.includes,
};
return expect(target).to.eql(expected);
});
it('should parse a complicate file with relative path', () => {
const expected = {
include: ['base.proto', 'basee.proto'],
};
const document = t.parse('dep/common.proto', {
root: path.resolve(__dirname, './idl'),
namespaceRefer: false,
cache: false,
});
const target = {
include: document.includes,
};
return expect(target).to.eql(expected);
});
it('should parse files from fileContentMap', () => {
const indexContent = `
syntax = "proto3";
import "./unify_dependent.proto";
message Request {
repeated string key1 = 1[(api.key) = 'f'];
unify_dep3.Foo key2 = 2;
}
service Example {
option (api.uri_prefix) = "//example.com";
rpc Biz1(Request) returns (unify_dep3.Response) {
option (api.uri) = '/api/biz1';
}
}
`;
const dependentContent = `
syntax = "proto3";
package unify_dep3;
message Foo {
string f_key1 = 1;
}
message Response {}
`;
const fileContentMap = {
'unify_index.proto': indexContent,
'unify_dependent.proto': dependentContent,
};
const expected = {
namespace: '',
unifyNamespace: 'root',
include: ['./unify_dependent.proto'],
};
const document = t.parse(
'unify_index.proto',
{ cache: false },
fileContentMap,
);
const target = {
namespace: document.namespace,
unifyNamespace: document.unifyNamespace,
include: document.includes,
};
return expect(target).to.eql(expected);
});
it('should parse files from fileContentMap with relative path', () => {
const indexContent = `
syntax = "proto3";
import "unify_dependent.proto";
message Request {
repeated string key1 = 1[(api.key) = 'f'];
unify_dep3.Foo key2 = 2;
}
service Example {
option (api.uri_prefix) = "//example.com";
rpc Biz1(Request) returns (unify_dep3.Response) {
option (api.uri) = '/api/biz1';
}
}
`;
const dependentContent = `
syntax = "proto3";
package unify_dep3;
message Foo {
string f_key1 = 1;
}
message Response {}
`;
const fileContentMap = {
'relative/unify_index.proto': indexContent,
'relative/unify_dependent.proto': dependentContent,
};
const expected = {
namespace: '',
unifyNamespace: 'root',
include: ['unify_dependent.proto'],
};
const document = t.parse(
'relative/unify_index.proto',
{ cache: false },
fileContentMap,
);
const target = {
namespace: document.namespace,
unifyNamespace: document.unifyNamespace,
include: document.includes,
};
return expect(target).to.eql(expected);
});
it('should parse files with cache', () => {
const indexContent = `
syntax = "proto3";
message Foo {}
`;
const indexxContent = `
syntax = "proto3";
`;
const rootContent = `
import "unify_index_cache.proto";
syntax = "proto3";
`;
t.parse(
'unify_index_cache.proto',
{
cache: true,
},
{
'unify_index_cache.proto': indexContent,
},
);
t.parse(
'unify_root.proto',
{ cache: false },
{
'unify_root.proto': rootContent,
},
);
const document = t.parse(
'unify_index_cache.proto',
{ cache: true },
{
'unify_index_cache.proto': indexxContent,
},
);
return expect(document.statements[0].name.value).to.eql('Foo');
});
it('should parse files ignoring import', () => {
const indexContent = `
syntax = "proto3";
import "google/protobuf/api.proto";
`;
t.parse(
'ignore.proto',
{ cache: false },
{
'ignore.proto': indexContent,
},
);
});
it('should search files from searchPaths', () => {
const idl = path.resolve(__dirname, 'idl/unify_search.proto');
const depDir = path.resolve(__dirname, 'idl/dep');
const document = t.parse(idl, {
cache: false,
searchPaths: [depDir],
});
const struct = document.statements[0] as t.InterfaceWithFields;
return expect(struct.type).to.eql(t.SyntaxType.StructDefinition);
});
it('should search files from relative searchPaths', () => {
const document = t.parse('unify_search.proto', {
root: path.resolve(__dirname, 'idl'),
cache: false,
searchPaths: ['./dep'],
});
const struct = document.statements[0] as t.InterfaceWithFields;
return expect(struct.type).to.eql(t.SyntaxType.StructDefinition);
});
it('should parse files with a syntax error', () => {
const indexContent = `
syntax = "proto3";
message Foo {
`;
try {
t.parse(
'error.proto',
{ cache: false },
{
'error.proto': indexContent,
},
);
} catch (err) {
return expect(err.message).to.eql("illegal token 'null', '=' expected");
}
return expect(0).to.eql(1);
});
it('should parse files with a entry file error within fileContentMap', () => {
try {
t.parse('specify_error.proto', { cache: false }, {});
} catch (err) {
return expect(err.message).to.equal(
'file "specify_error.proto" does not exist in fileContentMap',
);
}
return expect(0).to.eql(1);
});
it('should parse files with a file error', () => {
const indexContent = `
syntax = "proto3";
import "./dependent.proto";
`;
try {
t.parse(
'error.proto',
{ cache: false },
{
'error.proto': indexContent,
},
);
} catch (err) {
return expect(err.message).to.eql(
'file dependent.proto does not exist in fileContentMap',
);
}
return expect(0).to.eql(1);
});
it('should parse files with a real file error', () => {
try {
t.parse('real_error.proto', { cache: false });
} catch (err) {
return expect(err.message).to.contain('no such file');
}
return expect(0).to.eql(1);
});
});
describe('all index', () => {
it('should parse files with a file error', () => {
try {
t.parse('error', { cache: false });
} catch (err) {
return expect(err.message).to.eql('invalid filePath: "error"');
}
return expect(0).to.eql(1);
});
});
});

View File

@@ -0,0 +1,140 @@
/*
* 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 * as t from '../src';
import { filterKeys } from './common';
describe('unify-parser', () => {
describe('thrift const', () => {
it('should parse string const', () => {
const content = `
const string a = '1';
`;
const document = t.parse(
'index.thrift',
{ cache: false },
{ 'index.thrift': content },
);
const { fieldType, initializer } = document
.statements[0] as t.ConstDefinition;
expect((fieldType as t.BaseType).type).to.eql(t.SyntaxType.StringKeyword);
expect((initializer as t.StringLiteral).value).to.equal('1');
});
it('should parse list const', () => {
const content = `
const list<i32> b = [1]
`;
const document = t.parse(
'index.thrift',
{ cache: false },
{ 'index.thrift': content },
);
const { fieldType, initializer } = document
.statements[0] as t.ConstDefinition;
expect(((fieldType as t.ListType).valueType as t.BaseType).type).to.eql(
t.SyntaxType.I32Keyword,
);
expect(
((initializer as t.ConstList).elements[0] as t.IntConstant).value.value,
).to.equal('1');
});
it('should parse map const', () => {
const content = `
const map<string, i32> c = {'m': 1}
`;
const document = t.parse(
'index.thrift',
{ cache: false },
{ 'index.thrift': content },
);
const { fieldType, initializer } = document
.statements[0] as t.ConstDefinition;
expect(((fieldType as t.MapType).valueType as t.BaseType).type).to.eql(
t.SyntaxType.I32Keyword,
);
expect(
((initializer as t.ConstMap).properties[0].initializer as t.IntConstant)
.value.value,
).to.equal('1');
});
it('should not resolve const name', () => {
const content = `
const string a = '1';
`;
const document = t.parse(
'index.thrift',
{ cache: false, namespaceRefer: false },
{
'index.thrift': content,
},
);
const { name } = document.statements[0] as t.ConstDefinition;
return expect(filterKeys(name, ['value', 'namespaceValue'])).to.eql({
value: 'a',
namespaceValue: undefined,
});
});
});
describe('thrift typedef', () => {
it('should resolve typedef', () => {
const baseContent = `
namespace go unify_base
`;
const indexContent = `
include 'base.thrift'
typedef base.Foo MyFoo
typedef Bar MyBar
`;
const document = t.parse(
'index.thrift',
{ cache: false },
{
'index.thrift': indexContent,
'base.thrift': baseContent,
},
);
const { name: name0, definitionType: definitionType0 } = document
.statements[0] as t.TypedefDefinition;
const { definitionType: definitionType1 } = document
.statements[1] as t.TypedefDefinition;
expect(filterKeys(name0, ['value', 'namespaceValue'])).to.eql({
value: 'MyFoo',
namespaceValue: 'root.MyFoo',
});
expect(filterKeys(definitionType0, ['value', 'namespaceValue'])).to.eql({
value: 'base.Foo',
namespaceValue: 'unify_base.Foo',
});
expect(filterKeys(definitionType1, ['value', 'namespaceValue'])).to.eql({
value: 'Bar',
namespaceValue: 'root.Bar',
});
});
});
});

View File

@@ -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 * as t from '../src';
import { filterKeys } from './common';
describe('unify-parser', () => {
describe('thrift service', () => {
it('should convert service extenstions', () => {
const fileContent = `
service Foo {
} (api.uri_prefix = 'https://example.com')
`;
const document = t.parse(
'index.thrift',
{ cache: false },
{ 'index.thrift': fileContent },
);
const { extensionConfig, name } = document
.statements[0] as t.ServiceDefinition;
expect(extensionConfig).to.eql({ uri_prefix: 'https://example.com' });
expect(filterKeys(name, ['value', 'namespaceValue'])).to.eql({
value: 'Foo',
namespaceValue: 'root.Foo',
});
});
});
describe('proto service', () => {
it('should convert service extenstions', () => {
const fileContent = `
syntax = "proto3";
service Foo {
option (api.uri_prefix) = "//example.com";
}
`;
const document = t.parse(
'index.proto',
{ cache: false },
{ 'index.proto': fileContent },
);
const { extensionConfig, name } = document
.statements[0] as t.ServiceDefinition;
expect(extensionConfig).to.eql({ uri_prefix: '//example.com' });
expect(filterKeys(name, ['value', 'namespaceValue'])).to.eql({
value: 'Foo',
namespaceValue: 'root.Foo',
});
});
});
});

View File

@@ -0,0 +1,12 @@
{
"operationSettings": [
{
"operationName": "test:cov",
"outputFolderNames": ["coverage"]
},
{
"operationName": "ts-check",
"outputFolderNames": ["dist"]
}
]
}

View File

@@ -0,0 +1,15 @@
const { defineConfig } = require('@coze-arch/eslint-config');
module.exports = defineConfig({
packageRoot: __dirname,
preset: 'node',
rules: {
'@typescript-eslint/naming-convention': 'off',
'unicorn/filename-case': 'off',
'@coze-arch/no-batch-import-or-export': 'off',
'max-statements-per-line': 'off',
'max-lines': 'off',
'@typescript-eslint/no-explicit-any': 'off',
'@coze-arch/max-line-per-function': 'off',
},
});

View File

@@ -0,0 +1,30 @@
{
"name": "@coze-arch/idl-parser",
"version": "0.0.1",
"description": "idl parser",
"license": "Apache-2.0",
"author": "fanwenjie.fe@bytedance.com",
"maintainers": [],
"main": "src/index.ts",
"scripts": {
"build": "exit 0",
"lint": "eslint ./ --cache",
"test": "vitest --run --passWithNoTests",
"test:cov": "npm run test -- --coverage"
},
"dependencies": {
"@coze-arch/rush-logger": "workspace:*",
"@lancewuz/thrift-parser": "0.0.13",
"proto-parser": "0.0.9"
},
"devDependencies": {
"@coze-arch/eslint-config": "workspace:*",
"@coze-arch/ts-config": "workspace:*",
"@coze-arch/vitest-config": "workspace:*",
"@types/node": "^18",
"@vitest/coverage-v8": "~3.0.5",
"sucrase": "^3.32.0",
"vitest": "~3.0.5"
}
}

View File

@@ -0,0 +1,124 @@
/*
* 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 const POSITIONS = [
'query',
'body',
'path',
'header',
'entire_body',
'raw_body',
'status_code',
];
export const SERIALIZERS = ['json', 'form', 'urlencoded'];
export const UPPERCASE_METHODS = [
'GET',
'POST',
'PUT',
'DELETE',
'PATCH',
'HEAD',
];
export const LOWERCASE_METHODS = [
'get',
'post',
'put',
'delete',
'patch',
'head',
];
export const SERVICE_EXTENSTION_CONFIG_KEYS = ['uri_prefix'];
export const FUNCTION_EXTENSTION_CONFIG_KEYS = [
'serializer',
'uri',
'method',
'group',
'custom',
'version',
...LOWERCASE_METHODS,
];
export const FIELD_EXTENSTION_CONFIG_KEYS = [
'position',
'key',
'web_type',
'value_type',
'tag',
...POSITIONS,
];
export type Position =
| 'query'
| 'body'
| 'path'
| 'header'
| 'entire_body'
| 'raw_body'
| 'status_code';
export type Serializer = 'json' | 'form' | 'urlencoded';
export type Method = 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH' | 'HEAD';
export interface ServiceExtensionConfig {
uri_prefix?: string;
}
export interface FunctionExtensionConfig {
serializer?: Serializer;
uri?: string;
method?: Method;
group?: string;
custom?: string;
// NOTE: used in bytedance
version?: string;
get?: string;
post?: string;
put?: string;
delete?: string;
}
export interface FieldExtensionConfig {
position?: Position;
key?: string;
web_type?: string;
value_type?: string;
query?: string;
body?: string;
path?: string;
header?: string;
entire_body?: string;
raw_body?: string;
status_code?: string;
tag?: string;
}
export interface ExtensionConfig
extends ServiceExtensionConfig,
FunctionExtensionConfig,
FieldExtensionConfig {}
export type ExtensionConfigStringKey = Exclude<
keyof ExtensionConfig,
'serializer' | 'method' | 'position'
>;

View File

@@ -0,0 +1,203 @@
/*
* 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 {
POSITIONS,
SERIALIZERS,
UPPERCASE_METHODS,
LOWERCASE_METHODS,
SERVICE_EXTENSTION_CONFIG_KEYS,
FUNCTION_EXTENSTION_CONFIG_KEYS,
FIELD_EXTENSTION_CONFIG_KEYS,
type ExtensionConfig,
type ExtensionConfigStringKey,
type FieldExtensionConfig,
type FunctionExtensionConfig,
type ServiceExtensionConfig,
type Serializer,
type Method,
type Position,
} from './extension_type';
const goJsonTagRegExp = /^\s*json:(\\?[',"])(.*?)\1/;
function getTag(newtag = '', inputStr = '') {
const tags: string[] = [];
if (inputStr.includes('omitempty')) {
tags.push('omitempty');
}
if (inputStr.includes('required')) {
tags.push('required');
}
if (inputStr === 'int2str') {
tags.push('int2str');
}
if (tags.length === 0) {
return newtag;
}
return newtag + (newtag.length > 0 ? ',' : '') + tags.join(',');
}
// NOTE: agw is a similar specification in Bytedance,
// so we should cover the agw specification here.
// some old rules should also be covered here.
// eslint-disable-next-line complexity
export function extractExtensionConfig(
key: string,
value: string,
ignoreTag = false,
) {
const config: ExtensionConfig = {};
// an old rule: go.tag = "json:\"id,omitempty\""
if (!ignoreTag && /^go\.tag/.test(key)) {
const matches = value.match(goJsonTagRegExp);
if (matches) {
/* istanbul ignore next */
const tagValues = (matches[2] || '').split(',');
const tagKey = tagValues[0];
/* istanbul ignore else */
if (tagKey) {
if (tagKey === '-') {
config.tag = 'ignore';
} else if (/^[a-zA-Z0-9_-]+$/.test(tagKey)) {
config.key = tagKey;
}
}
const extraInfos = tagValues.slice(1).map(item => item.trim());
if (extraInfos.includes('string')) {
config.value_type = 'string';
}
const newTag = getTag(config.tag, matches[2]);
if (newTag) {
config.tag = newTag;
}
}
return config;
}
// the agw rules: agw.source = 'header' or agw.target = 'http_code'
if (key === 'source' || key === 'target') {
/* istanbul ignore else */
if (value === 'http_code') {
config.position = 'status_code';
} else if (POSITIONS.includes(value)) {
config.position = value as Position;
}
} else if (key === 'method') {
// the agw rule: agw.method = 'POST|GET'
const method = value.split('|')[0];
if (UPPERCASE_METHODS.includes(method)) {
config.method = method as Method;
}
} else if (key === 'position') {
if (POSITIONS.includes(value)) {
config.position = value as Position;
}
} else if (key === 'serializer') {
if (SERIALIZERS.includes(value)) {
config.serializer = value as Serializer;
}
} else if (LOWERCASE_METHODS.includes(key)) {
config.method = key.toUpperCase() as Method;
config.uri = value;
} else if (POSITIONS.includes(key)) {
config.position = key as Position;
// cover an old rule: (api.body = "tags, omitempty")
const parts = value.split(',');
config.key = parts[0].trim().replace(/\[\]$/, '');
const newTag = getTag(config.tag, value);
if (newTag) {
config.tag = newTag;
}
} else if (
[
'uri_prefix',
'uri',
'group',
'custom',
'version',
'key',
'web_type',
'value_type',
'tag',
].includes(key)
) {
config[key as 'uri_prefix'] = value;
} else if (key === 'req.headers') {
// NOTE: Compliance with old specifications
if (value.includes('x-www-form-urlencoded')) {
config.serializer = 'urlencoded';
} else if (value.includes('form-data')) {
config.serializer = 'form';
} else if (value.includes('json')) {
config.serializer = 'json';
}
} else if (key === 'js_conv' && ['string', 'str', 'true'].includes(value)) {
// NOTE: used in bytedance
const newTag = getTag(config.tag, 'int2str');
if (newTag) {
config.tag = newTag;
}
}
return config;
}
export function filterConfig(
config: ExtensionConfig,
keys: string[],
): ExtensionConfig {
const filteredConfig: ExtensionConfig = {};
Object.keys(config).forEach(key => {
if (keys.includes(key)) {
filteredConfig[key as ExtensionConfigStringKey] =
config[key as ExtensionConfigStringKey];
}
});
return filteredConfig;
}
export function filterFieldExtensionConfig(config: ExtensionConfig) {
return filterConfig(
config,
FIELD_EXTENSTION_CONFIG_KEYS,
) as FieldExtensionConfig;
}
export function filterFunctionExtensionConfig(config: ExtensionConfig) {
return filterConfig(
config,
FUNCTION_EXTENSTION_CONFIG_KEYS,
) as FunctionExtensionConfig;
}
export function filterServiceExtensionConfig(config: ExtensionConfig) {
return filterConfig(
config,
SERVICE_EXTENSTION_CONFIG_KEYS,
) as ServiceExtensionConfig;
}

View File

@@ -0,0 +1,17 @@
/*
* 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 './unify';

View File

@@ -0,0 +1,199 @@
/*
* 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.
*/
/* eslint no-param-reassign: ["error", { "props": false }] */
import * as path from 'path';
import * as fs from 'fs';
import { parse as protoParse } from 'proto-parser';
import { logAndThrowError } from '../utils';
import {
type ProtoDocument,
type ProtoError,
SyntaxType,
type MessageDefinition,
type ServiceDefinition,
type MethodDefinition,
type NamespaceBase,
type NamespaceDefinition,
type FieldExtensionConfig,
type ServiceExtensionConfig,
type FunctionExtensionConfig,
} from './type';
import * as extensionUtil from '../common/extension_util';
export * from './type';
// NOTE: cover old Rules: (api_method) = 'POST'
const oldRuleRegExp =
/\(api_req\)\.|\(api_resp\)\.|\(api_method\)\.|\(pb_idl\.api_method\)\./;
function extractExtensionConfigFromOption(
optionKey: string,
optionValue: string,
) {
let key = '';
if (/^\(api\.(.*)\)/.test(optionKey)) {
key = optionKey.replace(/^\(api\.(.*)\)/, '$1');
} else if (oldRuleRegExp.test(optionKey)) {
key = optionKey.replace(oldRuleRegExp, '');
} else {
return false;
}
const config = extensionUtil.extractExtensionConfig(key, optionValue);
return config;
}
function convertFieldOptions(message: MessageDefinition) {
const { name, fields } = message;
for (const field of Object.values(fields)) {
if (field.options && Object.keys(field.options).length > 0) {
const extensionConfig: FieldExtensionConfig = {};
const fieldName = field.name;
const fieldType = field.type;
for (const key of Object.keys(field.options)) {
const value = field.options[key];
const config = extractExtensionConfigFromOption(key, value);
/* istanbul ignore else */
if (config) {
if ('key' in config) {
// a key which is the same with the field name will make no sense
if (config.key === fieldName) {
delete config.key;
}
} else if (config.position === 'path') {
if (
['double', 'float', 'bool', 'bytes'].includes(fieldType.value)
) {
const errorMessage = `the type of path parameter '${fieldName}' in '${name}' should be string or integer`;
logAndThrowError(errorMessage);
}
}
Object.assign(extensionConfig, config);
}
}
field.extensionConfig =
extensionUtil.filterFieldExtensionConfig(extensionConfig);
}
}
}
function convertMethodOptions(method: MethodDefinition) {
if (!(method.options && Object.keys(method.options).length > 0)) {
return;
}
const extensionConfig: FunctionExtensionConfig = {};
for (const key of Object.keys(method.options)) {
const value = method.options[key];
const config = extractExtensionConfigFromOption(key, value);
/* istanbul ignore else */
if (config) {
Object.assign(extensionConfig, config);
}
}
method.extensionConfig =
extensionUtil.filterFunctionExtensionConfig(extensionConfig);
}
// NOTE: omit message definitions nested in a service definition
function convertServiceOptions(service: ServiceDefinition) {
if (service.options && Object.keys(service.options).length > 0) {
const extensionConfig: ServiceExtensionConfig = {};
for (const key of Object.keys(service.options)) {
const value = service.options[key];
const config = extractExtensionConfigFromOption(key, value);
/* istanbul ignore else */
if (config) {
Object.assign(extensionConfig, config);
}
}
service.extensionConfig =
extensionUtil.filterServiceExtensionConfig(extensionConfig);
}
if (Object.keys(service.methods).length > 0) {
Object.keys(service.methods).forEach(serviceName => {
convertMethodOptions(service.methods[serviceName]);
});
}
}
function convertNamespace(namespaceBase: NamespaceBase) {
const { nested } = namespaceBase;
/* istanbul ignore next */
if (!(nested && Object.keys(nested).length > 0)) {
return;
}
for (const name of Object.keys(nested)) {
const nestedStatement = nested[name];
const { syntaxType } = nestedStatement;
/* istanbul ignore else */
if (syntaxType === SyntaxType.ServiceDefinition) {
convertServiceOptions(nestedStatement as ServiceDefinition);
} else if (syntaxType === SyntaxType.MessageDefinition) {
convertFieldOptions(nestedStatement as MessageDefinition);
} else if (syntaxType === SyntaxType.NamespaceDefinition) {
convertNamespace(nestedStatement as NamespaceDefinition);
}
}
}
export function parse(source: string) {
let content: string;
let filePath = 'source';
if (/\.proto$/.test(source)) {
filePath = path.resolve(process.cwd(), source);
if (!fs.existsSync(filePath)) {
const message = `no such file: ${filePath}`;
logAndThrowError(message);
}
content = fs.readFileSync(filePath, 'utf8');
} else {
content = source;
}
const document: ProtoDocument | ProtoError = protoParse(content);
if ((document as ProtoError).syntaxType === SyntaxType.ProtoError) {
const { line, message } = document as ProtoError;
const fullMessage = `${message}(${filePath}:${line}:0)`;
logAndThrowError(fullMessage);
}
convertNamespace((document as ProtoDocument).root);
return document as ProtoDocument;
}

View File

@@ -0,0 +1,154 @@
/*
* 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 FieldExtensionConfig,
type ServiceExtensionConfig,
type FunctionExtensionConfig,
} from '../common/extension_type';
export * from '../common/extension_type';
export type KeywordType =
| 'double'
| 'float'
| 'int32'
| 'int64'
| 'uint32'
| 'uint64'
| 'sint32'
| 'sint64'
| 'fixed32'
| 'fixed64'
| 'sfixed32'
| 'sfixed64'
| 'bool'
| 'string'
| 'bytes';
export type FieldRule = 'repeated' | 'required';
export interface BaseType {
syntaxType: SyntaxType.BaseType;
value: KeywordType;
}
export interface Identifier {
syntaxType: SyntaxType.Identifier;
value: string;
resolvedValue?: string;
}
export type FieldType = BaseType | Identifier;
export interface ReflectionObject {
name: string;
fullName?: string;
options?: Record<string, string>;
comment?: string;
}
export interface FieldDefinition extends ReflectionObject {
syntaxType: SyntaxType.FieldDefinition;
id: number;
type: FieldType;
rule?: FieldRule;
optional: boolean;
required: boolean;
repeated: boolean;
map: boolean;
extend?: string;
keyType?: FieldType;
extensionConfig?: FieldExtensionConfig;
}
export interface OneofDefinition extends ReflectionObject {
syntaxType: SyntaxType.OneofDefinition;
oneof: string[];
}
export interface MethodDefinition extends ReflectionObject {
syntaxType: SyntaxType.MethodDefinition;
requestType: FieldType;
responseType: FieldType;
extensionConfig?: FunctionExtensionConfig;
}
export interface NamespaceBase extends ReflectionObject {
syntaxType: SyntaxType;
nested?: Record<string, NamespaceBase>;
}
export interface NamespaceDefinition extends NamespaceBase {
syntaxType: SyntaxType.NamespaceDefinition;
}
export interface MessageDefinition extends NamespaceBase {
syntaxType: SyntaxType.MessageDefinition;
fields: Record<string, FieldDefinition>;
oneofs: Record<string, OneofDefinition>;
extensions?: string[];
reserved?: number[] | string;
}
export interface EnumDefinition extends NamespaceBase {
syntaxType: SyntaxType.EnumDefinition;
values: Record<string, number>;
reserved?: number[] | string;
}
export interface ServiceDefinition extends NamespaceBase {
syntaxType: SyntaxType.ServiceDefinition;
methods: Record<string, MethodDefinition>;
extensionConfig?: ServiceExtensionConfig;
}
export interface ProtoRoot extends NamespaceBase {
syntaxType: SyntaxType.ProtoRoot;
}
export interface ProtoDocument {
syntaxType: SyntaxType.ProtoDocument;
imports?: string[];
weakImports?: string[];
package?: string;
syntax: 'proto2' | 'proto3';
root: ProtoRoot;
}
export interface ProtoError {
syntaxType: SyntaxType.ProtoError;
line: number;
message: string;
error: Error;
}
export enum SyntaxType {
BaseType = 'BaseType',
Identifier = 'Identifier',
OneofDefinition = 'OneofDefinition',
FieldDefinition = 'FieldDefinition',
MethodDefinition = 'MethodDefinition',
NamespaceDefinition = 'NamespaceDefinition',
MessageDefinition = 'MessageDefinition',
EnumDefinition = 'EnumDefinition',
ServiceDefinition = 'ServiceDefinition',
ProtoRoot = 'ProtoRoot',
ProtoDocument = 'ProtoDocument',
ProtoError = 'ProtoError',
}

View File

@@ -0,0 +1,288 @@
/*
* 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.
*/
/* eslint no-param-reassign: ["error", { "props": false }] */
import * as path from 'path';
import * as fs from 'fs';
import { parse as thriftParse } from '@lancewuz/thrift-parser';
import { logAndThrowError } from '../utils';
import {
type ThriftDocument,
type ThriftErrors,
SyntaxType,
type InterfaceWithFields,
type Annotation,
type ServiceDefinition,
type FunctionDefinition,
type EnumDefinition,
type FieldExtensionConfig,
type ServiceExtensionConfig,
type FunctionExtensionConfig,
type Comment,
} from './type';
import * as extensionUtil from '../common/extension_util';
// export statements
export * from './type';
// global variables
let reviseTailComment = true;
function extractExtensionConfigFromAnnotation(annotation: Annotation) {
let key = annotation.name.value;
const value = (annotation.value && annotation.value.value) || '';
if (/^((agw\.)|(api\.)|(go\.tag))/.test(key) === false) {
return false;
}
if (/^((agw\.)|(api\.))/.test(key)) {
key = key.slice(4);
}
const config = extensionUtil.extractExtensionConfig(key, value);
return config;
}
function convertFieldAnnotations(struct: InterfaceWithFields) {
const name = struct.name.value;
const { fields } = struct;
for (const field of fields) {
if (field.annotations && field.annotations.annotations.length > 0) {
const extensionConfig: FieldExtensionConfig = {};
const fieldName = field.name.value;
const fieldSyntaxType = field.fieldType.type;
for (const annotation of field.annotations.annotations) {
const config = extractExtensionConfigFromAnnotation(annotation);
if (config) {
if ('key' in config) {
// a key which is the same with the field name will make no sense
if (config.key === fieldName) {
delete config.key;
}
} else if (config.position === 'path') {
if (
[
SyntaxType.DoubleKeyword,
SyntaxType.BoolKeyword,
SyntaxType.ByteKeyword,
SyntaxType.ListKeyword,
SyntaxType.SetKeyword,
SyntaxType.MapKeyword,
SyntaxType.VoidKeyword,
].includes(fieldSyntaxType)
) {
const message = `the type of path parameter '${fieldName}' in '${name}' should be string or integer`;
logAndThrowError(message);
}
}
Object.assign(extensionConfig, config);
}
}
field.extensionConfig =
extensionUtil.filterFieldExtensionConfig(extensionConfig);
}
}
}
function convertFunctionAnnotations(func: FunctionDefinition) {
const { annotations } = func;
if (!(annotations && annotations.annotations.length > 0)) {
return;
}
const extensionConfig: FunctionExtensionConfig = {};
for (const annotation of annotations.annotations) {
const config = extractExtensionConfigFromAnnotation(annotation);
/* istanbul ignore next */
if (config) {
Object.assign(extensionConfig, config);
}
}
func.extensionConfig =
extensionUtil.filterFunctionExtensionConfig(extensionConfig);
}
function convertServiceAnnotations(service: ServiceDefinition) {
const { annotations } = service;
if (annotations && annotations.annotations.length > 0) {
const extensionConfig: ServiceExtensionConfig = {};
for (const annotation of annotations.annotations) {
const config = extractExtensionConfigFromAnnotation(annotation);
/* istanbul ignore else */
if (config) {
Object.assign(extensionConfig, config);
}
}
service.extensionConfig =
extensionUtil.filterServiceExtensionConfig(extensionConfig);
}
if (service.functions.length > 0) {
service.functions.forEach(func => {
convertFunctionAnnotations(func);
});
}
}
function reviseFieldComments(struct: InterfaceWithFields) {
const { fields } = struct;
/* istanbul ignore next */
if (fields.length < 2) {return;}
for (let i = fields.length - 1; i > 0; i--) {
const currentField = fields[i];
const prevField = fields[i - 1];
const prevFieldEndLine = prevField.loc.end.line;
const prevFieldComments = prevField.comments;
for (let j = 0; j < prevFieldComments.length; j++) {
if (prevFieldComments[j].loc.end.line > prevFieldEndLine) {
const dislocatedComments = prevFieldComments.splice(
j,
prevFieldComments.length - j,
);
currentField.comments = [
...dislocatedComments,
...currentField.comments,
];
break;
}
}
}
}
function reviseEnumMemberComments(enm: EnumDefinition) {
const { members } = enm;
/* istanbul ignore next */
if (members.length < 2) {return;}
for (let i = 0; i < members.length - 1; i++) {
const currentMember = members[i];
const nextMember = members[i + 1];
const currentMemberEndLine = currentMember.loc.end.line;
const nextMemberFirstComment = nextMember.comments[0];
if (
nextMemberFirstComment &&
nextMemberFirstComment.loc.end.line === currentMemberEndLine
) {
const dislocatedComment = nextMember.comments.shift() as Comment;
currentMember.comments.push(dislocatedComment);
}
}
}
function reviseFunctionComments(service: ServiceDefinition) {
const { functions } = service;
if (functions.length < 2) {return;}
for (let i = 0; i < functions.length - 1; i++) {
const currentFunction = functions[i];
const nextFunction = functions[i + 1];
const currentFunctionEndLine = currentFunction.loc.end.line;
const nextFunctionFirstComment = nextFunction.comments[0];
if (
nextFunctionFirstComment &&
nextFunctionFirstComment.loc.end.line === currentFunctionEndLine
) {
const dislocatedComment = nextFunction.comments.shift() as Comment;
currentFunction.comments.push(dislocatedComment);
}
}
}
export interface ParseOption {
reviseTailComment?: boolean;
}
const defualtParseOption = {
reviseTailComment: true,
};
export function parse(source: string, option?: ParseOption): ThriftDocument {
let content: string;
let filePath = 'source';
if (/\.thrift$/.test(source)) {
filePath = path.resolve(process.cwd(), source);
if (!fs.existsSync(filePath)) {
const message = `no such file: ${filePath}`;
logAndThrowError(message);
}
content = fs.readFileSync(filePath, 'utf8');
} else {
content = source;
}
const document: ThriftDocument | ThriftErrors = thriftParse(content);
if ((document as ThriftErrors).type === SyntaxType.ThriftErrors) {
const error = (document as ThriftErrors).errors[0];
const { start } = error.loc;
const message = `${error.message}(${filePath}:${start.line}:${start.column})`;
logAndThrowError(message);
}
const parseOption = { ...defualtParseOption, ...option };
reviseTailComment = parseOption.reviseTailComment;
for (const statement of (document as ThriftDocument).body) {
/* istanbul ignore else */
if (
[SyntaxType.StructDefinition, SyntaxType.UnionDefinition].includes(
statement.type,
)
) {
convertFieldAnnotations(statement as InterfaceWithFields);
if (reviseTailComment) {
reviseFieldComments(statement as InterfaceWithFields);
}
} else if (statement.type === SyntaxType.ServiceDefinition) {
convertServiceAnnotations(statement);
if (reviseTailComment) {
reviseFunctionComments(statement);
}
} else if (statement.type === SyntaxType.EnumDefinition) {
/* istanbul ignore else */
if (reviseTailComment) {
reviseEnumMemberComments(statement);
}
}
}
return document as ThriftDocument;
}

View File

@@ -0,0 +1,460 @@
/*
* 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 FieldExtensionConfig,
type ServiceExtensionConfig,
type FunctionExtensionConfig,
} from '../common/extension_type';
export * from '../common/extension_type';
export interface ThriftError {
type: ErrorType;
message: string;
loc: TextLocation;
}
export interface ParseError extends ThriftError {
type: ErrorType.ParseError;
}
export interface ScanError extends ThriftError {
type: ErrorType.ScanError;
}
export interface Node {
type: SyntaxType;
}
export interface SyntaxNode extends Node {
loc: TextLocation;
}
export interface StructLike {
name: Identifier;
fields: Array<FieldDefinition>;
annotations?: Annotations;
comments: Array<Comment>;
loc: TextLocation;
}
export interface TextLocation {
start: TextPosition;
end: TextPosition;
}
export interface TextPosition {
line: number;
column: number;
index: number;
}
export interface Token extends SyntaxNode {
text: string;
}
export interface ThriftDocument extends Node {
type: SyntaxType.ThriftDocument;
body: Array<ThriftStatement>;
tokens?: Array<Token>;
}
export interface ThriftErrors {
type: SyntaxType.ThriftErrors;
errors: Array<ThriftError>;
}
export type ThriftStatement =
| NamespaceDefinition
| IncludeDefinition
| CppIncludeDefinition
| ConstDefinition
| StructDefinition
| EnumDefinition
| ExceptionDefinition
| UnionDefinition
| TypedefDefinition
| ServiceDefinition;
export type CommentType = SyntaxType.CommentLine | SyntaxType.CommentBlock;
export type Comment = CommentLine | CommentBlock;
export interface CommentLine extends SyntaxNode {
type: SyntaxType.CommentLine;
value: string;
}
export interface CommentBlock extends SyntaxNode {
type: SyntaxType.CommentBlock;
value: Array<string>;
}
export interface Annotations extends SyntaxNode {
annotations: Array<Annotation>;
}
export interface Annotation extends SyntaxNode {
name: Identifier;
value?: StringLiteral;
}
export interface PrimarySyntax extends SyntaxNode {
comments: Array<Comment>;
}
export type FieldType = BaseType | ContainerType | Identifier;
export type FunctionType = FieldType | VoidType;
export type KeywordType =
| SyntaxType.StringKeyword
| SyntaxType.DoubleKeyword
| SyntaxType.BoolKeyword
| SyntaxType.I8Keyword
| SyntaxType.I16Keyword
| SyntaxType.I32Keyword
| SyntaxType.I64Keyword
| SyntaxType.BinaryKeyword
| SyntaxType.ByteKeyword;
export interface VoidType extends SyntaxNode {
type: SyntaxType.VoidKeyword;
}
export type ContainerType = SetType | MapType | ListType;
export interface BaseType extends SyntaxNode {
type: KeywordType;
annotations?: Annotations;
}
export interface SetType extends SyntaxNode {
type: SyntaxType.SetType;
valueType: FieldType;
annotations?: Annotations;
}
export interface ListType extends SyntaxNode {
type: SyntaxType.ListType;
valueType: FieldType;
annotations?: Annotations;
}
export interface MapType extends SyntaxNode {
type: SyntaxType.MapType;
keyType: FieldType;
valueType: FieldType;
annotations?: Annotations;
}
export type ConstValue =
| StringLiteral
| IntConstant
| DoubleConstant
| BooleanLiteral
| ConstMap
| ConstList
| Identifier;
export interface NamespaceDefinition extends PrimarySyntax {
type: SyntaxType.NamespaceDefinition;
scope: Identifier;
name: Identifier;
}
export interface ConstDefinition extends PrimarySyntax {
type: SyntaxType.ConstDefinition;
name: Identifier;
fieldType: FieldType;
initializer: ConstValue;
annotations?: Annotations;
}
export type FieldRequired = 'required' | 'optional';
export interface IncludeDefinition extends PrimarySyntax {
type: SyntaxType.IncludeDefinition;
path: StringLiteral;
}
export interface CppIncludeDefinition extends PrimarySyntax {
type: SyntaxType.CppIncludeDefinition;
path: StringLiteral;
}
export interface InterfaceWithFields extends PrimarySyntax {
name: Identifier;
fields: Array<FieldDefinition>;
annotations?: Annotations;
}
export interface StructDefinition extends InterfaceWithFields {
type: SyntaxType.StructDefinition;
}
export interface UnionDefinition extends InterfaceWithFields {
type: SyntaxType.UnionDefinition;
}
export interface ExceptionDefinition extends InterfaceWithFields {
type: SyntaxType.ExceptionDefinition;
}
export interface FieldDefinition extends PrimarySyntax {
type: SyntaxType.FieldDefinition;
name: Identifier;
fieldID: FieldID | null;
fieldType: FunctionType;
requiredness: FieldRequired | null;
defaultValue: ConstValue | null;
annotations?: Annotations;
extensionConfig?: FieldExtensionConfig;
}
export interface FieldID extends SyntaxNode {
type: SyntaxType.FieldID;
value: number;
}
export interface EnumDefinition extends PrimarySyntax {
type: SyntaxType.EnumDefinition;
name: Identifier;
members: Array<EnumMember>;
annotations?: Annotations;
}
export interface EnumMember extends PrimarySyntax {
type: SyntaxType.EnumMember;
name: Identifier;
initializer: IntConstant | null;
annotations?: Annotations;
}
export interface TypedefDefinition extends PrimarySyntax {
type: SyntaxType.TypedefDefinition;
name: Identifier;
definitionType: FieldType;
annotations?: Annotations;
}
export interface ServiceDefinition extends PrimarySyntax {
type: SyntaxType.ServiceDefinition;
name: Identifier;
extends: Identifier | null;
functions: Array<FunctionDefinition>;
annotations?: Annotations;
extensionConfig?: ServiceExtensionConfig;
}
export interface FunctionDefinition extends PrimarySyntax {
type: SyntaxType.FunctionDefinition;
name: Identifier;
oneway: boolean;
returnType: FunctionType;
fields: Array<FieldDefinition>;
throws: Array<FieldDefinition>;
modifiers: Array<Token>;
annotations?: Annotations;
extensionConfig?: FunctionExtensionConfig;
}
export interface ParametersDefinition extends SyntaxNode {
type: SyntaxType.ParametersDefinition;
fields: Array<FieldDefinition>;
}
export interface ThrowsDefinition extends SyntaxNode {
type: SyntaxType.ThrowsDefinition;
fields: Array<FieldDefinition>;
}
export interface StringLiteral extends SyntaxNode {
type: SyntaxType.StringLiteral;
value: string;
}
export interface BooleanLiteral extends SyntaxNode {
type: SyntaxType.BooleanLiteral;
value: boolean;
}
export interface IntegerLiteral extends SyntaxNode {
type: SyntaxType.IntegerLiteral;
value: string;
}
export interface HexLiteral extends SyntaxNode {
type: SyntaxType.HexLiteral;
value: string;
}
export interface FloatLiteral extends SyntaxNode {
type: SyntaxType.FloatLiteral;
value: string;
}
export interface ExponentialLiteral extends SyntaxNode {
type: SyntaxType.ExponentialLiteral;
value: string;
}
export interface IntConstant extends SyntaxNode {
type: SyntaxType.IntConstant;
value: IntegerLiteral | HexLiteral;
}
export interface DoubleConstant extends SyntaxNode {
type: SyntaxType.DoubleConstant;
value: FloatLiteral | ExponentialLiteral;
}
export interface ConstMap extends SyntaxNode {
type: SyntaxType.ConstMap;
properties: Array<PropertyAssignment>;
}
export interface ConstList extends SyntaxNode {
type: SyntaxType.ConstList;
elements: Array<ConstValue>;
}
export interface PropertyAssignment extends SyntaxNode {
type: SyntaxType.PropertyAssignment;
name: ConstValue;
initializer: ConstValue;
}
export interface Identifier extends SyntaxNode {
type: SyntaxType.Identifier;
value: string;
annotations?: Annotations;
}
export enum ErrorType {
ParseError = 'ParseError',
ScanError = 'ScanError',
}
export enum SyntaxType {
ThriftDocument = 'ThriftDocument',
ThriftErrors = 'ThriftErrors',
Identifier = 'Identifier',
FieldID = 'FieldID',
// Statements
NamespaceDefinition = 'NamespaceDefinition',
IncludeDefinition = 'IncludeDefinition',
CppIncludeDefinition = 'CppIncludeDefinition',
ConstDefinition = 'ConstDefinition',
StructDefinition = 'StructDefinition',
EnumDefinition = 'EnumDefinition',
ServiceDefinition = 'ServiceDefinition',
ExceptionDefinition = 'ExceptionDefinition',
TypedefDefinition = 'TypedefDefinition',
UnionDefinition = 'UnionDefinition',
// Fields
FieldDefinition = 'FieldDefinition',
FunctionDefinition = 'FunctionDefinition',
ParametersDefinition = 'ParametersDefinition',
ThrowsDefinition = 'ThrowsDefinition',
// Type Annotations
FieldType = 'FieldType',
BaseType = 'BaseType',
SetType = 'SetType',
MapType = 'MapType',
ListType = 'ListType',
// Values
ConstValue = 'ConstValue',
IntConstant = 'IntConstant',
DoubleConstant = 'DoubleConstant',
ConstList = 'ConstList',
ConstMap = 'ConstMap',
EnumMember = 'EnumMember',
// Literals
CommentLine = 'CommentLine',
CommentBlock = 'CommentBlock',
StringLiteral = 'StringLiteral',
IntegerLiteral = 'IntegerLiteral',
FloatLiteral = 'FloatLiteral',
HexLiteral = 'HexLiteral',
ExponentialLiteral = 'ExponentialLiteral',
BooleanLiteral = 'BooleanLiteral',
PropertyAssignment = 'PropertyAssignment',
// Tokens
LeftParenToken = 'LeftParenToken',
RightParenToken = 'RightParenToken',
LeftBraceToken = 'LeftBraceToken',
RightBraceToken = 'RightBraceToken',
LeftBracketToken = 'LeftBracketToken',
RightBracketToken = 'RightBracketToken',
CommaToken = 'CommaToken',
DotToken = 'DotToken',
MinusToken = 'MinusToken',
SemicolonToken = 'SemicolonToken',
ColonToken = 'ColonToken',
StarToken = 'StarToken',
EqualToken = 'EqualToken',
LessThanToken = 'LessThanToken',
GreaterThanToken = 'GreaterThanToken',
// Keywords
NamespaceKeyword = 'NamespaceKeyword',
IncludeKeyword = 'IncludeKeyword',
CppIncludeKeyword = 'CppIncludeKeyword',
ExceptionKeyword = 'ExceptionKeyword',
ServiceKeyword = 'ServiceKeyword',
ExtendsKeyword = 'ExtendsKeyword',
RequiredKeyword = 'RequiredKeyword',
OptionalKeyword = 'OptionalKeyword',
FalseKeyword = 'FalseKeyword',
TrueKeyword = 'TrueKeyword',
ConstKeyword = 'ConstKeyword',
DoubleKeyword = 'DoubleKeyword',
StructKeyword = 'StructKeyword',
TypedefKeyword = 'TypedefKeyword',
UnionKeyword = 'UnionKeyword',
StringKeyword = 'StringKeyword',
BinaryKeyword = 'BinaryKeyword',
BoolKeyword = 'BoolKeyword',
ByteKeyword = 'ByteKeyword',
EnumKeyword = 'EnumKeyword',
SenumKeyword = 'SenumKeyword',
ListKeyword = 'ListKeyword',
SetKeyword = 'SetKeyword',
MapKeyword = 'MapKeyword',
I8Keyword = 'I8Keyword',
I16Keyword = 'I16Keyword',
I32Keyword = 'I32Keyword',
I64Keyword = 'I64Keyword',
ThrowsKeyword = 'ThrowsKeyword',
VoidKeyword = 'VoidKeyword',
OnewayKeyword = 'OnewayKeyword',
// Other
Annotation = 'Annotation',
Annotations = 'Annotations',
EOF = 'EOF',
}

View File

@@ -0,0 +1,144 @@
/*
* 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 * as path from 'path';
import * as fs from 'fs';
import { logAndThrowError , mergeObject, getPosixPath } from '../utils';
import { parseThriftContent } from './thrift';
import { parseProtoContent } from './proto';
import { type UnifyDocument } from './type';
type FileType = 'thrift' | 'proto';
// export statements
export * from './type';
export interface ParseOption {
root?: string;
namespaceRefer?: boolean;
cache?: boolean;
ignoreGoTag?: boolean;
ignoreGoTagDash?: boolean;
preproccess?: (param: { content: string; path?: string }) => string;
searchPaths?: string[];
}
const parseOptionDefault: ParseOption = {
root: '.',
namespaceRefer: true,
cache: false,
ignoreGoTag: false,
ignoreGoTagDash: false,
searchPaths: [],
};
// the key of fileContentMap should be absolute path
export function parse(
filePath: string,
option: ParseOption = {},
fileContentMap?: Record<string, string>,
): UnifyDocument {
const {
root,
namespaceRefer,
cache,
ignoreGoTag,
ignoreGoTagDash,
preproccess,
searchPaths,
} = mergeObject(parseOptionDefault, option) as Required<ParseOption>;
const fullRootDir = getPosixPath(path.resolve(process.cwd(), root));
let fullFilePath = getPosixPath(path.resolve(fullRootDir, filePath));
let idlFileType: FileType = 'thrift';
let content = '';
if (/\.thrift$/.test(filePath)) {
fullFilePath = getPosixPath(path.resolve(fullRootDir, filePath));
if (fileContentMap) {
content = fileContentMap[filePath];
if (typeof content === 'undefined') {
logAndThrowError(`file "${filePath}" does not exist in fileContentMap`);
}
} else {
if (!fs.existsSync(fullFilePath)) {
const message = `no such file: ${fullFilePath}`;
logAndThrowError(message);
}
content = fs.readFileSync(fullFilePath, 'utf8');
}
} else if (/\.proto$/.test(filePath)) {
idlFileType = 'proto';
fullFilePath = getPosixPath(path.resolve(fullRootDir, filePath));
if (fileContentMap) {
content = fileContentMap[filePath];
if (typeof content === 'undefined') {
logAndThrowError(`file "${filePath}" does not exist in fileContentMap`);
}
} else {
if (!fs.existsSync(fullFilePath)) {
const message = `no such file: ${fullFilePath}`;
logAndThrowError(message);
}
content = fs.readFileSync(fullFilePath, 'utf8');
}
} else {
const message = `invalid filePath: "${filePath}"`;
logAndThrowError(message);
}
const absoluteFilePath = getPosixPath(
path.relative(fullRootDir, fullFilePath),
);
if (typeof preproccess === 'function') {
content = preproccess({ content, path: absoluteFilePath });
}
if (idlFileType === 'thrift') {
const looseAbsoluteFilePath = absoluteFilePath.replace(/\.thrift$/, '');
const document = parseThriftContent(
content,
{
loosePath: looseAbsoluteFilePath,
rootDir: fullRootDir,
namespaceRefer,
cache,
ignoreGoTag,
ignoreGoTagDash,
searchPaths,
},
fileContentMap,
);
return document;
}
const looseAbsoluteFilePath = absoluteFilePath.replace(/\.proto$/, '');
const document = parseProtoContent(
content,
{
loosePath: looseAbsoluteFilePath,
rootDir: fullRootDir,
cache,
searchPaths,
},
fileContentMap,
);
return document;
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,712 @@
/*
* 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.
*/
/* eslint no-param-reassign: ["error", { "props": false }], import/prefer-default-export: off */
import * as path from 'path';
import * as fs from 'fs';
import * as t from '@lancewuz/thrift-parser';
import { logAndThrowError, getPosixPath } from '../utils';
import * as extensionUtil from '../common/extension_util';
import { convertIntToString } from './util';
import {
SyntaxType,
type Annotation,
type ServiceDefinition,
type FunctionDefinition,
type EnumDefinition,
type ExtensionConfig,
type FieldExtensionConfig,
type ServiceExtensionConfig,
type FunctionExtensionConfig,
type Comment,
type Identifier,
type ContainerType,
type MapType,
type StructDefinition,
type TypedefDefinition,
type TextLocation,
type FunctionType,
type FieldType,
type UnifyStatement,
type Annotations,
type EnumMember,
type UnifyDocument,
type FieldDefinition,
type ConstDefinition,
type ConstValue,
} from './type';
// cache parsed document
const fileDocumentMap: Record<string, t.ThriftDocument> = {};
const namespaceDefault = 'root';
let enumNames: string[] = [];
let cache = true;
let absoluteFileContentMap: Record<string, string> | undefined;
let rootDir = '';
let entryLooseAbsoluteFilePath = '';
let ignoreGoTag = false;
let ignoreGoTagDash = false;
let addNamespaceValue: ((fieldType: FunctionType) => void) | undefined;
let searchPaths: string[] = [];
const mergeConfig = (
config: Partial<ExtensionConfig>,
config2: Partial<ExtensionConfig>,
): ExtensionConfig => {
const mergedTags: string[] = [];
if (config?.tag) {
mergedTags.push(config.tag);
}
if (config2?.tag) {
mergedTags.push(config2.tag);
}
const res = Object.assign(config, config2);
if (mergedTags.length > 0) {
res.tag = mergedTags.join(',');
}
return res;
};
function extractExtensionConfigFromAnnotation(
annotation: Annotation,
): false | ExtensionConfig {
let key = annotation.name.value;
const value = (annotation.value && annotation.value.value) || '';
if (/^((agw\.)|(api\.)|(go\.tag))/.test(key) === false) {
return false;
}
if (/^((agw\.)|(api\.))/.test(key)) {
key = key.slice(4);
}
const config = extensionUtil.extractExtensionConfig(key, value, ignoreGoTag);
return config;
}
function getFieldExtensionConfig(
fieldName: string,
fieldType: FunctionType,
annotations?: Annotations,
): FieldExtensionConfig {
if (!annotations) {
return {};
}
const extensionConfig: FieldExtensionConfig = {};
for (const annotation of annotations.annotations) {
const config = extractExtensionConfigFromAnnotation(annotation);
if (config) {
if ('key' in config) {
// a key which is the same with the field name will make no sense
if (config.key === fieldName) {
delete config.key;
}
} else if (config.position === 'path') {
if (
[
SyntaxType.DoubleKeyword,
SyntaxType.BoolKeyword,
SyntaxType.ByteKeyword,
SyntaxType.ListKeyword,
SyntaxType.SetKeyword,
SyntaxType.MapKeyword,
SyntaxType.VoidKeyword,
].includes(fieldType.type)
) {
const fullFilePath = path.resolve(
rootDir,
`${entryLooseAbsoluteFilePath}.proto`,
);
const message = `path parameter '${fieldName}' should be string or integer`;
const fullMessage = `${message} (${fullFilePath})`;
logAndThrowError(fullMessage, message);
}
}
mergeConfig(extensionConfig, config);
}
}
const res = extensionUtil.filterFieldExtensionConfig(extensionConfig);
return res;
}
function getFuncExtensionConfig(
annotations?: Annotations,
): FunctionExtensionConfig {
if (!annotations) {
return {};
}
const extensionConfig: FunctionExtensionConfig = {};
for (const annotation of annotations.annotations) {
const config = extractExtensionConfigFromAnnotation(annotation);
/* istanbul ignore next */
if (config) {
Object.assign(extensionConfig, config);
}
}
return extensionUtil.filterFunctionExtensionConfig(extensionConfig);
}
function getServiceExtensionConfig(
annotations?: Annotations,
): ServiceExtensionConfig {
if (!annotations) {
return {};
}
const extensionConfig: ServiceExtensionConfig = {};
for (const annotation of annotations.annotations) {
const config = extractExtensionConfigFromAnnotation(annotation);
/* istanbul ignore else */
if (config) {
Object.assign(extensionConfig, config);
}
}
return extensionUtil.filterServiceExtensionConfig(extensionConfig);
}
function reviseFieldComments(fields: (FieldDefinition | EnumMember)[]): void {
/* istanbul ignore next */
if (fields.length < 2) {
return;
}
// move previous comments to current field
for (let i = fields.length - 1; i > 0; i--) {
const currentField = fields[i];
const prevField = fields[i - 1];
const prevFieldEndLine = (prevField.loc as TextLocation).end.line;
const prevFieldComments = prevField.comments;
for (let j = 0; j < prevFieldComments.length; j++) {
if (
(prevFieldComments[j].loc as TextLocation).end.line > prevFieldEndLine
) {
const dislocatedComments = prevFieldComments.splice(
j,
prevFieldComments.length - j,
);
currentField.comments = [
...dislocatedComments,
...currentField.comments,
];
break;
}
}
}
// move next comments to current field
for (let i = 0; i < fields.length - 1; i++) {
const currentField = fields[i];
const nextField = fields[i + 1];
const currentFieldEndLine = (currentField.loc as TextLocation).end.line;
const nextFieldFirstComment = nextField.comments[0];
if (
nextFieldFirstComment &&
(nextFieldFirstComment.loc as TextLocation).end.line ===
currentFieldEndLine
) {
const dislocatedComment = nextField.comments.shift() as Comment;
currentField.comments.push(dislocatedComment);
}
}
}
function reviseFuncComments(functions: FunctionDefinition[]): void {
if (functions.length < 2) {
return;
}
for (let i = 0; i < functions.length - 1; i++) {
const currentFunc = functions[i];
const nextFunc = functions[i + 1];
const currentFuncEndLine = (currentFunc.loc as TextLocation).end.line;
const nextFuncFirstComment = nextFunc.comments[0];
if (
nextFuncFirstComment &&
(nextFuncFirstComment.loc as TextLocation).end.line === currentFuncEndLine
) {
const dislocatedComment = nextFunc.comments.shift() as Comment;
currentFunc.comments.push(dislocatedComment);
}
}
}
function getUnifyNamespace(
looseAbsoluteFilePath: string,
astNamespaces?: t.NamespaceDefinition[],
): {
namespace: string;
unifyNamespace: string;
} {
let namespace = '';
let unifyNamespace = '';
if (astNamespaces && astNamespaces.length > 0) {
const namespaceMap: Record<string, t.NamespaceDefinition> = {};
for (const astNamespace of astNamespaces) {
const scopeName = astNamespace.scope.value;
namespaceMap[scopeName] = astNamespace;
}
const astNamespaceCurrent =
namespaceMap.js || namespaceMap.go || namespaceMap.py;
if (astNamespaceCurrent) {
namespace = astNamespaceCurrent.name.value;
unifyNamespace = namespace;
} else if (namespaceMap.java) {
namespace = namespaceMap.java.name.value.split('.').pop() as string;
unifyNamespace = namespace;
} else {
const message = 'a js namespace should be specifed';
const fullFilePath = path.resolve(
rootDir,
`${looseAbsoluteFilePath}.thrift`,
);
const infoMessage = `${message} (${fullFilePath})`;
logAndThrowError(infoMessage, message);
}
} else {
namespace = '';
unifyNamespace = namespaceDefault;
}
unifyNamespace = unifyNamespace
.replace(/\./g, '_')
.replace(/[^a-zA-Z0-9_]/g, '');
return { namespace, unifyNamespace };
}
function createAddNamespaceReferValue(
filenameNamespaceMap: Record<string, string>,
namespace: string,
): (fieldType: FunctionType) => void {
const regExpNamespaceMap = new Map<RegExp, string>();
Object.keys(filenameNamespaceMap).forEach(filename => {
const regExp = new RegExp(`^${filename}(\\.[^\\.]*)$`);
regExpNamespaceMap.set(regExp, filenameNamespaceMap[filename]);
});
function addNamespaceReferValue(fieldType: FunctionType): void {
if ((fieldType as Identifier).type === SyntaxType.Identifier) {
const identifierValue = (fieldType as Identifier).value;
if (!identifierValue.includes('.')) {
(fieldType as Identifier).namespaceValue =
`${namespace}.${identifierValue}`;
} else {
const parts = identifierValue.split('.');
if (parts.length === 2 && enumNames.includes(parts[0])) {
(fieldType as Identifier).namespaceValue =
`${namespace}.${identifierValue}`;
} else {
for (const regExp of regExpNamespaceMap.keys()) {
if (regExp.test(identifierValue)) {
const namespaceName = regExpNamespaceMap.get(regExp);
(fieldType as Identifier).namespaceValue =
identifierValue.replace(regExp, `${namespaceName}$1`);
break;
}
}
}
}
} else if ((fieldType as ContainerType).valueType) {
addNamespaceReferValue(
(fieldType as ContainerType).valueType as FunctionType,
);
if ((fieldType as MapType).keyType) {
addNamespaceReferValue((fieldType as MapType).keyType as FunctionType);
}
}
}
return addNamespaceReferValue;
}
function getAddNamespaceReferValue(
astIncludes: t.IncludeDefinition[],
namespace: string,
): ((fieldType: FunctionType) => void) | undefined {
const filenameNamespaceMap: Record<string, string> = {};
for (const astInclude of astIncludes) {
const idlFilePath = astInclude.path.value;
const looseIdlFilePath = idlFilePath.replace(/\.thrift$/, '');
const looseFilename = looseIdlFilePath.split('/').pop() as string;
// try relative path
let looseAbsoluteFilePath = getPosixPath(
path.join(path.dirname(entryLooseAbsoluteFilePath), looseIdlFilePath),
);
// try absulote path
const alternativeLooseAbsoluteFilePath = looseIdlFilePath;
let document =
fileDocumentMap[looseAbsoluteFilePath] ||
fileDocumentMap[alternativeLooseAbsoluteFilePath];
if (!document) {
let content = '';
if (absoluteFileContentMap) {
content = absoluteFileContentMap[`${looseAbsoluteFilePath}.thrift`];
if (typeof content === 'undefined') {
content =
absoluteFileContentMap[
`${alternativeLooseAbsoluteFilePath}.thrift`
];
if (typeof content === 'undefined') {
logAndThrowError(
`file ${looseAbsoluteFilePath}.thrift does not exist in fileContentMap`,
);
}
looseAbsoluteFilePath = alternativeLooseAbsoluteFilePath;
}
} else {
let fullFilePath = getPosixPath(
path.resolve(rootDir, `${looseAbsoluteFilePath}.thrift`),
);
// Search
if (!fs.existsSync(fullFilePath)) {
const filePaths = [rootDir, ...searchPaths].map(searchPath =>
getPosixPath(
path.resolve(
rootDir,
searchPath,
`${alternativeLooseAbsoluteFilePath}.thrift`,
),
),
);
const existedFilePath = filePaths.find(filePath =>
fs.existsSync(filePath),
);
if (typeof existedFilePath === 'undefined') {
logAndThrowError(`file ${filePaths[0]} does not exist`);
} else {
fullFilePath = existedFilePath;
looseAbsoluteFilePath = path
.relative(rootDir, existedFilePath)
.replace(/\.thrift$/, '');
}
}
content = fs.readFileSync(fullFilePath, 'utf8');
}
document = parseContent(content, looseAbsoluteFilePath);
} else if (!fileDocumentMap[looseAbsoluteFilePath]) {
looseAbsoluteFilePath = alternativeLooseAbsoluteFilePath;
}
const astNamespaces: t.NamespaceDefinition[] = [];
for (const statement of document.body) {
if (statement.type === t.SyntaxType.NamespaceDefinition) {
astNamespaces.push(statement as t.NamespaceDefinition);
}
}
const { unifyNamespace } = getUnifyNamespace(
looseAbsoluteFilePath,
astNamespaces,
);
filenameNamespaceMap[looseFilename] = unifyNamespace;
}
return createAddNamespaceReferValue(filenameNamespaceMap, namespace);
}
function convertTypedefDefinition(
astTypedef: TypedefDefinition,
): TypedefDefinition {
const typedefDefinition: TypedefDefinition = { ...astTypedef };
// const { name, definitionType} = typedefDefinition
if (typeof addNamespaceValue === 'function') {
addNamespaceValue(typedefDefinition.definitionType);
addNamespaceValue(typedefDefinition.name);
}
return typedefDefinition;
}
function addNamespaceValueToConstValue(constValue: ConstValue) {
if (typeof addNamespaceValue === 'function') {
if (constValue.type === SyntaxType.Identifier) {
addNamespaceValue(constValue);
} else if (constValue.type === SyntaxType.ConstMap) {
for (const property of constValue.properties) {
addNamespaceValueToConstValue(property.name);
addNamespaceValueToConstValue(property.initializer);
}
} else if (constValue.type === SyntaxType.ConstList) {
for (const element of constValue.elements) {
addNamespaceValueToConstValue(element);
}
}
}
}
function convertConstDefinition(astConst: ConstDefinition): ConstDefinition {
const constDefinition: ConstDefinition = { ...astConst };
if (typeof addNamespaceValue === 'function') {
addNamespaceValue(constDefinition.name);
addNamespaceValue(constDefinition.fieldType);
addNamespaceValueToConstValue(constDefinition.initializer);
}
return constDefinition;
}
function convertStructDefinition(
astStruct: StructDefinition,
): StructDefinition {
const structDefinition: StructDefinition = { ...astStruct };
const { name, fields } = structDefinition;
const newName = name;
if (addNamespaceValue) {
addNamespaceValue(newName);
}
for (const field of fields) {
const { fieldType, annotations } = field;
const fieldName = field.name.value;
if (addNamespaceValue) {
addNamespaceValue(fieldType);
}
field.extensionConfig = getFieldExtensionConfig(
fieldName,
fieldType,
annotations,
);
if ((field.extensionConfig.tag || '').includes('omitempty')) {
field.requiredness = 'optional';
}
}
reviseFieldComments(fields);
const newFields: FieldDefinition[] = [];
for (const field of fields) {
const tag = (field.extensionConfig && field.extensionConfig.tag) || '';
if (tag.includes('int2str')) {
field.fieldType = convertIntToString(field.fieldType as FieldType);
}
if (ignoreGoTagDash || !tag.includes('ignore')) {
newFields.push(field);
}
}
structDefinition.fields = newFields;
structDefinition.name = newName;
return structDefinition;
}
function convertEnumDefinition(astEnum: EnumDefinition): EnumDefinition {
const enumDefinition: EnumDefinition = { ...astEnum };
const { name, members } = enumDefinition;
enumNames.push(name.value);
reviseFieldComments(members);
if (addNamespaceValue) {
addNamespaceValue(name);
}
return enumDefinition;
}
function convertFunctionDefinition(
astFunc: FunctionDefinition,
): FunctionDefinition {
const functionDefinition: FunctionDefinition = { ...astFunc };
const { returnType, fields, annotations, name } = functionDefinition;
if (addNamespaceValue) {
addNamespaceValue(name);
addNamespaceValue(returnType);
for (const field of fields) {
addNamespaceValue(field.fieldType);
}
}
functionDefinition.extensionConfig = getFuncExtensionConfig(annotations);
return functionDefinition;
}
function convertServiceDefinition(
astService: ServiceDefinition,
): ServiceDefinition {
const serviceDefinition: ServiceDefinition = { ...astService };
const { annotations, name } = serviceDefinition;
const functions: FunctionDefinition[] = [];
for (const astFunc of astService.functions) {
functions.push(convertFunctionDefinition(astFunc));
}
if (addNamespaceValue) {
addNamespaceValue(name);
}
reviseFuncComments(functions);
serviceDefinition.functions = functions;
serviceDefinition.extensionConfig = getServiceExtensionConfig(annotations);
return serviceDefinition;
}
function parseContent(
content: string,
looseAbsoluteFilePath: string,
): t.ThriftDocument {
if (fileDocumentMap[looseAbsoluteFilePath]) {
return fileDocumentMap[looseAbsoluteFilePath];
}
const document: t.ThriftDocument | t.ThriftErrors = t.parse(content);
if ((document as t.ThriftErrors).type === t.SyntaxType.ThriftErrors) {
const error = (document as t.ThriftErrors).errors[0];
const { start } = error.loc;
const fullFilePath = path.resolve(
rootDir,
`${looseAbsoluteFilePath}.thrift`,
);
const message = `${error.message}(${fullFilePath}:${start.line}:${start.column})`;
logAndThrowError(message, error.message);
}
if (cache) {
fileDocumentMap[looseAbsoluteFilePath] = document as t.ThriftDocument;
}
return document as t.ThriftDocument;
}
export function parseThriftContent(
content: string,
option: {
loosePath: string;
rootDir: string;
cache: boolean;
searchPaths: string[];
namespaceRefer: boolean;
ignoreGoTag: boolean;
ignoreGoTagDash: boolean;
},
fileContentMap?: Record<string, string>,
): UnifyDocument {
rootDir = option.rootDir;
entryLooseAbsoluteFilePath = option.loosePath;
cache = option.cache;
ignoreGoTag = option.ignoreGoTag;
ignoreGoTagDash = option.ignoreGoTagDash;
searchPaths = option.searchPaths;
absoluteFileContentMap = fileContentMap;
addNamespaceValue = undefined;
enumNames = [];
// parse file content
const document = parseContent(content, entryLooseAbsoluteFilePath);
const statementGroup: Record<string, t.ThriftStatement[]> = {};
for (const statement of (document as t.ThriftDocument).body) {
let { type } = statement;
// NOTE: in the latest version of Thrift, union is similar to struct except that all fields are converted to 'optional'.
// the idl parse shields the difference, so we can dispose them together.
if (type === t.SyntaxType.UnionDefinition) {
type = t.SyntaxType.StructDefinition;
statement.type = t.SyntaxType.StructDefinition;
}
if (!statementGroup[type]) {
statementGroup[type] = [statement];
} else {
statementGroup[type].push(statement);
}
}
const { unifyNamespace, namespace } = getUnifyNamespace(
entryLooseAbsoluteFilePath,
statementGroup[t.SyntaxType.NamespaceDefinition] as t.NamespaceDefinition[],
);
if (option.namespaceRefer) {
addNamespaceValue = getAddNamespaceReferValue(
(statementGroup[t.SyntaxType.IncludeDefinition] ||
[]) as t.IncludeDefinition[],
unifyNamespace,
);
}
const statements: UnifyStatement[] = [];
if (statementGroup[t.SyntaxType.TypedefDefinition]) {
for (const astTypedef of statementGroup[t.SyntaxType.TypedefDefinition]) {
statements.push(convertTypedefDefinition(astTypedef as any));
}
}
if (statementGroup[SyntaxType.EnumDefinition]) {
for (const astEnum of statementGroup[t.SyntaxType.EnumDefinition]) {
statements.push(convertEnumDefinition(astEnum as any));
}
}
if (statementGroup[t.SyntaxType.ConstDefinition]) {
for (const astConst of statementGroup[t.SyntaxType.ConstDefinition]) {
statements.push(convertConstDefinition(astConst as any));
}
}
if (statementGroup[SyntaxType.StructDefinition]) {
for (const astStruct of statementGroup[t.SyntaxType.StructDefinition]) {
statements.push(convertStructDefinition(astStruct as any));
}
}
if (statementGroup[SyntaxType.ServiceDefinition]) {
for (const astService of statementGroup[t.SyntaxType.ServiceDefinition]) {
statements.push(convertServiceDefinition(astService as any));
}
}
const includes: string[] = [];
if (statementGroup[t.SyntaxType.IncludeDefinition]) {
for (const astInclude of statementGroup[t.SyntaxType.IncludeDefinition]) {
includes.push((astInclude as t.IncludeDefinition).path.value);
}
}
const unifyDocument: UnifyDocument = {
type: SyntaxType.UnifyDocument,
namespace,
unifyNamespace,
includes,
statements,
includeRefer: {},
};
return unifyDocument;
}

View File

@@ -0,0 +1,407 @@
/*
* 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 FieldExtensionConfig,
type ServiceExtensionConfig,
type FunctionExtensionConfig,
} from '../common/extension_type';
export * from '../common/extension_type';
export interface Node {
type: SyntaxType;
}
export interface SyntaxNode extends Node {
loc?: TextLocation;
}
export interface StructLike {
name: Identifier;
fields: Array<FieldDefinition>;
annotations?: Annotations;
comments: Array<Comment>;
loc: TextLocation;
}
export interface TextLocation {
start: TextPosition;
end: TextPosition;
}
export interface TextPosition {
line: number;
column: number;
index: number;
}
export interface Token extends SyntaxNode {
text: string;
}
export interface UnifyDocument extends Node {
type: SyntaxType.UnifyDocument;
statements: UnifyStatement[];
namespace: string;
unifyNamespace: string;
includes: string[];
includeRefer: Record<string, string>;
}
export type UnifyStatement =
| EnumDefinition
| StructDefinition
| TypedefDefinition
| ConstDefinition
| ServiceDefinition;
export type CommentType = SyntaxType.CommentLine | SyntaxType.CommentBlock;
export type Comment = CommentLine | CommentBlock;
export interface CommentLine extends SyntaxNode {
type: SyntaxType.CommentLine;
value: string;
}
export interface CommentBlock extends SyntaxNode {
type: SyntaxType.CommentBlock;
value: Array<string>;
}
export interface Annotations extends SyntaxNode {
annotations: Array<Annotation>;
}
export interface Annotation extends SyntaxNode {
name: Identifier;
value?: StringLiteral;
}
export interface PrimarySyntax extends SyntaxNode {
comments: Array<Comment>;
}
export type FieldType = BaseType | ContainerType | Identifier;
export type FunctionType = FieldType | VoidType;
export type KeywordType =
| SyntaxType.StringKeyword
| SyntaxType.DoubleKeyword
| SyntaxType.BoolKeyword
| SyntaxType.I8Keyword
| SyntaxType.I16Keyword
| SyntaxType.I32Keyword
| SyntaxType.I64Keyword
| SyntaxType.BinaryKeyword
| SyntaxType.ByteKeyword;
export interface VoidType extends SyntaxNode {
type: SyntaxType.VoidKeyword;
}
export type ContainerType = SetType | MapType | ListType;
export interface BaseType extends SyntaxNode {
type: KeywordType;
annotations?: Annotations;
}
export interface SetType extends SyntaxNode {
type: SyntaxType.SetType;
valueType: FieldType;
annotations?: Annotations;
}
export interface ListType extends SyntaxNode {
type: SyntaxType.ListType;
valueType: FieldType;
annotations?: Annotations;
}
export interface MapType extends SyntaxNode {
type: SyntaxType.MapType;
keyType: FieldType;
valueType: FieldType;
annotations?: Annotations;
}
export type ConstValue =
| StringLiteral
| IntConstant
| DoubleConstant
| BooleanLiteral
| ConstMap
| ConstList
| Identifier;
export interface ConstDefinition extends PrimarySyntax {
type: SyntaxType.ConstDefinition;
name: Identifier;
fieldType: FieldType;
initializer: ConstValue;
annotations?: Annotations;
}
export type FieldRequired = 'required' | 'optional';
export interface InterfaceWithFields extends PrimarySyntax {
name: Identifier;
fields: Array<FieldDefinition>;
annotations?: Annotations;
options?: Record<string, string>;
nested?: Record<string, EnumDefinition | StructDefinition>;
}
export interface StructDefinition extends InterfaceWithFields {
type: SyntaxType.StructDefinition;
}
export interface FieldDefinition extends PrimarySyntax {
type: SyntaxType.FieldDefinition;
name: Identifier;
fieldID: FieldID | null;
fieldType: FunctionType;
requiredness?: FieldRequired | null;
defaultValue?: ConstValue | null;
annotations?: Annotations;
options?: Record<string, string>;
extensionConfig?: FieldExtensionConfig;
}
export interface FieldID extends SyntaxNode {
type: SyntaxType.FieldID;
value: number;
}
export interface EnumDefinition extends PrimarySyntax {
type: SyntaxType.EnumDefinition;
name: Identifier;
members: Array<EnumMember>;
annotations?: Annotations;
options?: Record<string, string>;
}
export interface EnumMember extends PrimarySyntax {
type: SyntaxType.EnumMember;
name: Identifier;
initializer?: IntConstant | null;
annotations?: Annotations;
}
export interface TypedefDefinition extends PrimarySyntax {
type: SyntaxType.TypedefDefinition;
name: Identifier;
definitionType: FieldType;
annotations?: Annotations;
}
export interface ServiceDefinition extends PrimarySyntax {
type: SyntaxType.ServiceDefinition;
name: Identifier;
extends?: Identifier | null;
functions: Array<FunctionDefinition>;
annotations?: Annotations;
options?: Record<string, string>;
extensionConfig?: ServiceExtensionConfig;
}
export interface FunctionDefinition extends PrimarySyntax {
type: SyntaxType.FunctionDefinition;
name: Identifier;
oneway: boolean;
returnType: FunctionType;
fields: Array<FieldDefinition>;
throws: Array<FieldDefinition>;
modifiers: Array<Token>;
annotations?: Annotations;
options?: Record<string, string>;
extensionConfig?: FunctionExtensionConfig;
}
export interface ParametersDefinition extends SyntaxNode {
type: SyntaxType.ParametersDefinition;
fields: Array<FieldDefinition>;
}
export interface StringLiteral extends SyntaxNode {
type: SyntaxType.StringLiteral;
value: string;
}
export interface BooleanLiteral extends SyntaxNode {
type: SyntaxType.BooleanLiteral;
value: boolean;
}
export interface IntegerLiteral extends SyntaxNode {
type: SyntaxType.IntegerLiteral;
value: string;
}
export interface HexLiteral extends SyntaxNode {
type: SyntaxType.HexLiteral;
value: string;
}
export interface FloatLiteral extends SyntaxNode {
type: SyntaxType.FloatLiteral;
value: string;
}
export interface ExponentialLiteral extends SyntaxNode {
type: SyntaxType.ExponentialLiteral;
value: string;
}
export interface IntConstant extends SyntaxNode {
type: SyntaxType.IntConstant;
value: IntegerLiteral | HexLiteral;
}
export interface DoubleConstant extends SyntaxNode {
type: SyntaxType.DoubleConstant;
value: FloatLiteral | ExponentialLiteral;
}
export interface ConstMap extends SyntaxNode {
type: SyntaxType.ConstMap;
properties: Array<PropertyAssignment>;
}
export interface ConstList extends SyntaxNode {
type: SyntaxType.ConstList;
elements: Array<ConstValue>;
}
export interface PropertyAssignment extends SyntaxNode {
type: SyntaxType.PropertyAssignment;
name: ConstValue;
initializer: ConstValue;
}
export interface Identifier extends SyntaxNode {
type: SyntaxType.Identifier;
value: string;
// value with one level namespace
namespaceValue?: string;
annotations?: Annotations;
}
export enum SyntaxType {
UnifyDocument = 'UnifyDocument',
Identifier = 'Identifier',
FieldID = 'FieldID',
// Statements
ConstDefinition = 'ConstDefinition',
StructDefinition = 'StructDefinition',
EnumDefinition = 'EnumDefinition',
ServiceDefinition = 'ServiceDefinition',
TypedefDefinition = 'TypedefDefinition',
// Fields
FieldDefinition = 'FieldDefinition',
FunctionDefinition = 'FunctionDefinition',
ParametersDefinition = 'ParametersDefinition',
// Type Annotations
FieldType = 'FieldType',
BaseType = 'BaseType',
SetType = 'SetType',
MapType = 'MapType',
ListType = 'ListType',
// Values
ConstValue = 'ConstValue',
IntConstant = 'IntConstant',
DoubleConstant = 'DoubleConstant',
ConstList = 'ConstList',
ConstMap = 'ConstMap',
EnumMember = 'EnumMember',
// Literals
CommentLine = 'CommentLine',
CommentBlock = 'CommentBlock',
StringLiteral = 'StringLiteral',
IntegerLiteral = 'IntegerLiteral',
FloatLiteral = 'FloatLiteral',
HexLiteral = 'HexLiteral',
ExponentialLiteral = 'ExponentialLiteral',
BooleanLiteral = 'BooleanLiteral',
PropertyAssignment = 'PropertyAssignment',
// Tokens
LeftParenToken = 'LeftParenToken',
RightParenToken = 'RightParenToken',
LeftBraceToken = 'LeftBraceToken',
RightBraceToken = 'RightBraceToken',
LeftBracketToken = 'LeftBracketToken',
RightBracketToken = 'RightBracketToken',
CommaToken = 'CommaToken',
DotToken = 'DotToken',
MinusToken = 'MinusToken',
SemicolonToken = 'SemicolonToken',
ColonToken = 'ColonToken',
StarToken = 'StarToken',
EqualToken = 'EqualToken',
LessThanToken = 'LessThanToken',
GreaterThanToken = 'GreaterThanToken',
// Keywords
NamespaceKeyword = 'NamespaceKeyword',
IncludeKeyword = 'IncludeKeyword',
CppIncludeKeyword = 'CppIncludeKeyword',
ExceptionKeyword = 'ExceptionKeyword',
ServiceKeyword = 'ServiceKeyword',
ExtendsKeyword = 'ExtendsKeyword',
RequiredKeyword = 'RequiredKeyword',
OptionalKeyword = 'OptionalKeyword',
FalseKeyword = 'FalseKeyword',
TrueKeyword = 'TrueKeyword',
ConstKeyword = 'ConstKeyword',
DoubleKeyword = 'DoubleKeyword',
StructKeyword = 'StructKeyword',
TypedefKeyword = 'TypedefKeyword',
UnionKeyword = 'UnionKeyword',
StringKeyword = 'StringKeyword',
BinaryKeyword = 'BinaryKeyword',
BoolKeyword = 'BoolKeyword',
ByteKeyword = 'ByteKeyword',
EnumKeyword = 'EnumKeyword',
SenumKeyword = 'SenumKeyword',
ListKeyword = 'ListKeyword',
SetKeyword = 'SetKeyword',
MapKeyword = 'MapKeyword',
I8Keyword = 'I8Keyword',
I16Keyword = 'I16Keyword',
I32Keyword = 'I32Keyword',
I64Keyword = 'I64Keyword',
ThrowsKeyword = 'ThrowsKeyword',
VoidKeyword = 'VoidKeyword',
OnewayKeyword = 'OnewayKeyword',
// Other
Annotation = 'Annotation',
Annotations = 'Annotations',
EOF = 'EOF',
}

View File

@@ -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 { SyntaxType, type ContainerType, type MapType, type FieldType } from './type';
export function convertIntToString(fType: FieldType): FieldType {
const fieldType = { ...fType };
const intTypes = [
SyntaxType.I8Keyword,
SyntaxType.I16Keyword,
SyntaxType.I32Keyword,
SyntaxType.I64Keyword,
];
if (intTypes.includes(fieldType.type)) {
fieldType.type = SyntaxType.StringKeyword;
} else if ((fieldType as ContainerType).valueType) {
(fieldType as ContainerType).valueType = convertIntToString(
(fieldType as ContainerType).valueType,
);
if ((fieldType as MapType).keyType) {
(fieldType as MapType).keyType = convertIntToString(
(fieldType as MapType).keyType,
);
}
}
return fieldType;
}

View File

@@ -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 { logger } from '@coze-arch/rush-logger';
export const logAndThrowError = (logMessage: string, errorMessage?: string) => {
logger.error(logMessage);
const message = errorMessage || logMessage;
throw new Error(message);
};
export function mergeObject(
target: { [key: string]: any },
...sources: { [key: string]: any }[]
): { [key: string]: any } {
const newObj = { ...target };
if (!sources) {return newObj;}
for (const source of sources) {
for (const key of Object.keys(source)) {
if (typeof source[key] !== 'undefined') {
newObj[key] = source[key];
}
}
}
return newObj;
}
export function getPosixPath(filePath: string) {
return filePath.replace(/\\/g, '/');
}

View File

@@ -0,0 +1,28 @@
{
"extends": "@coze-arch/ts-config/tsconfig.node.json",
"$schema": "https://json.schemastore.org/tsconfig",
"compilerOptions": {
"outDir": "dist",
"rootDir": "src",
"module": "CommonJS",
"target": "ES2020",
"moduleResolution": "node",
"tsBuildInfoFile": "dist/tsconfig.build.tsbuildinfo"
},
"include": ["src"],
"exclude": ["node_modules", "dist"],
"references": [
{
"path": "../../../config/eslint-config/tsconfig.build.json"
},
{
"path": "../../../config/ts-config/tsconfig.build.json"
},
{
"path": "../../../config/vitest-config/tsconfig.build.json"
},
{
"path": "../../utils/rush-logger/tsconfig.build.json"
}
]
}

View File

@@ -0,0 +1,15 @@
{
"$schema": "https://json.schemastore.org/tsconfig",
"exclude": ["**/*"],
"compilerOptions": {
"composite": true
},
"references": [
{
"path": "./tsconfig.build.json"
},
{
"path": "./tsconfig.misc.json"
}
]
}

View File

@@ -0,0 +1,18 @@
{
"extends": "@coze-arch/ts-config/tsconfig.node.json",
"$schema": "https://json.schemastore.org/tsconfig",
"compilerOptions": {
"rootDir": "./",
"outDir": "./dist",
"module": "CommonJS",
"target": "ES2020",
"moduleResolution": "node"
},
"include": ["__tests__", "vitest.config.ts"],
"exclude": ["./dist"],
"references": [
{
"path": "./tsconfig.build.json"
}
]
}

View File

@@ -0,0 +1,22 @@
/*
* 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 { defineConfig } from '@coze-arch/vitest-config';
export default defineConfig({
dirname: __dirname,
preset: 'node',
});

View File

@@ -0,0 +1,63 @@
# @coze-arch/idl2ts-cli
@coze-arch/idl2ts-cli
## Overview
This package is part of the Coze Studio monorepo and provides architecture functionality. It serves as a core component in the Coze ecosystem.
## Getting Started
### Installation
Add this package to your `package.json`:
```json
{
"dependencies": {
"@coze-arch/idl2ts-cli": "workspace:*"
}
}
```
Then run:
```bash
rush update
```
### Usage
```typescript
import { /* exported functions/components */ } from '@coze-arch/idl2ts-cli';
// Example usage
// TODO: Add specific usage examples
```
## Features
- Core functionality for Coze Studio
- TypeScript support
- Modern ES modules
## API Reference
Please refer to the TypeScript definitions for detailed API documentation.
## Development
This package is built with:
- TypeScript
- Modern JavaScript
- Vitest for testing
- ESLint for code quality
## Contributing
This package is part of the Coze Studio monorepo. Please follow the monorepo contribution guidelines.
## License
Apache-2.0

View File

@@ -0,0 +1,12 @@
{
"operationSettings": [
{
"operationName": "test:cov",
"outputFolderNames": ["coverage"]
},
{
"operationName": "ts-check",
"outputFolderNames": ["dist"]
}
]
}

View File

@@ -0,0 +1,15 @@
const { defineConfig } = require('@coze-arch/eslint-config');
module.exports = defineConfig({
packageRoot: __dirname,
preset: 'node',
rules: {
'@typescript-eslint/naming-convention': 'off',
'unicorn/filename-case': 'off',
'max-statements-per-line': 'off',
'max-lines': 'off',
'@typescript-eslint/no-explicit-any': 'off',
'@coze-arch/max-line-per-function': 'off',
'@typescript-eslint/consistent-type-assertions': 'off',
},
});

View File

@@ -0,0 +1,38 @@
{
"name": "@coze-arch/idl2ts-cli",
"version": "0.1.7",
"description": "@coze-arch/idl2ts-cli",
"homepage": "",
"license": "Apache-2.0",
"author": "fanwenjie.fe@bytedance.com",
"bin": {
"idl2ts": "./src/cli.js"
},
"scripts": {
"build": "exit 0",
"lint": "eslint ./ --cache",
"test": "vitest --run --passWithNoTests",
"test:cov": "npm run test -- --coverage"
},
"dependencies": {
"@babel/types": "^7.20.7",
"@coze-arch/idl2ts-generator": "workspace:*",
"@coze-arch/idl2ts-helper": "workspace:*",
"@coze-arch/idl2ts-plugin": "workspace:*",
"@faker-js/faker": "~9.3.0",
"commander": "^12.0.0",
"dayjs": "^1.11.7",
"ora": "^5.3.1",
"prettier": "~3.3.3"
},
"devDependencies": {
"@coze-arch/eslint-config": "workspace:*",
"@coze-arch/ts-config": "workspace:*",
"@coze-arch/vitest-config": "workspace:*",
"@types/node": "^18",
"@vitest/coverage-v8": "~3.0.5",
"sucrase": "^3.32.0",
"tsx": "^4.19.2",
"vitest": "~3.0.5"
}
}

View File

@@ -0,0 +1,139 @@
/*
* 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 { genClient } from '@coze-arch/idl2ts-generator';
import { lookupConfig } from './utils';
import { type ApiConfig, type ApiTypeConfig } from './types';
import { MockPlugin } from './plugins/mock-plugin';
import { LocalConfigPlugin } from './plugins/local-config';
import { FormatPlugin } from './plugins/formatter';
import { FilterTypesPlugin } from './plugins/filter-types-plugin';
import { AliasPlugin } from './plugins/alias';
interface GenOptions {
formatConfig?: string;
}
export const gen = (projectRoot: string, options: GenOptions) => {
const configs = lookupConfig(projectRoot);
function genSingle(config: ApiConfig) {
const {
entries,
plugins = [],
commonCodePath,
aggregationExport,
formatter,
} = config;
const aliasMap = new Map();
const idlRoot = path.resolve(projectRoot, config.idlRoot);
const output = path.resolve(projectRoot, config.output);
const realEntries = [] as string[];
Object.keys(entries).forEach(i => {
aliasMap.set(path.resolve(idlRoot, entries[i]), i);
realEntries.push(entries[i]);
});
genClient({
entries: realEntries,
idlRoot: path.resolve(projectRoot, idlRoot),
genSchema: false,
genClient: true,
genMock: false,
plugins: [
new MockPlugin(),
new AliasPlugin(aliasMap),
new FormatPlugin({
path: path.resolve(
process.cwd(),
options.formatConfig || '.prettierrc',
),
formatter,
}),
new LocalConfigPlugin({ outputDir: output, projectRoot, idlRoot }),
...plugins,
],
entryName: aggregationExport || 'index',
outputDir: output,
commonCodePath,
});
}
configs.forEach(c => {
genSingle(c);
});
};
export const genTypes = (projectRoot: string, options: GenOptions) => {
const configs = lookupConfig<ApiTypeConfig>(projectRoot, 'api.filter.js');
function genSingle(config: ApiTypeConfig) {
const {
entries,
plugins = [],
commonCodePath,
aggregationExport,
formatter,
filters,
} = config;
const aliasMap = new Map();
const idlRoot = path.resolve(projectRoot, config.idlRoot);
const output = path.resolve(projectRoot, config.output);
const realEntries = [] as string[];
Object.keys(entries).forEach(i => {
aliasMap.set(path.resolve(idlRoot, entries[i]), i);
realEntries.push(entries[i]);
});
genClient({
entries: realEntries,
idlRoot: path.resolve(projectRoot, idlRoot),
genSchema: false,
genClient: true,
genMock: false,
plugins: [
new MockPlugin(),
new AliasPlugin(aliasMap),
new FormatPlugin({
path: path.resolve(
process.cwd(),
options.formatConfig || '.prettierrc',
),
formatter,
}),
new LocalConfigPlugin({ outputDir: output, projectRoot, idlRoot }),
new FilterTypesPlugin(filters, output),
...plugins,
],
entryName: aggregationExport || 'index',
outputDir: output,
commonCodePath,
});
}
configs.forEach(c => {
genSingle(c);
});
};
export function defineConfig(c: ApiConfig[]) {
return c;
}
export function defineApiTpeConfig(c: ApiTypeConfig[]) {
return c;
}

View File

@@ -0,0 +1,3 @@
require('sucrase/register/ts');
require('./cli.ts');

View File

@@ -0,0 +1,79 @@
/*
* 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 ora from 'ora';
import { Command } from 'commander';
import { gen, genTypes } from './actions';
const main = () => {
const program = new Command();
program
.command('gen')
.description('gen api code by thrift or pb')
.argument('<projectRoot>', 'project root')
.option(
'-f --format-config <formatConfig>',
'prettier config file',
'.prettierrc',
)
.action(
(projectRoot, options: { genMock: boolean; formatConfig: string }) => {
const spinner = ora(
'Generating api. It may take a few seconds',
).start();
try {
gen(projectRoot, {
formatConfig: options.formatConfig,
});
spinner.succeed('Generate api successfully');
} catch (error) {
spinner.fail('Generate api fail');
console.error(error);
process.exit(1);
}
},
);
program
.command('filter')
.description('filter api types')
.argument('<projectRoot>', 'project root')
.option(
'-f --format-config <formatConfig>',
'prettier config file',
'.prettierrc',
)
.action((projectRoot, options: { formatConfig: string }) => {
const spinner = ora(
'Generating filtered types. It may take a few seconds',
).start();
try {
genTypes(projectRoot, options);
spinner.succeed('Generate filtered types successfully');
} catch (error) {
spinner.fail('Generate filtered types fail');
console.error(error);
process.exit(1);
}
});
program.parse(process.argv);
};
main();

View File

@@ -0,0 +1,105 @@
/*
* 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 ApiConfig } from './types';
let hasShowHint = false;
function requiredWithoutCache(src, onError?) {
let data;
// eslint-disable-next-line @typescript-eslint/no-require-imports
const { Module } = require('module');
try {
// disable 了 require 的缓存,这样可以改变了 mock 数据后,无需重启服务。
const originCache = Module._cache;
Module._cache = {};
// eslint-disable-next-line security/detect-non-literal-require, @typescript-eslint/no-require-imports
data = require(src);
Module._cache = originCache;
} catch (error) {
console.error(error);
if (onError) {
onError(error);
} else {
console.error(error);
}
}
return data;
}
export function createProxy({
root,
handleResponseData,
}: {
root: string;
handleResponseData?: (service: string, method: string, data: any) => any;
}) {
// eslint-disable-next-line security/detect-non-literal-require, @typescript-eslint/no-require-imports
const apiConfig = require(path.resolve(root, 'api.config.js')) as ApiConfig[];
// eslint-disable-next-line max-params
return async function proxyResWithMock(_, __, req, resp) {
if (!req.headers['x-svc-method']) {
return Promise.resolve();
}
const config = requiredWithoutCache(
path.resolve(root, './api.dev.local.js'),
() => {
if (!hasShowHint) {
console.warn(
'can not find mock config, please run "gen-api" command if you want to mock',
);
hasShowHint = true;
}
},
);
if (
config &&
config.mock.includes(req.headers['x-svc-method'].split('_').join('.'))
) {
const [svc, method] = req.headers['x-svc-method'].split('_');
const target = apiConfig.find(i => i.entries[svc].length > 0);
if (!target) {
return Promise.resolve();
}
const src = path.resolve(
root,
target.output,
target.entries[svc].replace(/\.(thrift|proto)$/, '.mock.js'),
);
const data = requiredWithoutCache(src);
if (data) {
try {
if (resp) {
resp.statusCode = 200;
resp.setHeader('Content-Type', 'application/json');
} else {
console.warn('resp is not defined');
}
const res = await data[svc][method].res(req);
if (handleResponseData) {
return await handleResponseData(svc, method, res);
}
return res;
} catch (error) {
return error;
}
}
}
return Promise.resolve();
};
}

View File

@@ -0,0 +1,59 @@
/*
* 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, after } from '@coze-arch/idl2ts-plugin';
import {
type IParseEntryCtx,
isServiceDefinition,
} from '@coze-arch/idl2ts-helper';
import { HOOK } from '@coze-arch/idl2ts-generator';
interface IOptions {
patch: {
[service: string]: {
prefix?: string;
method?: { [name: string]: 'GET' | 'POST' };
};
};
}
export class PatchPlugin implements IPlugin {
private options: IOptions;
constructor(options: IOptions) {
this.options = options;
}
apply(p: Program) {
p.register(after(HOOK.PARSE_ENTRY), (ctx: IParseEntryCtx) => {
ctx.ast = ctx.ast.map(i => {
i.statements.map(s => {
if (isServiceDefinition(s) && this.options.patch[s.name.value]) {
const { prefix = '/', method = {} } =
this.options.patch[s.name.value];
s.functions.forEach(f => {
f.extensionConfig = {
uri: `${prefix}/${f.name.value}`,
method: method[f.name.value] || 'POST',
};
});
}
return s;
});
return i;
});
return ctx;
});
}
}

View File

@@ -0,0 +1,17 @@
/*
* 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 { PatchPlugin } from './forward';

View File

@@ -0,0 +1,47 @@
/*
* 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, on } from '@coze-arch/idl2ts-plugin';
import {
type IParseEntryCtx,
isServiceDefinition,
} from '@coze-arch/idl2ts-helper';
import { HOOK } from '@coze-arch/idl2ts-generator';
export class AliasPlugin {
alias = new Map();
constructor(alias: Map<string, string>) {
this.alias = alias;
}
apply(program: Program) {
program.register(on(HOOK.PARSE_ENTRY), this.setAlias.bind(this));
}
setAlias(ctx: IParseEntryCtx) {
ctx.ast.forEach(i => {
if (i.isEntry) {
i.statements.forEach(s => {
if (isServiceDefinition(s) && this.alias.has(i.idlPath)) {
s.name.value = this.alias.get(i.idlPath);
}
});
}
});
return ctx;
}
}

View File

@@ -0,0 +1,72 @@
/*
* 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, after } from '@coze-arch/idl2ts-plugin';
import { type IParseEntryCtx } from '@coze-arch/idl2ts-helper';
import { HOOK } from '@coze-arch/idl2ts-generator';
/**
*
* @param {string} content
* @param {"CommentBlock" | "CommentLine"} type
* @returns {{
* value: string;
* type: "CommentBlock" | "CommentLine";
* }}
*/
function createComment(content, type = 'CommentBlock') {
return {
value: content,
type,
};
}
export class CommentPlugin implements IPlugin {
config: { comments: string[] };
comments: any[] = [];
/**
* @param {{comments: string[]}} config
*/
constructor(config) {
this.config = config;
}
apply(program: Program) {
program.register(after(HOOK.GEN_FILE_AST), this.addComment.bind(this));
}
addComment(ctx: IParseEntryCtx) {
const { files } = ctx;
for (const [file, res] of files.entries()) {
if (
res.type === 'babel' &&
file.includes('/auto-gen/') &&
file.endsWith('.ts')
) {
res.content.leadingComments = this.getComments();
}
}
return ctx;
}
getComments() {
if (this.comments) {
return this.comments;
}
this.comments = this.config.comments.map(i => createComment(i));
return this.comments;
}
}

View File

@@ -0,0 +1,230 @@
/*
* 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 Program, after } from '@coze-arch/idl2ts-plugin';
import {
type Comment,
type EnumDefinition,
type FieldDefinition,
type FieldType,
type FunctionType,
type IParseEntryCtx,
type IParseResultItem,
type Identifier,
type ProcessIdlCtx,
type StructDefinition,
SyntaxType,
type UnifyStatement,
createFile,
findDefinition,
getParseResultFromNamespace,
getStatementById,
getValuesFromEnum,
isBaseType,
isEnumDefinition,
isIdentifier,
isListType,
isMapType,
isServiceDefinition,
isSetType,
isStructDefinition,
parseIdFiledType,
withExportDeclaration,
} from '@coze-arch/idl2ts-helper';
import { HOOK } from '@coze-arch/idl2ts-generator';
// eslint-disable-next-line @coze-arch/no-batch-import-or-export
import * as t from '@babel/types';
export class FilterTypesPlugin {
methods: Record<string, string[]>;
statements: Record<string, UnifyStatement[]> = {};
enums: Record<string, EnumDefinition[]> = {};
output: string;
constructor(methods: Record<string, string[]>, output: string) {
this.methods = methods;
this.output = output;
}
apply(program: Program) {
program.register(after(HOOK.PARSE_ENTRY), this.filterTypes.bind(this));
program.register(
after(HOOK.PROCESS_IDL_AST),
this.genEnumsFiles.bind(this),
);
}
genEnumsFiles(ctx: ProcessIdlCtx) {
const file = createFile('');
Object.keys(this.enums).forEach(key => {
const defs = this.enums[key];
const block = t.tsModuleBlock([]);
defs.forEach(d => {
const values = getValuesFromEnum(d);
const objExps = d.members.map((m, index) => {
const valueProps = t.objectProperty(
t.identifier('value'),
t.numericLiteral(values[index]),
);
t.addComment(valueProps, 'trailing', 'm.name.value');
return t.objectExpression([
valueProps,
t.objectProperty(
t.identifier('label'),
t.stringLiteral(m.name.value),
),
]);
});
const constNode = t.variableDeclaration('const', [
t.variableDeclarator(
t.identifier(d.name.value),
t.arrayExpression(objExps),
),
]);
block.body.push(withExportDeclaration(constNode));
});
const exportNode = withExportDeclaration(
t.tsModuleDeclaration(t.identifier(key), block),
);
file.program.body.push(exportNode);
});
ctx.output.set(path.resolve(this.output, 'enums.ts'), {
type: 'babel',
content: file,
});
return ctx;
}
filterTypes(ctx: IParseEntryCtx) {
ctx.ast.forEach(i => {
if (i.isEntry) {
i.statements.forEach(s => {
if (isServiceDefinition(s) && this.methods[s.name.value]) {
for (const f of s.functions) {
if (this.methods[s.name.value].includes(f.name.value)) {
if (isIdentifier(f.returnType)) {
this.lookupTypes(f.returnType, i);
}
const fieldType = f.fields[0]?.fieldType;
if (isIdentifier(fieldType)) {
this.lookupTypes(fieldType, i);
}
}
}
}
});
}
});
ctx.ast = ctx.ast
.filter(i => this.statements[i.idlPath])
.map(i => ({ ...i, statements: this.statements[i.idlPath] }));
return ctx;
}
private lookupTypes(id: Identifier, current: IParseResultItem) {
const { namespace, refName } = parseIdFiledType(id);
if (namespace) {
const next = getParseResultFromNamespace(namespace, current);
const nextID = findDefinition(next.statements, refName);
if (nextID) {
this.lookupTypes(nextID.name, next);
}
} else {
const statement = getStatementById(id, current);
if (statement) {
if (this.statements[current.idlPath]) {
if (!this.statements[current.idlPath].includes(statement)) {
this.statements[current.idlPath].push(statement);
}
} else {
this.statements[current.idlPath] = [statement];
}
if (isStructDefinition(statement)) {
this.lookupStructTypes(statement, current);
}
}
}
}
private lookupStructTypes(
statement: StructDefinition,
current: IParseResultItem,
) {
for (const field of statement.fields) {
const { fieldType } = field;
this.processFiledType(fieldType, current, field);
}
}
private processFiledType(
fieldType: FieldType | FunctionType,
current: IParseResultItem,
field: FieldDefinition,
) {
if (isBaseType(fieldType)) {
return;
} else if (isListType(fieldType) || isSetType(fieldType)) {
const { valueType } = fieldType;
return this.processFiledType(valueType, current, field);
} else if (isMapType(fieldType)) {
const { valueType } = fieldType;
return this.processFiledType(valueType, current, field);
} else if (isIdentifier(fieldType)) {
const statement = getStatementById(fieldType, current);
if (isEnumDefinition(statement)) {
// 强制转位 number
// @ts-expect-error fixme late
fieldType.type = SyntaxType.I32Keyword;
let namespace = current.unifyNamespace;
const parsedFieldType = parseIdFiledType(fieldType);
if (parsedFieldType.namespace) {
const next = getParseResultFromNamespace(
parsedFieldType.namespace,
current,
);
namespace = next.unifyNamespace;
}
const extraComment = {
type: SyntaxType.CommentLine,
value: `@see${fieldType.value}`,
} as Comment;
if (field.comments) {
field.comments.push(extraComment);
} else {
field.comments = [extraComment];
}
if (this.enums[namespace]) {
if (
this.enums[namespace].some(
i => i.name.value === statement.name.value,
)
) {
return;
}
this.enums[namespace].push(statement);
} else {
this.enums[namespace] = [statement];
}
return;
} else {
return this.lookupTypes(fieldType, current);
}
}
throw new Error(`unknown type:${fieldType.type}`);
}
}

View File

@@ -0,0 +1,83 @@
/*
* 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 fs from 'fs';
import { format } from 'prettier';
import { type Program, on } from '@coze-arch/idl2ts-plugin';
import { HOOK, type WriteFileCtx } from '@coze-arch/idl2ts-generator';
interface IOption {
path: string;
formatter?: (content: string, filename: string) => string;
}
function isPromise(p: any) {
return (
p.then &&
typeof p.then === 'function' &&
typeof p.catch === 'function' &&
typeof p.finally === 'function'
);
}
function readConfig(file: string) {
let config = {};
try {
// eslint-disable-next-line security/detect-non-literal-require, @typescript-eslint/no-require-imports
config = require(file);
if (!config) {
const content = fs.readFileSync(file, { encoding: 'utf8' });
config = JSON.parse(content);
}
// eslint-disable-next-line @coze-arch/no-empty-catch
} catch (error) {
// just
}
return config;
}
export class FormatPlugin {
private config: any;
private formatter?: (content: string, filename: string) => string;
constructor(op: IOption) {
this.config = readConfig(op.path);
this.formatter = op.formatter;
}
apply(program: Program) {
program.register(on(HOOK.WRITE_FILE), this.format.bind(this));
}
format(ctx: WriteFileCtx) {
if (this.formatter) {
ctx.content = this.formatter(ctx.content, ctx.filename);
return ctx;
}
if (ctx.filename.endsWith('ts')) {
try {
const content = format(ctx.content, {
...this.config,
parser: 'typescript',
});
if (!isPromise(content)) {
// @ts-expect-error fixme late
ctx.content = content;
}
} catch (error) {
console.warn(error);
}
}
return ctx;
}
}

View File

@@ -0,0 +1,61 @@
/*
* 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, after } from '@coze-arch/idl2ts-plugin';
import { type IParseEntryCtx } from '@coze-arch/idl2ts-helper';
import { HOOK } from '@coze-arch/idl2ts-generator';
interface Config {
idlRoot: string;
outputDir: string;
projectRoot: string;
}
export class LocalConfigPlugin implements IPlugin {
config: Config;
/**
* @param {} config
*/
constructor(config: Config) {
this.config = config;
}
apply(program: Program) {
program.register(after(HOOK.GEN_FILE_AST), this.genLocalConfig.bind(this));
}
genLocalConfig(ctx: IParseEntryCtx) {
const mockFile = { mock: [] };
const target = path.resolve(this.config.projectRoot, './api.dev.local.js');
try {
// eslint-disable-next-line security/detect-non-literal-require, @typescript-eslint/no-require-imports
const local_config = require(target);
mockFile.mock = local_config.mock || [];
// eslint-disable-next-line @coze-arch/no-empty-catch, no-empty
} catch (error) {}
const content = `
module.exports = {
mock:[${mockFile.mock.map(i => `"${i}"`).join(', ')}],
}
`;
ctx.files.set(target, { type: 'text', content });
return ctx;
}
}

View File

@@ -0,0 +1,101 @@
/*
* 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 dayjs from 'dayjs';
import { faker } from '@faker-js/faker';
import { type IPlugin, type Program, before } from '@coze-arch/idl2ts-plugin';
import {
type IntConstant,
isBaseType,
SyntaxType,
getBaseTypeConverts,
} from '@coze-arch/idl2ts-helper';
import { type GenMockFieldCtx, HOOK } from '@coze-arch/idl2ts-generator';
// eslint-disable-next-line @coze-arch/no-batch-import-or-export
import * as t from '@babel/types';
const NumMapper = {
total: 1,
code: 0,
};
const StrMapper = {
name: faker.person.lastName(),
};
export class MockPlugin implements IPlugin {
apply(program: Program) {
program.register(before(HOOK.GEN_MOCK_FILED), this.genMockValue.bind(this));
}
// eslint-disable-next-line complexity
genMockValue = (ctx: GenMockFieldCtx) => {
const { context, fieldType, defaultValue } = 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 as any).value;
}
if (context) {
const { fieldDefinition } = context;
const fieldName = fieldDefinition.name.value;
// 各类 ID
if (fieldName.toLocaleUpperCase().endsWith('ID')) {
value = String(faker.number.int());
}
// email 处理
if (fieldName.includes('Email')) {
value = `${faker.person.lastName()}@foo.com`;
}
// 直接映射值
value = StrMapper[fieldName] || value;
}
ctx.output = t.stringLiteral(value);
} else if (type === 'number') {
let value = faker.number.int({ min: 0, max: 10000 });
if (defaultValue && defaultValue.type === SyntaxType.IntConstant) {
value = Number((defaultValue as IntConstant).value.value);
}
if (context) {
const { fieldDefinition } = context;
const fieldName = fieldDefinition.name.value;
const formatName = fieldName.toLocaleUpperCase();
// 各类 ID
if (formatName.endsWith('ID')) {
value = faker.number.int();
}
// 时间戳
if (formatName.endsWith('TIME') || formatName.includes('TIMESTAMP')) {
value = dayjs(faker.date.anytime()).valueOf();
}
// 类型状态
if (formatName.endsWith('STATUS') || formatName.includes('TYPE')) {
value = faker.number.int({ min: 0, max: 1 });
}
// 直接映射值
const mapVal = NumMapper[fieldName];
value = typeof mapVal !== 'undefined' ? mapVal : value;
}
ctx.output = t.numericLiteral(value);
}
}
return ctx;
};
}

View File

@@ -0,0 +1,53 @@
/*
* 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 } from '@coze-arch/idl2ts-generator';
export interface ApiConfig {
// idl 入口
entries: Record<string, string>;
// idl 根目录
idlRoot: string;
// 服务别名
// 自定义 api 方法
commonCodePath: string;
// api 产物目录
output: string;
// 仓库信息设置
repository?: {
// 仓库地址
url: string;
// clone 到本地的位置
dest: string;
};
// 插件
plugins?: IPlugin[];
// 聚合导出的文件名
aggregationExport?: string;
// 格式化文件
formatter: (name: string, content: string) => string;
idlFetchConfig?: {
source: string;
branch?: string;
commit?: string;
rootDir?: string;
};
}
export interface ApiTypeConfig extends ApiConfig {
// 需要过滤的方法
filters: Record<string, string[]>;
}

View File

@@ -0,0 +1,33 @@
/*
* 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 ApiConfig } from './types';
export function lookupConfig<T = ApiConfig>(
projectRoot: string,
configName = 'api.config',
) {
const apiConfigPath = path.resolve(process.cwd(), projectRoot, configName);
try {
require.resolve(apiConfigPath);
} catch (error) {
throw Error(`Can not find api config in path ${process.cwd()}`);
}
// eslint-disable-next-line security/detect-non-literal-require, @typescript-eslint/no-require-imports
return require(apiConfigPath) as T[];
}

View File

@@ -0,0 +1,34 @@
{
"extends": "@coze-arch/ts-config/tsconfig.node.json",
"$schema": "https://json.schemastore.org/tsconfig",
"compilerOptions": {
"outDir": "dist",
"rootDir": "src",
"module": "CommonJS",
"target": "ES2020",
"moduleResolution": "node",
"tsBuildInfoFile": "dist/tsconfig.build.tsbuildinfo"
},
"include": ["src"],
"exclude": ["node_modules", "dist"],
"references": [
{
"path": "../../../config/eslint-config/tsconfig.build.json"
},
{
"path": "../../../config/ts-config/tsconfig.build.json"
},
{
"path": "../../../config/vitest-config/tsconfig.build.json"
},
{
"path": "../idl2ts-generator/tsconfig.build.json"
},
{
"path": "../idl2ts-helper/tsconfig.build.json"
},
{
"path": "../idl2ts-plugin/tsconfig.build.json"
}
]
}

View File

@@ -0,0 +1,15 @@
{
"$schema": "https://json.schemastore.org/tsconfig",
"exclude": ["**/*"],
"compilerOptions": {
"composite": true
},
"references": [
{
"path": "./tsconfig.build.json"
},
{
"path": "./tsconfig.misc.json"
}
]
}

View File

@@ -0,0 +1,18 @@
{
"extends": "@coze-arch/ts-config/tsconfig.node.json",
"$schema": "https://json.schemastore.org/tsconfig",
"compilerOptions": {
"rootDir": "./",
"outDir": "./dist",
"module": "CommonJS",
"target": "ES2020",
"moduleResolution": "node"
},
"include": ["__tests__", "vitest.config.ts"],
"exclude": ["./dist"],
"references": [
{
"path": "./tsconfig.build.json"
}
]
}

View File

@@ -0,0 +1,22 @@
/*
* 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 { defineConfig } from '@coze-arch/vitest-config';
export default defineConfig({
dirname: __dirname,
preset: 'node',
});

View File

@@ -0,0 +1,68 @@
# @coze-arch/idl2ts-generator
@coze-arch/idl2ts-generator
## Overview
This package is part of the Coze Studio monorepo and provides architecture functionality. It serves as a core component in the Coze ecosystem.
## Getting Started
### Installation
Add this package to your `package.json`:
```json
{
"dependencies": {
"@coze-arch/idl2ts-generator": "workspace:*"
}
}
```
Then run:
```bash
rush update
```
### Usage
```typescript
import { /* exported functions/components */ } from '@coze-arch/idl2ts-generator';
// Example usage
// TODO: Add specific usage examples
```
## Features
- Core functionality for Coze Studio
- TypeScript support
- Modern ES modules
## API Reference
### Exports
- `*`
For detailed API documentation, please refer to the TypeScript definitions.
## Development
This package is built with:
- TypeScript
- Modern JavaScript
- Vitest for testing
- ESLint for code quality
## Contributing
This package is part of the Coze Studio monorepo. Please follow the monorepo contribution guidelines.
## License
Apache-2.0

View File

@@ -0,0 +1,12 @@
{
"operationSettings": [
{
"operationName": "test:cov",
"outputFolderNames": ["coverage"]
},
{
"operationName": "ts-check",
"outputFolderNames": ["dist"]
}
]
}

View File

@@ -0,0 +1,17 @@
const { defineConfig } = require('@coze-arch/eslint-config');
module.exports = defineConfig({
packageRoot: __dirname,
preset: 'node',
rules: {
'@typescript-eslint/naming-convention': 'off',
'unicorn/filename-case': 'off',
'@coze-arch/no-batch-import-or-export': 'off',
'max-statements-per-line': 'off',
'max-lines': 'off',
'@typescript-eslint/no-explicit-any': 'off',
'@typescript-eslint/consistent-type-assertions': 'off',
'@coze-arch/max-line-per-function': 'off',
'@typescript-eslint/no-shadow': 'off',
},
});

View File

@@ -0,0 +1,47 @@
{
"name": "@coze-arch/idl2ts-generator",
"version": "0.1.6",
"description": "@coze-arch/idl2ts-generator",
"homepage": "",
"license": "Apache-2.0",
"author": "fanwenjie.fe@bytedance.com",
"main": "./src/index.ts",
"scripts": {
"build": "exit 0",
"lint": "eslint ./ --cache",
"test": "vitest --run --passWithNoTests",
"test:cov": "npm run test -- --coverage"
},
"dependencies": {
"@babel/core": "^7.20.2",
"@babel/generator": "^7",
"@babel/helpers": "^7.6.2",
"@babel/parser": "^7.12.14",
"@babel/plugin-transform-typescript": "^7.7.2",
"@babel/preset-typescript": "^7.7.2",
"@babel/template": "^7.6.0",
"@babel/traverse": "^7",
"@babel/types": "^7.20.7",
"@coze-arch/idl-parser": "workspace:*",
"@coze-arch/idl2ts-helper": "workspace:*",
"@coze-arch/idl2ts-plugin": "workspace:*",
"@faker-js/faker": "~9.3.0",
"ajv": "~8.12.0",
"camelcase": "^6.2.0",
"fs-extra": "^9.1.0",
"lodash": "^4.17.21"
},
"devDependencies": {
"@coze-arch/eslint-config": "workspace:*",
"@coze-arch/ts-config": "workspace:*",
"@coze-arch/vitest-config": "workspace:*",
"@types/fs-extra": "^9.0.5",
"@types/jssha": "^2.0.0",
"@types/lodash": "^4.14.137",
"@types/node": "^18",
"@types/yaml": "^1.2.0",
"@vitest/coverage-v8": "~3.0.5",
"tsx": "^4.19.2",
"vitest": "~3.0.5"
}
}

View File

@@ -0,0 +1,127 @@
/*
* 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 { SchemaObject, JSONSchemaType } from 'ajv';
import { type Ctxs } from '@coze-arch/idl2ts-plugin';
import {
type ProcessIdlCtx,
type IParseEntryCtx,
type IGenTemplateCtx,
type FieldType,
type FunctionType,
type ConstValue,
type FieldDefinition,
type StructDefinition,
type IParseResultItem,
type ServiceDefinition,
type FunctionDefinition,
type IMeta,
} from '@coze-arch/idl2ts-helper';
import type * as t from '@babel/types';
export * from '@coze-arch/idl2ts-plugin';
export enum HOOK {
PARSE_ENTRY = 'PARSE_ENTRY',
GEN_FILE_AST = 'GEN_FILE_AST',
PARSE_FUN_META = 'PARSE_FUN_META',
PARSE_FUN_META_ITEM = 'PARSE_FUN_META_ITEM',
PROCESS_IDL_AST = 'PROCESS_IDL_AST',
PROCESS_IDL_NODE = 'PROCESS_IDL_NODE',
GEN_FUN_TEMPLATE = 'GEN_FUN_TEMPLATE',
GEN_MOCK_FILED = 'GEN_MOCK_FILED',
WRITE_FILE = 'WRITE_FILE',
}
export type ListType = JSONSchemaType<any[]>;
export type StringType = JSONSchemaType<string>;
export type NumberType = JSONSchemaType<number>;
export type StructType = JSONSchemaType<{}>;
export interface EnumType {
enum: number[];
}
export type BoolType = JSONSchemaType<boolean>;
export interface RefType {
$ref: string;
}
// eslint-disable-next-line @typescript-eslint/no-empty-interface
export interface AnyType {}
export interface ConstType {
const: string | number;
}
export type AjvType =
| ListType
| StringType
| NumberType
| StructType
| EnumType
| RefType
| BoolType
| AnyType
| ConstType;
export interface Schema extends SchemaObject {
definitions: Record<string, AjvType>;
}
export interface ProcessIdlCtxWithSchema extends ProcessIdlCtx {
schema: Schema;
}
export interface GenMockFieldCtx {
fieldType: FieldType | FunctionType;
defaultValue?: ConstValue;
context?: {
fieldDefinition: FieldDefinition;
struct: StructDefinition;
ast: IParseResultItem;
};
output?:
| t.ObjectExpression
| t.ArrayExpression
| t.CallExpression
| t.Identifier
| t.StringLiteral
| t.NumericLiteral
| t.BooleanLiteral
| t.MemberExpression;
}
export interface WriteFileCtx {
ast: IParseResultItem[];
content: string;
filename: string;
}
export interface IProcessMetaItemCtx {
meta: IMeta;
ast: IParseResultItem;
service: ServiceDefinition;
method: FunctionDefinition;
}
export interface Contexts extends Ctxs {
[HOOK.GEN_FILE_AST]: IParseEntryCtx;
[HOOK.GEN_FUN_TEMPLATE]: IGenTemplateCtx;
[HOOK.PARSE_ENTRY]: IParseEntryCtx;
[HOOK.PARSE_FUN_META]: ProcessIdlCtx;
[HOOK.PARSE_FUN_META_ITEM]: IProcessMetaItemCtx;
[HOOK.PROCESS_IDL_AST]: ProcessIdlCtxWithSchema;
[HOOK.PROCESS_IDL_NODE]: ProcessIdlCtxWithSchema;
[HOOK.GEN_MOCK_FILED]: GenMockFieldCtx;
[HOOK.WRITE_FILE]: WriteFileCtx;
}

View File

@@ -0,0 +1,220 @@
/*
* 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 { Program, on } from '@coze-arch/idl2ts-plugin';
import {
createFile,
ignoreField,
parseFile as parseJs,
type IParseEntryCtx,
type ProcessIdlCtx,
type IGentsRes,
type IParseResultItem,
parseDSL,
isServiceDefinition,
safeWriteFile,
} from '@coze-arch/idl2ts-helper';
import generator from '@babel/generator';
import { type Options } from './types';
import {
AdapterPlugin,
ClientPlugin,
MetaPlugin,
MockTransformerPlugin,
SchemaPlugin,
PkgEntryPlugin,
AutoFixPathPlugin,
IgnoreStructFiledPlugin,
AutoFixDuplicateIncludesPlugin,
CommentFormatPlugin,
} from './plugin';
import { type Contexts, HOOK } from './context';
type IParseEntryContext = IParseEntryCtx<ClientGenerator>;
export class ClientGenerator {
private entries: string[];
private program: Program<Contexts>;
private options: Options;
private nsList: IParseResultItem[] = [];
private output: IGentsRes = new Map();
static PLUGIN_PRIORITY = 0;
private processFile = new Set<string>();
constructor(options: Options) {
this.entries = options.entries.map(i => path.resolve(options.idlRoot, i));
this.options = options;
const { plugins = [] } = options;
this.program = Program.create([
...plugins,
new AutoFixPathPlugin(),
new CommentFormatPlugin(),
new AutoFixDuplicateIncludesPlugin(),
new AdapterPlugin(options),
new MetaPlugin(options),
new IgnoreStructFiledPlugin({ filter: ignoreField }),
]);
if (options.genClient) {
this.program.loadPlugins([
new ClientPlugin(options),
new PkgEntryPlugin(options),
]);
}
if (options.genSchema) {
this.program.loadPlugins([new SchemaPlugin(options)]);
}
if (options.genMock) {
this.program.loadPlugins([new MockTransformerPlugin(options)]);
}
}
private parseAst(parseEntriesCtx: IParseEntryContext) {
this.program.register(
on(HOOK.PARSE_ENTRY),
ctx => {
ctx.ast = parseDSL(
this.entries,
this.options.parsedResult || [],
this.options.idlRoot,
);
return ctx;
},
ClientGenerator.PLUGIN_PRIORITY,
);
return this.program.trigger(HOOK.PARSE_ENTRY, parseEntriesCtx);
}
private genFiles(parseEntriesCtx: IParseEntryContext) {
this.program.register(
on(HOOK.GEN_FILE_AST),
ctx => {
const files = this.process(ctx.ast);
ctx.files = files;
return ctx;
},
ClientGenerator.PLUGIN_PRIORITY,
);
return this.program.trigger(HOOK.GEN_FILE_AST, parseEntriesCtx);
}
gen(): IParseEntryContext {
const parseEntriesCtx = this.parseAst({
entries: this.entries,
instance: this,
ast: this.options.parsedResult || [],
files: this.output,
});
return this.genFiles(parseEntriesCtx);
}
run() {
const res = this.gen();
this.program.register(on(HOOK.WRITE_FILE), ctx => {
const { content, filename } = ctx;
safeWriteFile(filename, content);
return ctx;
});
for (const [source, file] of res.files) {
let code = '';
switch (file.type) {
case 'babel':
code = generator(file.content, { comments: true }).code;
break;
case 'json':
code = JSON.stringify(file.content, undefined, 2);
break;
case 'text':
code = file.content;
break;
default:
break;
}
this.program.trigger(HOOK.WRITE_FILE, {
filename: source,
content: code,
ast: res.ast,
});
}
}
private process(processRes: IParseResultItem[]) {
const output = new Map() as IGentsRes;
for (const ast of processRes) {
this.nsList.push(ast);
const res = this.processIdlAst(ast);
for (const [key, val] of res) {
output.set(key, val);
}
this.nsList.pop();
}
return output;
}
private processIdlAst(ast: IParseResultItem) {
try {
// 新的解析器貌似不是按原来位置排序的,这里要重新排序
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
ast.statements.sort((a, b) => a.loc!.start.line - b.loc!.start.line);
} catch (error) {
console.error(error);
}
const filename = this.getMockFile(ast);
const ctx: ProcessIdlCtx = {
ast,
dts: createFile(''),
mock: fs.existsSync(filename)
? parseJs(this.getMockFile(ast))
: createFile(''),
output: new Map(),
mockStatements: [],
meta: [],
};
this.program.register(
on(HOOK.PROCESS_IDL_AST),
ctx => {
const { ast } = ctx;
if (this.processFile.has(ast.idlPath)) {
return ctx;
}
for (const node of ast.statements) {
const nodeCtx = Object.assign({ node }, ctx);
if (isServiceDefinition(node) && ast.isEntry) {
this.program.trigger(HOOK.PARSE_FUN_META, nodeCtx);
}
const { mockStatements } = this.program.trigger(
HOOK.PROCESS_IDL_NODE,
nodeCtx,
);
ctx.mockStatements = mockStatements;
}
this.processFile.add(ast.idlPath);
return ctx;
},
ClientGenerator.PLUGIN_PRIORITY,
);
return this.program.trigger(HOOK.PROCESS_IDL_AST, ctx).output;
}
private getMockFile(ast: IParseResultItem) {
const { idlPath } = ast;
const targetName = path.resolve(
this.options.outputDir,
path.relative(this.options.idlRoot, idlPath),
);
return targetName.replace(/\.(thrift|proto)$/, '.mock.js');
}
}

View File

@@ -0,0 +1,25 @@
/*
* 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 IGenOptions } from './types';
import { ClientGenerator } from './core';
export * from './context';
export function genClient(params: IGenOptions) {
const clientGenerator = new ClientGenerator(params);
clientGenerator.run();
}

View 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;
}
}

View File

@@ -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;
});
}
}

View File

@@ -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;
});
}
}

View 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;
}
}

View File

@@ -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;
});
}
}

View File

@@ -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;
});
}
}

View 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';

View 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] || [];
}
}
}

View File

@@ -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 '';
}
}

Some files were not shown because too many files have changed in this diff Show More