Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adding Chuck Norris Endpoint Feature #261

Merged
merged 24 commits into from
Oct 24, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
a9ac56c
Adding static file support
computamike Oct 20, 2022
703ecdb
adding chucknorris.io
computamike Oct 20, 2022
29f1f9d
scrape script - grab some Chuck Norris facts from api.chucknorris.com
computamike Oct 21, 2022
8f91e6e
End point - category list doesn't work (yet)
computamike Oct 21, 2022
9338338
Updating Chuck Norris End Point
computamike Oct 21, 2022
1684ab1
Updating tests
computamike Oct 21, 2022
8f8927f
adding fact file, fixing test
computamike Oct 23, 2022
57caae6
remove unused code
computamike Oct 23, 2022
64585e8
fact downloader refactored.
computamike Oct 23, 2022
bf88f55
Merge branch 'dev' into chuck-norris
computamike Oct 23, 2022
7498ffd
Somehoe I managed to move Chuck Norris :
computamike Oct 23, 2022
9f7b472
Chuck Norris tests were outputtig to console.
computamike Oct 23, 2022
f82aaeb
Updating swagger interface -
computamike Oct 23, 2022
9a3687b
Merge pull request #1 from computamike/chuck-norris
computamike Oct 23, 2022
696f3a7
adding vscode to .gitignore
computamike Oct 23, 2022
c38ee60
removing .vscode/launch (following review
computamike Oct 23, 2022
ee800df
removing from .vscode following review
computamike Oct 23, 2022
60602a7
Merge branch 'feature/chuck-norris-endpoint' of https://github.com/co…
computamike Oct 23, 2022
c2b2df1
removed duplicated comment
computamike Oct 23, 2022
2bb7271
removed categories - not reqd
computamike Oct 23, 2022
2ab3645
Merge branch 'dev' into feature/chuck-norris-endpoint
ageddesi Oct 23, 2022
6e9cba1
Missing semicolon
computamike Oct 23, 2022
57d4949
Moved the chuckfacts.json file following changes to modulke registration
computamike Oct 23, 2022
336c601
Updated path for data
computamike Oct 23, 2022
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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")
ageddesi marked this conversation as resolved.
Show resolved Hide resolved
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