-
-
Notifications
You must be signed in to change notification settings - Fork 85
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #261 from computamike/feature/chuck-norris-endpoint
Adding Chuck Norris Endpoint Feature
- Loading branch information
Showing
12 changed files
with
332 additions
and
11 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -6,3 +6,4 @@ npm-debug.log | |
.DS_Store | ||
.idea | ||
coverage | ||
.vscode |
This file was deleted.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
}); | ||
|
||
|
||
|
||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
}; |
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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)) | ||
}) | ||
}) | ||
|
||
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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)); | ||
} | ||
|
Oops, something went wrong.