uwapi
Version:
Promise based library for accessing the UW Open Data API
234 lines (196 loc) • 7.72 kB
JavaScript
module.exports = function(apiToken) {
var Url = require('url');
var Q = require('q');
//Utils
//Should probably move this into a seperate module
var getJSON = function(url, data, cb, cberr) {
url = (url.constructor === Url.Url) ? url : Url.parse(url, true);
delete url.search;
for (var key in data) url.query[key] = data[key];
var Http;
if (url.protocol === 'http:')
Http = require('http');
else if (url.protocol === 'https:')
Http = require('https');
else {
cberr(new Error('Unrecognized protocol'));
return;
}
Http.get(url.format(), function(resp) {
resp.setEncoding('utf8');
var payload = '';
resp.on('data', function(chunk) {
payload += chunk;
});
resp.on('end', function() {
try {
payload = JSON.parse(payload);
} catch(e) {
cberr(new Error('Request did not yield valid JSON data'));
return;
}
cb(payload);
});
}).on('error', cberr);
};
//Consumes an object and a template and returns the appropriate endpoint URL.
var constructEndpoint = function(string, obj) {
var params = string.match(/{(.*?)}/g) || [];
var args = params.map(function(e){return e.replace(/{|}/g,'');});
if(args.length !== Object.keys(obj).length) return null;
for (var i in args) {
if (typeof obj[args[i]] === 'undefined')
return null;
string = string.replace('{' + args[i] + '}', obj[args[i]]);
}
return string;
};
//Consumes a map of name-(template list) pairs of api endpoints
//and returns an object with the specified methods.
var compileAPISpec = function(spec) {
var baseURL='https://api.uwaterloo.ca/v2';
var result = {};
for (var name in spec) {
var templates = spec[name];
var cached = {};
var endpointFunction = (function(templates, fname, cached) {
return function(options, getParams) {
options = options || {};
getParams = getParams || {};
var deferred = Q.defer();
var path = null;
for (var i in templates) {
path = constructEndpoint(templates[i], options);
if(path)
break;
}
if(!path) {
deferred.reject(new Error('Invalid parameters passed in to ' + fname));
return deferred.promise;
}
if(cached[path]) {
deferred.resolve(cached[path]);
return deferred.promise;
}
var url = baseURL + path + '.json';
getParams.key = apiToken;
getJSON(url, getParams, function(payload){
if(Math.floor(payload.meta.status / 100) !== 2) {
deferred.reject(new Error('API Error: ' + payload.meta.message));
return;
}
cached[path] = payload.data;
deferred.resolve(payload.data);
}, function(e) {
deferred.reject(e);
});
return deferred.promise;
};
})(templates, name, cached);
result[name] = endpointFunction;
}
return result;
};
//Consumes raw API templates and generates a spec object
var genSpec = function(templates) {
//Suggests a function name from the provided API template
var getName = function(url) {
url = (url instanceof Url.Url) ? url : Url.parse(url);
var args = url.path.replace(/^\/|\/$/g,'').split('/');
if (args[args.length-1][0] === '{' || args.length === 1)
return args[0];
else
return args[0] + args[args.length-1][0].toUpperCase() +
args[args.length-1].slice(1);
};
var result = {};
for(var i=0;i<templates.length;i++) {
var name = getName(templates[i]);
if(!result[name]) result[name] = [];
result[name].push(templates[i]);
}
return result;
};
//Spec must contain a mapping of function names to templates. No two templates
//in the list can accept the same set of parameteres..
var validSpec = function(spec) {
for(var name in spec) {
var seen = {};
for(var i in spec[name]) {
var template = spec[name][i];
var params = template.match(/{.*?}/g) || [];
var hash = params.sort().join();
if(seen[hash])
return new Error('Found duplicate parameter list for templates in ' + name);
seen[hash] = 1;
}
}
return true;
};
var templates = [
'/foodservices/menu',
'/foodservices/notes',
'/foodservices/diets',
'/foodservices/outlets',
'/foodservices/locations',
'/foodservices/watcard',
'/foodservices/announcements',
'/foodservices/products/{product_id}',
'/foodservices/products/search',
'/foodservices/{year}/{week}/menu',
'/foodservices/{year}/{week}/notes',
'/foodservices/{year}/{week}/announcements',
'/courses/{subject}',
'/courses/{course_id}',
'/courses/{class_number}/schedule',
'/courses/{subject}/{catalog_number}',
'/courses/{subject}/{catalog_number}/schedule',
'/courses/{subject}/{catalog_number}/prerequisites',
'/courses/{subject}/{catalog_number}/examschedule',
'/events',
'/events/{site}',
'/events/{site}/{id}',
'/events/holidays',
'/news',
'/news/{site}',
'/news/{site}/{id}',
'/weather/current',
'/terms/list',
'/terms/{term_id}/examschedule',
'/terms/{term_id}/{subject}/schedule',
'/terms/{term_id}/{subject}/{catalog_number}/schedule',
'/terms/{term_id}/infosessions',
'/resources/tutors',
'/resources/printers',
'/resources/infosessions',
'/resources/goosewatch',
'/resources/sites',
'/codes/units',
'/codes/terms',
'/codes/groups',
'/codes/subjects',
'/codes/instructions',
'/buildings/list',
'/buildings/{building_acronym}',
'/buildings/{building_acronym}/{room_number}/courses',
'/api/usage',
'/api/services',
'/api/methods',
'/api/versions',
'/api/changelog',
'/server/time',
'/server/codes'
];
var spec = genSpec(templates);
//Return spec if apiToken is not provided
if(apiToken === null) {
return spec;
}
var result = validSpec(spec);
if (result instanceof Error) {
throw new Error('Could not generate valid spec using the given template' +
result.message);
}
var uwapi = compileAPISpec(spec);
return uwapi;
};