@cosmology/ast
Version:
Cosmos TypeScript AST generation
301 lines (300 loc) • 13.1 kB
JavaScript
import { classMethod, classProperty, commentBlock, identifier } from '../../../../utils';
import { camel } from '@cosmology/utils';
import { returnReponseType, optionalBool, processRpcComment } from '../utils/rpc';
import { headersInit, initRequest, getInitReqProperties } from './utils';
import * as t from '@babel/types';
const getFetchReqArgsService = (name, packageImport) => {
// this hack around shouldn't exist, contact sdk team to modify the path for broadcastTx
if (name === 'broadcastTx') {
name = 'txs';
}
const fetchArgs = [];
// first argument of fetchReq
const argTemplateLiteral = t.templateLiteral([
t.templateElement({
raw: '/' + packageImport.replace(/\./g, "/") + '/' + name,
cooked: '/' + packageImport.replace(/\./g, "/") + '/' + name
}, true)
], // quasis
[]);
// adds proto path to fetchReq
fetchArgs.push(argTemplateLiteral);
// initReqProperties (contains information for initReq parameter in fetchReq) arguments:
const initReqProperties = getInitReqProperties();
const fetchArgsInitReqObj = t.objectExpression(initReqProperties);
// adds initReq parameter to fetchReq
fetchArgs.push(fetchArgsInitReqObj);
return fetchArgs;
};
const grpcGatewayPOSTServiceMethodDefinition = (name, svc, packageImport, leadingComments) => {
const requestType = svc.requestType;
const responseType = svc.responseType;
const fieldNames = Object.keys(svc.fields ?? {});
const hasParams = fieldNames.length > 0;
const optional = optionalBool(hasParams, fieldNames);
// first parameter in method
// ex: static Send(request: MsgSend)
// paramRequest is an object representing everything in brackets here
const paramRequest = identifier('request', t.tsTypeAnnotation(t.tsTypeReference(t.identifier(requestType))), optional);
// fetchArgs will be used in method body's return statement expression.
// Contains arguments to fm.fetchReq
// this one is different from the Msg, especially the package name
const fetchArgs = getFetchReqArgsService(name, packageImport);
// method's body
const body = t.blockStatement([
t.returnStatement(t.callExpression(t.memberExpression(t.identifier('fm'), t.identifier('fetchReq')), fetchArgs))
]);
return classMethod('method', t.identifier(name), [paramRequest, initRequest], // params
body, returnReponseType(responseType), leadingComments, false, true);
};
const staticExpressionsNoParams = t.callExpression(t.memberExpression(t.identifier('fm'), t.identifier('renderURLSearchParams'), false), [
t.objectExpression([
t.spreadElement(t.identifier('request'))
]),
t.arrayExpression([])
]);
// {...initReq, method: "GET"}
const staticSecondFetchReqArg = t.objectExpression([
t.spreadElement(t.identifier('initRequest')),
t.objectProperty(t.identifier('method'), t.stringLiteral('GET'), false, false)
]);
const getQuasisNoParams = (path) => {
let quasis = [];
// path?
// ex: /cosmos/bank/v1beta1/supply?
quasis.push(t.templateElement({
raw: path + '?',
cooked: path + '?'
}, false));
// add empty tail element
quasis.push(t.templateElement({
raw: '',
cooked: ''
}, true));
return quasis;
};
// get quasis (string expressions) when there is an Params element
const getQuasisParams = (path, indicesLeft, indicesRight) => {
let quasis = [];
// add left path element to quasis (path before Params element)
const firstPath = path.slice(0, indicesLeft[0]);
quasis.push(t.templateElement({
raw: firstPath,
cooked: firstPath,
}, false));
// check if path end with param or quasis, get that quasis if any
let lastPath = '';
if (indicesRight[indicesRight.length - 1] != path.length - 1) {
lastPath = path.slice(indicesRight[indicesRight.length - 1] + 1, path.length);
// console.log(lastPath);
}
// get paths in between params
for (let i = 0; i < indicesLeft.length - 1; i++) {
const tempPath = path.slice(indicesRight[i] + 1, indicesLeft[i + 1]);
quasis.push(t.templateElement({
raw: tempPath,
cooked: tempPath,
}, false));
}
// add remaining path (if exists) or only '?' sign
quasis.push(t.templateElement({
raw: lastPath != '' ? lastPath + '?' : '?',
cooked: lastPath != '' ? lastPath + '?' : '?'
}, false));
// add empty tail element
quasis.push(t.templateElement({
raw: '',
cooked: ''
}, true));
return quasis;
};
const getExpressionsNoParams = () => {
return [staticExpressionsNoParams];
};
// Get expressions for a path with Params.
// Returning array must be of length 2.
// example expressions: ${req["denom"]} AND ${fm.renderURLSearchParams(req, ["denom"])}
const getExpressionsParams = (paramsName) => {
let expressions = [];
let arrParams = [];
for (let i = 0; i < paramsName.length; i++) {
// ${req["denom"]}
expressions.push(t.memberExpression(t.identifier('request'), t.stringLiteral(paramsName[i]), true));
arrParams.push(t.stringLiteral(paramsName[i]));
}
// ${fm.renderURLSearchParams(req, ["denom"])}
expressions.push(t.callExpression(t.memberExpression(t.identifier('fm'), t.identifier('renderURLSearchParams'), false), [
t.objectExpression([
t.spreadElement(t.identifier('request'))
]),
t.arrayExpression(arrParams)
]));
return expressions;
};
// Get fm.fetchReq arguments if there is no Params element
// In this case, len of quasis must be 2 and len of expressions must be 1.
const getFetchReqArgsNoParams = (path) => {
let args = [];
const quasis = getQuasisNoParams(path);
const expressions = getExpressionsNoParams();
args.push(t.templateLiteral(quasis, expressions));
// {...initReq, method: "GET"}
args.push(staticSecondFetchReqArg);
return args;
};
// Get fm.fetchReq arguments if there is an Params element
const getFetchReqArgsParams = (path, indicesLeft, indicesRight) => {
let args = [];
// first argument
// ex: `/cosmos/staking/v1beta1/delegators/${req["delegator_addr"]}/validators/${req["validator_addr"]}? => quasis
// ${fm.renderURLSearchParams(req, ["delegator_addr", "validator_addr"])}` => expressions
let paramsName = [];
for (let i = 0; i < indicesLeft.length; i++) {
paramsName.push(path.slice(indicesLeft[i] + 1, indicesRight[i]));
}
const quasis = getQuasisParams(path, indicesLeft, indicesRight);
const expressions = getExpressionsParams(paramsName);
args.push(t.templateLiteral(quasis, expressions));
// {...initReq, method: "GET"}
args.push(staticSecondFetchReqArg);
return args;
};
// fetchArgs will be used in method body's return statement expression.
// Contains arguments to fm.fetchReq
const getFetchReqArgs = (context, svc) => {
// getPath ex:
// rpc Grants(QueryGrantsRequest) returns (QueryGrantsResponse) {
// option (google.api.http).get = "/cosmos/authz/v1beta1/grants";
// }
let getPath;
try {
getPath = svc.options['(google.api.http).get'];
}
catch { }
if ((typeof getPath) === 'undefined') {
getPath = context.ref.proto.package + '.' + svc.name;
}
let args;
// check if getPath contains params
// ex: "/cosmos/bank/v1beta1/balances/{address}"
// NOTE: {address} here is param
// contains params
if (getPath.indexOf('{') > -1) {
// get all indices of '{' and '}' from path.
const indicesLeft = [...getPath.matchAll(/{/g)].map(match => match.index);
const indicesRight = [...getPath.matchAll(/}/g)].map(match => match.index);
args = getFetchReqArgsParams(getPath, indicesLeft, indicesRight);
}
else {
// contains no params
args = getFetchReqArgsNoParams(getPath);
}
return args;
};
// function to define a method of grpc-gateway fetch request
const grpcGatewayMethodDefinition = (context, name, svc, leadingComments) => {
const requestType = svc.requestType;
const responseType = svc.responseType;
// first parameter in method
// ex: static Send(request: MsgSend)
// paramRequest is an object representing everything in brackets here
const paramRequest = identifier('request', t.tsTypeAnnotation(t.tsTypeReference(t.identifier(requestType))), false);
// fm.fetchReq(fetchArgs are here)
const fetchArgs = getFetchReqArgs(context, svc);
// class method body (only return statement)
const body = t.blockStatement([
t.returnStatement(t.callExpression(t.memberExpression(t.identifier('fm'), t.identifier('fetchReq'), false), fetchArgs))
]);
return classMethod('method', t.identifier(name), [paramRequest, initRequest], // params
body, returnReponseType(responseType), leadingComments, false, true);
};
export const createGRPCGatewayQueryClass = (context, service) => {
// adds import
context.addUtil('fm');
const camelRpcMethods = context.pluginValue('rpcClients.camelCase');
const keys = Object.keys(service.methods ?? {});
//two different ways to generate methods for Query and Service
let methods;
//case Query
if (service.name === "Query") {
methods = keys
.map(key => {
const method = service.methods[key];
const name = camelRpcMethods ? camel(key) : key;
const leadingComments = method.comment ? [commentBlock(processRpcComment(method))] : [];
return grpcGatewayMethodDefinition(context, name, method, leadingComments);
});
}
else {
//case Service
methods = keys
.map(key => {
const isGet = key.substring(0, 3) === "Get";
const method = service.methods[key];
const name = camelRpcMethods ? camel(key) : key;
const leadingComments = method.comment ? [commentBlock(processRpcComment(method))] : [];
if (!isGet) {
//POST METHOD
return grpcGatewayPOSTServiceMethodDefinition(name, method, context.ref.proto.package, leadingComments);
}
else {
return grpcGatewayMethodDefinition(context, name, method, leadingComments);
}
});
}
return t.exportNamedDeclaration(t.classDeclaration(t.identifier(service.name), null, t.classBody([
...methods,
]), []));
};
// function to define a method of grpc-gateway style
const grpcGatewayQuerierMethodDefinition = (serviceName, context, name, svc, leadingComments) => {
const requestType = svc.requestType;
const responseType = svc.responseType;
// first parameter in method
// ex: static Send(request: MsgSend)
// paramRequest is an object representing everything in brackets here
const paramRequest = identifier('req', t.tsTypeAnnotation(t.tsTypeReference(t.identifier(requestType))), false);
// class method body (only return statement)
const body = t.blockStatement([
t.returnStatement(t.callExpression(t.memberExpression(t.identifier(serviceName), t.identifier(name), false), [
t.identifier('req'),
t.objectExpression([
t.objectProperty(t.identifier('headers'), t.identifier('headers'), false, true),
t.objectProperty(t.identifier('pathPrefix'), t.memberExpression(t.thisExpression(), t.identifier('url')))
])
]))
]);
return classMethod('method', t.identifier(name), [paramRequest, headersInit], // params
body, returnReponseType(responseType), leadingComments, false, false, // static
false, true // async
);
};
export const createGRPCGatewayWrapperClass = (context, service) => {
const serviceName = service.name;
let className;
if (serviceName === 'Query') {
// QueryClientImp for wrapper class
className = 'QueryClientImpl';
}
else {
className = 'ServiceClientImpl';
}
const camelRpcMethods = context.pluginValue('rpcClients.camelCase');
const keys = Object.keys(service.methods ?? {});
const methods = keys
.map(key => {
const method = service.methods[key];
const name = camelRpcMethods ? camel(key) : key;
const leadingComments = method.comment ? [commentBlock(processRpcComment(method))] : [];
return grpcGatewayQuerierMethodDefinition(serviceName, context, name, method, leadingComments);
});
return t.exportNamedDeclaration(t.classDeclaration(t.identifier(className), null, t.classBody([
classProperty(t.identifier('url'), null, t.tsTypeAnnotation(t.tsStringKeyword()), [], false, false, true, "private"),
t.classMethod('constructor', t.identifier('constructor'), [
identifier('url', t.tsTypeAnnotation(t.tsStringKeyword()))
], t.blockStatement([
t.expressionStatement(t.assignmentExpression('=', t.memberExpression(t.thisExpression(), t.identifier('url')), t.identifier('url')))
])),
...methods,
]), []));
};