Last active
August 29, 2015 14:18
-
-
Save raysuelzer/0b01033055e4b95fe350 to your computer and use it in GitHub Desktop.
Accepts Asp.NET Routing Attributes and generates a chainable nested ajax api interface.
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
/*jslint node: true */ | |
/*jslint esnext: true*/ | |
"use strict"; | |
let AppDispatcher = require('../dispatcher/AppDispatcher'); | |
let ApiClient = require('../api-client/api-client'); | |
let URITemplate = require('URIjs/src/URITemplate'); | |
let RouteTemplates = require('../constants/constants').ROUTE_TEMPLATES; | |
let _ = require('lodash'); | |
let DeepGet = require('../utils/DeepGet'); | |
let UriMole = function (routes) { | |
/** Based upon d3 burrow with major modifications. | |
UriMole takes a JSON object array here with a `.template` | |
property that is the server side routing template uri. | |
For example: {template: "/api/v1/parent_employers/{parentEmployerId}/work_sites/... } | |
that represents the structure of the API URIS*/ | |
routes.forEach(function(d) { | |
d.keys = []; | |
var parts = d.template.split("/"); | |
for(let k = 0; k < parts.length;k++) { //this is where for loops are handy | |
if (parts[k].indexOf('{') !== -1) { | |
d.keys[d.keys.length-1].parameters.push(parts[k]); | |
continue; | |
} | |
d.keys.push( | |
{ | |
name: _.camelCase(parts[k]), | |
uriPart: parts[k], | |
parameters: [] | |
}); | |
} | |
}); | |
/***Burrow the routes***/ | |
//TODO: remove underscores from params | |
let burrowed = {}; //Let's hold the object here | |
_.each(routes, function(d) { | |
let _obj = burrowed; | |
//Go through each of the keys, removing the first | |
_.each(d.keys.slice(1), function(key,depth) { | |
//If they route hasn't been burrowed yet, create it | |
if (_obj[key.name] === undefined) { | |
_obj[key.name] = { | |
uriPart: key.uriPart, | |
parameters: key.parameters, | |
name: key.name, | |
children: {} | |
}; | |
//build the key | |
_obj[key.name].children = key || {}; | |
} | |
else { // We already have seen this object type before... | |
// | |
// This will happen when you have multiple endpoints | |
// worksites/shifts, worksites/departments | |
// We want to merge these different options into | |
// a single object | |
_.merge(_obj[key.name], key, function (a,b){ | |
//Merging the parameters array isn't so easy | |
//Merge overwrites arrays, so override this behavior | |
if (_.isArray(a)) { | |
// We want to remove param ids that are prefixed | |
// in the uri templates from ASP.NET | |
// for example /work_sites/*{workSiteId}* | |
// we want to replace {workSiteId} with a simple {id} | |
// as the javascript constructs uri's by component | |
// not an entire template at once | |
let tempKey = key.name.toLowerCase(); //lower case it for matching | |
//Check if the string is pluralized, | |
//this isn't foolpoof, but will work in most cases | |
let isPluralized = tempKey.length === tempKey.lastIndexOf('s')+1; | |
if (isPluralized) { | |
tempKey = tempKey.substring(0, tempKey.length - 1); | |
} | |
//Look to see if there is a match in the index | |
let matchParamIndexA = _.findIndex(a, function (p) { | |
return p.toLowerCase() === '{'+tempKey+'id}'; | |
}); | |
let matchParamIndexB = _.findIndex(b, function (p) { | |
return p.toLowerCase() === '{'+tempKey+'id}'; | |
}); | |
// If we found matches pull them | |
// and push in a simple {id} parameter. | |
// | |
// Note: adding duplicates is probably | |
// fine here because _.uniq removes them | |
// with less overhead that a check | |
// here would introduce | |
if (matchParamIndexA > -1) { | |
_.pullAt(a, matchParamIndexA); | |
a.push('{id}'); | |
} | |
if (matchParamIndexB > -1) { | |
_.pullAt(b, matchParamIndexB); | |
b.push('{id}'); | |
} | |
//Return back the unique concattenated array | |
return _.uniq(a.concat(b)); //dont overwrite params array! | |
} | |
}); | |
} | |
let children = _obj[key.name].children; | |
// Remove uneeded keys from children now that we are done with them | |
delete children.uriPart; | |
delete children.parameters; | |
//We need the name though for recurssion ;) | |
_obj = _obj[key.name].children; | |
}); | |
}); | |
return burrowed; | |
}; | |
let SmartRequest = function (routes, entryUri) { | |
let self = SmartRequest; | |
//TODO: Make sure that the end points don't start with a slash | |
//That will break this method | |
//Also if there is more than one slash at the actual endpoint. | |
let _uri = entryUri.split('/')[0]; | |
//Getter method here to clean up the URI */ | |
if (self.uri === undefined) { | |
Object.defineProperty(self, 'uri', { | |
get: function() { | |
if (_uri.includes("//")) { | |
return _uri.split('//').join('/'); | |
} | |
return _uri; | |
} | |
}); | |
} | |
// Turn the given entry uri into a camelCased array | |
let entryUriArray = entryUri.split('/').map(function (v){ | |
return _.camelCase(v); | |
}); | |
/* Since UriMole doesn't return the name of the first leaf node | |
we assign it here so it's easy for DeepGet to return the | |
appropriate entry point desired by the user as the default | |
For example: the endpiont for all API calls might be | |
`/api/v1` | |
UriMole return an object like {v1: {}} | |
so we put that object into the first leaf: | |
{api: {v1: {...}}} */ | |
let startPoint = entryUriArray[0]; | |
let apiTree = {}; | |
// UriMole will do the digging and return a | |
// heirachicaly object representing our api | |
apiTree[startPoint] = new UriMole(routes); | |
/************** <Ajax Functionality> ********************** | |
* Note: | |
* Any Ajax library that returns promises can be swapped | |
* in here. | |
* | |
* This implementation also uses the flux dispatcher | |
* to dispatch events in case you don't want to deal with | |
* promises or chainging. | |
**********************************************************/ | |
function getUriAndReset() { | |
//TODO: Deal with additional parameter adjustments. | |
return self.uri; | |
} | |
let ajaxFunctions = { | |
fetch(dispatchObject) { | |
if (typeof dispatchObject === "undefined") | |
return ApiClient.fetch(getUriAndReset()); | |
return this.fetchAndDispatch(dispatchObject); | |
}, | |
/* Helper for event driven design patterns */ | |
fetchAndDispatch(dispatchObject) { | |
return ApiClient.fetch(getUriAndReset()).then(function(response) { | |
dispatchObject.payload = response; | |
AppDispatcher.dispatch(dispatchObject); | |
return response; | |
}); | |
}, | |
post(payload, dispatchObject) { | |
if (typeof dispatchObject === "undefined") | |
return ApiClient.post(getUriAndReset(), payload); | |
}, | |
put(payload, dispatchObject) { | |
if (typeof dispatchObject === "undefined") | |
return ApiClient.put(getUriAndReset(), payload); | |
}, | |
"delete": function (payload, dispatchObject) { | |
if (typeof dispatchObject === "undefined") | |
return ApiClient.delete(getUriAndReset(), payload); | |
} | |
}; | |
/* </Ajax Functionality> */ | |
/************** <Core Functionality> ********************** | |
** NOTES: | |
* The ApiEndPoint function below is how we transform the | |
* the route hierarchy object into a set of functions | |
* | |
* This creates nested functions recursively. Which handle | |
* URI Template creation and firing off ajax requests | |
**********************************************************/ | |
let ApiEndPoint = function (endPointObject) { | |
let me = this; | |
/*Keeps track of the URI we have built*/ | |
me.currentUri = _uri; | |
me.uriPart = endPointObject.uriPart; | |
me.name = endPointObject.name; | |
me.parameters = endPointObject.parameters; | |
me.uriTemplate = '/' + endPointObject.uriPart; | |
/*Expose our ajax functions*/ | |
me.do = ajaxFunctions; | |
// Creates the inital URI Component from | |
// the parameters array | |
if (me.parameters && me.parameters.length > 0) | |
me.uriTemplate = me.uriTemplate + '/' + me.parameters[0]; | |
if (me.parameters && me.parameters.length > 1) | |
me.uriTemplate = me.uriTemplate + '?' + _.rest(me.parameters).join('&'); | |
/* Recursively add more child functions * | |
* Children must be greater than one to ignore the name * | |
property, which represents its parent node */ | |
if (_.keys(endPointObject.children).length > 1) { | |
_.each(endPointObject.children, function (child) { | |
me.addChild(child); | |
}); | |
} | |
/*This is what is actually invoked when you call an endpoint | |
It's the most important part! */ | |
return function (uriParams) { | |
if (typeof uriParams === "number") { //passing a number will be assumed as an id | |
uriParams = {id: uriParams}; | |
} | |
//Buiild the uri | |
if (uriParams !== undefined) { | |
_uri = _uri + '/' + new URITemplate(me.uriTemplate).expand(uriParams); | |
} | |
else { | |
_uri = _uri + '/' + me.uriPart; | |
} | |
me.currentUri = _uri; | |
return me; //Return the invoked functions children | |
}; | |
}; | |
/*Prototype for adding children*/ | |
ApiEndPoint.prototype.addChild = function (child) { | |
let _me = this; | |
if (child.name !== undefined) | |
_me[child.name] = new ApiEndPoint(child); | |
}; | |
/*Returning SmartRequest */ | |
let entryObject = DeepGet(apiTree, entryUriArray); //burrow in and get the right desired entry | |
self.entryPoint = new ApiEndPoint(entryObject); | |
return self.entryPoint; | |
}; | |
//CommonJS Closure | |
let init = function () { | |
let instance = _.memoize(function () { | |
//Hard coded for now for testing | |
return SmartRequest(RouteTemplates, "api/v1"); | |
}); | |
return instance(); | |
}; | |
module.exports = init; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment