@cosmology/ast
Version:
Cosmos TypeScript AST generation
662 lines (596 loc) • 18.7 kB
text/typescript
import { GenericParseContext } from '../../../../encoding';
import { ProtoService, ProtoServiceMethod } from '@cosmology/types';
import { arrowFunctionExpression, classDeclaration, classMethod, classProperty, commentBlock, commentLine, identifier, tsMethodSignature } 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: string,
packageImport: string
) => {
// 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
[], // empty expressions
)
// 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: string,
svc: ProtoServiceMethod,
packageImport: string,
leadingComments?: t.CommentBlock[]
) => {
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, // static
)
}
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: string
) => {
let quasis: any[] = [];
// 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: string,
indicesLeft: number[],
indicesRight: number[]
) => {
let quasis: any[] = [];
// 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: string[]
) => {
let expressions: any[] = [];
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: string
) => {
let args: any[] = [];
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: string,
indicesLeft: number[],
indicesRight: number[],
) => {
let args: any[] = [];
// 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: GenericParseContext,
svc: ProtoServiceMethod,
) => {
// getPath ex:
// rpc Grants(QueryGrantsRequest) returns (QueryGrantsResponse) {
// option (google.api.http).get = "/cosmos/authz/v1beta1/grants";
// }
let getPath: string | undefined;
try {
getPath = svc.options['(google.api.http).get']
} catch { }
if ((typeof getPath!) === 'undefined') {
getPath = context.ref.proto.package + '.' + svc.name
}
let args: any[];
// 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: GenericParseContext,
name: string,
svc: ProtoServiceMethod,
leadingComments?: t.CommentBlock[]
) => {
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, // static
)
}
export const createGRPCGatewayQueryClass = (
context: GenericParseContext,
service: ProtoService
) => {
// 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: string,
context: GenericParseContext,
name: string,
svc: ProtoServiceMethod,
leadingComments?: t.CommentBlock[]
) => {
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: GenericParseContext,
service: ProtoService
) => {
const serviceName = service.name;
let className: string;
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,
]
),
[]
),
)
}