Add ability to output the CLI in JSON format #124
10 changed files with 568 additions and 435 deletions
9
app.ts
9
app.ts
|
@ -1,9 +0,0 @@
|
||||||
import randomBunny from "./dist/index.js";
|
|
||||||
|
|
||||||
async function app() {
|
|
||||||
const result = await randomBunny('rabbits', 'hot');
|
|
||||||
|
|
||||||
console.log(result);
|
|
||||||
}
|
|
||||||
|
|
||||||
app();
|
|
10
package.json
10
package.json
|
@ -23,8 +23,7 @@
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "tsc",
|
"build": "tsc",
|
||||||
"start": "ts-node app.ts",
|
"start": "node dist/cli.js",
|
||||||
"cli": "ts-node src/cli.ts",
|
|
||||||
"test": "jest",
|
"test": "jest",
|
||||||
"lint": "eslint .",
|
"lint": "eslint .",
|
||||||
"release": "np --no-publish"
|
"release": "np --no-publish"
|
||||||
|
@ -37,21 +36,20 @@
|
||||||
"funding": "https://ko-fi.com/vylpes",
|
"funding": "https://ko-fi.com/vylpes",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/eslint": "^8.21.1",
|
"@types/eslint": "^8.21.1",
|
||||||
|
"@types/jest": "^29.5.8",
|
||||||
"@types/node": "^20.0.0",
|
"@types/node": "^20.0.0",
|
||||||
"@typescript-eslint/eslint-plugin": "^6.0.0",
|
"@typescript-eslint/eslint-plugin": "^6.0.0",
|
||||||
"@types/jest": "^29.5.8",
|
|
||||||
"@typescript-eslint/parser": "^5.54.0",
|
"@typescript-eslint/parser": "^5.54.0",
|
||||||
"eslint": "^8.49.0",
|
"eslint": "^8.49.0",
|
||||||
"jest": "^29.7.0",
|
"jest": "^29.7.0",
|
||||||
"jest-mock-extended": "^3.0.3",
|
"jest-mock-extended": "^3.0.3",
|
||||||
"np": "^8.0.0",
|
"np": "^9.0.0",
|
||||||
"ts-jest": "^29.1.1",
|
"ts-jest": "^29.1.1",
|
||||||
"ts-mockito": "^2.6.1",
|
"ts-mockito": "^2.6.1",
|
||||||
"ts-node": "^10.9.1",
|
|
||||||
"typescript": "^5.0.0"
|
"typescript": "^5.0.0"
|
||||||
},
|
},
|
||||||
"resolutions": {
|
"resolutions": {
|
||||||
"np/**/got": "^13.0.0",
|
"np/**/got": "^14.0.0",
|
||||||
"**/semver": "^7.5.2",
|
"**/semver": "^7.5.2",
|
||||||
"@babel/traverse": "^7.23.2"
|
"@babel/traverse": "^7.23.2"
|
||||||
},
|
},
|
||||||
|
|
13
src/cli.ts
13
src/cli.ts
|
@ -1,4 +1,4 @@
|
||||||
import { Command } from "commander";
|
import { Command, Option } from "commander";
|
||||||
import randomBunny from "./index";
|
import randomBunny from "./index";
|
||||||
import ICliOptions from "./contracts/ICliOptions";
|
import ICliOptions from "./contracts/ICliOptions";
|
||||||
import { exit } from "process";
|
import { exit } from "process";
|
||||||
|
@ -10,13 +10,15 @@ program
|
||||||
.description('Get a random image url from a subreddit of your choosing')
|
.description('Get a random image url from a subreddit of your choosing')
|
||||||
.version('2.2')
|
.version('2.2')
|
||||||
.option('-s, --subreddit <subreddit>', 'The subreddit to search', 'rabbits')
|
.option('-s, --subreddit <subreddit>', 'The subreddit to search', 'rabbits')
|
||||||
.option('-j, --json', 'Output as JSON');
|
.option('-j, --json', 'Output as JSON')
|
||||||
|
.option('-q, --query-metadata', 'Include query metadata in result')
|
||||||
|
.addOption(new Option('--sort <sort>', 'Sort by').default('hot').choices(['hot', 'new', 'top']));
|
||||||
|
|
||||||
program.parse();
|
program.parse();
|
||||||
|
|
||||||
const options: ICliOptions = program.opts();
|
const options: ICliOptions = program.opts();
|
||||||
|
|
||||||
randomBunny(options.subreddit)
|
randomBunny(options.subreddit, options.sort)
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
if (response.IsSuccess) {
|
if (response.IsSuccess) {
|
||||||
const result = response.Result!;
|
const result = response.Result!;
|
||||||
|
@ -38,6 +40,11 @@ randomBunny(options.subreddit)
|
||||||
outputLines.push(`Upvotes = ${result.Ups}`);
|
outputLines.push(`Upvotes = ${result.Ups}`);
|
||||||
outputLines.push(`Url = ${result.Url}`);
|
outputLines.push(`Url = ${result.Url}`);
|
||||||
|
|
||||||
|
if (options.queryMetadata != null) {
|
||||||
|
outputLines.push(`Query.Subreddit = ${response.Query.subreddit}`);
|
||||||
|
outputLines.push(`Query.Sort By = ${response.Query.sortBy}`);
|
||||||
|
}
|
||||||
|
|
||||||
console.log(outputLines.join("\n"));
|
console.log(outputLines.join("\n"));
|
||||||
exit(0);
|
exit(0);
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -1,4 +1,6 @@
|
||||||
export default interface ICliOptions {
|
export default interface ICliOptions {
|
||||||
subreddit: string,
|
subreddit: string,
|
||||||
json?: boolean,
|
json?: boolean,
|
||||||
|
sort: string,
|
||||||
|
queryMetadata?: boolean,
|
||||||
}
|
}
|
|
@ -1,8 +1,10 @@
|
||||||
import IError from "./IError.js";
|
import IError from "./IError.js";
|
||||||
import IRedditResult from "./IRedditResult.js";
|
import IRedditResult from "./IRedditResult.js";
|
||||||
|
import QueryResult from "./QueryResult.js";
|
||||||
|
|
||||||
export default interface IReturnResult {
|
export default interface IReturnResult {
|
||||||
IsSuccess: boolean;
|
IsSuccess: boolean;
|
||||||
|
Query: QueryResult;
|
||||||
Result?: IRedditResult;
|
Result?: IRedditResult;
|
||||||
Error?: IError;
|
Error?: IError;
|
||||||
}
|
}
|
4
src/contracts/QueryResult.ts
Normal file
4
src/contracts/QueryResult.ts
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
export default interface QueryResult {
|
||||||
|
subreddit: string,
|
||||||
|
sortBy: string,
|
||||||
|
}
|
18
src/index.ts
18
src/index.ts
|
@ -15,7 +15,7 @@ const sortable = [
|
||||||
export default async function randomBunny(subreddit: string, sortBy: string = 'hot'): Promise<IReturnResult> {
|
export default async function randomBunny(subreddit: string, sortBy: string = 'hot'): Promise<IReturnResult> {
|
||||||
if (!sortable.includes(sortBy)) sortBy = 'hot';
|
if (!sortable.includes(sortBy)) sortBy = 'hot';
|
||||||
|
|
||||||
const result = await fetch(`https://reddit.com/r/${subreddit}/${sortBy}.json`)
|
const result = await fetch(`https://reddit.com/r/${subreddit}/${sortBy}.json?limit=100`)
|
||||||
.then((res) => {
|
.then((res) => {
|
||||||
return res;
|
return res;
|
||||||
})
|
})
|
||||||
|
@ -26,6 +26,10 @@ export default async function randomBunny(subreddit: string, sortBy: string = 'h
|
||||||
if (!result) {
|
if (!result) {
|
||||||
return {
|
return {
|
||||||
IsSuccess: false,
|
IsSuccess: false,
|
||||||
|
Query: {
|
||||||
|
subreddit: subreddit,
|
||||||
|
sortBy: sortBy,
|
||||||
|
},
|
||||||
Error: {
|
Error: {
|
||||||
Code: ErrorCode.FailedToFetchReddit,
|
Code: ErrorCode.FailedToFetchReddit,
|
||||||
Message: ErrorMessages.FailedToFetchReddit,
|
Message: ErrorMessages.FailedToFetchReddit,
|
||||||
|
@ -38,6 +42,10 @@ export default async function randomBunny(subreddit: string, sortBy: string = 'h
|
||||||
if (!json) {
|
if (!json) {
|
||||||
return {
|
return {
|
||||||
IsSuccess: false,
|
IsSuccess: false,
|
||||||
|
Query: {
|
||||||
|
subreddit: subreddit,
|
||||||
|
sortBy: sortBy,
|
||||||
|
},
|
||||||
Error: {
|
Error: {
|
||||||
Code: ErrorCode.UnableToParseJSON,
|
Code: ErrorCode.UnableToParseJSON,
|
||||||
Message: ErrorMessages.UnableToParseJSON,
|
Message: ErrorMessages.UnableToParseJSON,
|
||||||
|
@ -56,6 +64,10 @@ export default async function randomBunny(subreddit: string, sortBy: string = 'h
|
||||||
if (dataWithImages.length == 0) {
|
if (dataWithImages.length == 0) {
|
||||||
return {
|
return {
|
||||||
IsSuccess: false,
|
IsSuccess: false,
|
||||||
|
Query: {
|
||||||
|
subreddit: subreddit,
|
||||||
|
sortBy: sortBy,
|
||||||
|
},
|
||||||
Error: {
|
Error: {
|
||||||
Code: ErrorCode.NoImageResultsFound,
|
Code: ErrorCode.NoImageResultsFound,
|
||||||
Message: ErrorMessages.NoImageResultsFound,
|
Message: ErrorMessages.NoImageResultsFound,
|
||||||
|
@ -83,6 +95,10 @@ export default async function randomBunny(subreddit: string, sortBy: string = 'h
|
||||||
|
|
||||||
return {
|
return {
|
||||||
IsSuccess: true,
|
IsSuccess: true,
|
||||||
|
Query: {
|
||||||
|
subreddit: subreddit,
|
||||||
|
sortBy: sortBy,
|
||||||
|
},
|
||||||
Result: redditResult
|
Result: redditResult
|
||||||
};
|
};
|
||||||
}
|
}
|
|
@ -70,23 +70,23 @@ describe('subreddit', () => {
|
||||||
}, 5000);
|
}, 5000);
|
||||||
|
|
||||||
test('GIVEN -s is supplied, EXPECT subreddit to be changed', async () => {
|
test('GIVEN -s is supplied, EXPECT subreddit to be changed', async () => {
|
||||||
const result = await cli(['-s', 'horses'], '.');
|
const result = await cli(['-s', 'pics'], '.');
|
||||||
|
|
||||||
const subreddit = result.stdout.split('\n')
|
const subreddit = result.stdout.split('\n')
|
||||||
.find(x => x && x.length > 0 && x.split(' = ')[0] == 'Subreddit')!
|
.find(x => x && x.length > 0 && x.split(' = ')[0] == 'Subreddit')!
|
||||||
.split(' = ')[1];
|
.split(' = ')[1];
|
||||||
|
|
||||||
expect(subreddit).toBe('Horses');
|
expect(subreddit).toBe('pics');
|
||||||
}, 5000);
|
}, 5000);
|
||||||
|
|
||||||
test('GIVEN --subreddit is supplied, EXPECT subreddit to be changed', async () => {
|
test('GIVEN --subreddit is supplied, EXPECT subreddit to be changed', async () => {
|
||||||
const result = await cli(['--subreddit', 'horses'], '.');
|
const result = await cli(['--subreddit', 'pics'], '.');
|
||||||
|
|
||||||
const subreddit = result.stdout.split('\n')
|
const subreddit = result.stdout.split('\n')
|
||||||
.find(x => x && x.length > 0 && x.split(' = ')[0] == 'Subreddit')!
|
.find(x => x && x.length > 0 && x.split(' = ')[0] == 'Subreddit')!
|
||||||
.split(' = ')[1];
|
.split(' = ')[1];
|
||||||
|
|
||||||
expect(subreddit).toBe('Horses');
|
expect(subreddit).toBe('pics');
|
||||||
}, 5000);
|
}, 5000);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -105,6 +105,64 @@ describe('json', () => {
|
||||||
const json = JSON.parse(result.stdout);
|
const json = JSON.parse(result.stdout);
|
||||||
|
|
||||||
expect(json).toBeDefined();
|
expect(json).toBeDefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('sort', () => {
|
||||||
|
test('GIVEN --sort is not supplied, EXPECT sort to be defaulted', async () => {
|
||||||
|
const result = await cli(['-q'], '.');
|
||||||
|
|
||||||
|
const sortBy = result.stdout.split('\n')
|
||||||
|
.find(x => x && x.length > 0 && x.split(' = ')[0] == 'Query.Sort By')!
|
||||||
|
.split(' = ')[1];
|
||||||
|
|
||||||
|
expect(sortBy).toBe('hot');
|
||||||
|
}, 5000);
|
||||||
|
|
||||||
|
test('GIVEN --sort is supplied WITH a valid input, EXPECT sort to be used', async () => {
|
||||||
|
const result = await cli(['-q', '--sort', 'new'], '.');
|
||||||
|
|
||||||
|
const sortBy = result.stdout.split('\n')
|
||||||
|
.find(x => x && x.length > 0 && x.split(' = ')[0] == 'Query.Sort By')!
|
||||||
|
.split(' = ')[1];
|
||||||
|
|
||||||
|
expect(sortBy).toBe('new');
|
||||||
|
}, 5000);
|
||||||
|
|
||||||
|
test('GIVEN --sort is supplied WITH an invalid input, EXPECT error', async () => {
|
||||||
|
const result = await cli(['-q', '--sort', 'invalid'], '.');
|
||||||
|
|
||||||
|
expect(result.code).toBe(1);
|
||||||
|
expect(result.stderr).toBe("error: option '--sort <sort>' argument 'invalid' is invalid. Allowed choices are hot, new, top.\n");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('query-metadata', () => {
|
||||||
|
test('GIVEN --query-metadata is not supplied, EXPECT no query metadata returned', async () => {
|
||||||
|
const result = await cli([], '.');
|
||||||
|
|
||||||
|
const query = result.stdout.split('\n')
|
||||||
|
.find(x => x && x.length > 0 && x.split(' = ')[0].startsWith('Query'));
|
||||||
|
|
||||||
|
expect(query).toBeUndefined();
|
||||||
|
}, 5000);
|
||||||
|
|
||||||
|
test('GIVEN --query-metadata is not supplied, EXPECT no query metadata returned', async () => {
|
||||||
|
const result = await cli(['--query-metadata'], '.');
|
||||||
|
|
||||||
|
const query = result.stdout.split('\n')
|
||||||
|
.find(x => x && x.length > 0 && x.split(' = ')[0].startsWith('Query'));
|
||||||
|
|
||||||
|
expect(query).toBeDefined();
|
||||||
|
}, 5000);
|
||||||
|
|
||||||
|
test('GIVEN -q is not supplied, EXPECT no query metadata returned', async () => {
|
||||||
|
const result = await cli(['-q'], '.');
|
||||||
|
|
||||||
|
const query = result.stdout.split('\n')
|
||||||
|
.find(x => x && x.length > 0 && x.split(' = ')[0].startsWith('Query'));
|
||||||
|
|
||||||
|
expect(query).toBeDefined();
|
||||||
}, 5000);
|
}, 5000);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -36,7 +36,7 @@ describe('randomBunny', () => {
|
||||||
expect(result.Result).toBeDefined();
|
expect(result.Result).toBeDefined();
|
||||||
expect(result.Error).toBeUndefined();
|
expect(result.Error).toBeUndefined();
|
||||||
|
|
||||||
expect(fetchMock).toBeCalledWith('https://reddit.com/r/rabbits/new.json');
|
expect(fetchMock).toBeCalledWith('https://reddit.com/r/rabbits/new.json?limit=100');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('GIVEN sortBy is NOT supplied, expect it to default to hot', async () => {
|
test('GIVEN sortBy is NOT supplied, expect it to default to hot', async () => {
|
||||||
|
@ -68,7 +68,7 @@ describe('randomBunny', () => {
|
||||||
expect(result.Result).toBeDefined();
|
expect(result.Result).toBeDefined();
|
||||||
expect(result.Error).toBeUndefined();
|
expect(result.Error).toBeUndefined();
|
||||||
|
|
||||||
expect(fetchMock).toBeCalledWith('https://reddit.com/r/rabbits/hot.json');
|
expect(fetchMock).toBeCalledWith('https://reddit.com/r/rabbits/hot.json?limit=100');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('GIVEN sortBy is NOT valid, expect it to default to hot', async () => {
|
test('GIVEN sortBy is NOT valid, expect it to default to hot', async () => {
|
||||||
|
@ -100,7 +100,7 @@ describe('randomBunny', () => {
|
||||||
expect(result.Result).toBeDefined();
|
expect(result.Result).toBeDefined();
|
||||||
expect(result.Error).toBeUndefined();
|
expect(result.Error).toBeUndefined();
|
||||||
|
|
||||||
expect(fetchMock).toBeCalledWith('https://reddit.com/r/rabbits/hot.json');
|
expect(fetchMock).toBeCalledWith('https://reddit.com/r/rabbits/hot.json?limit=100');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('GIVEN the fetch fails, EXPECT failure result', async () => {
|
test('GIVEN the fetch fails, EXPECT failure result', async () => {
|
||||||
|
@ -115,7 +115,7 @@ describe('randomBunny', () => {
|
||||||
expect(result.Error!.Code).toBe(ErrorCode.FailedToFetchReddit);
|
expect(result.Error!.Code).toBe(ErrorCode.FailedToFetchReddit);
|
||||||
expect(result.Error!.Message).toBe(ErrorMessages.FailedToFetchReddit);
|
expect(result.Error!.Message).toBe(ErrorMessages.FailedToFetchReddit);
|
||||||
|
|
||||||
expect(fetchMock).toBeCalledWith('https://reddit.com/r/rabbits/new.json');
|
expect(fetchMock).toBeCalledWith('https://reddit.com/r/rabbits/new.json?limit=100');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('GIVEN the result is NOT valid JSON, EXPECT failure result', async () => {
|
test('GIVEN the result is NOT valid JSON, EXPECT failure result', async () => {
|
||||||
|
@ -132,7 +132,7 @@ describe('randomBunny', () => {
|
||||||
expect(result.Error!.Code).toBe(ErrorCode.UnableToParseJSON);
|
expect(result.Error!.Code).toBe(ErrorCode.UnableToParseJSON);
|
||||||
expect(result.Error!.Message).toBe(ErrorMessages.UnableToParseJSON);
|
expect(result.Error!.Message).toBe(ErrorMessages.UnableToParseJSON);
|
||||||
|
|
||||||
expect(fetchMock).toBeCalledWith('https://reddit.com/r/rabbits/new.json');
|
expect(fetchMock).toBeCalledWith('https://reddit.com/r/rabbits/new.json?limit=100');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('GIVEN randomSelect does NOT find a response, EXPECT failure result', async () => {
|
test('GIVEN randomSelect does NOT find a response, EXPECT failure result', async () => {
|
||||||
|
@ -153,7 +153,7 @@ describe('randomBunny', () => {
|
||||||
expect(result.Error!.Code).toBe(ErrorCode.NoImageResultsFound);
|
expect(result.Error!.Code).toBe(ErrorCode.NoImageResultsFound);
|
||||||
expect(result.Error!.Message).toBe(ErrorMessages.NoImageResultsFound);
|
expect(result.Error!.Message).toBe(ErrorMessages.NoImageResultsFound);
|
||||||
|
|
||||||
expect(fetchMock).toBeCalledWith('https://reddit.com/r/rabbits/new.json');
|
expect(fetchMock).toBeCalledWith('https://reddit.com/r/rabbits/new.json?limit=100');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('GIVEN randomSelect does NOT find a valid response, EXPECT failure result', async () => {
|
test('GIVEN randomSelect does NOT find a valid response, EXPECT failure result', async () => {
|
||||||
|
@ -188,6 +188,6 @@ describe('randomBunny', () => {
|
||||||
expect(result.Error!.Code).toBe(ErrorCode.NoImageResultsFound);
|
expect(result.Error!.Code).toBe(ErrorCode.NoImageResultsFound);
|
||||||
expect(result.Error!.Message).toBe(ErrorMessages.NoImageResultsFound);
|
expect(result.Error!.Message).toBe(ErrorMessages.NoImageResultsFound);
|
||||||
|
|
||||||
expect(fetchMock).toBeCalledWith('https://reddit.com/r/rabbits/new.json');
|
expect(fetchMock).toBeCalledWith('https://reddit.com/r/rabbits/new.json?limit=100');
|
||||||
});
|
});
|
||||||
});
|
});
|
Loading…
Reference in a new issue