/* * 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); }); }); });