UNPKG

@cosmology/ast

Version:
299 lines (298 loc) 13.3 kB
import * as t from '@babel/types'; import { arrowFunctionExpression, callExpression, classMethod, classProperty, identifier, objectPattern } from '../../../utils'; const getResponseTypeName = (context, name) => { return name + (context.options.useSDKTypes ? 'SDKType' : ''); }; const returnReponseType = (context, name) => { return t.tsTypeAnnotation(t.tsTypeReference(t.identifier('Promise'), t.tsTypeParameterInstantiation([ t.tsTypeReference(t.identifier(getResponseTypeName(context, name))) ]))); }; const firstLower = (s) => s = s.charAt(0).toLowerCase() + s.slice(1); const firstUpper = (s) => s = s.charAt(0).toUpperCase() + s.slice(1); const returnAwaitRequest = (context, responseType, // method: 'get' | 'post', hasOptions = false) => { const args = [ t.identifier('endpoint') ]; // if (method === 'post') { // args.push(t.identifier('body')); // } if (hasOptions) { args.push(t.identifier('options')); } let returned = 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, name, svc) => { 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) => { 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, noLeadingSlash = true) { 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 = []; 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) => { return [{ type: 'CommentBlock', value: ` ${comment} ` }]; }; const buildRequestMethod = (context, serviceMethod) => { 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 = 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)); } return classMethod('method', t.identifier(methodName), [ methodArgs ], body, returnReponseType(context, serviceMethod.responseType), makeComment(comment), false, false, false, true // async ); }; // MARKED AS NOT DRY (used in rpc/lcd) const bindThis = (name) => { 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, clientName, methods, service) => { 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 = 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, service) => { const methods = Object.keys(service.methods).map(key => { const method = 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, services, clientName) => { context.addUtil('LCDClient'); const methods = services.reduce((m, service) => { const innerMethods = Object.keys(service.methods).map(key => { const method = 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); };