mirror of
https://github.com/valitydev/frontend-thrift-codegen.git
synced 2024-11-06 02:15:17 +00:00
Thrift client 2.0 (#23)
This commit is contained in:
parent
661afde3f6
commit
246fc01328
4
.github/workflows/pr.yaml
vendored
4
.github/workflows/pr.yaml
vendored
@ -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
6
.gitignore
vendored
@ -1 +1,5 @@
|
||||
node_modules
|
||||
node_modules
|
||||
proto
|
||||
dist
|
||||
clients
|
||||
tools/stub.d.ts
|
||||
|
@ -1,4 +1,7 @@
|
||||
package.json
|
||||
package-lock.json
|
||||
node_modules
|
||||
.github
|
||||
.github
|
||||
proto
|
||||
dist
|
||||
clients
|
||||
|
25
README.md
25
README.md
@ -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.
|
||||
|
@ -1,3 +1,3 @@
|
||||
#!/usr/bin/env node
|
||||
'use strict';
|
||||
require('../lib/cli').default();
|
||||
require('../tools/cli').default();
|
||||
|
11232
package-lock.json
generated
11232
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
16
package.json
16
package.json
@ -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
62
tools/build.js
Normal 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
125
tools/cli.js
Normal 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;
|
@ -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;
|
77
tools/generate-service-template.js
Normal file
77
tools/generate-service-template.js
Normal 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
1
tools/stub.ts
Normal file
@ -0,0 +1 @@
|
||||
export const tsconfigIncludeStub = '';
|
32
tools/templates/__exportName__.ts
Normal file
32
tools/templates/__exportName__.ts
Normal 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
|
||||
);
|
||||
};
|
1
tools/templates/__typesNamespace__.ts
Normal file
1
tools/templates/__typesNamespace__.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from './internal/__typesNamespace__';
|
3
tools/templates/index.ts
Normal file
3
tools/templates/index.ts
Normal file
@ -0,0 +1,3 @@
|
||||
export * from './utils/types';
|
||||
|
||||
__export__;
|
22
tools/tsconfig.json
Normal file
22
tools/tsconfig.json
Normal 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
3
tools/types/metadata.json.d.ts
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
import { ThriftAstMetadata } from './utils/types/thrift-ast-metadata';
|
||||
|
||||
export type Metadata = ThriftAstMetadata[];
|
23
tools/utils/call-thrift-service.ts
Normal file
23
tools/utils/call-thrift-service.ts
Normal 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);
|
||||
});
|
||||
}),
|
||||
]);
|
||||
};
|
95
tools/utils/codegen-client-reducer.ts
Normal file
95
tools/utils/codegen-client-reducer.ts
Normal 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;
|
||||
}
|
||||
},
|
||||
});
|
102
tools/utils/create-thrift-instance.ts
Normal file
102
tools/utils/create-thrift-instance.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
31
tools/utils/get-methods-metadata.ts
Normal file
31
tools/utils/get-methods-metadata.ts
Normal 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
2
tools/utils/index.ts
Normal file
@ -0,0 +1,2 @@
|
||||
export * from './get-methods-metadata';
|
||||
export * from './codegen-client-reducer';
|
78
tools/utils/namespace-type.ts
Normal file
78
tools/utils/namespace-type.ts
Normal 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 };
|
||||
}
|
104
tools/utils/thrift-instance-to-object.ts
Normal file
104
tools/utils/thrift-instance-to-object.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
10
tools/utils/types/connect-options.ts
Normal file
10
tools/utils/types/connect-options.ts
Normal 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;
|
||||
}
|
6
tools/utils/types/index.ts
Normal file
6
tools/utils/types/index.ts
Normal 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;
|
7
tools/utils/types/thrift-ast-metadata.ts
Normal file
7
tools/utils/types/thrift-ast-metadata.ts
Normal file
@ -0,0 +1,7 @@
|
||||
import { JsonAST } from '@vality/thrift-ts';
|
||||
|
||||
export interface ThriftAstMetadata {
|
||||
path: string;
|
||||
name: string;
|
||||
ast: JsonAST;
|
||||
}
|
1
tools/utils/types/thrift-namespace-context.ts
Normal file
1
tools/utils/types/thrift-namespace-context.ts
Normal file
@ -0,0 +1 @@
|
||||
export type ThriftInstanceContext = { [N in string]: unknown };
|
Loading…
Reference in New Issue
Block a user