Thrift client 2.0 (#23)

This commit is contained in:
Ildar Galeev 2022-12-14 14:04:36 +03:00 committed by GitHub
parent 661afde3f6
commit 246fc01328
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
28 changed files with 12055 additions and 42 deletions

View File

@ -10,5 +10,5 @@ jobs:
- uses: actions/checkout@v3
- uses: valitydev/action-frontend/setup@v0.1
- run: npm ci
- name: Check
run: npm run check
- name: Prettier check
run: npm run prettier:check

6
.gitignore vendored
View File

@ -1 +1,5 @@
node_modules
node_modules
proto
dist
clients
tools/stub.d.ts

View File

@ -1,4 +1,7 @@
package.json
package-lock.json
node_modules
.github
.github
proto
dist
clients

View File

@ -1,9 +1,28 @@
# Frontend Thrift Codegen CLI
Generate NodeJS/JS code, create models and metadata by Thrift services.
This project allows generation of JS client library, typings and json metadata automatically given a thrift spec.
## Usage
```shell
thirft-codegen
```
thrift-codegen [options]
```
Options:
```
-i, --inputs List of thrift file folders for compilation. [array] [required]
-n, --namespaces List of service namespaces which will be included. [array] [required]
-t, --types List of types namespaces witch will be exported. [array] [required]
-p, --path Default service connection path. [string] [required]
```
## Testing
- Copy thrift spec to `proto` directory. For example [damsel](https://github.com/valitydev/damsel).
- Run
npm run codegen -- --i ./proto --n domain_config --t domain_config domain --p /wachter
- Codegen client will be available in `dist` directory.

View File

@ -1,3 +1,3 @@
#!/usr/bin/env node
'use strict';
require('../lib/cli').default();
require('../tools/cli').default();

11232
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,10 +1,11 @@
{
"name": "@vality/thrift-codegen",
"version": "1.1.0",
"version": "2.0.0",
"description": "",
"main": "./lib/codegen.js",
"scripts": {
"check": "prettier \"**/*.{html,js,ts,css,scss,md,json,prettierrc,svg,huskyrc,yml,yaml}\" --list-different"
"prettier:check": "prettier \"**\" --list-different --ignore-unknown",
"prettier:write": "prettier \"**\" --write --ignore-unknown",
"codegen": "bin/index.js"
},
"author": "Vality",
"license": "Apache-2.0",
@ -17,7 +18,16 @@
},
"dependencies": {
"@vality/thrift-ts": "2.4.1-8ad5123.0",
"@vality/woody": "^0.1.1",
"fs-extra": "^10.1.0",
"generate-template-files": "^3.2.1",
"glob": "^8.0.3",
"lodash": "^4.17.21",
"rimraf": "^3.0.2",
"ts-loader": "^9.4.1",
"typescript": "4.6.2",
"webpack": "^5.74.0",
"webpack-cli": "^4.10.0",
"yargs": "17.3.1"
},
"devDependencies": {

62
tools/build.js Normal file
View File

@ -0,0 +1,62 @@
const path = require('path');
const webpack = require('webpack');
const build = async () =>
new Promise((resolve, reject) => {
webpack(
{
name: 'thrift-codegen',
mode: 'production',
entry: path.resolve('clients/index.ts'),
devtool: 'source-map',
module: {
rules: [
{
test: /\.ts?$/,
use: [
{
loader: 'ts-loader',
options: {
context: __dirname,
configFile: path.resolve(__dirname, 'tsconfig.json'),
},
},
],
exclude: /node_modules/,
},
],
},
resolve: {
extensions: ['.ts', '.js'],
alias: {
thrift: path.resolve('node_modules/@vality/woody/dist/thrift'),
},
},
output: {
filename: 'thrift-codegen.bundle.js',
path: path.resolve('dist'),
globalObject: 'this',
library: {
name: 'thriftCodegen',
type: 'umd',
},
},
},
(err, stats) => {
if (err) {
console.error(err);
reject(err);
return;
}
console.log(
stats.toString({
chunks: false,
colors: true,
})
);
resolve();
}
);
});
module.exports = build;

125
tools/cli.js Normal file
View File

@ -0,0 +1,125 @@
const fs = require('fs');
const fse = require('fs-extra');
const glob = require('glob');
const path = require('path');
const rimraf = require('rimraf');
const camelCase = require('lodash/camelCase');
const yargs = require('yargs/yargs');
const { hideBin } = require('yargs/helpers');
const compileProto = require('./compile-proto');
const generateServiceTemplate = require('./generate-service-template');
const build = require('./build');
/**
* Dist with compiled proto contains files with name: '{namespace}-{serviceName}.ext'
* Pair of namespace and serviceName requires for preparing codegen client.
*/
const prepareGenerateServiceConfig = (compiledDist, includedNamespaces) => {
const result = fs
.readdirSync(compiledDist)
.map((filePath) => path.parse(filePath))
.filter(({ ext, name }) => ext === '.js' && name.includes('-'))
.map(({ name }) => {
const [namespace, serviceName] = name.split('-');
return {
namespace,
serviceName,
};
})
.reduce((acc, curr) => {
const duplicate = acc.find(({ serviceName }) => serviceName === curr.serviceName);
const result = {
...curr,
exportName: duplicate
? camelCase(`${camelCase(curr.namespace)}${curr.serviceName}`)
: curr.serviceName,
};
return [...acc, result];
}, []);
if (includedNamespaces.length === 0) {
return result;
}
return result.reduce(
(acc, curr) => (includedNamespaces.includes(curr.namespace) ? [...acc, curr] : [...acc]),
[]
);
};
const rm = (path) =>
new Promise((resolve, reject) => rimraf(path, (err) => (err ? reject(err) : resolve())));
const clean = async () => {
await rm(path.resolve('clients'));
await rm(path.resolve('dist'));
};
const copyTypes = async () =>
new Promise((resolve, reject) => {
glob(path.resolve('clients/**/*.d.ts'), (err, files) => {
if (err) reject(err);
for (const file of files) {
const parsed = path.parse(file);
const output = parsed.dir.replace(path.resolve('clients'), '');
fse.copySync(file, path.resolve(`dist/types/${output}/${parsed.base}`));
resolve();
}
});
});
const copyMetadata = async () =>
Promise.all([
fse.copy(
path.resolve('clients/internal/metadata.json'),
path.resolve('dist/metadata.json')
),
fse.copy(
path.resolve(__dirname, 'types/metadata.json.d.ts'),
path.resolve('dist/types/metadata.json.d.ts')
),
]);
const copyTsUtils = async () =>
fse.copy(path.resolve(__dirname, 'utils'), path.resolve('clients/utils'));
async function codegenClient() {
const argv = yargs(hideBin(process.argv)).options({
inputs: {
alias: 'i',
demandOption: true,
type: 'array',
description: 'List of thrift file folders for compilation.',
},
namespaces: {
alias: 'n',
demandOption: true,
type: 'array',
description: 'List of service namespaces which will be included.',
},
types: {
alias: 't',
demandOption: true,
type: 'array',
description: 'List of types namespaces witch will be exported.',
},
path: {
alias: 'p',
demandOption: true,
type: 'string',
description: 'Default service connection path.',
},
}).argv;
await clean();
const outputPath = './clients';
const outputProtoPath = `${outputPath}/internal`;
await compileProto(argv.inputs, outputProtoPath);
const serviceTemplateConfig = prepareGenerateServiceConfig(outputProtoPath, argv.namespaces);
await generateServiceTemplate(serviceTemplateConfig, argv.types, outputPath, argv.path);
await copyTsUtils();
await build();
await copyMetadata();
await copyTypes();
}
module.exports = codegenClient;
module.exports.default = codegenClient;

View File

@ -1,7 +1,6 @@
const fs = require('fs');
const path = require('path');
const { exec } = require('child_process');
const yargs = require('yargs');
async function execWithLog(cmd, cwd = process.cwd()) {
console.log(`> ${cmd}`);
@ -45,7 +44,7 @@ async function codegen(protoPath, depsPaths, outputPath) {
${protos
.map((proto) => `import * as ${proto} from './gen-nodejs/${proto}_types.js';`)
.join('\n')}
export default {${protos.join(',')}}
`
);
@ -65,20 +64,12 @@ function isThriftFile(file) {
return path.parse(file).ext === '.thrift';
}
async function codegenAll() {
const argv = yargs.command('gen').option('dist', {
alias: 'd',
type: 'string',
description: 'Dist directory',
default: './lib',
}).argv;
async function compileProto(protoPaths, resultDist) {
const PROJECT_PATH = process.cwd();
const PROTO_PATH = path.join(PROJECT_PATH, argv._[0]);
const DEPS_PATHS = argv._.slice(1).map((d) => path.join(PROJECT_PATH, d));
const DIST_PATH = path.join(PROJECT_PATH, argv.dist);
const PROTO_PATH = path.join(PROJECT_PATH, protoPaths[0]);
const DEPS_PATHS = protoPaths.slice(1).map((d) => path.join(PROJECT_PATH, d));
const DIST_PATH = path.join(PROJECT_PATH, resultDist);
const PROTOS_FILES = fs.readdirSync(PROTO_PATH).filter((proto) => isThriftFile(proto));
const codegens = [];
for (const protoPath of PROTOS_FILES.map((proto) => path.join(PROTO_PATH, proto))) {
codegens.push(codegen(protoPath, DEPS_PATHS, DIST_PATH));
@ -96,5 +87,4 @@ async function codegenAll() {
);
}
module.exports = codegenAll;
module.exports.default = codegenAll;
module.exports = compileProto;

View File

@ -0,0 +1,77 @@
const path = require('path');
const { generateTemplateFilesBatch } = require('generate-template-files');
const prepareIndexFileContent = (config, typeExportNamespaces) => {
const serviceExports = config.reduce(
(acc, { exportName }) => acc.concat(`export * from './${exportName}';\n`),
''
);
return typeExportNamespaces.reduce(
(acc, typeNamespace) =>
acc.concat(`export * as ${typeNamespace} from './${typeNamespace}';\n`),
serviceExports
);
};
const generateServiceTemplate = async (
config,
typeExportNamespaces,
outputPath,
connectionPath
) => {
await generateTemplateFilesBatch([
...config.map(({ serviceName, namespace, exportName }) => ({
option: 'Create thrift client',
defaultCase: '(noCase)',
entry: {
folderPath: path.resolve(__dirname, 'templates/__exportName__.ts'),
},
dynamicReplacers: [
{ slot: '__exportName__', slotValue: exportName },
{ slot: '__serviceName__', slotValue: serviceName },
{ slot: '__namespace__', slotValue: namespace },
{ slot: '__utilsPath__', slotValue: './utils' },
{ slot: '__connectionPath__', slotValue: connectionPath },
],
output: {
path: `${outputPath}/__exportName__.ts`,
pathAndFileNameDefaultCase: '(noCase)',
overwrite: true,
},
})),
...typeExportNamespaces.map((typesNamespace) => ({
option: 'Create types namespace',
defaultCase: '(noCase)',
entry: {
folderPath: path.resolve(__dirname, 'templates/__typesNamespace__.ts'),
},
dynamicReplacers: [{ slot: '__typesNamespace__', slotValue: typesNamespace }],
output: {
path: `${outputPath}/__typesNamespace__.ts`,
pathAndFileNameDefaultCase: '(noCase)',
overwrite: true,
},
})),
{
option: 'Create index file with exports',
defaultCase: '(noCase)',
entry: {
folderPath: path.resolve(__dirname, 'templates/index.ts'),
},
dynamicReplacers: [
{
slot: '__export__',
slotValue: prepareIndexFileContent(config, typeExportNamespaces),
},
],
output: {
path: `${outputPath}/index.ts`,
pathAndFileNameDefaultCase: '(noCase)',
overwrite: true,
},
},
]);
};
module.exports = generateServiceTemplate;

1
tools/stub.ts Normal file
View File

@ -0,0 +1 @@
export const tsconfigIncludeStub = '';

View File

@ -0,0 +1,32 @@
import { CodegenClient as __exportName__CodegenClient } from './internal/__namespace__-__serviceName__';
import context from './internal/__namespace__/context';
import * as service from './internal/__namespace__/gen-nodejs/__serviceName__';
import { getMethodsMetadata, codegenClientReducer } from '__utilsPath__';
import { ConnectOptions } from '__utilsPath__/types';
export { CodegenClient as __exportName__CodegenClient } from './internal/__namespace__-__serviceName__';
export const __exportName__ = async (
options: ConnectOptions
): Promise<__exportName__CodegenClient> => {
const serviceName = '__serviceName__';
const namespace = '__namespace__';
const methodsMeta = getMethodsMetadata(options.metadata, namespace, serviceName);
const connectionContext = {
path: options.path || '__connectionPath__',
service,
headers: options.headers,
deadlineConfig: options.deadlineConfig,
};
const loggingContext = { namespace, serviceName, logging: options.logging || false };
return methodsMeta.reduce(
codegenClientReducer<__exportName__CodegenClient>(
connectionContext,
options.metadata,
loggingContext,
context
),
{} as __exportName__CodegenClient
);
};

View File

@ -0,0 +1 @@
export * from './internal/__typesNamespace__';

3
tools/templates/index.ts Normal file
View File

@ -0,0 +1,3 @@
export * from './utils/types';
__export__;

22
tools/tsconfig.json Normal file
View File

@ -0,0 +1,22 @@
{
"compilerOptions": {
"target": "es2020",
"module": "esnext",
"strict": true,
"forceConsistentCasingInFileNames": true,
"noImplicitOverride": true,
"noPropertyAccessFromIndexSignature": true,
"noImplicitReturns": true,
"noImplicitAny": false,
"noFallthroughCasesInSwitch": true,
"noEmit": false,
"moduleResolution": "node",
"sourceMap": false,
"isolatedModules": false,
"esModuleInterop": true,
"resolveJsonModule": true,
"declaration": true
},
"include": ["./"],
"exclude": ["./templates", "./utils"]
}

3
tools/types/metadata.json.d.ts vendored Normal file
View File

@ -0,0 +1,3 @@
import { ThriftAstMetadata } from './utils/types/thrift-ast-metadata';
export type Metadata = ThriftAstMetadata[];

View File

@ -0,0 +1,23 @@
const TIMEOUT_MS = 60_000;
const later = (delay: number) => new Promise((resolve) => setTimeout(resolve, delay));
export const callThriftService = (connection: any, methodName: string, args: any[]) => {
const serviceMethod = connection[methodName];
if (serviceMethod === null || serviceMethod === undefined) {
throw new Error(`Service method: "${methodName}" is not found in thrift client`);
}
return Promise.race([
later(TIMEOUT_MS).then(() => {
throw new Error(`Service method ${methodName} call timeout`);
}),
new Promise((resolve, reject) => {
serviceMethod.call(connection, ...args, (exception: unknown, result: unknown) => {
if (exception) {
reject(exception);
}
resolve(result);
});
}),
]);
};

View File

@ -0,0 +1,95 @@
import { ArgOrExecption, Method } from '@vality/thrift-ts';
import connectClient from '@vality/woody';
import { DeadlineConfig, KeyValue } from '@vality/woody/src/connect-options';
import { createThriftInstance } from './create-thrift-instance';
import { ThriftAstMetadata, ThriftService } from './types';
import { callThriftService } from './call-thrift-service';
import { thriftInstanceToObject } from './thrift-instance-to-object';
export type ThriftContext = any;
export interface ConnectionContext {
path: string;
service: ThriftService;
headers?: KeyValue;
deadlineConfig?: DeadlineConfig;
}
export interface LoggingContext {
serviceName: string;
namespace: string;
logging: boolean;
}
const createArgInstances = (
argObjects: object[],
argsMetadata: ArgOrExecption[],
metadata: ThriftAstMetadata[],
namespace: string,
context: ThriftContext
) =>
argObjects.map((argObj, id) => {
const type = argsMetadata[id].type;
return createThriftInstance(metadata, context, namespace, type, argObj);
});
export const codegenClientReducer =
<T>(
{ path, service, headers, deadlineConfig }: ConnectionContext,
meta: ThriftAstMetadata[],
{ serviceName, namespace, logging }: LoggingContext,
context: ThriftContext
) =>
(acc: T, { name, args, type }: Method) => ({
...acc,
[name]: async (...objectArgs: object[]): Promise<object> => {
const thriftMethod = (): Promise<object> =>
new Promise(async (resolve, reject) => {
const thriftArgs = createArgInstances(
objectArgs,
args,
meta,
namespace,
context
);
/**
* Connection errors come with HTTP errors (!= 200) and should be handled with errors from the service.
* You need to have 1 free connection per request. Otherwise, the error cannot be caught or identified.
*/
const connection = connectClient(
location.hostname,
location.port,
path,
service,
{
headers,
deadlineConfig,
},
(err) => {
reject(err);
}
) as any;
const thriftResponse = await callThriftService(connection, name, thriftArgs);
const response = thriftInstanceToObject(meta, namespace, type, thriftResponse);
if (logging) {
console.info(`🟢 ${namespace}.${serviceName}.${name}`, {
args: objectArgs,
response,
});
}
resolve(response);
});
try {
return await thriftMethod();
} catch (error: any) {
if (logging) {
console.error(`🔴 ${namespace}.${serviceName}.${name}`, {
error,
args,
});
}
throw error;
}
},
});

View File

@ -0,0 +1,102 @@
import type { Field, JsonAST, ValueType } from '@vality/thrift-ts';
import Int64 from '@vality/thrift-ts/lib/int64';
import {
isComplexType,
isPrimitiveType,
isThriftObject,
parseNamespaceType,
parseNamespaceObjectType,
} from './namespace-type';
import { ThriftAstMetadata } from './types';
export function createThriftInstance<V>(
metadata: ThriftAstMetadata[],
instanceContext: any,
namespaceName: string,
indefiniteType: ValueType,
value: V,
include?: JsonAST['include']
): V {
if (isThriftObject(value)) {
return value;
}
const { namespace, type } = parseNamespaceType(indefiniteType, namespaceName);
const internalCreateThriftInstance = (t: ValueType, v: V, include: JsonAST['include']) =>
createThriftInstance(metadata, instanceContext, namespace, t, v, include);
if (isComplexType(type)) {
switch (type.name) {
case 'map':
return new Map(
Array.from(value as unknown as Map<any, any>).map(([k, v]) => [
internalCreateThriftInstance(type.keyType, k, include),
internalCreateThriftInstance(type.valueType, v, include),
])
) as unknown as V;
case 'list':
return (value as unknown as any[]).map((v) =>
internalCreateThriftInstance(type.valueType, v, include)
) as unknown as V;
case 'set':
return Array.from(value as unknown as Set<any>).map((v) =>
internalCreateThriftInstance(type.valueType, v, include)
) as unknown as V;
default:
throw new Error('Unknown complex thrift type');
}
} else if (isPrimitiveType(type)) {
switch (type) {
case 'i64':
return new Int64(value as any) as any;
default:
return value;
}
}
const {
namespaceMetadata,
objectType,
include: objectInclude,
} = parseNamespaceObjectType(metadata, namespace, type, include);
switch (objectType) {
case 'enum':
return value;
case 'exception':
throw new Error('Unsupported structure type: exception');
default: {
const typeMeta = namespaceMetadata.ast[objectType][type];
try {
if (objectType === 'typedef') {
const typedefMeta = (typeMeta as { type: ValueType }).type;
return internalCreateThriftInstance(typedefMeta, value, objectInclude);
}
const instance = new instanceContext[namespace][type]();
for (const [k, v] of Object.entries(value as any)) {
type StructOrUnionType = Field[];
const fieldTypeMeta = (typeMeta as StructOrUnionType).find((m) => m.name === k);
if (!fieldTypeMeta) {
throw new Error('fieldTypeMeta is null');
}
instance[k] = internalCreateThriftInstance(
fieldTypeMeta.type,
v as any,
objectInclude
);
}
return instance;
} catch (error) {
console.error(
'Thrift structure',
objectType,
'creation error:',
namespace,
type,
'(meta type:',
typeMeta,
'), value:',
value
);
throw error;
}
}
}
}

View File

@ -0,0 +1,31 @@
import { Method, Service } from '@vality/thrift-ts';
import { ThriftAstMetadata } from './types';
const getServiceMetadata = (
metadata: ThriftAstMetadata[],
namespace: string,
serviceName: string
): Service => {
const namespaceMeta = metadata.find((m) => m.name === namespace);
const servicesMeta = namespaceMeta?.ast?.service;
if (!servicesMeta) {
throw new Error(`Service metadata is not found with namespace ${namespace}`);
}
const serviceMeta = servicesMeta[serviceName];
if (!serviceMeta) {
throw new Error(`Service metadata is not found with serviceName ${serviceName}`);
}
return serviceMeta;
};
const toMethodsMetadata = (serviceMetadata: Service): Method[] =>
Object.entries(serviceMetadata.functions).map(([_, method]) => method);
export const getMethodsMetadata = (
metadata: ThriftAstMetadata[],
namespace: string,
serviceName: string
): Method[] => {
const serviceMeta = getServiceMetadata(metadata, namespace, serviceName);
return toMethodsMetadata(serviceMeta);
};

2
tools/utils/index.ts Normal file
View File

@ -0,0 +1,2 @@
export * from './get-methods-metadata';
export * from './codegen-client-reducer';

View File

@ -0,0 +1,78 @@
import type { ListType, MapType, SetType, ThriftType, ValueType } from '@vality/thrift-ts';
import { JsonAST } from '@vality/thrift-ts';
export const PRIMITIVE_TYPES = [
'int',
'bool',
'i8',
'i16',
'i32',
'i64',
'string',
'double',
'binary',
] as const;
export function isThriftObject(value: any): boolean {
return typeof value?.write === 'function' && typeof value?.read === 'function';
}
export function isComplexType(type: ValueType): type is SetType | ListType | MapType {
return typeof type === 'object';
}
export function isPrimitiveType(type: ValueType): type is ThriftType {
return PRIMITIVE_TYPES.includes(type as never);
}
export const STRUCTURE_TYPES = ['typedef', 'struct', 'union', 'exception', 'enum'] as const;
export type StructureType = typeof STRUCTURE_TYPES[number];
export interface NamespaceObjectType {
namespaceMetadata: any;
objectType: StructureType;
include: JsonAST['include'];
}
export function parseNamespaceObjectType(
metadata: any[],
namespace: string,
type: string,
include?: JsonAST['include']
): NamespaceObjectType {
// metadata reverse find - search for the last matching protocol if the names match (files are overwritten in the same order)
let namespaceMetadata: any;
if (include)
namespaceMetadata = metadata.reverse().find((m) => m.path === include[namespace].path);
if (!namespaceMetadata)
namespaceMetadata = metadata.reverse().find((m) => m.name === namespace);
const objectType = (Object.keys(namespaceMetadata.ast) as StructureType[]).find(
(t) => namespaceMetadata.ast[t][type]
);
if (!objectType || !STRUCTURE_TYPES.includes(objectType)) {
throw new Error(`Unknown thrift structure type: ${objectType}`);
}
return {
namespaceMetadata,
objectType,
include: {
...namespaceMetadata.ast.include,
...{ [namespace]: { path: namespaceMetadata.path } },
},
};
}
export interface NamespaceType<T extends ValueType = ValueType> {
namespace: string;
type: T;
}
export function parseNamespaceType<T extends ValueType>(
type: T,
namespace: string
): NamespaceType<T> {
if (!isPrimitiveType(type) && !isComplexType(type) && type.includes('.')) {
[namespace, type as unknown] = type.split('.');
}
return { namespace, type };
}

View File

@ -0,0 +1,104 @@
import type { Field, Int64, JsonAST, ValueType } from '@vality/thrift-ts';
import {
isComplexType,
isPrimitiveType,
parseNamespaceType,
parseNamespaceObjectType,
} from './namespace-type';
import { ThriftAstMetadata } from './types';
export function thriftInstanceToObject(
metadata: ThriftAstMetadata[],
namespaceName: string,
indefiniteType: ValueType,
value: any,
include?: JsonAST['include']
): any {
if (typeof value !== 'object' || value === null || value === undefined) {
return value;
}
const { namespace, type } = parseNamespaceType(indefiniteType, namespaceName);
const internalThriftInstanceToObject = (t: ValueType, v: any, include: JsonAST['include']) =>
thriftInstanceToObject(metadata, namespace, t, v, include);
if (isComplexType(type)) {
switch (type.name) {
case 'map':
return new Map(
Array.from(value as unknown as Map<any, any>).map(([k, v]) => [
internalThriftInstanceToObject(type.keyType, k, include),
internalThriftInstanceToObject(type.valueType, v, include),
])
) as unknown;
case 'list':
return (value as unknown as any[]).map((v) =>
internalThriftInstanceToObject(type.valueType, v, include)
) as unknown;
case 'set':
return new Set(
Array.from(value as unknown as Set<any>).map((v) =>
internalThriftInstanceToObject(type.valueType, v, include)
)
) as unknown;
default:
throw new Error('Unknown complex thrift type');
}
} else if (isPrimitiveType(type)) {
switch (type) {
case 'i64':
return (value as unknown as Int64).toNumber() as unknown;
default:
return value;
}
}
const {
namespaceMetadata,
objectType,
include: objectInclude,
} = parseNamespaceObjectType(metadata, namespace, type);
const typeMeta = namespaceMetadata.ast[objectType][type];
switch (objectType) {
case 'exception':
throw new Error('Unsupported structure type: exception');
case 'typedef': {
type TypedefType = {
type: ValueType;
};
return internalThriftInstanceToObject(
(typeMeta as TypedefType).type,
value,
objectInclude
);
}
case 'union': {
const entries: any = Object.entries(value).find(([, v]) => v !== null);
const [key, val] = entries;
type UnionType = Field[];
const fieldTypeMeta = (typeMeta as UnionType).find((m) => m.name === key);
if (!fieldTypeMeta) {
throw new Error('fieldTypeMeta is null');
}
return {
[key]: internalThriftInstanceToObject(fieldTypeMeta.type, val, objectInclude),
} as any;
}
default: {
const result: any = {};
for (const [k, v] of Object.entries(value)) {
type StructType = Field[];
const fieldTypeMeta = (typeMeta as StructType).find((m) => m.name === k);
if (!fieldTypeMeta) {
throw new Error('fieldTypeMeta is null');
}
if (v !== null && v !== undefined) {
result[k] = internalThriftInstanceToObject(
fieldTypeMeta.type,
v,
objectInclude
);
}
}
return result;
}
}
}

View File

@ -0,0 +1,10 @@
import { DeadlineConfig, KeyValue } from '@vality/woody/src/connect-options';
import { ThriftAstMetadata } from './thrift-ast-metadata';
export interface ConnectOptions {
metadata: ThriftAstMetadata[];
headers: KeyValue;
path?: string;
deadlineConfig?: DeadlineConfig;
logging?: boolean;
}

View File

@ -0,0 +1,6 @@
export * from './thrift-ast-metadata';
export * from './thrift-namespace-context';
export * from './connect-options';
export type Connection = any;
export type ThriftService = any;

View File

@ -0,0 +1,7 @@
import { JsonAST } from '@vality/thrift-ts';
export interface ThriftAstMetadata {
path: string;
name: string;
ast: JsonAST;
}

View File

@ -0,0 +1 @@
export type ThriftInstanceContext = { [N in string]: unknown };