Skip to content

Commit

Permalink
Merge pull request #68 from HRKings/feature/overload
Browse files Browse the repository at this point in the history
Overloads and cleanup
  • Loading branch information
HRKings authored Dec 20, 2022
2 parents dfcc9f4 + f84a9e7 commit 341b433
Show file tree
Hide file tree
Showing 14 changed files with 5,249 additions and 8,667 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: ['12', '14', '16']
node-version: ['14', '16', '18']
steps:
- name: Clone repository
uses: actions/checkout@v2
Expand Down
2,268 changes: 1,088 additions & 1,180 deletions dist/src/index.js

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions dist/src/interfaces/PokeAPIOptions.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
class PokeAPIOptions {
/* eslint-disable default-param-last */
constructor(config = {}, cache) {
this.protocol = 'https';
this.hostName = '://pokeapi.co';
Expand Down
49 changes: 26 additions & 23 deletions dist/src/utils/Getter.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
/* eslint-disable import/no-unresolved */
import axios from 'axios';
import handleError from './ErrorHandler.js';
// eslint-disable-next-line consistent-return
async function getJSON(values, url,
// eslint-disable-next-line no-unused-vars
callback) {
Expand All @@ -12,38 +11,42 @@ callback) {
try {
// Retrieve possible content from memory-cache
const cachedResult = values.cache.get(url);
// If we have in cache
if (callback && cachedResult) {
// Call callback without errors
callback(cachedResult);
}
// Return the cache
if (cachedResult) {
if (callback) {
// Call callback without errors
callback(cachedResult);
}
return cachedResult;
}
// If we don't have in cache
// get the data from the API
const response = await axios.get(url, options);
// if there is an error
// If there is an error on the request
if (response.status !== 200) {
handleError(response, callback);
throw response;
}
// If everything was good
// set the data
const responseData = response.data;
// Cache the object in memory-cache
// only if cacheLimit > 0
if (values.cacheLimit > 0) {
values.cache.set(url, responseData, values.cacheLimit);
}
else {
// If everything was good
// cache the object in memory-cache
// only if cacheLimit > 0
const responseData = response.data;
if (values.cacheLimit > 0) {
values.cache.set(url, responseData, values.cacheLimit);
}
// If a callback is present
if (callback) {
// Call it, without errors
callback(responseData);
}
else {
return responseData;
}
// If a callback is present
if (callback) {
// Call it, without errors
callback(responseData);
}
return responseData;
}
catch (error) {
handleError(error, callback);
}
// If we return nothing and the error handler fails
// reject the promise
return Promise.reject();
}
export default getJSON;
175 changes: 124 additions & 51 deletions generator/Main.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import fs from 'fs';
import path from 'path';
import { Project } from 'ts-morph';
import { MethodDeclarationStructure, OptionalKind, Project } from 'ts-morph';

import endpoints from '../src/utils/Endpoints.js';
import rootEndpoints from '../src/utils/RootEndpoints.js';
Expand Down Expand Up @@ -115,34 +115,40 @@ console.timeLog(mainLabel, ' - Base generated, generating methods...');

// Add the get generic resource array method
const getResourceCode = `try {
// Fail if the endpoint is not supplied
if (!endpoint) {
throw new Error('Param "endpoint" is required needs to be a string or array of strings');
}
// Fail if the input types aren't accepted
if (!Array.isArray(endpoint) && typeof endpoint !== 'string') {
throw new Error('Param "endpoint" needs to be a string or array of strings');
}
/// If the user has submitted a string, return the JSON promise
if (typeof endpoint === 'string') {
return getJSON<any>(this.options, endpoint, callback);
} else if (typeof endpoint === 'object') {
const mapper = async (endpoints: string) => {
const queryRes = await getJSON<any>(this.options, endpoints);
return queryRes;
};
}
// Fetch data asynchronously to be faster
const mappedResults = await pMap(endpoint, mapper, { concurrency: 4 });
// If the user has submitted an Array return a new promise which will resolve when all getJSON calls are ended
const mapper = async (endpoints: string) => {
const queryRes = await getJSON<any>(this.options, endpoints);
return queryRes;
};
if (callback) {
callback(mappedResults);
}
// Fetch data asynchronously to be faster
const mappedResults = await pMap(endpoint, mapper, { concurrency: 4 });
return mappedResults;
} else {
throw new Error('Param "endpoint" needs to be a string or array of strings');
if (callback) {
callback(mappedResults);
}
return mappedResults;
} catch (error) {
handleError(error, callback);
}`;

let methodStructure = {
let methodStructure: OptionalKind<MethodDeclarationStructure> = {
name: 'getResource',
isAsync: true,
parameters: [{
Expand All @@ -155,6 +161,36 @@ let methodStructure = {
hasQuestionToken: true,
}],
returnType: 'Promise<any | any[]>',
overloads: [
{
parameters: [
{
name: 'endpoint',
type: 'string',
},
{
name: 'callback',
type: '(result: any, error?: any) => any',
hasQuestionToken: true,
},
],
returnType: 'Promise<any>',
},
{
parameters: [
{
name: 'endpoint',
type: 'string[]',
},
{
name: 'callback',
type: '(result: any[], error?: any) => any',
hasQuestionToken: true,
},
],
returnType: 'Promise<any[]>',
},
],
};

pokeApiClass.addMethod(methodStructure).setBodyText(getResourceCode);
Expand Down Expand Up @@ -182,6 +218,11 @@ for (const [methodName, endpoint, jsdocs] of endpoints) {
const inputParam = methodName.match(/ByName$/) ? 'nameOrId' : 'id';
const inputParamType = methodName.match(/ByName$/) ? 'string | number | Array<string | number>' : 'number | number[]';

const singleParamType = methodName.match(/ByName$/) ? 'string | number' : 'number';
const multipleParamType = methodName.match(/ByName$/) ? 'Array<string | number>' : 'number[]';

const returnType = `PokeAPITypes.${apiMap[endpoint]}`;

methodStructure = {
name: methodName,
isAsync: true,
Expand All @@ -191,50 +232,80 @@ for (const [methodName, endpoint, jsdocs] of endpoints) {
},
{
name: 'callback',
type: `(result: PokeAPITypes.${apiMap[endpoint]} | PokeAPITypes.${apiMap[endpoint]}[], error?: any) => any`,
type: `((result: ${returnType}, error?: any) => any) & ((result: ${returnType}[], error?: any) => any)`,
hasQuestionToken: true,
}],
returnType: `Promise<PokeAPITypes.${apiMap[endpoint]} | PokeAPITypes.${apiMap[endpoint]}[]>`,
returnType: `Promise<${returnType} | ${returnType}[]>`,
overloads: [
{
parameters: [
{
name: inputParam,
type: singleParamType,
},
{
name: 'callback',
type: `(result: ${returnType}, error?: any) => any`,
hasQuestionToken: true,
},
],
returnType: `Promise<${returnType}>`,
},
{
parameters: [
{
name: inputParam,
type: multipleParamType,
},
{
name: 'callback',
type: `(result: ${returnType}[], error?: any) => any`,
hasQuestionToken: true,
},
],
returnType: `Promise<${returnType}[]>`,
},
],
};

const generatedMethod = pokeApiClass.addMethod(methodStructure).setBodyText(`try {
if (${inputParam}) {
// If the user has submitted a Name or an ID, return the JSON promise
if (typeof ${inputParam} === 'number' || typeof ${inputParam} === 'string') {
return getJSON<PokeAPITypes.${apiMap[endpoint]}>(this.options, \`\${this.options.protocol}\${this.options.hostName}\${this.options.versionPath}${endpoint}/\${${inputParam}}/\`, callback);
}
// Fail if the param is not supplied
if (!${inputParam}) {
throw new Error('Param "${inputParam}" is required (Must be a ${methodName.match(/ByName$/) ? 'string, array of strings or array of string and/or numbers' : 'number or array of numbers'} )');
}
// If the user has submitted an Array return a new promise which will
// resolve when all getJSON calls are ended
else if (typeof ${inputParam} === 'object') {
const mapper = async (${inputParam}s: ${inputParamType}) => {
const queryRes = await getJSON<PokeAPITypes.${apiMap[endpoint]}>(this.options, \`\${this.options.protocol}\${this.options.hostName}\${this.options.versionPath}${endpoint}/\${${inputParam}s}/\`);
return queryRes;
};
// Fail if the input types aren't accepted
if (!Array.isArray(${inputParam}) && typeof ${inputParam} !== 'number' && typeof ${inputParam} !== 'string') {
throw new Error('Param "${inputParam}" must be a ${methodName.match(/ByName$/) ? 'string, array of strings or array of string and/or numbers' : 'number or array of numbers'}');
}
// Fetch data asynchronously to be faster
const mappedResults = await pMap(${inputParam}, mapper, { concurrency: 4 });
// If the user has submitted a Name or an ID, return the JSON promise
if (typeof ${inputParam} === 'number' || typeof ${inputParam} === 'string') {
return getJSON<${returnType}>(this.options, \`\${this.options.protocol}\${this.options.hostName}\${this.options.versionPath}${endpoint}/\${${inputParam}}/\`, callback);
}
if (callback) {
callback(mappedResults);
}
// If the user has submitted an Array return a new promise which will resolve when all getJSON calls are ended
const mapper = async (${inputParam}s: ${inputParamType}) => {
const queryRes = await getJSON<${returnType}>(this.options, \`\${this.options.protocol}\${this.options.hostName}\${this.options.versionPath}${endpoint}/\${${inputParam}s}/\`);
return queryRes;
};
return mappedResults;
} else {
throw new Error('Param "${inputParam}" must be a ${methodName.match(/ByName$/) ? 'string, array of strings or array of string and/or numbers' : 'number or array of numbers'}');
}
} else {
throw new Error('Param "${inputParam}" is required (Must be a ${methodName.match(/ByName$/) ? 'string, array of strings or array of string and/or numbers' : 'number or array of numbers'} )');
// Fetch data asynchronously to be faster
const mappedResults = await pMap(${inputParam}, mapper, { concurrency: 4 });
// Invoke the callback if we have one
if (callback) {
callback(mappedResults);
}
} catch (error) {
return mappedResults;
} catch (error) {
handleError(error, callback);
}`);
}`);

// Add the declaration to the types file
// Sanitizing the namespace and remove the async keyword
// Removing the async keyword
methodStructure.isAsync = false;
methodStructure.parameters[1].type = methodStructure.parameters[1].type.replace(/PokeAPITypes/g, 'PokeAPI');
methodStructure.returnType = methodStructure.returnType.replace(/PokeAPITypes/g, 'PokeAPI');
const declaredMethod = declarationClass.addMethod(methodStructure);

// If the method has a JSDoc, add it
Expand Down Expand Up @@ -263,7 +334,7 @@ for (const [method, rawEndpoint, jsdocs] of rootEndpoints) {
}

// Infer the return type from the name
const returnType = apiMap[endpoint].includes('NamedList') ? 'NamedAPIResourceList' : 'APIResourceList';
const returnType = `PokeAPITypes.${apiMap[endpoint].includes('NamedList') ? 'NamedAPIResourceList' : 'APIResourceList'}`;

const methodStructure = {
name: method,
Expand All @@ -275,10 +346,10 @@ for (const [method, rawEndpoint, jsdocs] of rootEndpoints) {
},
{
name: 'callback',
type: `(result: PokeAPITypes.${returnType}, error?: any) => any`,
type: `(result: ${returnType}, error?: any) => any`,
hasQuestionToken: true,
}],
returnType: `Promise<PokeAPITypes.${returnType}>`,
returnType: `Promise<${returnType}>`,
};

const generatedMethod = pokeApiClass.addMethod(methodStructure).setBodyText(`try {
Expand All @@ -300,10 +371,8 @@ for (const [method, rawEndpoint, jsdocs] of rootEndpoints) {
}`);

// Add the declaration to the types file
// Sanitizing the namespace and remove the async keyword
// Removing the async keyword
methodStructure.isAsync = false;
methodStructure.parameters[1].type = methodStructure.parameters[1].type.replace(/PokeAPITypes/g, 'PokeAPI');
methodStructure.returnType = methodStructure.returnType.replace(/PokeAPITypes/g, 'PokeAPI');
const declaredMethod = declarationClass.addMethod(methodStructure);

// If the method has a JSDoc, add it
Expand Down Expand Up @@ -378,6 +447,10 @@ declarationClass.getParentModule().addExportAssignment({
expression: 'PokeAPI',
});

// Sanitize the namespaces of the declaration file and format it again
declarationClass.replaceWithText(declarationClass.getFullText().replace(/PokeAPITypes/g, 'PokeAPI'));
declarationClass.formatText();

// Top level async function
(async () => {
// Save and compile to JS
Expand Down
10 changes: 6 additions & 4 deletions generator/TypesGenerator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ async function generateFinalFile(types: string) {
* Code by: HRKings <https://github.com/HRKings/>
* And: Christian Garza <https://github.com/C-Garza/>
* Code inspired by: Mudkip <https://github.com/mudkipme/>
* Execute \`npm run generate\` to regenerate
* Execute \`npm run generate:types\` to regenerate
*/`, { overwrite: true });

// Create the root module
Expand All @@ -58,12 +58,14 @@ async function generateFinalFile(types: string) {
namespace.setBodyText(types);

// Remove interfaces that are wrongly generated
namespace.getInterface('EvolutionChainElement').remove();
namespace.getInterface('GenerationElement').remove();
namespace.getInterface('VersionGroupNamedList').remove();
namespace.getInterface('EvolutionChainElement')?.remove();
namespace.getInterface('ResultElement')?.remove();
namespace.getInterface('GenerationElement')?.remove();
namespace.getInterface('VersionGroupNamedList')?.remove();

// Replace the wrong definitions with the correct ones
namespace.setBodyText(namespace.getBodyText()
.replace(/ResultElement/g, 'APIResource')
.replace(/EvolutionChainElement/g, 'APIResource')
.replace(/GenerationElement/g, 'NamedAPIResource'));

Expand Down
Loading

0 comments on commit 341b433

Please sign in to comment.