I'm refactoring some code, essentially a dot-notation walker for objects. I've abstracted things quite a bit, I'm to the point any more and I begin to loose readability.
It all hinges on this one function objectWalker, which does the actual object traversals. I can control what happens by passing in different callbacks to handle key points in the processes.
function objectWalker (paths, obj, onEnd, enRoute, onEach) {
idx = -1,
nextContext = prevContext = obj;
const len = paths ? paths.length : 0;
const lastStep = len ? len - 1 : 0;
while (nextContext != null && ++idx < len) {
if (onEach) nextContext = onEach(nextContext, idx, paths, prevContext);
if (idx !== lastStep) {
prevContext = nextContext;
nextContext = (!dotWalker.objectHas(nextContext, paths[idx])) ? enRoute(nextContext, idx, paths, prevContext) : nextContext[paths[idx]];
} else {
prevContext = paths.length === 1 ? nextContext : prevContext[paths[idx - 1]];
return onEnd(nextContext, idx, paths, prevContext );
}
if(!nextContext) return nextContext;
}
return nextContext;
}
Pretty simple, and here are some of the callbacks i can send in.
dotWalker.onEnd = function(context, idx, dotPathss, prevContext) {
if (idx === 0) {
return dotWalker.objectHas(context, dotPathss[idx]) ? context[dotPathss[idx]] : undefined;
} else {
return prevContext[dotPathss[idx]];
}
}
dotWalker.onEndAssigner = function(value){
return function(context, idx, dotPathss, prevContext) {
return prevContext[dotPathss[idx]] = value, prevContext[dotPathss[idx]];
}
}
dotWalker.enRouteMapper = function(context, idx, dotPathss, prevContext) {
return context[dotPathss[idx]] = {}, context[dotPathss[idx]];
}
dotWalker.enRoute = function(context, idx, dotPathss, prevContext) {
return dotWalker.objectHas(context, dotPathss[idx]) ? context[dotPathss[idx]] : undefined ;
}
dotWalker.onEach = function(context, idx, dotPaths, prevContext) {
return context;
}
I'm pretty pleased with the above, the issue i have is with the following "pathFinder" function. It seems to me to be doing too much, but I can't decide where to break it down and refactor further.
dotWalker.pathFinder = function(paths, c, namespace, pd, nd) {
if (!c && !paths) return [];
pd = pd || '.';
nd = nd || ':';
const lastChar = paths[paths.length - 1];
const isNamespacedDotPath = paths.indexOf(nd) > -1;
if ( !!dotWalker.objectHas(c, 'ns') ) {
if ( lastChar === nd ) { // "label:" -> namespaced but no dotPaths
namespace = c[paths.slice(0, -1)], paths = undefined;
}
if ( isNamespacedDotPath ) { // "label:some.sub.path" -> namespaced but with dotPaths
paths = paths.split(nd), namespace = c.ns[paths[0]], paths = paths.slice(1)[0].split(pd);
}
if(!namespace){
paths = paths.split(pd), namespace = c.selected || c.ns.root;
}
} else { /* Attempting to use namespaceses on simple object */
if ( isNamespacedDotPath ) {
paths = paths.split(nd);
if( !c || !dotWalker.objectHas(c, paths[0])){
namespace = undefined, paths = [];
} else {
namespace = c[paths[0]], paths = paths.slice(1).join('.').split(pd);
}
}
if (typeof paths === 'string' && paths.indexOf(pd) > -1) {
paths = paths.split(pd);
}
if ( lastChar === nd){
paths = '';
}
namespace = namespace || c;
}
paths = paths[0] === '' || !paths ? undefined : [].concat(paths);
return !c ? paths : { dotPaths: paths, context: namespace };
}
Any feedback is welcome, thanks.
I've included the full code here in a little snippet, you can run it to see how it works. I've also included a crude suite of tests, to highlight intended behavior.
I've added two more functions to demonstrate how one might leverage the objectWalker.
dotWalker.assign = function (dotPath, value, context) {
return dotWalker(dotPath, context, dotWalker.onEndAssigner(value), dotWalker.enRouteMapper);
}
dotWalker.get = function (dotPath, context) {
return dotWalker(dotPath, context, dotWalker.onEnd, dotWalker.enRoute);
}
And I added two more tests demonstrating their use.
/**
DotWalker accepts a path a context/object and two callbacks.
The path, can be a dotPaths, as the context, is being walked
*/
const dotWalker = (function() {
const dotWalker = function (dotPaths, context, end, enRoute, onEach){
end = end || dotWalker.onEnd;
enRoute = enRoute || dotWalker.enRoute;
var { context, dotPaths } = dotWalker.pathFinder( dotPaths || '', context);
return objectWalker(dotPaths, context, end, enRoute, onEach);
}
dotWalker.assign = function (dotPath, value, context) {
return dotWalker(dotPath, context, dotWalker.onEndAssigner(value), dotWalker.enRouteMapper);
}
dotWalker.pathFinder = function(paths, c, namespace, pd, nd) {
if (!c && !paths) return [];
pd = pd || '.';
nd = nd || ':';
const lastChar = paths[paths.length - 1];
const isNamespacedDotPath = paths.indexOf(nd) > -1;
if ( !!dotWalker.objectHas(c, 'ns') ) {
if ( lastChar === nd ) { // "label:" -> namespaced but no dotPaths
namespace = c[paths.slice(0, -1)], paths = undefined;
}
if ( isNamespacedDotPath ) { // "label:some.sub.path" -> namespaced but with dotPaths
paths = paths.split(nd), namespace = c.ns[paths[0]], paths = paths.slice(1)[0].split(pd);
}
if(!namespace){
paths = paths.split(pd), namespace = c.selected || c.ns.root;
}
} else { /* Attempting to use namespaceses on simple object */
if ( isNamespacedDotPath ) {
paths = paths.split(nd);
if( !c || !dotWalker.objectHas(c, paths[0])){
namespace = undefined, paths = [];
} else {
namespace = c[paths[0]], paths = paths.slice(1).join('.').split(pd);
}
}
if (typeof paths === 'string' && paths.indexOf(pd) > -1) {
paths = paths.split(pd);
}
if ( lastChar === nd){
paths = '';
}
namespace = namespace || c;
}
paths = paths[0] === '' || !paths ? undefined : [].concat(paths);
return !c ? paths : { dotPaths: paths, context: namespace };
}
function objectWalker (paths, obj, onEnd, enRoute, onEach) {
idx = -1,
nextContext = prevContext = obj;
const len = paths ? paths.length : 0;
const lastStep = len ? len - 1 : 0;
while (nextContext != null && ++idx < len) {
if (onEach) nextContext = onEach(nextContext, idx, paths, prevContext);
if (idx !== lastStep) {
prevContext = nextContext;
nextContext = (!dotWalker.objectHas(nextContext, paths[idx])) ? enRoute(nextContext, idx, paths, prevContext) : nextContext[paths[idx]];
} else {
prevContext = paths.length === 1 ? nextContext : prevContext[paths[idx - 1]];
return onEnd(nextContext, idx, paths, prevContext );
}
if(!nextContext) return nextContext;
}
return nextContext;
}
dotWalker.onEnd = function(context, idx, dotPathss, prevContext) {
if (idx === 0) {
return dotWalker.objectHas(context, dotPathss[idx]) ? context[dotPathss[idx]] : undefined;
} else {
return prevContext[dotPathss[idx]];
}
}
dotWalker.onEndAssigner = function(value){
return function(context, idx, dotPathss, prevContext) {
return prevContext[dotPathss[idx]] = value, prevContext[dotPathss[idx]];
}
}
dotWalker.assign = function (dotPath, value, context) {
return dotWalker(dotPath, context, dotWalker.onEndAssigner(value), dotWalker.enRouteMapper);
}
dotWalker.get = function (dotPath, context) {
return dotWalker(dotPath, context, dotWalker.onEnd, dotWalker.enRoute);
}
dotWalker.enRouteMapper = function(context, idx, dotPathss, prevContext) {
return context[dotPathss[idx]] = {}, context[dotPathss[idx]];
}
dotWalker.enRoute = function(context, idx, dotPathss, prevContext) {
return dotWalker.objectHas(context, dotPathss[idx]) ? context[dotPathss[idx]] : undefined ;
}
dotWalker.onEach = function(context, idx, dotPaths, prevContext) {
return context;
}
dotWalker.objectHas = function(obj, prop) {
return obj && Object.prototype.hasOwnProperty.call(obj, prop);
}
dotWalker.isObject = function(value) {
const type = typeof value
return value != null && (type == 'object' || type == 'function')
};
return dotWalker;
})();
/////// EXAMPLES AND TESTS
console.clear();
var tcontext = {};
var tmp;
console.log('Testing pathFinder functionalities')
console.log('_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _')
tmp = dotWalker.pathFinder('some.path');
console.log(
'returns an array of paths, if no context is provided.',
(tmp.length !== 0)
);
tmp = dotWalker.pathFinder();
console.log(
'returns an empty set if no context or dotPaths is supplied.',
(tmp.length === 0)
);
tmp = dotWalker.pathFinder('some.path', { name: 'empty' });
console.log(
'returns a context object like { context:..., dotPaths:...}',
!!(tmp.context && tmp.dotPaths)
);
tmp = dotWalker.pathFinder('app:person.name');
console.log(
'returns undefined when no context is supplied.',
(tmp.context === undefined)
);
tmp = dotWalker.pathFinder('app:person.name');
console.log(
'returns undefined when no matching namespace exists.',
(tmp.context === undefined)
);
tmp = dotWalker.pathFinder('app:', { app: true });
console.log(
'returns a context based on namespace when one is specified in the path.',
(tmp.context === true)
);
tmp = dotWalker.pathFinder('app:', { app: true });
console.log(
'returns a context object with an empty dotPaths when none provided',
'following a namespace.',
(tmp.context === true)
);
tmp = dotWalker.pathFinder('name', { name: 'John Doe' });
console.log(
'can use dotPathss for simple objects.',
(tmp.context.name === 'John Doe')
);
tmp = dotWalker.pathFinder('name', { ns: { root: { name: 'John Doe' } } });
console.log(
'will use the root namespace if none is specified.',
(tmp.context.name === 'John Doe')
);
tmp = dotWalker.pathFinder('app:name', { ns: { root: { name: 'John Doe' }, app: { name: 'App Name' } } });
console.log(
'will use namespaces to resolve values when supplied.',
(tmp.context.name === "App Name")
);
tmp = dotWalker.pathFinder('app:prop.that.does.not.exist', { ns: { root: { name: 'John Doe' }, app: { name: 'App Name' } } });
console.log(
'will return a valid namespace for paths that may not exist',
(tmp.context.name === "App Name")
);
console.log('- - - - - - - - - - - - - - - - - - - - - - - - - -')
console.log('Testing dotWalker functionalities')
console.log('_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _')
tmp = dotWalker();
console.log(
'will return undefined, if no dotPaths or context is provided',
(tmp === undefined)
);
tmp = dotWalker('some.path');
console.log(
'will return undefined, if no context is provided',
(tmp === undefined)
);
tmp = dotWalker(undefined, {
name: 'app name'
});
console.log(
'will return context, if no dotPaths is provided',
(tmp.name === "app name")
);
tmp = dotWalker('', {
name: 'app name'
});
console.log(
'will return context, if dotPaths is an empty string',
(tmp.name === "app name")
);
tmp = dotWalker('name', { name: 'app name' });
console.log(
'With a simple object, without any supplied namespace, will ',
'return a prop that matches the dotPaths',
(tmp === "app name")
);
var tmp = dotWalker('name.firstName', { name: {firstName: 'John Doe'}});
console.log(
'With a simple object, will retrieve a value of a nested dotPathss',
(tmp === 'John Doe')
);
// Namespaces
console.log('Namespaces...............');
tmp = dotWalker('name:', { name: 'app name' });
console.log(
'With a simple object, where a namespace was provided ',
'and the dotPaths was undefined will return a prop value ',
'that matches the namespace.',
(tmp === "app name")
);
tmp = dotWalker('name:firstName', {name: {firstName: 'John Doe'}
}, dotWalker.onEnd, dotWalker.enRoute);
console.log(
'With a simple object, will retrieve a value of a dotPathss ',
'when using namespaces',
(tmp === 'John Doe')
);
tmp = dotWalker('name:firstName.english.two',{ name: {firstName: { english: { two: 'John Doe'}}}});
console.log(
'With a simple object, will retrieve a value of a nested ',
'dotPaths, when using namespaces',
(tmp === "John Doe")
);
tmp = dotWalker('name:firstName.english.four', { name: {firstName: { english: { two: 'John Doe'}}}});
console.log(
'With a simple object, will return undefined for an invalid path ',
'dotPaths, when using namespaces',
( tmp === undefined )
);
tmp = dotWalker('name:firstName.apple.four', { name: {firstName: { english: { two: 'John Doe'}}}});
console.log(
'With a simple object, will return undefined for an invalid path ',
'dotPaths, when using namespaces',
( tmp === undefined )
);
// Assignments
console.log('Simple Object Context Assignments...............');
tcontext = {};
tmp = dotWalker(
'name',
tcontext,
dotWalker.onEndAssigner('Sally Sue'),
dotWalker.enRouteMapper
);
console.log(
'Can assign values to an object',
( tmp === tcontext.name )
);
tcontext = {name: {}};
tmp = dotWalker(
'name.firstName',
tcontext,
dotWalker.onEndAssigner('Sally'),
dotWalker.enRouteMapper
);
console.log(
'Can assign a value using dotPathss',
( tmp === tcontext.name.firstName )
);
tcontext = {name: {}};
tmp = dotWalker(
'person.name.firstName',
tcontext,
dotWalker.onEndAssigner('Sally'),
dotWalker.enRouteMapper
);
console.log(
'Can assign values using nested dotPathss, and it will fill in the missing links',
( tmp === tcontext.person.name.firstName )
);
tcontext = {we:{}};
tmp = dotWalker(
'we:person.name.firstName',
tcontext,
dotWalker.onEndAssigner('Sally'),
dotWalker.enRouteMapper
);
console.log(
'Can assign values using nested dotPathss and namespaces, and it will fill in the missing links',
( tcontext.we.person.name.firstName === tmp )
);
// Assignments
console.log('Namespaced Context Assignments...............');
tcontext = {ns:{}};
tmp = dotWalker(
'name',
tcontext,
dotWalker.onEndAssigner('Sally Sue'),
dotWalker.enRouteMapper
);
console.log(
'Can assign values to an object',
( tmp === tcontext.name )
);
tcontext = {name: {}};
tmp = dotWalker(
'name.firstName',
tcontext,
dotWalker.onEndAssigner('Sally'),
dotWalker.enRouteMapper
);
console.log(
'Can assign a value using dotPathss',
( tmp === tcontext.name.firstName )
);
tcontext = {name: {}};
tmp = dotWalker(
'person.name.firstName',
tcontext,
dotWalker.onEndAssigner('Sally'),
dotWalker.enRouteMapper
);
console.log(
'Can assign values using nested dotPathss, and it will fill in the missing links',
( tmp === tcontext.person.name.firstName )
);
tcontext = {we:{}};
tmp = dotWalker(
'we:person.name.firstName',
tcontext,
dotWalker.onEndAssigner('Sally'),
dotWalker.enRouteMapper
);
console.log(
'Can assign values using nested dotPathss and namespaces, and it will fill in the missing links',
( tcontext.we.person.name.firstName === tmp )
);
tcontext = undefined;
tmp = dotWalker(
'we:person.name.firstName',
tcontext,
dotWalker.onEndAssigner('Sally'),
dotWalker.enRouteMapper
);
console.log(
'returns undefined when no context is given',
( tmp === undefined )
);
tcontext = {we:{}};
tmp = dotWalker(
'we:person.name.firstName',
tcontext,
dotWalker.onEnd,
dotWalker.enRoute
);
console.log(
'returns undefined when no value exists at path',
( tmp === undefined )
);
tcontext = {we:{name: "John Doe"}};
tmp = dotWalker.get(
'we:name.firstName',
tcontext
);
console.log(
'follows a dotPath, and returns a value or undefined.',
( tmp === undefined )
);
// Accessor Methods
console.log('Accessors...............');
tcontext = {we:{name:{}}};
tmp = dotWalker.assign(
'we:name.firstName',
'John',
tcontext
);
tmp = dotWalker.assign(
'we:name.lastName',
'Doe',
tcontext
);
console.log(
'assigns a value using onEndAssigner and enRouteMapper',
( tcontext.we.name.firstName === "John" )
);
tmp = dotWalker.get(
'we:name.firstName',
tcontext
);
console.log(
'follows a dotPath, and returns a value or undefined.',
( tmp === "John" )
);
console.log('_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _')