Skip to content

Instantly share code, notes, and snippets.

@raysuelzer
Last active August 29, 2015 14:18
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save raysuelzer/0b01033055e4b95fe350 to your computer and use it in GitHub Desktop.
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.
/*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