Where developers come to connect, share, build and be inspired.

2

Object Path Validation - A solution to validate deep objects

898 views

[Leggi la versione italiana]

Preamble

What is an object path? It's a personal interpretation of the identifier that defines a clear path from a root object to a sub-object or property.

The Problem

How many times we've had the need to check if object.foo.bar.baz is a valid object path?

Index

  1. The Solution #1 - IF Statement
  2. The Solution #2 - Functional Statement
  3. Variation #1 - The Binding
  4. Variation #2 - Prototyping

The Solution #1 - IF Statement

Well, as first solution we can look at a very basic statement, often used in this cases:

if ( object && object.foo && object.foo.bar && object.foo.bar.baz ) {
    // do something
}

The if-statement will check the validity (that is, if it exists) for each identifier, and only whether all the identifiers are valid will execute its block of code.

PRO

  • Its intent is clear to every one who look at that code.
  • It's fast due to the short-circuit evaluation of the if-statement (it stops at the very first invalid identifier).

CONS

  • The deeper is the path, the more identifiers will be required to evaluate the statement. In particular cases this could bring us to have a really long and unclear if-statement
  • If used with DOM objects, the execution will be slower due to DOM's low performance access to its objects.

The Solution #2 - Functional Statement

As we seen above, the if-statement solution is good enough when the object path isn't much deep or it hasn't to check for a DOM object.

But, how would if we have a very deep object to check? Even worst, how would if we have to check for object paths with different depths?

The above solution isn't as good as it could seems. But, we can solve this problem using a functional approach.

So, first thing first, we have to define our function:

var safeObjectPath = function safeObjectPath( object, properties ) {
    var path = [],
        root = object,
        prop;

    if ( !root ) {
        // if the root object is null we immediately returns
        return false;
    }

    if ( typeof properties === 'string' ) {
        // if a string such as 'foo.bar.baz' is passed,
        // first we convert it into an array of property names
        path = properties ? properties.split('.') : [];
    } else {
        if ( Object.prototype.toString.call( properties ) === '[object Array]' ) {
            // if an array is passed, we don't need to do anything but
            // to assign it to the internal array
            path = properties;
        } else {
            if ( properties ) {
                // if not a string or an array is passed, and the parameter
                // is not null or undefined, we return with false
                return false;
            }
        }
    }

    // if the path is valid or empty we return with true (because the
    // root object is itself a valid path); otherwise false is returned.
    while ( prop = path.shift() ) {
        // UPDATE: before it was used only the if..else statement, but
        // could generate an exception if a inexistent member was found.
        // Now I fixed with a try..catch statement. Thanks to <a href="/tarikozket">@tarikozket</a>
        // (https://coderwall.com/tarikozket) for the contribution!
        try {
            if ( prop in root ) {
                root = root[prop];
            } else {
                return false;
            }
        } catch(e) {
            return false;
        }
    }

    return true;
}

Now, we can check for our object paths by using a functional approach, as follow:

var module = { foo: { bar: { baz: 'hello' } } };

// checks if `module` isn't null or undefined
safeObjectPath( module ) // -> true

// checks if `foo.bar.baz` is a valid path in `module`
safeObjectPath( module, 'foo.bar.baz' ) // -> true

// same as above, but with an array
safeObjectPath( module, ['foo', 'bar', 'baz'] ) // -> true

// checks if `foo.baz` is a valid path in `module`
safeObjectPath( module, ['foo', 'baz'] ) // -> false

As we can see, this approach allows us to check for different kind of path (string or array, so we can even dynamically generate those paths), and with a simple statement (our function) it's possible to check for any depth of path we want. No matter whether 1, 5, 10, or 100 properties in depth.

PRO

  • Shorter statements even with deep paths
  • Support for string or array paths
  • Support for paths of any depth
  • Intermediate paths are cached. Processing DOM objects is faster than the solution #1

CONS

  • Functions are slower than if-statements
  • It's purpose might not be immediately clear to whom read the source code

Variation #1 - The Binding

Now that we've seen the way to check for a valid object path, we can try to push ourselves over than the 2nd solution, and look for some more advanced change.

As we've seen, the function works on every data (since in javascript almost everything is an object). So, a first step it could be to remove the object argument and take advantage of the binding. To do this, we have to make some slight changes:

var safeObjectPath = function safeObjectPath( properties ) {
    var path = [],
        root = this, // before, this was `object`
        prop;

We've removed object from the arguments, and set root as this. Now the function will always do reference to the context of the object binded to it. So, to use this modified version we'll have different ways:

safeObjectPath.call( object, 'foo.bar.baz' )

safeObjectPath.apply( object, [ ['foo', 'bar', 'baz'] ] )

(safeObjectPath.bind( object ))( 'foo.bar.baz' )

However, as we can see, the new changes we've introduced don't really give us any practical improvement, because we still have to bind it with apply(), call(), or bind() methods, making it longer than before, and more complex. So, now we can see what is the final next step to improve this solution and make it usefull for our goals.

Variation #2 - Prototyping

Yes, I can hear all the legions of programmers out there complaining because it's a bad practice to directly prototype into a native object. Yes, you're right. But we can't take not in account the big help this kind of method can give to us. (BTW, Prototype.js has made its fortune thanks to this approach, so why at least don't take a look at it? ;).

;(function(root, factory, undefined){
    var Object = root.Object;

    try {
        // We take care to not do a mess with `Object`.
        // If the method doesn't exists, an exception is thrown and
        // the it will be added.
        Object.isSafePath();
    } catch (e) {
        // This will automatically makes the new method available to all
        // the other objects.
        Object.prototype.isSafePath = factory();
    }
}( window, function(){
    return function ( properties ){
        var path = [],
            root = this,
            prop;

        if ( typeof properties === 'string' ) {
            // if a string such as 'foo.bar.baz' is passed,
            // first we convert it into an array of property names
            path = properties ? properties.split('.') : [];
        } else {
            if ( Object.prototype.toString.call( properties ) === '[object Array]' ) {
                // if an array is passed, we don't need to do anything but
                // to assign it to the internal array
                path = properties;
            } else {
                if ( properties ) {
                    // if not a string or an array is passed, and the parameter
                    // is not null or undefined, we return with false
                    return false;
                }
            }
        }

        // if the path is valid or empty we return with true (because the
        // root object is itself a valid path); otherwise false is returned.
        while ( prop = path.shift() ) {
            try {
                if ( prop in root ) {
                    root = root[prop];
                } else {
                    return false;
                }
            } catch(e) {
                    return false;
            }
        }

        return true;
    };
}));

What we've done here it's pretty simple (I hope :D). We use an IIFE to install our method into the Object prototype by using a factory pattern. In this way we can be pretty sure to not overwrite an eventually already present method with the same name.

We also have removed the internal check for a valid initial root because, in this case, if we don't have a valid object, we also don't have a reference to the method.

That said, all we have to do will be to call the new method, every time we want to test for a valid object path:

object.isSafePath( 'foo.bar.baz' )
object.isSafePath( ['foo', 'bar', 'baz'] )
Object.prototype.isSafePath.apply( object, ['foo', 'bar', 'baz'] )
Object.prototype.isSafePath.call( object, 'foo.bar.baz' )

That's all folks! Hope it will be helpful to someone.

Source code is available on Gist.

Performance test is available on jsPerf. (Thanks to @tarikozket).

7E67F7C4750AD1619A0C2D381CD9BD6E

Comments

Add a comment