javascript - get parameters from string

Viewed 93

I have some scripts which require a json with config passed into. From this JSON I want to be able to extract parameters (and more). Below is an example

const config = {
    name: 'foo',
    lookup: {
        lookup1: 'value1',
    },
    parameters: {
        bar: 'baz',
        test: 'hello',
    },
    actons: [
        {
            type: 'move',
            value: '[parameters(bar)]',
        },
        {
            type: 'tag',
            value: '[parameters(lookup(lookup1))]',
        },
        {
            type: 'date',
            value: '[date(-1, "days")]',
        },
        {
            type: 'tag',
            value: 'Just a normal text value',
        },
        {
            type: 'blabla',
            value: { object: 'also permitted' },
        },
    ],
};

This example shows some json-values which start with [ and end with ] and have some "function" in between. This format is actually stolen from how Azure RM works with their parameters: https://docs.microsoft.com/en-us/azure/azure-resource-manager/templates/template-parameters

Now, what I want to do is process those functions. At the moment I have the following functions: date(), parameters(), concat(), lookup() (but more to come). Those are the only functions that are allowed to be executed. Thats where my question comes in.

My first thought was to remove the begin and end bracket and then just execute this string with eval(), but this is dangerous because any code can exist between those brackets.

So how would I solve this then? I am thinking of 2 different routes to accomplish this (both are ok):
1. How to make sure the string only contains allowed functions? That way I can safely use eval() (check string before executing )
2. How to execute those functions from this string from the inside out. This is important because functions can also be nested (see example: parameters(lookup(lookup1)) )

1 Answers

Core idea

Destructure function calls into something safer to parse.

Model (TS types):

With just one arg by design:

interface Func {
  func: string;
  arg: Func | string;
}

With multiple args:

interface Func {
  func: string;
  args: Array<Func | string>;
}


Implementation options

1. Changing object structure

Only one arg:

{
  func: "paramaters",
  arg: {
    func: "lookup",
    arg: "lookup1"
  }
}

Multiple args:

{
  func: "paramaters",
  args: [{
    func: "lookup",
    args: ["lookup1"]
  }]
}

2. Without changing structure, using JSON.parse:

Use the same approach with a JSON string.

{
  type: 'move',
  value: "{ \"func\": \"parameters\", \"args\": [ \"bar\" ] }",
},

So you can get it with: JSON.parse(actions[0].value).


Running a function (JSON option)

As an example of final execution, you could have a dictionary of allowed functions:

const parameters = (...args) => console.log(...args);
const lookup = (arg) => console.warn(arg);

const allowedFunctions = {
  parameters,
  lookup,
};

And some helpers to check and run them:

const isFunction = (functionCall) =>
  functionCall.hasOwnProperty("func") &&
  functionCall.hasOwnProperty("args") &&
  Array.isArray(functionCall.args);

const runFunction = (functionCall) => {
  const func = allowedFunctions[functionCall.func];
  if (!func) {
    throw new Error(`Unknown function: ${functionCall.func}`);
  } else {
    const args = functionCall.args.map((arg) => {
      if (isFunction(arg)) {
        return runFunction(arg);
      }
      return arg;
    });
    return func.apply(null, args);
  }
};

So the main call could look like this:

for (const action of actions) {
  if ("string" === typeof action.value) {
    try {
      const value = JSON.parse(action.value);
      if (isFunction(value)) {
        console.log("Function returns:", runFunction(value));
      } else {
        throw new Error("Not a function object");
      }
    } catch (ex) {
      console.error("Not a function or unknown", ex);
    }
  }
}

These functions are not final, you would want to add more checks or another possible values that would need further parsing, but I hope the main idea remains clear.

Related