feat: manually mirror opencoze's code from bytedance
Change-Id: I09a73aadda978ad9511264a756b2ce51f5761adf
This commit is contained in:
68
frontend/infra/idl/idl-parser/README.md
Normal file
68
frontend/infra/idl/idl-parser/README.md
Normal 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
|
||||
24
frontend/infra/idl/idl-parser/__tests__/common.ts
Normal file
24
frontend/infra/idl/idl-parser/__tests__/common.ts
Normal 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;
|
||||
}
|
||||
36
frontend/infra/idl/idl-parser/__tests__/demo.proto.ts
Normal file
36
frontend/infra/idl/idl-parser/__tests__/demo.proto.ts
Normal 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));
|
||||
42
frontend/infra/idl/idl-parser/__tests__/demo.thrift.ts
Normal file
42
frontend/infra/idl/idl-parser/__tests__/demo.thrift.ts
Normal 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));
|
||||
213
frontend/infra/idl/idl-parser/__tests__/demo.unify.ts
Normal file
213
frontend/infra/idl/idl-parser/__tests__/demo.unify.ts
Normal file
@@ -0,0 +1,213 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
/* 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);
|
||||
@@ -0,0 +1,3 @@
|
||||
syntax = "proto3";
|
||||
|
||||
message Base {}
|
||||
@@ -0,0 +1 @@
|
||||
struct Base {}
|
||||
@@ -0,0 +1,3 @@
|
||||
syntax = "proto3";
|
||||
|
||||
message Basee {}
|
||||
@@ -0,0 +1 @@
|
||||
struct Basee {}
|
||||
@@ -0,0 +1,7 @@
|
||||
syntax = "proto3";
|
||||
package common;
|
||||
|
||||
import "base.proto";
|
||||
import "basee.proto";
|
||||
|
||||
message Common {}
|
||||
@@ -0,0 +1,6 @@
|
||||
namespace go common
|
||||
|
||||
include 'base.thrift'
|
||||
include 'basee.thrift'
|
||||
|
||||
struct Common {}
|
||||
4
frontend/infra/idl/idl-parser/__tests__/idl/error.proto
Normal file
4
frontend/infra/idl/idl-parser/__tests__/idl/error.proto
Normal file
@@ -0,0 +1,4 @@
|
||||
syntax = "proto3";
|
||||
message Foo {
|
||||
string k1 = 1;,
|
||||
}
|
||||
3
frontend/infra/idl/idl-parser/__tests__/idl/error.thrift
Normal file
3
frontend/infra/idl/idl-parser/__tests__/idl/error.thrift
Normal file
@@ -0,0 +1,3 @@
|
||||
struct Foo {
|
||||
1: string k1,,
|
||||
}
|
||||
5
frontend/infra/idl/idl-parser/__tests__/idl/index.proto
Normal file
5
frontend/infra/idl/idl-parser/__tests__/idl/index.proto
Normal file
@@ -0,0 +1,5 @@
|
||||
syntax = "proto3";
|
||||
|
||||
service Foo {
|
||||
option (api.uri_prefix) = "//example.com";
|
||||
}
|
||||
5
frontend/infra/idl/idl-parser/__tests__/idl/index.thrift
Normal file
5
frontend/infra/idl/idl-parser/__tests__/idl/index.thrift
Normal file
@@ -0,0 +1,5 @@
|
||||
|
||||
service Foo {
|
||||
} (
|
||||
api.uri_prefix = 'https://example.com'
|
||||
)
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
syntax = "proto3";
|
||||
|
||||
import "./unify_dependent1.proto";
|
||||
|
||||
package unify_idx;
|
||||
|
||||
enum Number {
|
||||
ONE = 1;
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
include './unify_dependent1.thrift'
|
||||
|
||||
enum Number {
|
||||
ONE = 1,
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
include "./unify_base.thrift"
|
||||
include "./unify_base1.thrift"
|
||||
@@ -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';
|
||||
}
|
||||
}
|
||||
@@ -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'
|
||||
)
|
||||
@@ -0,0 +1,7 @@
|
||||
syntax = "proto3";
|
||||
|
||||
import "base.proto";
|
||||
|
||||
message Foo {
|
||||
Base key1 = 1;
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
include 'base.thrift'
|
||||
|
||||
struct Foo {
|
||||
1: base.Base key1
|
||||
}
|
||||
12
frontend/infra/idl/idl-parser/__tests__/idl/weird.proto
Normal file
12
frontend/infra/idl/idl-parser/__tests__/idl/weird.proto
Normal 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: "*"
|
||||
};
|
||||
}
|
||||
}
|
||||
140
frontend/infra/idl/idl-parser/__tests__/proto.field.test.ts
Normal file
140
frontend/infra/idl/idl-parser/__tests__/proto.field.test.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
});
|
||||
77
frontend/infra/idl/idl-parser/__tests__/proto.index.test.ts
Normal file
77
frontend/infra/idl/idl-parser/__tests__/proto.index.test.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
});
|
||||
108
frontend/infra/idl/idl-parser/__tests__/proto.method.test.ts
Normal file
108
frontend/infra/idl/idl-parser/__tests__/proto.method.test.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
55
frontend/infra/idl/idl-parser/__tests__/thrift.enum.test.ts
Normal file
55
frontend/infra/idl/idl-parser/__tests__/thrift.enum.test.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
});
|
||||
226
frontend/infra/idl/idl-parser/__tests__/thrift.field.test.ts
Normal file
226
frontend/infra/idl/idl-parser/__tests__/thrift.field.test.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
});
|
||||
116
frontend/infra/idl/idl-parser/__tests__/thrift.function.test.ts
Normal file
116
frontend/infra/idl/idl-parser/__tests__/thrift.function.test.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
});
|
||||
79
frontend/infra/idl/idl-parser/__tests__/thrift.index.test.ts
Normal file
79
frontend/infra/idl/idl-parser/__tests__/thrift.index.test.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
7
frontend/infra/idl/idl-parser/__tests__/tsconfig.json
Normal file
7
frontend/infra/idl/idl-parser/__tests__/tsconfig.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"esModuleInterop": true,
|
||||
"allowSyntheticDefaultImports": true
|
||||
},
|
||||
"include": ["."]
|
||||
}
|
||||
174
frontend/infra/idl/idl-parser/__tests__/unify.enum.test.ts
Normal file
174
frontend/infra/idl/idl-parser/__tests__/unify.enum.test.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
});
|
||||
1510
frontend/infra/idl/idl-parser/__tests__/unify.field.test.ts
Normal file
1510
frontend/infra/idl/idl-parser/__tests__/unify.field.test.ts
Normal file
File diff suppressed because it is too large
Load Diff
343
frontend/infra/idl/idl-parser/__tests__/unify.function.test.ts
Normal file
343
frontend/infra/idl/idl-parser/__tests__/unify.function.test.ts
Normal 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',
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
750
frontend/infra/idl/idl-parser/__tests__/unify.index.test.ts
Normal file
750
frontend/infra/idl/idl-parser/__tests__/unify.index.test.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
});
|
||||
140
frontend/infra/idl/idl-parser/__tests__/unify.other.test.ts
Normal file
140
frontend/infra/idl/idl-parser/__tests__/unify.other.test.ts
Normal 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',
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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',
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
12
frontend/infra/idl/idl-parser/config/rush-project.json
Normal file
12
frontend/infra/idl/idl-parser/config/rush-project.json
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"operationSettings": [
|
||||
{
|
||||
"operationName": "test:cov",
|
||||
"outputFolderNames": ["coverage"]
|
||||
},
|
||||
{
|
||||
"operationName": "ts-check",
|
||||
"outputFolderNames": ["dist"]
|
||||
}
|
||||
]
|
||||
}
|
||||
15
frontend/infra/idl/idl-parser/eslint.config.js
Normal file
15
frontend/infra/idl/idl-parser/eslint.config.js
Normal 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',
|
||||
},
|
||||
});
|
||||
30
frontend/infra/idl/idl-parser/package.json
Normal file
30
frontend/infra/idl/idl-parser/package.json
Normal 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"
|
||||
}
|
||||
}
|
||||
|
||||
124
frontend/infra/idl/idl-parser/src/common/extension_type.ts
Normal file
124
frontend/infra/idl/idl-parser/src/common/extension_type.ts
Normal 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'
|
||||
>;
|
||||
203
frontend/infra/idl/idl-parser/src/common/extension_util.ts
Normal file
203
frontend/infra/idl/idl-parser/src/common/extension_util.ts
Normal 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;
|
||||
}
|
||||
17
frontend/infra/idl/idl-parser/src/index.ts
Normal file
17
frontend/infra/idl/idl-parser/src/index.ts
Normal 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';
|
||||
199
frontend/infra/idl/idl-parser/src/proto/index.ts
Normal file
199
frontend/infra/idl/idl-parser/src/proto/index.ts
Normal 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;
|
||||
}
|
||||
154
frontend/infra/idl/idl-parser/src/proto/type.ts
Normal file
154
frontend/infra/idl/idl-parser/src/proto/type.ts
Normal 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',
|
||||
}
|
||||
288
frontend/infra/idl/idl-parser/src/thrift/index.ts
Normal file
288
frontend/infra/idl/idl-parser/src/thrift/index.ts
Normal 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;
|
||||
}
|
||||
460
frontend/infra/idl/idl-parser/src/thrift/type.ts
Normal file
460
frontend/infra/idl/idl-parser/src/thrift/type.ts
Normal 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',
|
||||
}
|
||||
144
frontend/infra/idl/idl-parser/src/unify/index.ts
Normal file
144
frontend/infra/idl/idl-parser/src/unify/index.ts
Normal 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;
|
||||
}
|
||||
1014
frontend/infra/idl/idl-parser/src/unify/proto.ts
Normal file
1014
frontend/infra/idl/idl-parser/src/unify/proto.ts
Normal file
File diff suppressed because it is too large
Load Diff
712
frontend/infra/idl/idl-parser/src/unify/thrift.ts
Normal file
712
frontend/infra/idl/idl-parser/src/unify/thrift.ts
Normal 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;
|
||||
}
|
||||
407
frontend/infra/idl/idl-parser/src/unify/type.ts
Normal file
407
frontend/infra/idl/idl-parser/src/unify/type.ts
Normal 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',
|
||||
}
|
||||
41
frontend/infra/idl/idl-parser/src/unify/util.ts
Normal file
41
frontend/infra/idl/idl-parser/src/unify/util.ts
Normal 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;
|
||||
}
|
||||
45
frontend/infra/idl/idl-parser/src/utils/index.ts
Normal file
45
frontend/infra/idl/idl-parser/src/utils/index.ts
Normal 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, '/');
|
||||
}
|
||||
28
frontend/infra/idl/idl-parser/tsconfig.build.json
Normal file
28
frontend/infra/idl/idl-parser/tsconfig.build.json
Normal 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"
|
||||
}
|
||||
]
|
||||
}
|
||||
15
frontend/infra/idl/idl-parser/tsconfig.json
Normal file
15
frontend/infra/idl/idl-parser/tsconfig.json
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/tsconfig",
|
||||
"exclude": ["**/*"],
|
||||
"compilerOptions": {
|
||||
"composite": true
|
||||
},
|
||||
"references": [
|
||||
{
|
||||
"path": "./tsconfig.build.json"
|
||||
},
|
||||
{
|
||||
"path": "./tsconfig.misc.json"
|
||||
}
|
||||
]
|
||||
}
|
||||
18
frontend/infra/idl/idl-parser/tsconfig.misc.json
Normal file
18
frontend/infra/idl/idl-parser/tsconfig.misc.json
Normal 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"
|
||||
}
|
||||
]
|
||||
}
|
||||
22
frontend/infra/idl/idl-parser/vitest.config.ts
Normal file
22
frontend/infra/idl/idl-parser/vitest.config.ts
Normal 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',
|
||||
});
|
||||
63
frontend/infra/idl/idl2ts-cli/README.md
Normal file
63
frontend/infra/idl/idl2ts-cli/README.md
Normal 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
|
||||
12
frontend/infra/idl/idl2ts-cli/config/rush-project.json
Normal file
12
frontend/infra/idl/idl2ts-cli/config/rush-project.json
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"operationSettings": [
|
||||
{
|
||||
"operationName": "test:cov",
|
||||
"outputFolderNames": ["coverage"]
|
||||
},
|
||||
{
|
||||
"operationName": "ts-check",
|
||||
"outputFolderNames": ["dist"]
|
||||
}
|
||||
]
|
||||
}
|
||||
15
frontend/infra/idl/idl2ts-cli/eslint.config.js
Normal file
15
frontend/infra/idl/idl2ts-cli/eslint.config.js
Normal 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',
|
||||
},
|
||||
});
|
||||
38
frontend/infra/idl/idl2ts-cli/package.json
Normal file
38
frontend/infra/idl/idl2ts-cli/package.json
Normal 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"
|
||||
}
|
||||
}
|
||||
139
frontend/infra/idl/idl2ts-cli/src/actions.ts
Normal file
139
frontend/infra/idl/idl2ts-cli/src/actions.ts
Normal 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;
|
||||
}
|
||||
3
frontend/infra/idl/idl2ts-cli/src/cli.js
Executable file
3
frontend/infra/idl/idl2ts-cli/src/cli.js
Executable file
@@ -0,0 +1,3 @@
|
||||
require('sucrase/register/ts');
|
||||
|
||||
require('./cli.ts');
|
||||
79
frontend/infra/idl/idl2ts-cli/src/cli.ts
Normal file
79
frontend/infra/idl/idl2ts-cli/src/cli.ts
Normal 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();
|
||||
105
frontend/infra/idl/idl2ts-cli/src/mock-dev.ts
Normal file
105
frontend/infra/idl/idl2ts-cli/src/mock-dev.ts
Normal 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();
|
||||
};
|
||||
}
|
||||
59
frontend/infra/idl/idl2ts-cli/src/optional/forward.ts
Normal file
59
frontend/infra/idl/idl2ts-cli/src/optional/forward.ts
Normal 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;
|
||||
});
|
||||
}
|
||||
}
|
||||
17
frontend/infra/idl/idl2ts-cli/src/optional/index.ts
Normal file
17
frontend/infra/idl/idl2ts-cli/src/optional/index.ts
Normal 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';
|
||||
47
frontend/infra/idl/idl2ts-cli/src/plugins/alias.ts
Normal file
47
frontend/infra/idl/idl2ts-cli/src/plugins/alias.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
72
frontend/infra/idl/idl2ts-cli/src/plugins/comment.ts
Normal file
72
frontend/infra/idl/idl2ts-cli/src/plugins/comment.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
230
frontend/infra/idl/idl2ts-cli/src/plugins/filter-types-plugin.ts
Normal file
230
frontend/infra/idl/idl2ts-cli/src/plugins/filter-types-plugin.ts
Normal 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}`);
|
||||
}
|
||||
}
|
||||
83
frontend/infra/idl/idl2ts-cli/src/plugins/formatter.ts
Normal file
83
frontend/infra/idl/idl2ts-cli/src/plugins/formatter.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
61
frontend/infra/idl/idl2ts-cli/src/plugins/local-config.ts
Normal file
61
frontend/infra/idl/idl2ts-cli/src/plugins/local-config.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
101
frontend/infra/idl/idl2ts-cli/src/plugins/mock-plugin.ts
Normal file
101
frontend/infra/idl/idl2ts-cli/src/plugins/mock-plugin.ts
Normal 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;
|
||||
};
|
||||
}
|
||||
53
frontend/infra/idl/idl2ts-cli/src/types.ts
Normal file
53
frontend/infra/idl/idl2ts-cli/src/types.ts
Normal 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[]>;
|
||||
}
|
||||
33
frontend/infra/idl/idl2ts-cli/src/utils.ts
Normal file
33
frontend/infra/idl/idl2ts-cli/src/utils.ts
Normal 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[];
|
||||
}
|
||||
34
frontend/infra/idl/idl2ts-cli/tsconfig.build.json
Normal file
34
frontend/infra/idl/idl2ts-cli/tsconfig.build.json
Normal 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"
|
||||
}
|
||||
]
|
||||
}
|
||||
15
frontend/infra/idl/idl2ts-cli/tsconfig.json
Normal file
15
frontend/infra/idl/idl2ts-cli/tsconfig.json
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/tsconfig",
|
||||
"exclude": ["**/*"],
|
||||
"compilerOptions": {
|
||||
"composite": true
|
||||
},
|
||||
"references": [
|
||||
{
|
||||
"path": "./tsconfig.build.json"
|
||||
},
|
||||
{
|
||||
"path": "./tsconfig.misc.json"
|
||||
}
|
||||
]
|
||||
}
|
||||
18
frontend/infra/idl/idl2ts-cli/tsconfig.misc.json
Normal file
18
frontend/infra/idl/idl2ts-cli/tsconfig.misc.json
Normal 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"
|
||||
}
|
||||
]
|
||||
}
|
||||
22
frontend/infra/idl/idl2ts-cli/vitest.config.ts
Normal file
22
frontend/infra/idl/idl2ts-cli/vitest.config.ts
Normal 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',
|
||||
});
|
||||
68
frontend/infra/idl/idl2ts-generator/README.md
Normal file
68
frontend/infra/idl/idl2ts-generator/README.md
Normal 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
|
||||
12
frontend/infra/idl/idl2ts-generator/config/rush-project.json
Normal file
12
frontend/infra/idl/idl2ts-generator/config/rush-project.json
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"operationSettings": [
|
||||
{
|
||||
"operationName": "test:cov",
|
||||
"outputFolderNames": ["coverage"]
|
||||
},
|
||||
{
|
||||
"operationName": "ts-check",
|
||||
"outputFolderNames": ["dist"]
|
||||
}
|
||||
]
|
||||
}
|
||||
17
frontend/infra/idl/idl2ts-generator/eslint.config.js
Normal file
17
frontend/infra/idl/idl2ts-generator/eslint.config.js
Normal 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',
|
||||
},
|
||||
});
|
||||
47
frontend/infra/idl/idl2ts-generator/package.json
Normal file
47
frontend/infra/idl/idl2ts-generator/package.json
Normal 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"
|
||||
}
|
||||
}
|
||||
127
frontend/infra/idl/idl2ts-generator/src/context.ts
Normal file
127
frontend/infra/idl/idl2ts-generator/src/context.ts
Normal 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;
|
||||
}
|
||||
220
frontend/infra/idl/idl2ts-generator/src/core.ts
Normal file
220
frontend/infra/idl/idl2ts-generator/src/core.ts
Normal 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');
|
||||
}
|
||||
}
|
||||
25
frontend/infra/idl/idl2ts-generator/src/index.ts
Normal file
25
frontend/infra/idl/idl2ts-generator/src/index.ts
Normal 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();
|
||||
}
|
||||
213
frontend/infra/idl/idl2ts-generator/src/plugin/adapter-plugin.ts
Normal file
213
frontend/infra/idl/idl2ts-generator/src/plugin/adapter-plugin.ts
Normal file
@@ -0,0 +1,213 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import path from 'path';
|
||||
|
||||
import fs from 'fs-extra';
|
||||
import {
|
||||
type IPlugin,
|
||||
type Program,
|
||||
before,
|
||||
after,
|
||||
} from '@coze-arch/idl2ts-plugin';
|
||||
import {
|
||||
type IParseEntryCtx,
|
||||
isStructDefinition,
|
||||
type FunctionType,
|
||||
isSetType,
|
||||
isListType,
|
||||
SyntaxType,
|
||||
isMapType,
|
||||
type FieldType,
|
||||
createFile,
|
||||
getAnnotation,
|
||||
getOutputName,
|
||||
getTypeFromDynamicJsonAnnotation,
|
||||
removeFileExt,
|
||||
parseFile,
|
||||
genAst,
|
||||
} from '@coze-arch/idl2ts-helper';
|
||||
import * as t from '@babel/types';
|
||||
|
||||
import { type Options } from '../types';
|
||||
import { type Contexts, type ProcessIdlCtxWithSchema, HOOK } from '../context';
|
||||
|
||||
function isInt(fieldType: FieldType | FunctionType) {
|
||||
return [
|
||||
SyntaxType.I8Keyword,
|
||||
SyntaxType.I16Keyword,
|
||||
SyntaxType.I32Keyword,
|
||||
SyntaxType.I64Keyword,
|
||||
].some(i => i === fieldType.type);
|
||||
}
|
||||
|
||||
export class AdapterPlugin implements IPlugin {
|
||||
private patchTypes = new Map<string, Record<string, string[]>>();
|
||||
private options: Options;
|
||||
constructor(options: Options) {
|
||||
this.options = options;
|
||||
}
|
||||
apply(program: Program<Contexts>): void {
|
||||
program.register(
|
||||
before(HOOK.PROCESS_IDL_NODE),
|
||||
this.adaptStruct.bind(this),
|
||||
);
|
||||
program.register(after(HOOK.GEN_FILE_AST), this.genPatchFiles.bind(this));
|
||||
}
|
||||
|
||||
private genPatchFiles(ctx: IParseEntryCtx) {
|
||||
for (const [idlPath, res] of this.patchTypes.entries()) {
|
||||
const targetFile = getOutputName({
|
||||
source: `${removeFileExt(idlPath)}.ts`,
|
||||
idlRoot: this.options.idlRoot,
|
||||
outputDir:
|
||||
this.options.patchTypesOutput ||
|
||||
path.join(this.options.outputDir, '../patch-types'),
|
||||
});
|
||||
let file: t.File;
|
||||
if (!fs.existsSync(targetFile)) {
|
||||
file = createFile('');
|
||||
} else {
|
||||
file = parseFile(targetFile);
|
||||
}
|
||||
Object.keys(res).forEach(structName => {
|
||||
let target = file.program.body.find(
|
||||
i =>
|
||||
t.isExportNamedDeclaration(i) &&
|
||||
// @ts-expect-error fixme
|
||||
i.declaration?.id.name === structName,
|
||||
) as t.ExportNamedDeclaration;
|
||||
if (!target) {
|
||||
target = genAst(
|
||||
`export namespace ${structName} {}`,
|
||||
) as t.ExportNamedDeclaration;
|
||||
file.program.body.push(target);
|
||||
}
|
||||
const declaration = target.declaration as t.TSModuleDeclaration;
|
||||
for (const fieldName of res[structName]) {
|
||||
if (t.isTSModuleBlock(declaration.body)) {
|
||||
if (
|
||||
!declaration.body.body.some(i => {
|
||||
if (t.isExportNamedDeclaration(i)) {
|
||||
if (
|
||||
t.isTSTypeAliasDeclaration(i.declaration) ||
|
||||
t.isInterfaceDeclaration(i.declaration)
|
||||
) {
|
||||
return i.declaration.id.name === fieldName;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
})
|
||||
) {
|
||||
declaration.body.body.push(
|
||||
genAst(
|
||||
`export type ${fieldName}= unknown`,
|
||||
) as t.TSTypeAliasDeclaration,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
ctx.files.set(targetFile, { content: file, type: 'babel' });
|
||||
}
|
||||
return ctx;
|
||||
}
|
||||
private adaptStruct(ctx: ProcessIdlCtxWithSchema) {
|
||||
const { node, ast } = ctx;
|
||||
if (!node) {
|
||||
return ctx;
|
||||
}
|
||||
if (isStructDefinition(node)) {
|
||||
const decodeEncodeFields = [] as string[];
|
||||
// eslint-disable-next-line complexity
|
||||
node.fields = node.fields.map(f => {
|
||||
// req
|
||||
if (
|
||||
getAnnotation(f.annotations, 'api.converter') === 'atoi_comp_empty'
|
||||
) {
|
||||
if (isInt(f.fieldType)) {
|
||||
// 类型转换为 string
|
||||
f.fieldType.type = SyntaxType.StringKeyword;
|
||||
}
|
||||
}
|
||||
// api.converter 对 int 以及 map 类型生效
|
||||
if (getAnnotation(f.annotations, 'api.converter') === 'itoa') {
|
||||
if (isInt(f.fieldType)) {
|
||||
// 类型转换为 string
|
||||
f.fieldType.type = SyntaxType.StringKeyword;
|
||||
}
|
||||
if (isMapType(f.fieldType)) {
|
||||
const { valueType } = f.fieldType;
|
||||
if (isInt(valueType)) {
|
||||
f.fieldType.valueType.type = SyntaxType.StringKeyword;
|
||||
}
|
||||
}
|
||||
}
|
||||
// item_converter 对 list 类型生效
|
||||
if (
|
||||
['atoi_comp_empty', 'itoa'].includes(
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
getAnnotation(f.annotations, 'api.item_converter')!,
|
||||
)
|
||||
) {
|
||||
if (isSetType(f.fieldType) || isListType(f.fieldType)) {
|
||||
f.fieldType.valueType.type = SyntaxType.StringKeyword;
|
||||
}
|
||||
}
|
||||
|
||||
// 收集 decode encode 注解处理
|
||||
if (getTypeFromDynamicJsonAnnotation(f.annotations)) {
|
||||
decodeEncodeFields.push(f.name.value);
|
||||
}
|
||||
// api.json 注解处理
|
||||
const jsonAnnotation = getAnnotation(f.annotations, 'api.json');
|
||||
if (jsonAnnotation) {
|
||||
f.extensionConfig = f.extensionConfig || {};
|
||||
f.extensionConfig.key = jsonAnnotation;
|
||||
}
|
||||
// api.json_string 注解处理
|
||||
const jsonStrAnnotation = getAnnotation(
|
||||
f.annotations,
|
||||
'api.json_string',
|
||||
);
|
||||
if (jsonStrAnnotation) {
|
||||
if (isInt(f.fieldType)) {
|
||||
// 类型转换为 string
|
||||
f.fieldType.type = SyntaxType.StringKeyword;
|
||||
f.extensionConfig = f.extensionConfig || {};
|
||||
f.extensionConfig.key = jsonStrAnnotation;
|
||||
} else {
|
||||
throw new Error(
|
||||
'api.json_string is expected an annotation int type',
|
||||
);
|
||||
}
|
||||
}
|
||||
return f;
|
||||
});
|
||||
if (decodeEncodeFields.length > 0) {
|
||||
const currentAstRes = this.patchTypes.get(ast.idlPath);
|
||||
if (!currentAstRes) {
|
||||
this.patchTypes.set(ast.idlPath, {
|
||||
[node.name.value]: decodeEncodeFields,
|
||||
});
|
||||
} else {
|
||||
currentAstRes[node.name.value] = decodeEncodeFields;
|
||||
}
|
||||
}
|
||||
}
|
||||
return ctx;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { type Program, after, type IPlugin } from '@coze-arch/idl2ts-plugin';
|
||||
import { type IParseEntryCtx, isPbFile } from '@coze-arch/idl2ts-helper';
|
||||
|
||||
import { HOOK } from '../context';
|
||||
|
||||
export class AutoFixDuplicateIncludesPlugin implements IPlugin {
|
||||
apply(p: Program<{ PARSE_ENTRY: any }>) {
|
||||
p.register(after(HOOK.PARSE_ENTRY), (ctx: IParseEntryCtx) => {
|
||||
if (isPbFile(ctx.entries[0])) {
|
||||
return ctx;
|
||||
}
|
||||
ctx.ast = ctx.ast.map(i => {
|
||||
const res: string[] = [];
|
||||
for (const include of i.includes) {
|
||||
if (res.includes(include)) {
|
||||
console.error(
|
||||
`[${include}]` + `has be includes duplicate in file:${i.idlPath}`,
|
||||
);
|
||||
} else {
|
||||
res.push(include);
|
||||
}
|
||||
}
|
||||
i.includes = res;
|
||||
return i;
|
||||
});
|
||||
return ctx;
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { isAbsolute } from 'path';
|
||||
|
||||
import { type Program, after, type IPlugin } from '@coze-arch/idl2ts-plugin';
|
||||
|
||||
function ensureRelative(idlPath: string) {
|
||||
if (isAbsolute(idlPath)) {
|
||||
return idlPath;
|
||||
}
|
||||
if (!idlPath.startsWith('.')) {
|
||||
return `./${idlPath}`;
|
||||
}
|
||||
return idlPath;
|
||||
}
|
||||
|
||||
export class AutoFixPathPlugin implements IPlugin {
|
||||
apply(p: Program<{ PARSE_ENTRY: any }>) {
|
||||
p.register(after('PARSE_ENTRY'), ctx => {
|
||||
ctx.ast = ctx.ast.map(i => {
|
||||
i.includes = i.includes.map(ensureRelative);
|
||||
return i;
|
||||
});
|
||||
return ctx;
|
||||
});
|
||||
}
|
||||
}
|
||||
494
frontend/infra/idl/idl2ts-generator/src/plugin/client-plugin.ts
Normal file
494
frontend/infra/idl/idl2ts-generator/src/plugin/client-plugin.ts
Normal file
@@ -0,0 +1,494 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import path from 'path';
|
||||
|
||||
import {
|
||||
type IPlugin,
|
||||
type Program,
|
||||
on,
|
||||
after,
|
||||
} from '@coze-arch/idl2ts-plugin';
|
||||
import {
|
||||
type EnumDefinition,
|
||||
isStructDefinition,
|
||||
isIdentifier,
|
||||
type StructDefinition,
|
||||
isBaseType,
|
||||
type FieldType,
|
||||
type FunctionType,
|
||||
isEnumDefinition,
|
||||
isListType,
|
||||
isStringLiteral,
|
||||
type TypedefDefinition,
|
||||
type ConstDefinition,
|
||||
type ConstValue,
|
||||
isSetType,
|
||||
isMapType,
|
||||
isBooleanLiteral,
|
||||
isIntConstant,
|
||||
isDoubleConstant,
|
||||
isConstList,
|
||||
isConstMap,
|
||||
type BaseType,
|
||||
isConstDefinition,
|
||||
type ProcessIdlCtx,
|
||||
isTypedefDefinition,
|
||||
isServiceDefinition,
|
||||
type IParseResultItem,
|
||||
type ServiceDefinition,
|
||||
type IGenTemplateCtx,
|
||||
addComment,
|
||||
getAnnotation,
|
||||
parseFiledName,
|
||||
parseIdFiledType,
|
||||
getTypeFromDynamicJsonAnnotation,
|
||||
withExportDeclaration,
|
||||
uniformNs,
|
||||
removeFileExt,
|
||||
genAst,
|
||||
getOutputName,
|
||||
transformFieldId,
|
||||
getRelativePath,
|
||||
getFieldsAlias,
|
||||
SyntaxType,
|
||||
type UnifyStatement,
|
||||
} from '@coze-arch/idl2ts-helper';
|
||||
import * as t from '@babel/types';
|
||||
|
||||
import { type Options } from '../types';
|
||||
import { TypeMapper } from '../type-mapper';
|
||||
import { genFunc, genPublic } from '../template';
|
||||
import { type Contexts, HOOK } from '../context';
|
||||
|
||||
const hasEnumAnnotation = (statement: UnifyStatement) =>
|
||||
statement.annotations?.annotations.some(
|
||||
i => i.name.value === 'ts.enum' && i.value?.value === 'true',
|
||||
);
|
||||
|
||||
const findEnumItemIndex = (enumName: string, statements: UnifyStatement[]) => {
|
||||
const result: number[] = [];
|
||||
for (let i = 0; i < statements.length; i++) {
|
||||
const statement = statements[i];
|
||||
if (
|
||||
statement.type === SyntaxType.ConstDefinition &&
|
||||
statement.fieldType.type === SyntaxType.Identifier &&
|
||||
statement.fieldType.value === enumName
|
||||
) {
|
||||
result.push(i);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
export class ClientPlugin implements IPlugin {
|
||||
private options: Options;
|
||||
private program!: Program<Contexts>;
|
||||
private needPatchTypeFile = new Set<string>();
|
||||
constructor(options: Options) {
|
||||
this.options = options;
|
||||
}
|
||||
apply(program: Program<Contexts>): void {
|
||||
this.program = program;
|
||||
program.register(
|
||||
on(HOOK.PROCESS_IDL_NODE),
|
||||
ctx => {
|
||||
const { node, dts, ast } = ctx;
|
||||
if (!node) {
|
||||
throw new Error('node is undefined');
|
||||
}
|
||||
if (isStructDefinition(node)) {
|
||||
const { nested, struct } = this.processStructNode(node, ctx);
|
||||
dts.program.body.push(struct);
|
||||
if (nested) {
|
||||
dts.program.body.push(nested);
|
||||
}
|
||||
} else if (isEnumDefinition(node)) {
|
||||
dts.program.body.push(this.processEnumNode(node));
|
||||
} else if (isTypedefDefinition(node) && hasEnumAnnotation(node)) {
|
||||
dts.program.body.push(this.processTypedefEnumNode(node, ctx));
|
||||
} else if (isConstDefinition(node)) {
|
||||
dts.program.body.push(this.processConstNode(node));
|
||||
} else if (isTypedefDefinition(node)) {
|
||||
dts.program.body.push(this.processTypeDefNode(node));
|
||||
} else if (isServiceDefinition(node)) {
|
||||
dts.program.body = [
|
||||
...dts.program.body,
|
||||
...this.processServiceDefinition(node, ast, ctx),
|
||||
];
|
||||
}
|
||||
return ctx;
|
||||
},
|
||||
0,
|
||||
);
|
||||
program.register(after(HOOK.PROCESS_IDL_AST), ctx => {
|
||||
const { ast, dts } = ctx;
|
||||
if (ast.isEntry) {
|
||||
dts.program.body = [genPublic(ctx, this.options), ...dts.program.body];
|
||||
}
|
||||
if (ast.includes) {
|
||||
Object.keys(ast.includeMap).forEach(key => {
|
||||
dts.program.body = [
|
||||
...this.processIncludes(key, ast),
|
||||
...dts.program.body,
|
||||
];
|
||||
});
|
||||
}
|
||||
const outputFile = getOutputName({
|
||||
source: `${removeFileExt(ast.idlPath)}.ts`,
|
||||
outputDir: this.options.outputDir,
|
||||
idlRoot: this.options.idlRoot,
|
||||
});
|
||||
if (this.needPatchTypeFile.has(ast.idlPath)) {
|
||||
let pathName = '';
|
||||
if (this.options.patchTypesAliasOutput) {
|
||||
pathName = path.join(
|
||||
this.options.patchTypesAliasOutput,
|
||||
path.relative(
|
||||
this.options.idlRoot,
|
||||
ast.idlPath.replace('.thrift', ''),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
const patchTypeFile = path.join(
|
||||
this.options.patchTypesOutput ||
|
||||
path.join(this.options.outputDir, '../patch-types'),
|
||||
path.relative(this.options.idlRoot, ast.idlPath),
|
||||
);
|
||||
pathName = getRelativePath(outputFile, patchTypeFile);
|
||||
}
|
||||
|
||||
const code = `import type * as Patch from '${pathName}'`;
|
||||
dts.program.body.unshift(genAst<t.ImportDeclaration>(code));
|
||||
}
|
||||
ctx.output.set(outputFile, { type: 'babel', content: dts });
|
||||
return ctx;
|
||||
});
|
||||
this.program.register(
|
||||
on(HOOK.GEN_FUN_TEMPLATE),
|
||||
(ctx: IGenTemplateCtx) => {
|
||||
ctx.template = genFunc(ctx);
|
||||
return ctx;
|
||||
},
|
||||
0,
|
||||
);
|
||||
}
|
||||
private processEnumNode(node: EnumDefinition) {
|
||||
const { members, name, comments } = node;
|
||||
const enumArr = members.map(i => {
|
||||
const { name, comments, initializer } = i;
|
||||
|
||||
return addComment(
|
||||
t.tsEnumMember(
|
||||
t.identifier(name.value),
|
||||
initializer ? this.getExpFromConstValue(initializer) : undefined,
|
||||
),
|
||||
comments,
|
||||
);
|
||||
});
|
||||
const enumAst = t.tsEnumDeclaration(t.identifier(name.value), enumArr);
|
||||
return withExportDeclaration(enumAst, comments);
|
||||
}
|
||||
private processStructNode(node: StructDefinition, ctx: ProcessIdlCtx) {
|
||||
const { fields, name, comments, nested } = node;
|
||||
|
||||
const typeProps: t.ObjectTypeProperty[] = [];
|
||||
const processedFiledName = {} as Record<string, string>;
|
||||
|
||||
fields.forEach(i => {
|
||||
const fieldName = parseFiledName(i);
|
||||
if (processedFiledName[fieldName]) {
|
||||
return;
|
||||
}
|
||||
const { fieldType, requiredness, comments, annotations } = i;
|
||||
const isAnyType = getAnnotation(annotations, 'api.value_type') === 'any';
|
||||
const dynamicType = getTypeFromDynamicJsonAnnotation(annotations);
|
||||
let valueType: t.FlowType = this.processFiledType(fieldType);
|
||||
if (isAnyType) {
|
||||
valueType = t.anyTypeAnnotation();
|
||||
} else if (dynamicType) {
|
||||
valueType = t.genericTypeAnnotation(
|
||||
t.qualifiedTypeIdentifier(
|
||||
t.identifier(i.name.value),
|
||||
t.qualifiedTypeIdentifier(
|
||||
t.identifier(name.value),
|
||||
t.identifier('Patch'),
|
||||
),
|
||||
),
|
||||
);
|
||||
this.needPatchTypeFile.add(ctx.ast.idlPath);
|
||||
}
|
||||
const prop = t.objectTypeProperty(transformFieldId(fieldName), valueType);
|
||||
|
||||
if (requiredness === 'optional') {
|
||||
prop.optional = true;
|
||||
if (this.options.allowNullForOptional) {
|
||||
prop.value = t.unionTypeAnnotation([
|
||||
prop.value,
|
||||
t.nullLiteralTypeAnnotation(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
processedFiledName[fieldName] = getFieldsAlias(i);
|
||||
typeProps.push(addComment(prop, comments));
|
||||
});
|
||||
|
||||
const ast = t.interfaceDeclaration(
|
||||
t.identifier(name.value),
|
||||
null,
|
||||
[],
|
||||
t.objectTypeAnnotation(typeProps),
|
||||
);
|
||||
return {
|
||||
struct: withExportDeclaration(ast, comments),
|
||||
nested: this.processNested(nested, node, ctx),
|
||||
};
|
||||
}
|
||||
private processFiledType(fieldType: FieldType | FunctionType) {
|
||||
if (isBaseType(fieldType)) {
|
||||
return this.getTsTypeFromThriftBaseType(fieldType);
|
||||
} else if (isListType(fieldType) || isSetType(fieldType)) {
|
||||
const { valueType } = fieldType;
|
||||
return t.arrayTypeAnnotation(this.processFiledType(valueType));
|
||||
} else if (isMapType(fieldType)) {
|
||||
const { valueType } = fieldType;
|
||||
const valueFiledType = this.processFiledType(valueType);
|
||||
return t.objectTypeAnnotation(
|
||||
[],
|
||||
[
|
||||
t.objectTypeIndexer(
|
||||
t.identifier('key'),
|
||||
t.unionTypeAnnotation([
|
||||
t.stringTypeAnnotation(),
|
||||
t.numberTypeAnnotation(),
|
||||
]),
|
||||
valueFiledType,
|
||||
),
|
||||
],
|
||||
);
|
||||
} else if (isIdentifier(fieldType)) {
|
||||
const { namespace, refName } = parseIdFiledType(fieldType);
|
||||
|
||||
if (namespace) {
|
||||
return t.genericTypeAnnotation(
|
||||
t.qualifiedTypeIdentifier(
|
||||
t.identifier(refName),
|
||||
t.identifier(
|
||||
fieldType.namespaceValue?.startsWith('root')
|
||||
? namespace
|
||||
: uniformNs(namespace.replace('.', '_')),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
return t.genericTypeAnnotation(t.identifier(refName));
|
||||
}
|
||||
throw new Error(`unknown type:${fieldType.type}`);
|
||||
}
|
||||
|
||||
private processNested(
|
||||
nested: Record<string, StructDefinition | EnumDefinition> | undefined,
|
||||
parent: StructDefinition,
|
||||
ctx: ProcessIdlCtx,
|
||||
) {
|
||||
if (!nested) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const block = t.tsModuleBlock([]);
|
||||
|
||||
Object.keys(nested).forEach(key => {
|
||||
const node = nested[key];
|
||||
if (isStructDefinition(node)) {
|
||||
const { nested, struct } = this.processStructNode(node, ctx);
|
||||
block.body.push(struct);
|
||||
if (nested) {
|
||||
block.body.push(nested);
|
||||
}
|
||||
} else if (isEnumDefinition(node)) {
|
||||
block.body.push(this.processEnumNode(node));
|
||||
}
|
||||
});
|
||||
const namespaceModule = t.tsModuleDeclaration(
|
||||
t.identifier(parent.name.value),
|
||||
block,
|
||||
);
|
||||
return withExportDeclaration(namespaceModule);
|
||||
}
|
||||
|
||||
private processConstNode(node: ConstDefinition) {
|
||||
const { name, comments, initializer } = node;
|
||||
const exp = this.getExpFromConstValue(initializer);
|
||||
const declarator = t.variableDeclarator(t.identifier(name.value), exp);
|
||||
const ast = t.variableDeclaration('const', [declarator]);
|
||||
return withExportDeclaration(ast, comments);
|
||||
}
|
||||
|
||||
private getExpFromConstValue(initializer: ConstValue): t.Expression {
|
||||
let exp = null as t.Expression | null;
|
||||
if (isStringLiteral(initializer)) {
|
||||
exp = t.stringLiteral(initializer.value);
|
||||
} else if (isBooleanLiteral(initializer)) {
|
||||
exp = t.booleanLiteral(initializer.value);
|
||||
} else if (isIntConstant(initializer) || isDoubleConstant(initializer)) {
|
||||
exp = t.numericLiteral(Number(initializer.value.value));
|
||||
} else if (isConstList(initializer)) {
|
||||
const { elements } = initializer;
|
||||
exp = t.arrayExpression(elements.map(i => this.getExpFromConstValue(i)));
|
||||
} else if (isConstMap(initializer)) {
|
||||
exp = t.objectExpression(
|
||||
initializer.properties.map(i =>
|
||||
t.objectProperty(
|
||||
this.getExpFromConstValue(i.name),
|
||||
this.getExpFromConstValue(i.initializer),
|
||||
isIdentifier(i.name),
|
||||
),
|
||||
),
|
||||
);
|
||||
} else if (isIdentifier(initializer)) {
|
||||
exp = t.identifier(initializer.value);
|
||||
}
|
||||
if (!exp) {
|
||||
throw new Error(`Not support const type yet : ${initializer.type}`);
|
||||
}
|
||||
return exp;
|
||||
}
|
||||
|
||||
private processTypeDefNode(node: TypedefDefinition) {
|
||||
const { definitionType, name, comments } = node;
|
||||
// @ts-expect-error no fix
|
||||
if (node.definitionType?.value?.split('.').length > 2) {
|
||||
// @ts-expect-error no fix
|
||||
node.definitionType.value = node.definitionType.namespaceValue;
|
||||
}
|
||||
const ast = t.tsTypeAliasDeclaration(
|
||||
t.identifier(name.value),
|
||||
null,
|
||||
this.getTsTypeFromFiledType(definitionType),
|
||||
);
|
||||
const res = withExportDeclaration(ast, comments);
|
||||
return res;
|
||||
}
|
||||
|
||||
private processTypedefEnumNode(node: TypedefDefinition, ctx: ProcessIdlCtx) {
|
||||
const { name, comments } = node;
|
||||
const enumName = name.value;
|
||||
const { statements } = ctx.ast;
|
||||
const enumItemIndexArray = findEnumItemIndex(enumName, statements);
|
||||
const enumArr = enumItemIndexArray
|
||||
.map(i => statements[i])
|
||||
.map(i => {
|
||||
const { name, comments, initializer } = i as ConstDefinition;
|
||||
|
||||
return addComment(
|
||||
t.tsEnumMember(
|
||||
t.identifier(name.value.replace(new RegExp(`^${enumName}_`), '')),
|
||||
initializer ? this.getExpFromConstValue(initializer) : initializer,
|
||||
),
|
||||
comments,
|
||||
);
|
||||
});
|
||||
const enumAst = t.tsEnumDeclaration(t.identifier(name.value), enumArr);
|
||||
|
||||
// 从后向前删除枚举项,避免索引变化影响
|
||||
enumItemIndexArray
|
||||
.sort((a, b) => b - a)
|
||||
.forEach(index => {
|
||||
statements.splice(index, 1);
|
||||
});
|
||||
return withExportDeclaration(enumAst, comments);
|
||||
}
|
||||
|
||||
private getTsTypeFromFiledType(fieldType: FieldType) {
|
||||
if (isBaseType(fieldType)) {
|
||||
return this.getTsTypeFromThriftBaseType(fieldType, true);
|
||||
} else if (isListType(fieldType) || isSetType(fieldType)) {
|
||||
const { valueType } = fieldType;
|
||||
return t.tsArrayType(this.getTsTypeFromFiledType(valueType));
|
||||
} else if (isMapType(fieldType)) {
|
||||
const { keyType, valueType } = fieldType;
|
||||
return t.tsTypeReference(
|
||||
t.identifier('Record'),
|
||||
t.tsTypeParameterInstantiation([
|
||||
this.getTsTypeFromFiledType(keyType),
|
||||
this.getTsTypeFromFiledType(valueType),
|
||||
]),
|
||||
);
|
||||
} else if (isIdentifier(fieldType)) {
|
||||
return t.tsTypeReference(t.identifier(fieldType.value));
|
||||
}
|
||||
}
|
||||
|
||||
private getTsTypeFromThriftBaseType(fieldType: BaseType, isTsType = false) {
|
||||
const typeStr = TypeMapper.map(fieldType.type as any);
|
||||
if (typeStr === 'number') {
|
||||
return !isTsType ? t.numberTypeAnnotation() : t.tsNumberKeyword();
|
||||
} else if (typeStr === 'string') {
|
||||
return !isTsType ? t.stringTypeAnnotation() : t.tsStringKeyword();
|
||||
} else if (typeStr === 'object') {
|
||||
const id = t.identifier('Blob');
|
||||
return !isTsType ? t.genericTypeAnnotation(id) : t.tsTypeReference(id);
|
||||
}
|
||||
if (typeStr === 'boolean') {
|
||||
return !isTsType ? t.booleanTypeAnnotation() : t.tsBooleanKeyword();
|
||||
}
|
||||
throw new Error(`not support :${typeStr}`);
|
||||
}
|
||||
|
||||
private processIncludes(include: string, ast: IParseResultItem) {
|
||||
const includePath = getRelativePath(ast.idlPath, ast.includeMap[include]);
|
||||
const name = ast.includeRefer[include];
|
||||
let code = `import * as ${name} from '${includePath}';\n`;
|
||||
code += `export { ${name} };\n`;
|
||||
const res = genAst<t.ImportDeclaration[]>(code);
|
||||
// const res = template.ast(code, { plugins }) as ;
|
||||
if (!Array.isArray(res)) {
|
||||
return [res];
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
private processServiceDefinition(
|
||||
node: ServiceDefinition,
|
||||
ast: IParseResultItem,
|
||||
ctx: ProcessIdlCtx,
|
||||
): t.ExportNamedDeclaration[] {
|
||||
const { functions } = node;
|
||||
if (!ast.isEntry) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const result = [] as t.ExportNamedDeclaration[];
|
||||
functions.forEach(i => {
|
||||
const { comments, extensionConfig } = i;
|
||||
if (!extensionConfig?.method) {
|
||||
return;
|
||||
}
|
||||
const metaCtx = {
|
||||
ast,
|
||||
meta: ctx.meta.find(m => m.name === i.name.value),
|
||||
service: node,
|
||||
method: i,
|
||||
template: '',
|
||||
} as IGenTemplateCtx;
|
||||
// this.program.trigger(HOOK.PARSE_FUN_META, ctx);
|
||||
this.program.trigger(HOOK.GEN_FUN_TEMPLATE, metaCtx);
|
||||
result.push(withExportDeclaration(genAst(metaCtx.template), comments));
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { type Program, after, before } from '@coze-arch/idl2ts-plugin';
|
||||
import { isStructDefinition } from '@coze-arch/idl2ts-helper';
|
||||
|
||||
import { type Contexts, HOOK } from '../context';
|
||||
|
||||
const MAGIC_COMMENT_KEY = '\n*@magic-comment';
|
||||
|
||||
// 忽略 struct 中的字段
|
||||
export class CommentFormatPlugin {
|
||||
apply(p: Program<Contexts>) {
|
||||
p.register(after('PARSE_ENTRY'), ctx => {
|
||||
const result = ctx.ast;
|
||||
for (const item of result) {
|
||||
item.statements.forEach(i => {
|
||||
if (isStructDefinition(i)) {
|
||||
const { fields } = i;
|
||||
i.fields = fields.map(i => {
|
||||
const comments = i.comments || [];
|
||||
let value = '';
|
||||
if (comments.length === 1) {
|
||||
if (Array.isArray(comments[0].value)) {
|
||||
if (comments[0].value.length > 1) {
|
||||
return i;
|
||||
}
|
||||
value = comments[0].value[0];
|
||||
} else {
|
||||
value = comments[0].value;
|
||||
}
|
||||
|
||||
comments[0].value = MAGIC_COMMENT_KEY + value;
|
||||
}
|
||||
|
||||
return { ...i, comments };
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
ctx.ast = result;
|
||||
return ctx;
|
||||
});
|
||||
p.register(before(HOOK.WRITE_FILE), ctx => {
|
||||
ctx.content = ctx.content.replaceAll(
|
||||
`
|
||||
*@magic-comment`,
|
||||
'',
|
||||
);
|
||||
return ctx;
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { type Program, after } from '@coze-arch/idl2ts-plugin';
|
||||
import {
|
||||
isStructDefinition,
|
||||
type FieldDefinition,
|
||||
} from '@coze-arch/idl2ts-helper';
|
||||
|
||||
type Filter = (f: FieldDefinition) => boolean;
|
||||
|
||||
interface IPops {
|
||||
filter: Filter;
|
||||
}
|
||||
|
||||
// 忽略 struct 中的字段
|
||||
export class IgnoreStructFiledPlugin {
|
||||
private filter: Filter;
|
||||
constructor({ filter }: IPops) {
|
||||
this.filter = filter;
|
||||
}
|
||||
apply(p: Program<{ PARSE_ENTRY: { ast: any } }>) {
|
||||
p.register(after('PARSE_ENTRY'), ctx => {
|
||||
const result = ctx.ast;
|
||||
for (const item of result) {
|
||||
item.statements.forEach(i => {
|
||||
if (isStructDefinition(i)) {
|
||||
const { fields } = i;
|
||||
i.fields = fields.filter(f => this.filter(f));
|
||||
}
|
||||
});
|
||||
}
|
||||
ctx.ast = result;
|
||||
return ctx;
|
||||
});
|
||||
}
|
||||
}
|
||||
26
frontend/infra/idl/idl2ts-generator/src/plugin/index.ts
Normal file
26
frontend/infra/idl/idl2ts-generator/src/plugin/index.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export * from './adapter-plugin';
|
||||
export * from './client-plugin';
|
||||
export * from './meta-plugin';
|
||||
export * from './mock-transformer';
|
||||
export * from './schema-plugin';
|
||||
export * from './pkg-entry-plugin';
|
||||
export * from './auto-fix-path-plugin';
|
||||
export * from './ignore-struct-field';
|
||||
export * from './auto-fix-duplicate-plugin';
|
||||
export * from './comment-format-plugin';
|
||||
219
frontend/infra/idl/idl2ts-generator/src/plugin/meta-plugin.ts
Normal file
219
frontend/infra/idl/idl2ts-generator/src/plugin/meta-plugin.ts
Normal file
@@ -0,0 +1,219 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { type IPlugin, type Program, on } from '@coze-arch/idl2ts-plugin';
|
||||
import {
|
||||
type Identifier,
|
||||
type FunctionDefinition,
|
||||
type FieldDefinition,
|
||||
type IParseResultItem,
|
||||
isStructDefinition,
|
||||
isIdentifier,
|
||||
type FunctionType,
|
||||
type IMeta,
|
||||
type IHttpRpcMapping,
|
||||
findDefinition,
|
||||
type Position,
|
||||
type ServiceDefinition,
|
||||
getStatementById,
|
||||
parseIdFiledType,
|
||||
getFieldsAlias,
|
||||
parseId,
|
||||
isFullBody,
|
||||
getAstFromNamespace,
|
||||
getSchemaRootByPath,
|
||||
getAnnotation,
|
||||
} from '@coze-arch/idl2ts-helper';
|
||||
|
||||
import { type Contexts, HOOK, type IProcessMetaItemCtx } from '../context';
|
||||
|
||||
interface IOptions {
|
||||
outputDir: string;
|
||||
idlRoot: string;
|
||||
}
|
||||
|
||||
export class MetaPlugin implements IPlugin {
|
||||
options: IOptions;
|
||||
constructor(options: IOptions) {
|
||||
this.options = options;
|
||||
}
|
||||
apply(program: Program<Contexts>): void {
|
||||
program.register(
|
||||
on(HOOK.PARSE_FUN_META),
|
||||
ctx => {
|
||||
const node = ctx.node as ServiceDefinition;
|
||||
node.functions.forEach(fun => {
|
||||
// 过滤非泛化接口
|
||||
if (!fun.extensionConfig?.method) {
|
||||
return;
|
||||
}
|
||||
const { meta } = program.trigger<IProcessMetaItemCtx>(
|
||||
HOOK.PARSE_FUN_META_ITEM,
|
||||
{
|
||||
ast: ctx.ast,
|
||||
service: node,
|
||||
method: fun,
|
||||
} as IProcessMetaItemCtx,
|
||||
);
|
||||
ctx.meta.push(meta);
|
||||
});
|
||||
return ctx;
|
||||
},
|
||||
0,
|
||||
);
|
||||
program.register(on(HOOK.PARSE_FUN_META_ITEM), ctx => {
|
||||
const { ast, service, method } = ctx;
|
||||
const item = this.parseFunAnnotation(method, ast, service.name.value);
|
||||
ctx.meta = item;
|
||||
return ctx;
|
||||
});
|
||||
}
|
||||
parseFunAnnotation(
|
||||
params: FunctionDefinition,
|
||||
ast: IParseResultItem,
|
||||
service: string,
|
||||
) {
|
||||
const { name, returnType, fields, extensionConfig } = params;
|
||||
const reqType = fields[0].fieldType as any;
|
||||
const reqMapping = this.processPayloadFields(
|
||||
reqType,
|
||||
extensionConfig?.method === 'GET' ? 'query' : 'body',
|
||||
ast,
|
||||
);
|
||||
const res = {
|
||||
url: extensionConfig?.uri,
|
||||
method: extensionConfig?.method ?? 'POST',
|
||||
name: name.value,
|
||||
reqType: parseId(reqType.value),
|
||||
reqMapping,
|
||||
resType: parseId(this.processReqResPramsType(returnType, ast)),
|
||||
schemaRoot: getSchemaRootByPath(ast.idlPath, this.options.idlRoot),
|
||||
service,
|
||||
} as IMeta;
|
||||
// 不是 json 时,需要加上 serializer 标识
|
||||
if (extensionConfig?.serializer && extensionConfig?.serializer !== 'json') {
|
||||
res.serializer = extensionConfig?.serializer;
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
private processReqResPramsType(id: FunctionType, ast: IParseResultItem) {
|
||||
if (isIdentifier(id)) {
|
||||
const statement = getStatementById(id, ast);
|
||||
if (isStructDefinition(statement)) {
|
||||
const wholeBody = statement.fields.find(isFullBody);
|
||||
if (wholeBody) {
|
||||
// 处理 api.body="." 以及 api.full_body=''
|
||||
return `${id.value}['${getFieldsAlias(wholeBody)}']`;
|
||||
} else {
|
||||
return id.value;
|
||||
}
|
||||
}
|
||||
throw new Error('params must be identifier');
|
||||
} else {
|
||||
return 'void';
|
||||
}
|
||||
}
|
||||
|
||||
private processPayloadFields(
|
||||
id: Identifier,
|
||||
defaultPosition: 'query' | 'body',
|
||||
entry: IParseResultItem,
|
||||
): IHttpRpcMapping {
|
||||
const { namespace, refName } = parseIdFiledType(id);
|
||||
|
||||
if (namespace) {
|
||||
const ast = getAstFromNamespace(namespace, entry);
|
||||
const struct = findDefinition(ast, refName);
|
||||
if (!struct || !isStructDefinition(struct)) {
|
||||
throw new Error(`can not find Struct: ${refName} `);
|
||||
}
|
||||
return this.createMapping(struct.fields, defaultPosition);
|
||||
}
|
||||
|
||||
const struct = findDefinition(entry.statements, id.value);
|
||||
if (!struct || !isStructDefinition(struct)) {
|
||||
throw new Error(`can not find Struct: ${id.value} `);
|
||||
}
|
||||
|
||||
return this.createMapping(struct.fields, defaultPosition);
|
||||
}
|
||||
|
||||
private createMapping(
|
||||
fields: FieldDefinition[],
|
||||
defaultPosition: 'query' | 'body',
|
||||
): IHttpRpcMapping {
|
||||
const specificPositionFiled = new Set<string>();
|
||||
const mapping = {} as IHttpRpcMapping;
|
||||
fields.forEach(filed => {
|
||||
const jsonAnnotation = getAnnotation(filed.annotations, 'api.json');
|
||||
if (jsonAnnotation) {
|
||||
filed.extensionConfig = filed.extensionConfig || {};
|
||||
filed.extensionConfig.key = jsonAnnotation;
|
||||
}
|
||||
const { extensionConfig } = filed;
|
||||
const alias = getFieldsAlias(filed);
|
||||
if (extensionConfig) {
|
||||
if (isFullBody(filed)) {
|
||||
mapping.entire_body = [alias];
|
||||
return;
|
||||
}
|
||||
Object.keys(extensionConfig).forEach(key => {
|
||||
if (key === 'position' && extensionConfig.position) {
|
||||
const filedMapping = this.processMapping(
|
||||
mapping,
|
||||
extensionConfig.position,
|
||||
alias,
|
||||
);
|
||||
mapping[extensionConfig.position] = filedMapping;
|
||||
specificPositionFiled.add(alias);
|
||||
}
|
||||
});
|
||||
}
|
||||
// 如果没有指定,根据method默认指定为query 或者 body
|
||||
if (!specificPositionFiled.has(alias)) {
|
||||
const filedMapping = mapping[defaultPosition];
|
||||
mapping[defaultPosition] = filedMapping
|
||||
? [...filedMapping, alias]
|
||||
: [alias];
|
||||
}
|
||||
});
|
||||
return mapping;
|
||||
}
|
||||
|
||||
private processMapping(
|
||||
mapping: IHttpRpcMapping,
|
||||
position: Position,
|
||||
filedName: string,
|
||||
): string[] {
|
||||
const mappingKeys = [
|
||||
'path',
|
||||
'query',
|
||||
'status_code',
|
||||
'header',
|
||||
'cookie',
|
||||
'entire_body',
|
||||
'body',
|
||||
];
|
||||
if (mappingKeys.find(i => i === position)) {
|
||||
const data = mapping[position];
|
||||
return data ? [...data, filedName] : [filedName];
|
||||
} else {
|
||||
return mapping[position] || [];
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,469 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { faker } from '@faker-js/faker';
|
||||
import {
|
||||
type IPlugin,
|
||||
type Program,
|
||||
on,
|
||||
after,
|
||||
} from '@coze-arch/idl2ts-plugin';
|
||||
import {
|
||||
type IParseResultItem,
|
||||
type EnumDefinition,
|
||||
isServiceDefinition,
|
||||
isStructDefinition,
|
||||
isIdentifier,
|
||||
type StructDefinition,
|
||||
isBaseType,
|
||||
type FieldType,
|
||||
type FunctionType,
|
||||
isEnumDefinition,
|
||||
isListType,
|
||||
isStringLiteral,
|
||||
type TypedefDefinition,
|
||||
type ConstDefinition,
|
||||
type ConstValue,
|
||||
isSetType,
|
||||
isMapType,
|
||||
isIntConstant,
|
||||
isConstDefinition,
|
||||
type ProcessIdlCtx,
|
||||
isTypedefDefinition,
|
||||
type UnifyStatement,
|
||||
SyntaxType,
|
||||
type ServiceDefinition,
|
||||
getStatementById,
|
||||
parseIdFiledType,
|
||||
uniformNs,
|
||||
hasDynamicJsonAnnotation,
|
||||
getValuesFromEnum,
|
||||
isFullBody,
|
||||
removeFileExt,
|
||||
parseId,
|
||||
getOutputName,
|
||||
getFieldsAlias,
|
||||
getBaseTypeConverts,
|
||||
getRelativePath,
|
||||
} from '@coze-arch/idl2ts-helper';
|
||||
import * as t from '@babel/types';
|
||||
import template from '@babel/template';
|
||||
|
||||
import { type Options } from '../types';
|
||||
import { genMockPublic } from '../template';
|
||||
import { type Contexts, type GenMockFieldCtx, HOOK } from '../context';
|
||||
|
||||
interface ProcessIdlCtxWithMock extends ProcessIdlCtx {
|
||||
mockStatements: t.Statement[];
|
||||
}
|
||||
|
||||
export class MockTransformerPlugin implements IPlugin {
|
||||
private options: Options;
|
||||
private program!: Program<Contexts>;
|
||||
constructor(options: Options) {
|
||||
this.options = options;
|
||||
}
|
||||
apply(program: Program<Contexts>): void {
|
||||
this.program = program;
|
||||
program.register(
|
||||
on(HOOK.PROCESS_IDL_NODE),
|
||||
ctx => {
|
||||
const { node } = ctx;
|
||||
ctx.mockStatements = ctx.mockStatements || [];
|
||||
|
||||
if (!node) {
|
||||
throw new Error('node is undefined');
|
||||
}
|
||||
const statement = this.processIdlNode(node, ctx);
|
||||
if (statement) {
|
||||
ctx.mockStatements.push(statement);
|
||||
}
|
||||
return ctx;
|
||||
},
|
||||
0,
|
||||
);
|
||||
|
||||
program.register(on(HOOK.GEN_MOCK_FILED), ctx => this.genMockField(ctx));
|
||||
|
||||
program.register(after(HOOK.PROCESS_IDL_AST), ctx => {
|
||||
const { ast, mock, mockStatements } = ctx;
|
||||
const exportId = [] as string[];
|
||||
const mockVarOrder = this.getMockVarOrder(mock);
|
||||
const nextOrder = {} as Record<string, number>;
|
||||
mockStatements.forEach((i, index) => {
|
||||
if (t.isVariableDeclaration(i)) {
|
||||
const { name } = i.declarations[0].id as t.Identifier;
|
||||
exportId.push(name);
|
||||
nextOrder[name] = index;
|
||||
}
|
||||
});
|
||||
// 按照 mock 文件中的顺序优先排序
|
||||
const getOrder = (name: string) =>
|
||||
typeof mockVarOrder[name] !== 'undefined'
|
||||
? mockVarOrder[name]
|
||||
: nextOrder[name];
|
||||
const body = mockStatements.sort((a, b) => {
|
||||
if (t.isVariableDeclaration(a) && t.isVariableDeclaration(b)) {
|
||||
const { name: nameA } = a.declarations[0].id as t.Identifier;
|
||||
const { name: nameB } = b.declarations[0].id as t.Identifier;
|
||||
const result = getOrder(nameA) - getOrder(nameB);
|
||||
return result;
|
||||
}
|
||||
return 0;
|
||||
});
|
||||
if (ast.includes) {
|
||||
Object.keys(ast.includeMap).forEach(i => {
|
||||
body.unshift(this.processIncludes(i, ast));
|
||||
});
|
||||
}
|
||||
const temp = template.ast(
|
||||
`module.exports = {${exportId.join(',')}}`,
|
||||
) as t.Statement;
|
||||
body.push(temp);
|
||||
|
||||
mock.program.body = [genMockPublic(ctx, this.options), ...body];
|
||||
ctx.output.set(
|
||||
getOutputName({
|
||||
source: `${removeFileExt(ast.idlPath)}.mock.js`,
|
||||
outputDir: this.options.outputDir,
|
||||
idlRoot: this.options.idlRoot,
|
||||
}),
|
||||
{ type: 'babel', content: mock },
|
||||
);
|
||||
return ctx;
|
||||
});
|
||||
}
|
||||
|
||||
private getMockVarOrder(file: t.File) {
|
||||
const orders = {} as Record<string, number>;
|
||||
file.program.body.map((i, index) => {
|
||||
if (t.isVariableDeclaration(i)) {
|
||||
const identifier = i.declarations[0].id as t.Identifier;
|
||||
orders[identifier.name] = index;
|
||||
}
|
||||
});
|
||||
return orders;
|
||||
}
|
||||
private processIdlNode(
|
||||
statement: UnifyStatement,
|
||||
ctx: ProcessIdlCtxWithMock,
|
||||
) {
|
||||
if (isStructDefinition(statement)) {
|
||||
return this.processStructNode(statement, ctx);
|
||||
} else if (isTypedefDefinition(statement)) {
|
||||
return this.processTypeDefNode(statement, ctx);
|
||||
} else if (isEnumDefinition(statement)) {
|
||||
return this.processEnumDefNode(statement, ctx);
|
||||
} else if (isConstDefinition(statement)) {
|
||||
return this.processConstDefNode(statement, ctx);
|
||||
} else if (isServiceDefinition(statement)) {
|
||||
return this.processServiceDefinition(statement, ctx);
|
||||
}
|
||||
throw new Error(`can not process Node from statement type: ${statement}`);
|
||||
}
|
||||
|
||||
private processIncludes(include: string, ast: IParseResultItem) {
|
||||
const includePath = getRelativePath(ast.idlPath, ast.includeMap[include]);
|
||||
const name = ast.includeRefer[include];
|
||||
const temp = template.ast(
|
||||
`const ${name} = require('${`${includePath}.mock.js`}')`,
|
||||
) as t.ImportDeclaration;
|
||||
return temp;
|
||||
}
|
||||
private processServiceDefinition(
|
||||
srtuct: ServiceDefinition,
|
||||
ctx: ProcessIdlCtxWithMock,
|
||||
) {
|
||||
const { name, functions } = srtuct;
|
||||
if (this.findTarget(name.value, ctx)) {
|
||||
return;
|
||||
}
|
||||
const variableDeclaration = template.ast(
|
||||
`var ${name.value} = {${functions.map(f => {
|
||||
const { name, returnType, fields } = f;
|
||||
const reqType = fields[0].fieldType as any;
|
||||
const resType = this.processReqResPramsType(returnType, ctx.ast);
|
||||
return `${name.value}:{req:${parseId(reqType.value)},res:${parseId(resType)}}`;
|
||||
})}}`,
|
||||
) as t.ExpressionStatement;
|
||||
return variableDeclaration;
|
||||
}
|
||||
private processReqResPramsType(
|
||||
fieldType: FunctionType,
|
||||
ast: IParseResultItem,
|
||||
) {
|
||||
if (isIdentifier(fieldType)) {
|
||||
const statement = getStatementById(fieldType, ast);
|
||||
if (isStructDefinition(statement)) {
|
||||
const wholeBody = statement.fields.find(isFullBody);
|
||||
if (wholeBody) {
|
||||
// 处理 api.body="."
|
||||
const { annotations } = wholeBody;
|
||||
if (hasDynamicJsonAnnotation(annotations)) {
|
||||
return '{}';
|
||||
}
|
||||
return `${fieldType.value}['${getFieldsAlias(wholeBody)}']`;
|
||||
} else {
|
||||
return fieldType.value;
|
||||
}
|
||||
}
|
||||
throw new Error('params must be identifier');
|
||||
} else {
|
||||
return 'void';
|
||||
}
|
||||
}
|
||||
private processStructNode(
|
||||
struct: StructDefinition,
|
||||
ctx: ProcessIdlCtxWithMock,
|
||||
) {
|
||||
const { name, fields } = struct;
|
||||
if (this.findTarget(name.value, ctx)) {
|
||||
return;
|
||||
}
|
||||
const oldOne = this.getVariableDeclarationById(name.value, ctx);
|
||||
const variableDeclaration =
|
||||
oldOne ||
|
||||
(template.ast(
|
||||
`var ${name.value} = createStruct(()=>{ return {} })`,
|
||||
) as t.VariableDeclaration);
|
||||
const init = (variableDeclaration.declarations[0].init as t.CallExpression)
|
||||
.arguments[0] as t.ArrowFunctionExpression;
|
||||
const returnObj = (
|
||||
(init.body as t.BlockStatement).body.find(i =>
|
||||
t.isReturnStatement(i),
|
||||
) as t.ReturnStatement
|
||||
).argument as t.ObjectExpression;
|
||||
if (!returnObj) {
|
||||
throw new Error('struct mock must return obj');
|
||||
}
|
||||
const fieldNames = new Set(fields.map(i => getFieldsAlias(i)));
|
||||
const newPros = [] as t.ObjectProperty[];
|
||||
const includeFieldName = (pro: t.ObjectProperty) =>
|
||||
(t.isStringLiteral(pro.key) && fieldNames.has(pro.key.value)) ||
|
||||
(t.isIdentifier(pro.key) && fieldNames.has(pro.key.name));
|
||||
returnObj.properties.forEach(i => {
|
||||
if (t.isObjectProperty(i)) {
|
||||
if (includeFieldName(i)) {
|
||||
const key = t.isStringLiteral(i.key)
|
||||
? i.key.value
|
||||
: t.isIdentifier(i.key)
|
||||
? i.key.name
|
||||
: '';
|
||||
fieldNames.delete(key);
|
||||
}
|
||||
newPros.push(i);
|
||||
}
|
||||
});
|
||||
fields.forEach(f => {
|
||||
const { fieldType, defaultValue } = f;
|
||||
const fieldName = getFieldsAlias(f);
|
||||
if (!fieldNames.has(fieldName)) {
|
||||
return;
|
||||
}
|
||||
// 没有的,需要重新生成
|
||||
newPros.push(
|
||||
t.objectProperty(
|
||||
fieldName.includes('-')
|
||||
? t.stringLiteral(fieldName)
|
||||
: t.identifier(fieldName),
|
||||
this.processValue(fieldType, defaultValue || undefined, {
|
||||
fieldDefinition: f,
|
||||
struct,
|
||||
ast: ctx.ast,
|
||||
}),
|
||||
),
|
||||
);
|
||||
});
|
||||
returnObj.properties = newPros;
|
||||
// this.processNodes.set(name.value, variableDeclaration);
|
||||
return variableDeclaration;
|
||||
}
|
||||
private processValue(
|
||||
fieldType: FieldType | FunctionType,
|
||||
defaultValue?: ConstValue,
|
||||
context?: GenMockFieldCtx['context'],
|
||||
) {
|
||||
const ctx = this.program.trigger(HOOK.GEN_MOCK_FILED, {
|
||||
fieldType,
|
||||
defaultValue,
|
||||
context,
|
||||
} as GenMockFieldCtx);
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
return ctx.output!;
|
||||
}
|
||||
// eslint-disable-next-line complexity
|
||||
private genMockField(ctx: GenMockFieldCtx) {
|
||||
let { output } = ctx;
|
||||
const { defaultValue, fieldType } = ctx;
|
||||
if (output) {
|
||||
return ctx;
|
||||
}
|
||||
if (isBaseType(fieldType)) {
|
||||
const type = getBaseTypeConverts('number')[fieldType.type];
|
||||
|
||||
if (type === 'string') {
|
||||
let value = faker.word.words();
|
||||
if (defaultValue && defaultValue.type === SyntaxType.StringLiteral) {
|
||||
value = defaultValue.value;
|
||||
}
|
||||
output = t.stringLiteral(value);
|
||||
} else if (type === 'number') {
|
||||
let value = faker.number.int();
|
||||
if (defaultValue && defaultValue.type === SyntaxType.IntConstant) {
|
||||
value = Number(defaultValue.value.value);
|
||||
}
|
||||
output = t.numericLiteral(value);
|
||||
} else if (type === 'boolean') {
|
||||
let value = faker.datatype.boolean();
|
||||
if (defaultValue && defaultValue.type === SyntaxType.BooleanLiteral) {
|
||||
value = defaultValue.value;
|
||||
}
|
||||
output = t.booleanLiteral(value);
|
||||
} else if (type === 'object') {
|
||||
// binary
|
||||
output = t.callExpression(
|
||||
t.memberExpression(t.identifier('Buffer'), t.identifier('from')),
|
||||
[t.stringLiteral(faker.word.words())],
|
||||
);
|
||||
}
|
||||
} else if (isMapType(fieldType)) {
|
||||
const { valueType } = fieldType;
|
||||
output = t.objectExpression([
|
||||
t.objectProperty(
|
||||
t.identifier(faker.word.words()),
|
||||
this.processValue(valueType),
|
||||
),
|
||||
]);
|
||||
} else if (isListType(fieldType)) {
|
||||
const { valueType } = fieldType;
|
||||
output = t.arrayExpression([this.processValue(valueType)]);
|
||||
} else if (isSetType(fieldType)) {
|
||||
// set 处理成array校验
|
||||
const { valueType } = fieldType;
|
||||
output = t.arrayExpression([this.processValue(valueType)]);
|
||||
} else if (isIdentifier(fieldType)) {
|
||||
// 引用类型
|
||||
const { refName, namespace } = parseIdFiledType(fieldType);
|
||||
if (!namespace) {
|
||||
output = t.callExpression(t.identifier(refName), []);
|
||||
} else {
|
||||
output = t.callExpression(
|
||||
t.memberExpression(
|
||||
t.identifier(uniformNs(namespace)),
|
||||
t.identifier(refName),
|
||||
),
|
||||
[],
|
||||
);
|
||||
}
|
||||
}
|
||||
if (output) {
|
||||
return { fieldType, defaultValue, output };
|
||||
}
|
||||
throw new Error(`can not process fieldType : ${fieldType.type}`);
|
||||
}
|
||||
private processConst(constVal: ConstValue) {
|
||||
// 暂时统一处理成0
|
||||
if (isStringLiteral(constVal)) {
|
||||
return t.stringLiteral(constVal.value);
|
||||
}
|
||||
if (isIntConstant(constVal)) {
|
||||
return t.stringLiteral(constVal.value.value);
|
||||
}
|
||||
return t.numericLiteral(0);
|
||||
}
|
||||
|
||||
private processTypeDefNode(
|
||||
typeDef: TypedefDefinition,
|
||||
ctx: ProcessIdlCtxWithMock,
|
||||
) {
|
||||
const { definitionType, name } = typeDef;
|
||||
if (this.findTarget(name.value, ctx)) {
|
||||
return;
|
||||
}
|
||||
const builder = template(`var ${name.value}= () => %%value%% `);
|
||||
const variableDeclaration = builder({
|
||||
value: this.processValue(definitionType),
|
||||
}) as t.VariableDeclaration;
|
||||
return variableDeclaration;
|
||||
}
|
||||
private processEnumDefNode(def: EnumDefinition, ctx: ProcessIdlCtxWithMock) {
|
||||
const { name, members } = def;
|
||||
const values = getValuesFromEnum(def);
|
||||
|
||||
const commentValues = values.map((value, index) => {
|
||||
const { name } = members[index];
|
||||
return ` ${name.value}: ${value}`;
|
||||
});
|
||||
const comment = { type: 'CommentLine', value: commentValues } as any;
|
||||
const target = this.findTarget(name.value, ctx);
|
||||
if (target) {
|
||||
// 需要更新注释
|
||||
// target.trailingComments = [comment];
|
||||
return;
|
||||
}
|
||||
// 枚举类型统一处理成常量
|
||||
const builder = template(`var ${name.value}= () => %%value%% `);
|
||||
const node = builder({
|
||||
value: t.numericLiteral(values[0] || 0),
|
||||
}) as t.VariableDeclaration;
|
||||
const variableDeclaration = t.addComments(node, 'trailing', [comment]);
|
||||
return variableDeclaration;
|
||||
}
|
||||
private processConstDefNode(
|
||||
constDef: ConstDefinition,
|
||||
ctx: ProcessIdlCtxWithMock,
|
||||
) {
|
||||
const { name, initializer } = constDef;
|
||||
if (this.findTarget(name.value, ctx)) {
|
||||
return;
|
||||
}
|
||||
const builder = template(`var ${name.value}= () => %%value%% `);
|
||||
const node = builder({
|
||||
value: this.processConst(initializer),
|
||||
}) as t.VariableDeclaration;
|
||||
// const variableDeclaration = t.addComment(
|
||||
// ,
|
||||
// 'leading',
|
||||
// '暂时对const默认处理为0,如有需要请自行重新赋值'
|
||||
// );
|
||||
return node;
|
||||
}
|
||||
private getVariableDeclarationById(id: string, ctx: ProcessIdlCtxWithMock) {
|
||||
return ctx.mock.program.body.find(i => {
|
||||
if (t.isVariableDeclaration(i)) {
|
||||
const identifier = i.declarations[0].id as t.Identifier;
|
||||
if (identifier.name === id) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}) as t.VariableDeclaration | undefined;
|
||||
}
|
||||
private findTarget(id: string, ctx: ProcessIdlCtxWithMock) {
|
||||
return ctx.mockStatements.find(i => this.getIdName(i) === id);
|
||||
}
|
||||
private getIdName(statement: t.Statement): string {
|
||||
if (
|
||||
t.isVariableDeclaration(statement) &&
|
||||
t.isVariableDeclarator(statement.declarations[0])
|
||||
) {
|
||||
if (t.isIdentifier(statement.declarations[0].id)) {
|
||||
return statement.declarations[0].id.name;
|
||||
}
|
||||
}
|
||||
return '';
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user