Skip to content

Commit

Permalink
Merge pull request #261 from computamike/feature/chuck-norris-endpoint
Browse files Browse the repository at this point in the history
Adding Chuck Norris Endpoint Feature
  • Loading branch information
ageddesi committed Oct 24, 2022
2 parents ce83e2d + 336c601 commit c6f6566
Show file tree
Hide file tree
Showing 12 changed files with 332 additions and 11 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,4 @@ npm-debug.log
.DS_Store
.idea
coverage
.vscode
10 changes: 0 additions & 10 deletions .vscode/settings.json

This file was deleted.

3 changes: 3 additions & 0 deletions app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import express, { Request, Response } from 'express';
import { swaggerSpec } from './src/utils/swagger';
import swag from './swagger.json';
import { applicationRateLimiter } from './middleware/rate-limiter/RateLimiter';
import path from 'path';
const morgan = require('morgan');
const cors = require('cors');

Expand Down Expand Up @@ -51,6 +52,8 @@ app.get('/docs.json', (req: Request, res: Response) => {
res.send(swaggerSpec);
});

app.use(express.static(path.join(__dirname,'public')))

const schemaOptions = {
swaggerOptions: {
dom_id: '#swagger-ui',
Expand Down
11 changes: 11 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@
"test:watch": "jest --watch",
"contributors:add": "all-contributors add",
"contributors:generate": "all-contributors generate",
"contributors:check": "all-contributors check"
"contributors:check": "all-contributors check",
"chuck" : "ts-node modules\\chuck-norris\\utils\\scrape-facts.ts modules\\chuck-norris\\data\\chuckfacts.json"
},
"repository": {
"type": "git",
Expand Down Expand Up @@ -52,6 +53,7 @@
"jest": "29.2.1"
},
"dependencies": {
"chucknorris-io": "^1.0.5",
"cors": "^2.8.5",
"dotenv": "^16.0.2",
"express": "^4.18.1",
Expand Down
Binary file added public/warning.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
147 changes: 147 additions & 0 deletions src/modules/chuck-norris/api/chuck-norris-routes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
import { Request, Response } from 'express';
import * as core from 'express-serve-static-core';
import { getQtyFromRequest } from '../../../utils/route-utils';
import ColorErrors from '../consts/chuck-norris-errors';
import facts from "../data/chuckfacts.json"
/**
* @openapi
* definitions:
* ChuckNorrisFact:
* description: Chuck Norris Facts were obtained from api.chucknorris.io
* type: object
* properties:
* categories:
* type: array
* items:
* type: string
* example: ["science","dev"]
* description: A list of categories that this fact relates to.
* icon:
* type: string
* example: https://assets.chucknorris.host/img/avatar/chuck-norris.png
* description: URL for an icon - provided in the original data from api.chucknorris.com
* id:
* type: string
* example: izjeqnjzteeqms8l8xgdhw
* description: ID from api.chucknorris.io
* sourceUrl:
* type: string
* example: https://api.chucknorris.io/jokes/izjeqnjzteeqms8l8xgdhw
* description: URL for this Fact on api.chucknorris.io
* value:
* type: string
* example: Chuck Norris knows the last digit of pi.
*/

module.exports = function (app: core.Express) {
/**
* @openapi
* '/chuck-norris/fact/{category}':
* get:
* description: |-
* <img style="margin-right: 20px;float:left" title="image Title" alt="beware" src="/warning.png">
* **Retrieve a random Chuck Norris fact.**
* This endpoint returns a __single__ fact as it is not possible for the human brain to store more than 1 Chuck Norris fact without exploding (this is why we have Chuck Norris Databases)...
*
* Of course the counter argument to that is "this is an api - and the end recipient is a computer".
* Maybe but think of the liability
* tags:
* - Chuck Norris
* summary: Returns a random Chuck Norris Fact.
* parameters:
* - in: path
* name: category
* description: The category of Chuck Norris fact.
* required: true
* default: all
* type: string
* enum:
* - all
* - dev
* - food
* - sport
* - career
* - fashion
* - history
* - animal
* - movie
* - money
* - music
* - celebrity
* - science
* - political
* - travel
* - religion
* responses:
* '200':
* description: OK
* schema:
* type: array
* items:
* $ref: '#/definitions/ChuckNorrisFact'
* '400':
* description: Unexpected Error
* schema:
* $ref: '#/definitions/ErrorResponse'
*/
app.get('/chuck-norris/fact/:category/:qty?', (req: Request, res: Response) => {
try {
const qty = getQtyFromRequest(req,1);
if(qty < 1 ) {
throw ColorErrors.InvalidQuantityError
}
const category = req.params.category;
var categories = facts.map(item => item.categories[0]).filter((value, index, self) => self.indexOf(value) === index);

if (category !== "all" && !categories.includes(category)){
throw ColorErrors.InvalidCategoryError;
}
var factlist = []
var filtered = category==="all" ? facts : facts.filter((value) =>{ return value.categories.includes(category) });



for (var i = 0; i < qty; i++) {
var randomFact = {};
randomFact = filtered[Math.floor(Math.random() * filtered.length)];
factlist.push(randomFact)
}
res.json(factlist);
} catch (error) {
if (
error === ColorErrors.InvalidCategoryError ||
error === ColorErrors.InvalidQuantityError
) {
res.status(400).json({
error: error.message,
});

return;
}
}
});

/**
* @openapi
* '/chuck-norris/categories':
* get:
* tags:
* - Chuck Norris
* summary: Returns the available categories for a Chuck Norris Fact.
* responses:
* '200':
* description: OK
* schema:
* type: array
* items:
* type: string
* example: ["dev", "food", "sport", "career", "fashion", "history", "animal", "movie", "money", "music", "celebrity", "science", "political", "travel", "religion"]
*/
app.get('/chuck-norris/categories', (req: Request, res: Response) => {
var categories = facts.map(item => item.categories[0]).filter((value, index, self) => self.indexOf(value) === index);
res.json(categories);
});



};
12 changes: 12 additions & 0 deletions src/modules/chuck-norris/consts/chuck-norris-errors.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@

const InvalidCategoryMessage = "Invalid category"
const InvalidQuantityMessage = "Invalid quantity";
const InvalidCategoryError = new Error(InvalidCategoryMessage);
const InvalidQuantityError = new Error(InvalidQuantityMessage);

export default {
InvalidCategoryError,
InvalidQuantityError,
InvalidCategoryMessage,
InvalidQuantityMessage
};
1 change: 1 addition & 0 deletions src/modules/chuck-norris/data/chuckfacts.json

Large diffs are not rendered by default.

48 changes: 48 additions & 0 deletions src/modules/chuck-norris/tests/chuck-norris.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import request from 'supertest';
import app from "../../../../app";
const facts = require("../data/chuckfacts.json");
import chuckNorrisErrors from '../consts/chuck-norris-errors';
describe('Chuck Norris api endpoints', () => {
describe('GET /chuck-norris/fact/:category/:qty?', () =>{
it("Calling the endpoint with no qty returns 1", async () =>{
const response = await request(app).get(`/chuck-norris/fact/dev`);
expect(response.status).toBe(200);
expect(response.body.length).toBe(1);
})
it("Calling the endpoint with category all and no qty returns 1", async () =>{
const response = await request(app).get(`/chuck-norris/fact/all`);
expect(response.status).toBe(200);
expect(response.body.length).toBe(1);
})

it("Calling the endpoint with valid category but negative qty throws 400", async () =>{
const response = await request(app).get(`/chuck-norris/fact/dev/-1`);
expect(response.status).toBe(400);
expect(response.body.error).toBe(chuckNorrisErrors.InvalidQuantityMessage);
})

it("Calling the endpoint with invalid category throws 400", async () =>{
const response = await request(app).get(`/chuck-norris/fact/wimp`);
expect(response.status).toBe(400);
expect(response.body.error).toBe(chuckNorrisErrors.InvalidCategoryMessage);
})


})

describe('GET /chuck-norris/categories', () => {
it("Call the categories returns a list containing all the categories.", async () => {
//Arrange
const expectedCategories = await facts.map(item => item.categories[0]).filter((value, index, self) => self.indexOf(value) === index)

//Act
const response = await request(app).get(`/chuck-norris/categories`);

//Assert
expect(response.status).toBe(200);
expect(response.body.length).toBe(expectedCategories.length);
expect(response.body).toEqual(expect.arrayContaining(expectedCategories))
})
})

})
102 changes: 102 additions & 0 deletions src/modules/chuck-norris/utils/scrape-facts.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@

import {writeFileSync} from 'fs';
const path = require("path");

const Chuck = require('chucknorris-io'),
client = new Chuck();

const GetRandomFactForCategory = async( category): Promise<any> =>{
var joke = await client.getRandomJoke(category);
joke.categories.push(category); // fix bug #2 found in NodeJS Module
return joke;
}

/**
* Is the fact on a block list?
* @param joke
* @param blockListedFacts
* @returns
*/
function IsBlockListed(joke: any, blockListedFacts) {
return blockListedFacts.filter((id) =>{return id === joke.id})
}

/**
* Is the fact already in a list
* @param joke
* @param chuckFacts
* @returns
*/
function AlreadyOnList(joke: any, chuckFacts: any[]) {
return chuckFacts.filter((element) =>{return element.id === joke.id})
}

(async () => {

// Configuration
// -------------
const excludedCategories = ["explicit"] // Catgories from which Chuck Norris facts won't be retrieved.
const blockListedFacts = [ // A list of fact that were identified during testing that may be offensive, and won't be included in this data extract
"hyQV9kHDSN61rRIsYwSr4Q",
"66Eivn6tSV2IaNTzXEdgFQ",
"HNTUHUUDSMObOHUHJul5sw",
"-j1T5SaZT3yb1rwfKNDJvQ",
"BwMFn3FCQQaCSw0uEKkWLw"
]
const FactsPerCateory = 200 // Number of records for each category too download
var RetryCount = 10; // Number of times to re-attempt an extract for a category.

// Arguments
const destination = process.argv[2];
const OutputFile = path.resolve(destination);

console.log("👊 Obtaining Chuck Norris facts")
console.log(`📄 Output : ${OutputFile}\n`)

var chuckFacts = [] // An array of facts to return to a call to the Chuck Norris End Point service.
var summary = {} // A summary object - numbers of facts for each category

const categories = await client.getJokeCategories();
var filtered = categories.filter(function(i){return this.indexOf(i)<0},excludedCategories);

await Promise.all(filtered.map(async (category) => {
console.log(`✨ Obtaining Chuck Norris Facts from category ${category} `);
RetryCount = 10;
var downloadCount = FactsPerCateory
summary[category] = 0
while (downloadCount > 0 && RetryCount> 0) {

console.log(`Downloading fact ${downloadCount} for ${category}`)
var fact = await GetRandomFactForCategory(category); // fix bug #2 found in NodeJS Module

if(IsBlockListed(fact,blockListedFacts)== false && !AlreadyOnList(fact,chuckFacts)==false)
{
// this fact is good, and we should publish it to the collection
chuckFacts.push(fact)
console.log(`💥 Adding ${fact.id} to ${category} - fact ${FactsPerCateory - downloadCount}`)
downloadCount--;
summary[category] ++;
RetryCount = 10;
} else{
// this fact is bad, or we already have it in the collection. Let's try again
RetryCount --;
}
// wait for half a second - let's not overload the API endpoint.
await delay(500);
}
}));
console.log("✨ Download summary")
console.log(JSON.stringify(summary));

var JSONFileData = JSON.stringify(chuckFacts)
writeFileSync(OutputFile, JSONFileData, {
flag: 'w'
})

})();


function delay(time: number) {
return new Promise(resolve => setTimeout(resolve, time));
}

Loading

0 comments on commit c6f6566

Please sign in to comment.