TomFrost / Jexl

Javascript Expression Language: Powerful context-based expression parser and evaluator
MIT License
561 stars 92 forks source link

assignment expression #86

Closed wasimshariff closed 4 years ago

wasimshariff commented 4 years ago

Hi Tom,

Thank you for the great library !!!

I have an issue. Is there a way to achieve below scenario with the current library ?

const context = { age: 36 } const res = await jexl.eval('age = 32', context)

Thanks, Wasim

TomFrost commented 4 years ago

Thanks for your kind words @wasimshariff !

Jexl does not currently support assignment within expressions. Jexl expressions are more focused on creating a result value, rather than offering a Turing-complete programming capability. With that said, I'm always interested to hear others' use cases and will be happy to reconsider!

It's worth noting, though, that if Jexl ever does support assignment operators, I can say with confidence that it will not support changes to the context persisting after the expression completes. Jexl is built with absolute safety in mind, so it cannot allow a expression to modify anything in the parent javascript environment. That's what Javascript's native eval() does.

wasimshariff commented 4 years ago

Thanks for the reply Tom.

We are migrating our App from JSF to Angular. In JSF world, we were using Apache JEXL library for evaluating our business rules ( server side). Now, we are moving these rules to Angular world.

We have tons of business rules that is defined by the business in a file.

Eg : app is an List in jexl context.

<case name="TELESALES_SET_AGE"> <criteria>app[appIndex].distributionChannel eq "TELESALES"</criteria> <decision> app[appIndex].applicant.age = fn:calcAge(app[appIndex].applicant.dateOfBirth) </decision> </case>

We use assignment pretty extensively. Let me know your thoughts if this can be implemented in the library or if its not the use case of this library.

Thanks again for such quick reply :)

TomFrost commented 4 years ago

That is a super cool use case. I love it.

On the surface that seems like it would require writing back to the context, which at this point I'm not sure I like the idea of... though I'll consider safe options.

However! There's another way to approach this: What if the result of your Jexl expression were an object that you could merge onto your context after every expression, perhaps with lodash? Consider this:

const _ = require('lodash')
const context = {
  applicant: {
    dateOfBirth: '1984-03-09',
    name: 'John Doe'
  },
  distributionChannel: 'TELESALES'
}
const expressions = [
  "{ distributionChannel: 'DIRECT' }",
  "{ applicant: { age: applicant.dateOfBirth|toAge } }"
]

expressions.forEach(expr => {
  const result = jexl.evalSync(expr, context)
  _.merge(context, result)
})

// The context is now:
// {
//   applicant: {
//     name: 'John Doe',
//     dateOfBirth: '1984-03-09',
//     age: 36
//   },
//   distributionChannel: 'DIRECT'
// }

What do you think? This would even give you the opportunity to run validation on the result (to ensure the properties about to be written are legal and exist on the schema) before it's merged in, whereas directly modifying the context in the expression wouldn't give you that chance before the data's already overwritten.

TomFrost commented 4 years ago

Another thought! I'm about to cut a new release that adds top-level functions to Jexl (in addition to the transform functions it already has). If the above will work for you, you could define a function that will take two arguments: the "address" of the field to modify, and the new value. Then it would split the address on the dots and produce the object to be merged. Like this:

const result = jexl.evalSync("write('applicant.age', applicant.dateOfBirth|toAge)")
// result is now { applicant: { age: 36 } }

Same concept, but that syntax might be easier on the eyes.

wasimshariff commented 4 years ago

Thanks Tom. One constraint we have is , i cant make lot of modifications to business rule files. They have to be backward compatible.

I will look into your suggestions. I am closing this question for now. Thanks for the help !!!

TomFrost commented 4 years ago

Oo, I’m afraid that even with assignment operators in Jexl, the syntax is just too different from Apache’s Jexl that all the business logic expressions would still have to be rewritten. You might be better served by a small Java lambda function behind API Gateway (assuming AWS, but choose your provider’s equivalent) specifically to run expressions and pass back the result.

richardmward commented 4 years ago

Sorry for adding to a closed issue, but thought I'd ask if you've seen the FEEL (Friendly Enough Expression Language) used by a number of BPMN/DMN products (notably Camunda) and their handling of "context" expressions, which are essentially expressions where the expression is a JSON object - much like in your example "{ applicant: { age: applicant.dateOfBirth|toAge } }".

A couple of things they allow is for items in a context expression to rely on other properties in the expression, so long as it is defined before it, for example:

{ a: 1, b: a + 1 } is valid, and would return { "a": 1, "b": 2 }.

Given each expression in a context is essentially assigning to a property, then you get the assignment capability (something you already have), but also the ability to calculate things based on the context.

Of course, the same could be achieved right now in JEXL by merging the result back into the context (as per your example). That however brings me onto another FEEL feature that might be interesting and that makes a good case for it being handled "natively" - you can define functions within the context expression, for example:

{ a: 1, b: a + 1, sum(x,y) x + y, result: sum(a, 3) } would return { "a": 1, "b": 2, "result": 5}

Functions can only contain expressions that are valid against the grammar (i.e. they are not "javascript functions", just reusable functions for convenience).

I'm not suggesting for one moment that you'd want to be attempting to create a FEEL like grammar, but in the interest of these assignment based use cases, I thought it might be interesting to show examples of not dissimilar use cases I have seen elsewhere.