@cosmology/ast
Version:
Cosmos TypeScript AST generation
299 lines (298 loc) • 13.3 kB
JavaScript
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);
};