UNPKG

@cosmology/ast

Version:
585 lines (531 loc) 18.2 kB
import * as t from '@babel/types'; import { ProtoService, ProtoServiceMethod, ProtoServiceMethodInfo } from '@cosmology/types'; import { GenericParseContext } from '../../../encoding'; import { arrowFunctionExpression, callExpression, classMethod, classProperty, identifier, objectPattern } from '../../../utils'; const getResponseTypeName = ( context: GenericParseContext, name: string ) => { return name + (context.options.useSDKTypes ? 'SDKType' : ''); }; const returnReponseType = ( context: GenericParseContext, name: string ) => { return t.tsTypeAnnotation( t.tsTypeReference( t.identifier('Promise'), t.tsTypeParameterInstantiation( [ t.tsTypeReference( t.identifier(getResponseTypeName(context, name)) ) ] ) ) ); }; const firstLower = (s: string) => s = s.charAt(0).toLowerCase() + s.slice(1); const firstUpper = (s: string) => s = s.charAt(0).toUpperCase() + s.slice(1); const returnAwaitRequest = ( context: GenericParseContext, responseType: string, // method: 'get' | 'post', hasOptions: boolean = false ) => { const args = [ t.identifier('endpoint') ]; // if (method === 'post') { // args.push(t.identifier('body')); // } if (hasOptions) { args.push(t.identifier('options')); } let returned: t.Expression = t.awaitExpression( callExpression( t.memberExpression( t.memberExpression(t.thisExpression(), t.identifier('req')), t.identifier('get') ), args, t.tsTypeParameterInstantiation([ t.tsTypeReference( t.identifier(getResponseTypeName(context, responseType)) ) ]) ) ); if (context.pluginValue('useSDKTypes') && context.pluginValue('prototypes.methods.fromSDKJSON')) { //useSDKTypes && prototypes.methods.fromSDKJSON returned = t.callExpression( t.memberExpression( t.identifier(responseType), t.identifier('fromSDKJSON') ), [returned] ); } else if (!context.pluginValue('useSDKTypes') && context.pluginValue('prototypes.methods.fromJSON')) { //!useSDKTypes && prototypes.methods.fromJSON returned = t.callExpression( t.memberExpression(t.identifier(responseType), t.identifier('fromJSON')), [returned] ); } return t.returnStatement(returned); }; const makeOptionsObject = () => { return t.variableDeclaration( 'const', [ t.variableDeclarator( identifier( 'options', t.tsTypeAnnotation( t.tsAnyKeyword() ) ), t.objectExpression( [ t.objectProperty( t.identifier('params'), t.objectExpression([]) ) ] ) ) ] ) }; const setParamOption = ( context: GenericParseContext, name: string, svc: ProtoServiceMethod ) => { const flippedCasing = Object.keys(svc.info.casing).reduce((m, v) => { m[svc.info.casing[v]] = v; return m; }, {}); const queryParam = flippedCasing[name] ? flippedCasing[name] : name; const param = svc.info.paramMap[name]; // options.params.group_id = params.groupId; let expr = t.expressionStatement( t.assignmentExpression( '=', t.memberExpression( t.memberExpression( t.identifier('options'), t.identifier('params') ), t.identifier(queryParam) ), t.memberExpression( t.identifier('params'), t.identifier(param) ) ) ); if (name === 'pagination') { context.addUtil('setPaginationParams'); expr = t.expressionStatement( t.callExpression( t.identifier('setPaginationParams'), [ t.identifier('options'), t.memberExpression( t.identifier('params'), t.identifier('pagination'), false ) ] ) ) } return t.ifStatement( t.binaryExpression( '!==', t.unaryExpression( 'typeof', t.optionalMemberExpression( t.identifier('params'), t.identifier(param), false, true ) ), t.stringLiteral('undefined') ), t.blockStatement([ expr ]) ); }; // breaks a url string to prepare it for template strings export const getUrlTemplateString = (url: string) => { const parts = url.split('/').filter(a => a !== ''); let cur = [] let strs = []; let atEnd = false; for (let p = 0; p < parts.length; p++) { const part = parts[p]; if (/[{}]+/.test(part)) { if (p === parts.length - 1) atEnd = true; if (cur.length) { const vals = cur.join('/'); strs.push(vals); } else { strs.push('/') } cur = []; } else { cur.push(part); } } if (cur.length) { strs.push(cur.join('/')); } strs = strs.filter(str => str !== '').map((v, i) => { if (i === 0) { if (!v.endsWith('/')) v = `${v}/`; return v; } else if (i === strs.length - 1) { if (atEnd) { // we want them to end with / if it's an "atEnd" el if (!v.endsWith('/')) v = `${v}/`; return v; } // they should all start with "/" if (!v.startsWith('/')) v = `/${v}`; return v; } if (!v.endsWith('/')) v = `${v}/`; if (!v.startsWith('/')) v = `/${v}`; return v; }); return { strs, atEnd }; }; const routeRegexForReplace = /[^\{\}\\-\_\\.$/a-zA-Z0-9]+/g; export function makeTemplateTag(info: ProtoServiceMethodInfo, noLeadingSlash: boolean = true): t.TemplateLiteral { const route = info.url .split('/') .filter(a => a !== '') .map(a => { if (a.startsWith('{')) { // clean weird routes like this one: // /ibc/apps/transfer/v1/denom_traces/{hash=**} return a.replace(routeRegexForReplace, '') } else { return a; } }) .join('/'); const segments = route.split('/'); const expressions: (t.Identifier | t.MemberExpression)[] = []; const quasis = []; let accumulatedPath = ''; let isFirst = true; segments.forEach((segment, _index) => { if (noLeadingSlash && segment === '') return; if (segment.startsWith('{') && segment.endsWith('}')) { // Dynamic segment const paramName = segment.slice(1, -1); // Push the accumulated static text as a quasi before adding the expression quasis.push(t.templateElement({ raw: accumulatedPath + '/', cooked: accumulatedPath }, false)); accumulatedPath = ''; // Reset accumulated path after adding to quasis // expressions.push(t.identifier(`params.${paramName}`)); expressions.push(t.memberExpression(t.identifier('params'), t.identifier(info.casing?.[paramName] ? info.casing[paramName] : paramName))); // Prepare the next quasi to start with a slash if this is not the last segment isFirst = false; } else { // Accumulate static text, ensuring to prepend a slash if it's not the first segment accumulatedPath += (isFirst ? '' : '/') + segment; isFirst = false; } }); // Add the final accumulated static text as the last quasi quasis.push(t.templateElement({ raw: accumulatedPath, cooked: accumulatedPath }, true)); // Mark the last quasi as tail return t.templateLiteral(quasis, expressions); } const makeComment = (comment: string) => { return [{ type: 'CommentBlock', value: ` ${comment} ` }] } const buildRequestMethod = ( context: GenericParseContext, serviceMethod: ProtoServiceMethod ) => { const methodName = firstLower(serviceMethod.name); const comment = serviceMethod.comment ?? serviceMethod.name; if (!serviceMethod.info) { throw new Error('No Service URL!'); } const queryParams = serviceMethod.info.queryParams.map(param => { return setParamOption(context, param, serviceMethod); }); const optionsAst = []; if (serviceMethod.info.queryParams.length) { // options params object optionsAst.push(makeOptionsObject()) } // parse field types Object.entries(serviceMethod.fields ?? {}) .forEach(([key, value]) => { switch (value.parsedType.type) { case 'Type': // this gets the import for us and loads them into ctx // if later we need to get subtypes, we have it all w/ctx context.getTypeName(value); case 'native': } }); const fieldNames = Object.keys(serviceMethod.fields ?? {}) const hasParams = fieldNames.length > 0; const paramName = hasParams ? 'params' : '_params'; let methodArgs: t.Identifier | t.AssignmentPattern = identifier( paramName, t.tsTypeAnnotation( t.tsTypeReference( t.identifier(serviceMethod.requestType) ) ) ); // if no params, then let's default to empty object for cleaner API if (!hasParams) { methodArgs = t.assignmentPattern( methodArgs, t.objectExpression([]) ) } else if (hasParams && fieldNames.length === 1 && fieldNames.includes('pagination')) { const paginationDefaultFromPartial = context.pluginValue('prototypes.paginationDefaultFromPartial'); // if only argument "required" is pagination // also default to empty methodArgs = t.assignmentPattern( methodArgs, t.objectExpression([ t.objectProperty( t.identifier('pagination'), paginationDefaultFromPartial ? t.callExpression( t.memberExpression(t.identifier("PageRequest"), t.identifier("fromPartial")), [t.objectExpression([])] ) : t.identifier('undefined'), false, false ) ]) ) } const body = t.blockStatement([ ...optionsAst, // if optional params not undefined ...queryParams, // endpoint t.variableDeclaration( 'const', [ t.variableDeclarator ( t.identifier('endpoint'), makeTemplateTag(serviceMethod.info) ) ] ), // return returnAwaitRequest( context, serviceMethod.responseType, // serviceMethod.info.method, serviceMethod.info.queryParams.length > 0 ) ]); if (context.pluginValue('classesUseArrowFunctions')) { return classProperty( t.identifier(methodName), arrowFunctionExpression( [methodArgs], body, t.tsTypeAnnotation( t.tsTypeReference( t.identifier('Promise'), t.tsTypeParameterInstantiation( [ t.tsTypeReference( t.identifier(getResponseTypeName(context, serviceMethod.responseType)) ) ] ) ) ), true ), undefined, undefined, undefined, undefined, undefined, undefined, makeComment(comment) as t.CommentLine[], ); } return classMethod( 'method', t.identifier(methodName), [ methodArgs ], body, returnReponseType(context, serviceMethod.responseType), makeComment(comment) as t.CommentLine[], false, false, false, true // async ); } // MARKED AS NOT DRY (used in rpc/lcd) const bindThis = (name: string) => { return t.expressionStatement( t.assignmentExpression( '=', t.memberExpression( t.thisExpression(), t.identifier(name) ), t.callExpression( t.memberExpression( t.memberExpression( t.thisExpression(), t.identifier(name) ), t.identifier('bind') ), [ t.thisExpression() ] ) ) ); }; const createLCDClientClassBody = ( context: GenericParseContext, clientName: string, methods: (t.ClassMethod | t.ClassProperty)[], service?: ProtoService ) => { let boundMethods = []; // until the super() issue is figured out, we have to remove this if (service && !context.pluginValue('classesUseArrowFunctions')) { boundMethods = Object.keys(service.methods).map(key => { const method: ProtoServiceMethod = service.methods[key]; if (typeof method.options?.['(google.api.http).get'] !== 'undefined') { const methodName = firstLower(method.name); return bindThis(methodName) } }).filter(Boolean); } return t.exportNamedDeclaration( t.classDeclaration( t.identifier(clientName), null, t.classBody([ t.classProperty( t.identifier('req'), null, t.tsTypeAnnotation( t.tsTypeReference( t.identifier('LCDClient') ) ) ), // constructor t.classMethod( 'constructor', t.identifier('constructor'), [ objectPattern([ t.objectProperty( t.identifier('requestClient'), t.identifier('requestClient'), false, true )], t.tsTypeAnnotation( t.tsTypeLiteral([ t.tsPropertySignature( t.identifier('requestClient'), t.tsTypeAnnotation( t.tsTypeReference( t.identifier('LCDClient') ) ) ) ]) ) ) ], t.blockStatement([ t.expressionStatement( t.assignmentExpression( '=', t.memberExpression( t.thisExpression(), t.identifier('req') ), t.identifier('requestClient') ) ), /// methods ...boundMethods ]) ), ...methods ]) ) ) }; export const createLCDClient = ( context: GenericParseContext, service: ProtoService ) => { const methods = Object.keys(service.methods).map(key => { const method: ProtoServiceMethod = service.methods[key]; if (method.info && (typeof method.options?.['(google.api.http).get'] !== 'undefined') ) { return buildRequestMethod(context, method); } }).filter(Boolean); context.addUtil('LCDClient'); if (methods.length) { const clientName = 'LCDQueryClient' return createLCDClientClassBody( context, clientName, methods, service ); } }; export const createAggregatedLCDClient = ( context: GenericParseContext, services: ProtoService[], clientName: string ) => { context.addUtil('LCDClient'); const methods = services.reduce((m, service) => { const innerMethods = Object.keys(service.methods).map(key => { const method: ProtoServiceMethod = service.methods[key]; if (method.info && (typeof method.options?.['(google.api.http).get'] !== 'undefined') ) { return buildRequestMethod(context, method); } }).filter(Boolean); return [...m, ...innerMethods]; }, []); return createLCDClientClassBody(context, clientName, methods); };