-
Notifications
You must be signed in to change notification settings - Fork 42
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
Feature/wayback machine #286
base: master
Are you sure you want to change the base?
Changes from 20 commits
9d61a6f
35704a0
d10b1ea
92c7c8e
c2b2cae
9bc0f69
efc09fe
5514439
d4184a2
565491b
734ec81
4ace6fc
3cbd152
22c6015
0c7c183
bcb2da7
ced199e
d7b796c
167d287
abb91d7
fa153ac
698830e
74f93e5
a3364d7
d916d1e
f53d4f3
d76ca18
8e3ec2f
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,129 @@ | ||
// Third party imports | ||
const axios = require('axios'); | ||
const mustache = require('mustache'); | ||
const xml2js = require('xml2js'); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. you should install
to add it to package.json |
||
|
||
// Local imports | ||
const config = require('../config'); | ||
const {debug} = require('../utils/logger')('ia:actions:wayback-machine'); | ||
const dialog = require('../dialog'); | ||
const endpointProcessor = require('../network/endpoint-processor'); | ||
const traverse = require('../utils/traverse'); | ||
const waybackStrings = require('../strings').intents.wayback; | ||
|
||
/** | ||
* Handle wayback query action | ||
* - fill slots of wayback query | ||
* - perform data requests to archive and alexa rankings | ||
* - construct response speech for action | ||
* | ||
* @param app | ||
*/ | ||
function handler (app) { | ||
// Create wayback object | ||
const waybackObject = { | ||
url: '', | ||
earliestYear: 0, | ||
latestYear: 0, | ||
totalUniqueURLs: 0, | ||
alexaWorldRank: 0, | ||
alexaUSRank: 0, | ||
speech: waybackStrings.default, | ||
}; | ||
|
||
// Check to see that both parameters have content | ||
if (!app.params.getByName('wayback') && !app.params.getByName('url')) { | ||
debug('wayback action called by mistake'); | ||
dialog.ask(app, waybackObject); | ||
} | ||
|
||
// Get url parameter and make url queries | ||
waybackObject.url = app.params.getByName('url'); | ||
const archiveQueryURL = endpointProcessor.preprocess( | ||
config.wayback.ARCHIVE, app, waybackObject | ||
); | ||
const alexaQueryURL = endpointProcessor.preprocess( | ||
config.wayback.ALEXA, app, waybackObject | ||
); | ||
|
||
return Promise.all([axios.get(archiveQueryURL), axios.get(alexaQueryURL)]) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Please don't use callback nesting here. One of the main feature of Promises is avoiding callback hell. It should be something like: action.then(a => {
return Promise.all(/*....*/);
})
.then(b => {
return Promise.all(/*....*/);
})
.then(c => {
}); |
||
.then(function (requestData) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. btw you can make this part of code more readable by using arguments unpacking: .then(([archiveRes, alexaRes]) => {
return Promise.all([archiveEngine(archiveRes.data), xmlConverter(alexaRes.data)])
}) |
||
return Promise.all([archiveEngine(requestData[0].data, waybackObject), xmlConverter(requestData[1].data)]) | ||
.then(function (response) { | ||
return Promise.all([alexaEngine(response[1], waybackObject)]) | ||
.then(function () { | ||
// Construct response dialog for action | ||
if (waybackObject.alexaUSRank !== 0) { | ||
waybackObject.speech = mustache.render(waybackStrings.speech, waybackObject); | ||
waybackObject.speech += mustache.render(waybackStrings.additionalSpeech, waybackObject); | ||
} else { | ||
waybackObject.speech = mustache.render(waybackStrings.speech, waybackObject); | ||
waybackObject.speech += '.'; | ||
} | ||
dialog.close(app, waybackObject); | ||
}); | ||
}); | ||
}); // End of REQUEST promise | ||
} // End of handler | ||
|
||
function archiveEngine (archiveJSON, waybackObject) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this function also should be pure-function and it doesn't need to be async |
||
return new Promise(function (resolve, reject) { | ||
// debug('Inside archiveEngine promise...'); | ||
// Create array of capture years and then find earliest year | ||
// and most recent year. | ||
let yearsArray = Object.keys(archiveJSON.captures); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. btw, it is better to sort years, to be sure that they are in the order |
||
waybackObject.earliestYear = yearsArray[0]; | ||
waybackObject.latestYear = yearsArray[yearsArray.length - 1]; | ||
|
||
// Traverse URL category | ||
|
||
// Find baseline of URL count | ||
waybackObject.totalUniqueURLs += traverse(archiveJSON.urls[waybackObject.earliestYear]); | ||
// debug('Baseline url count: ' + waybackObject.totalUniqueURLs); | ||
|
||
waybackObject.totalUniqueURLs += traverse(archiveJSON.new_urls); | ||
// debug('Final url count: ' + waybackObject.totalUniqueURLs); | ||
|
||
if (waybackObject.totalUniqueURLs > 0) { | ||
// debug('archiveEngine promise successful!'); | ||
resolve(); | ||
} | ||
}); | ||
} | ||
|
||
function alexaEngine (alexaJSON, waybackObject) { | ||
return new Promise(function (resolve, reject) { | ||
// debug('Inside alexaEngine promise...'); | ||
waybackObject.alexaWorldRank = alexaJSON['ALEXA']['SD'][0]['POPULARITY'][0]['$']['TEXT']; | ||
try { | ||
waybackObject.alexaUSRank = alexaJSON['ALEXA']['SD'][0]['COUNTRY'][0]['$']['RANK']; | ||
} catch (e) { | ||
debug('Country not found'); | ||
debug(e); | ||
} | ||
if (waybackObject.alexaWorldRank > 0) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why do you need to check that There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'd recommend that function You don't need to mutate function getAlexaRanks (alexaJSON) {
//.... some in logic to extract ranks from alexa json
return {
alexaWorldRank: 123456,
alexsUSRank: 1345,
};
} and then you can merge the result inside of handler: res = {}
// it merges res to empty object and then merge getAlexaRanks
res = Object.assign({}, res, getAlexaRanks (alexaJSON)); |
||
// debug('alexaEngine promise successful!'); | ||
resolve(); | ||
} | ||
}); | ||
} | ||
|
||
function xmlConverter (data) { | ||
return new Promise(function (resolve, reject) { | ||
// debug('Inside xmlConverter promise...'); | ||
let XMLparser = new xml2js.Parser(); | ||
XMLparser.parseString(data, function (err, result) { | ||
if (err) { | ||
let error = new Error('The XML parser didn\'t work.'); | ||
reject(error); | ||
} else { | ||
// debug('xmlConverter promise successful!'); | ||
resolve(JSON.parse(JSON.stringify(result))); | ||
} | ||
}); | ||
}); | ||
} | ||
|
||
module.exports = { | ||
handler, | ||
}; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
const _ = require('lodash'); | ||
// const {debug} = require('../utils/logger')('ia:actions:utils:traverse'); | ||
|
||
/** | ||
* Traverse a given object | ||
* | ||
* @param {Object} obj | ||
*/ | ||
module.exports = function (obj) { | ||
let results = []; | ||
function traverse (obj) { | ||
_.forOwn(obj, (val, key) => { | ||
if (_.isArray(val)) { | ||
val.forEach(el => { | ||
traverse(el); | ||
}); | ||
} else if (_.isObject(val)) { | ||
traverse(val); | ||
} else { | ||
results.push(val); | ||
} | ||
}); | ||
} | ||
traverse(obj); | ||
let count = 0; | ||
while (results.length !== 0) { | ||
count += results.pop(); | ||
} | ||
// debug('final count inside traverse = ' + count); | ||
return count; | ||
}; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,64 @@ | ||
const axios = require('axios'); | ||
const {expect} = require('chai'); | ||
const rewire = require('rewire'); | ||
|
||
const action = rewire('../../src/actions/wayback-machine'); | ||
|
||
const mockApp = require('../_utils/mocking/platforms/app'); | ||
const mockDialog = require('../_utils/mocking/dialog'); | ||
|
||
describe('actions', () => { | ||
describe('wayback machine', () => { | ||
let app; | ||
let dialog; | ||
let archiveRequest = axios.get('http://web.archive.org/__wb/search/metadata?q=cnn.com'); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We can't request data for a test from an ourside. If you would need any data you should create fixture data. |
||
let alexaRequest = axios.get('http://data.alexa.com/data?cli=10&url=cnn.com'); | ||
|
||
beforeEach(() => { | ||
app = mockApp(); | ||
dialog = mockDialog(); | ||
action.__set__('dialog', dialog); | ||
}); | ||
|
||
it('check to see that overall action eventually returns a promise', () => { | ||
action.handler(app); | ||
expect(Promise.resolve()).to.be.a('promise'); | ||
}); | ||
|
||
it('check to see that axios request promises are working', () => { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. we shouldn't test axios library. |
||
expect(archiveRequest).to.not.be.undefined; | ||
expect(alexaRequest).to.not.be.undefined; | ||
}); | ||
|
||
it('check to see that archiveEngine is working', () => { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @adaveinthelife before merging this PR please fix those tests or note that they are not working and should be fixed |
||
let obj = { earliestYear: 0, latestYear: 0, totalUniqueURLs: 0 }; | ||
let result; | ||
|
||
action.__set__('archiveEngine', function (a, b) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. it seems this test has never come inside of this mock. You never call |
||
result = action.archiveEngine(archiveRequest, obj); | ||
expect(result).to.change(obj, 'earliestYear'); | ||
expect(result).to.change(obj, 'latestYear'); | ||
expect(result).to.change(obj, 'totalUniqueURLs'); | ||
}); | ||
}); | ||
|
||
it('check to see that alexaEngine is working', () => { | ||
let obj = { alexaWorldRank: 0 }; | ||
let result; | ||
|
||
action.__set__('alexaEngine', function (a, b) { | ||
result = action.alexaEngine(alexaRequest, obj); | ||
expect(result).to.change(obj, 'alexaWorldRank'); | ||
}); | ||
}); | ||
|
||
it('check to see that xmlConverter is working', () => { | ||
let result; | ||
|
||
action.__set__('xmlConverter', function (a) { | ||
result = action.xmlConverter(alexaRequest); | ||
expect(result).to.not.throw('The XML parser didn\'t work.'); | ||
}); | ||
}); | ||
}); | ||
}); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,35 @@ | ||
const {expect} = require('chai'); | ||
const traverse = require('../../src/utils/traverse'); | ||
|
||
let testJSON = {'captures': { | ||
'1999': { | ||
'text/html': 18360 | ||
}, | ||
'2000': { | ||
'application/x-director': 19, | ||
'video/quicktime': 1584, | ||
'application/x-troff-man': 1, | ||
'x-world/x-vrml': 1, | ||
'audio/x-pn-realaudio': 176, | ||
'audio/mpeg': 195, | ||
'audio/x-wav': 3098, | ||
'image/png': 97, | ||
'text/html': 901401, | ||
'video/x-ms-asf': 142, | ||
'image/gif': 17388, | ||
'text/plain': 394428, | ||
'image/jpeg': 82903, | ||
'application/x-shockwave-flash': 39, | ||
'application/zip': 108, | ||
'audio/x-aiff': 2767, | ||
'text/css': 55, | ||
'application/pdf': 291 | ||
}}}; | ||
|
||
describe('utils', () => { | ||
describe('traverse', () => { | ||
it('should traverse a given object to return the sum of it\'s leaf nodes', () => { | ||
expect(traverse(testJSON)).to.be.equal(1423053); | ||
}); | ||
}); | ||
}); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
all new imports should be installed by:
when you would need new lib for testing you should use instead: