1

Say I have a situation like this:

const foo = {
   bar: {
     star: {
       guitar: 'geetar'
      }
   }
}

and then I have:

const stew = {
   moo: () => foo.bar.star.guitar
}

then I call moo in the next tick of the event loop:

process.nextTick(function(){
   const guitar = stew.moo();
});

my question is - is there any way/trick to get the string representation of the path: "foo.bar.star.guitar"?

I could replace the code with a string:

    const stew = {
       moo: () => 'foo.bar.star.guitar'
    }

but I am looking to find a way to get a string representation. One important detail is that I want to generate a useful error message - if someone puts in an object path that doesn't exist. That is the whole purpose of getting a string representation - for a useful error message.

Alexander Mills
  • 90,741
  • 139
  • 482
  • 817

5 Answers5

1

One approach could be to use a reducer to extract the value from a supplied path. For a path such as bar.star.guitar, you could extract the value geetar from object:

const foo = {
   bar: {
     star: {
       guitar: 'geetar'
      }
   }
}

via the following:

const path = 'bar.star.guitar'; // Remove "foo" from your path

const foo = {
   bar: {
     star: {
       guitar: 'geetar'
      }
   }
}

const value = path
.split('.') // Break path into parts, splitting by '.'
.reduce((currentObject, pathPart) => {

  // Iterate through each part of the path in order, and incrementally 
  // extract and return corresponding value of currentObject if it 
  // exists. Repeat this for each path part until we find value from 
  // input object at end of the path
  if(currentObject) {
    currentObject = currentObject[ pathPart ]
  }
  
  return currentObject
}, foo);

console.log('path: ', path, 'value:', value)

The idea here is to iterate in order through each "part" (ie string separated by '.') of the input path, and incrementally extract and return the value corresponding to the part (key) of the input object being processed. We incrementally continue this process until the end of the path is reached, at which point we arrive at the desired value (if it exists)

Dacre Denny
  • 29,664
  • 5
  • 45
  • 65
  • I don't understand this answer - I don't have `'foo.bar.star.guitar'` as a string. I don't have that info. All I know is whether the object resolved or it threw an error "cannot read property x of undefined'. – Alexander Mills Nov 23 '18 at 00:10
  • @AlexanderMills right - so you have some arbitrary path string, and you need to extract the value corresponding to that path from input object? The snippet above is intended to illustrate how this can be done in general terms. Also, something to be aware of is that the path `foo.bar.star.guitar` is actually invalid for the object `foo` in your OP. Is there a format that you would prefer this answer to be presented in? – Dacre Denny Nov 23 '18 at 00:37
  • 1
    Your answer is how to resolve foo.bar.star.guitar from 'foo.bar.star.guitar'. But the question is about how to infer 'foo.bar.star.guitar' from foo.bar.star.guitar, for generating error messages for improper object access. – Lauren Nov 23 '18 at 07:34
1

This is for a library, so I won't have any other information besides:

const stew = {
   moo: () => foo.bar.star.guitar
}

so one thing I can do is:

let guitar;
try{
  guitar = stew.moo();
}
catch(err){
  console.error('Could not get value from:', stew.moo.toString());
}

aka, just log the string representation of the function. This is good enough information for the user to have to debug the problem.

For a full demo see: https://gist.github.com/ORESoftware/5a1626037cb8ba568cdffa69374eac1d

Alexander Mills
  • 90,741
  • 139
  • 482
  • 817
1
const stew = {
   moo: () => foo.bar.star.guitar
}

stew.moo.toString().match(/([a-z0-9_$]+\.?)+/)[0]
// returns "foo.bar.star.guitar"

This somewhat depends on people always using the same kind of function. Also I just used a short-hand for valid function name - the actual regex would be much longer.. What characters are valid for JavaScript variable names?

Lauren
  • 1,480
  • 1
  • 13
  • 36
1

based on your comments about this being for useful error messages, I believe you can get away with Error.captureStackTrace

const er = {};
Error.captureStackTrace(er);

const foo = {
    bar: {
        star: {
            guitar: "geetar"
        }
    }
};

const a = foo.bar.nope.guitar;

console.log(er.stack);

which logs

const a = foo.bar.nope.guitar;
                       ^
rlemon
  • 17,518
  • 14
  • 92
  • 123
1

If you are able to control the root variables that people can refer to, you could wrap them in a Proxy object which tracks the property access all the way down, like so:

function callStackTracker(propsSoFar) {
    return {
        get: function(obj, prop) {
            if (prop in obj) {
                return typeof obj[prop] === "object" ?
                    new Proxy(obj[prop], callStackTracker(propsSoFar.concat(prop))) :
                    obj[prop];
            } else {
                throw new Error("Couldn't resolve: " + propsSoFar.concat(prop).join('.'));
            }
        }
    };
};

let foo = {
   bar: {
     star: {
       guitar: 'geetar'
      }
   }
}

foo = new Proxy(foo, callStackTracker(['foo']));

console.log(foo.bar.star.guitar); // "geetar"
console.log(foo.bar.star.clarinet); // Error: Couldn't resolve: foo.bar.star.clarinet
console.log(foo.bar.boat); // Error: Couldn't resolve: foo.bar.boat

I am then throwing this resolved path as an error in this example, but you could do whatever you like with it.

Lauren
  • 1,480
  • 1
  • 13
  • 36