Skip to content

Commit

Permalink
Merge pull request #601 from camunda/11293-dynamic-path-get-value
Browse files Browse the repository at this point in the history
Add new get value function with dynamic path
  • Loading branch information
saig0 authored Mar 17, 2023
2 parents 960027f + 8d6657d commit c7da5fa
Show file tree
Hide file tree
Showing 6 changed files with 114 additions and 16 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,33 @@ get value({a: 1}, "b")
// null
```

## get value(context, keys)

Returns the value of the context entry for a context path defined by the given keys.

If `keys` contains the keys `[k1, k2]` then it returns the value at the nested entry `k1.k2` of the context.

If `keys` are empty or the nested entry defined by the keys doesn't exist in the context, it returns `null`.

**Function signature**

```js
get value(context: context, keys: list<string>): Any
```

**Examples**

```js
get value({x:1, y: {z:0}}, ["y", "z"])
// 0

get value({x: {y: {z:0}}}, ["x", "y"])
// {z:0}

get value({a: {b: 3}}, ["b"])
// null
```

## get entries(context)

Returns the entries of the context as a list of key-value-pairs.
Expand Down
2 changes: 1 addition & 1 deletion src/main/scala/org/camunda/feel/FeelEngine.scala
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,7 @@ class FeelEngine(
valueMapper = valueMapper,
variableProvider = VariableProvider.EmptyVariableProvider,
functionProvider = FunctionProvider.CompositeFunctionProvider(
List(new BuiltinFunctions(clock), functionProvider))
List(new BuiltinFunctions(clock, valueMapper), functionProvider))
)

def evalExpression(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,17 @@ import org.camunda.feel.context.Context
import org.camunda.feel.context.Context.{EmptyContext, StaticContext}
import org.camunda.feel.impl.builtin.BuiltinFunction.builtinFunction
import org.camunda.feel.syntaxtree.{Val, ValContext, ValError, ValList, ValNull, ValString}
import org.camunda.feel.valuemapper.ValueMapper

import scala.annotation.tailrec

object ContextBuiltinFunctions {
class ContextBuiltinFunctions(valueMapper: ValueMapper) {

def functions = Map(
"get entries" -> List(getEntriesFunction("context"),
getEntriesFunction("m")),
"get value" -> List(getValueFunction(List("m", "key")),
getValueFunction(List("context", "key"))),
getValueFunction(List("context", "key")), getValueFunction2),
"context put" -> List(contextPutFunction, contextPutFunction2),
"put" -> List(contextPutFunction), // deprecated function name
"context merge" -> List(contextMergeFunction),
Expand All @@ -35,13 +36,39 @@ object ContextBuiltinFunctions {
private def getValueFunction(parameters: List[String]) = builtinFunction(
params = parameters,
invoke = {
case List(context: ValContext, keys: ValList) => getValueFunction2.invoke(List(context, keys))
case List(ValContext(c), ValString(key)) =>
c.variableProvider
.getVariable(key)
.getOrElse(ValNull)
}
)

private def getValueFunction2 = builtinFunction(
params = List("context", "keys"),
invoke = {
case List(ValContext(context), ValList(keys)) if isListOfStrings(keys) =>
val listOfKeys = keys.asInstanceOf[List[ValString]].map(_.value)
getValueRecursive(context, listOfKeys)
case List(ValContext(_), ValList(_)) => ValNull
}
)

@tailrec
private def getValueRecursive(context: Context, keys: List[String]): Val = {
keys match {
case Nil => ValNull
case head :: tail =>
val result = context.variableProvider.getVariable(head).map(valueMapper.toVal)
result match {
case None => ValNull
case Some(value: Val) if tail.isEmpty => value
case Some(ValContext(nestedContext)) => getValueRecursive(nestedContext, tail)
case Some(_) => ValNull
}
}
}

private def contextPutFunction = builtinFunction(
params = List("context", "key", "value"),
invoke = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,19 +18,11 @@ package org.camunda.feel.impl.interpreter

import org.camunda.feel.FeelEngineClock
import org.camunda.feel.context.FunctionProvider
import org.camunda.feel.impl.builtin.{
BooleanBuiltinFunctions,
ContextBuiltinFunctions,
ConversionBuiltinFunctions,
ListBuiltinFunctions,
NumericBuiltinFunctions,
StringBuiltinFunctions,
TemporalBuiltinFunctions,
RangeBuiltinFunction
}
import org.camunda.feel.impl.builtin.{BooleanBuiltinFunctions, ContextBuiltinFunctions, ConversionBuiltinFunctions, ListBuiltinFunctions, NumericBuiltinFunctions, RangeBuiltinFunction, StringBuiltinFunctions, TemporalBuiltinFunctions}
import org.camunda.feel.syntaxtree.ValFunction
import org.camunda.feel.valuemapper.ValueMapper

class BuiltinFunctions(clock: FeelEngineClock) extends FunctionProvider {
class BuiltinFunctions(clock: FeelEngineClock, valueMapper: ValueMapper) extends FunctionProvider {

override def getFunctions(name: String): List[ValFunction] =
functions.getOrElse(name, List.empty)
Expand All @@ -43,7 +35,7 @@ class BuiltinFunctions(clock: FeelEngineClock) extends FunctionProvider {
StringBuiltinFunctions.functions ++
ListBuiltinFunctions.functions ++
NumericBuiltinFunctions.functions ++
ContextBuiltinFunctions.functions ++
new ContextBuiltinFunctions(valueMapper).functions ++
RangeBuiltinFunction.functions ++
new TemporalBuiltinFunctions(clock).functions

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ trait FeelIntegrationTest {

val rootContext: EvalContext = EvalContext.wrap(
Context.StaticContext(variables = Map.empty,
functions = new BuiltinFunctions(clock).functions)
functions = new BuiltinFunctions(clock, ValueMapper.defaultValueMapper).functions)
)(ValueMapper.defaultValueMapper)

def withClock(testCode: TimeTravelClock => Any): Unit = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
package org.camunda.feel.impl.builtin

import org.camunda.feel.context.Context.StaticContext
import org.camunda.feel.context.{CustomContext, VariableProvider}
import org.camunda.feel.impl.FeelIntegrationTest
import org.scalatest.matchers.should.Matchers
import org.scalatest.flatspec.AnyFlatSpec
Expand Down Expand Up @@ -76,6 +77,57 @@ class BuiltinContextFunctionsTest
eval(""" get value({}, "foo") """) should be(ValNull)
}

"A get value with path function" should "return the value when a path is provided" in {
eval("""get value({x: {y: {z:1}}}, ["x", "y", "z"])""") should be(ValNumber(1))
}

it should "return a context when a path is provided" in {
eval("""get value({x: {y: {z:1}}}, ["x", "y"]) = {z:1}""") should be(ValBoolean(true))
}

it should "return null if non-existing path is provided" in {
eval("""get value({x: {y: {z:1}}}, ["z"])""") should be(ValNull)
}

it should "return null if non-existing nested path is provided" in {
eval("""get value({x: {y: {z:1}}}, ["x", "z"])""") should be(ValNull)
}

it should "return null if non-String list of keys is provided" in {
eval("""get value({x: {y: {z:1}}}, ["1", 2])""") should be(ValNull)
}

it should "return null if an empty context is provided" in {
eval("""get value({}, ["z"])""") should be(ValNull)
}

it should "return null if an empty list is provided as a path" in {
eval("""get value({x: {y: {z:1}}}, [])""") should be(ValNull)
}

it should "return a value if named arguments are used" in {
eval("""get value(context: {x: {y: {z:1}}}, keys: ["x"]) = {y: {z:1}}""") should be(ValBoolean(true))
}

it should "return a value from a custom context" in {

class MyCustomContext extends CustomContext {
class MyVariableProvider extends VariableProvider {
private val entries = Map(
"x" -> Map("y" -> 1)
)

override def getVariable(name: String): Option[Any] = entries.get(name)

override def keys: Iterable[String] = entries.keys
}

override def variableProvider: VariableProvider = new MyVariableProvider
}

eval("""get value(context, ["x", "y"])""", Map("context" -> ValContext(new MyCustomContext))) should be(ValNumber(1))
}

"A context put function" should "add an entry to an empty context" in {

eval(""" context put({}, "x", 1) """) should be(
Expand Down

0 comments on commit c7da5fa

Please sign in to comment.