diff --git a/docs/docs/reference/builtin-functions/feel-built-in-functions-context.md b/docs/docs/reference/builtin-functions/feel-built-in-functions-context.md index 507743031..45f9f1f4d 100644 --- a/docs/docs/reference/builtin-functions/feel-built-in-functions-context.md +++ b/docs/docs/reference/builtin-functions/feel-built-in-functions-context.md @@ -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): 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. diff --git a/src/main/scala/org/camunda/feel/FeelEngine.scala b/src/main/scala/org/camunda/feel/FeelEngine.scala index 013e324f0..6d44afa3f 100644 --- a/src/main/scala/org/camunda/feel/FeelEngine.scala +++ b/src/main/scala/org/camunda/feel/FeelEngine.scala @@ -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( diff --git a/src/main/scala/org/camunda/feel/impl/builtin/ContextBuiltinFunctions.scala b/src/main/scala/org/camunda/feel/impl/builtin/ContextBuiltinFunctions.scala index 2194569b0..6050ef1be 100644 --- a/src/main/scala/org/camunda/feel/impl/builtin/ContextBuiltinFunctions.scala +++ b/src/main/scala/org/camunda/feel/impl/builtin/ContextBuiltinFunctions.scala @@ -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), @@ -35,6 +36,7 @@ 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) @@ -42,6 +44,31 @@ object ContextBuiltinFunctions { } ) + 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 = { diff --git a/src/main/scala/org/camunda/feel/impl/interpreter/BuiltinFunctions.scala b/src/main/scala/org/camunda/feel/impl/interpreter/BuiltinFunctions.scala index ee5db4120..26cfebf54 100644 --- a/src/main/scala/org/camunda/feel/impl/interpreter/BuiltinFunctions.scala +++ b/src/main/scala/org/camunda/feel/impl/interpreter/BuiltinFunctions.scala @@ -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) @@ -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 diff --git a/src/test/scala/org/camunda/feel/impl/FeelIntegrationTest.scala b/src/test/scala/org/camunda/feel/impl/FeelIntegrationTest.scala index f9157e152..8fcb3fa8c 100644 --- a/src/test/scala/org/camunda/feel/impl/FeelIntegrationTest.scala +++ b/src/test/scala/org/camunda/feel/impl/FeelIntegrationTest.scala @@ -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 = { diff --git a/src/test/scala/org/camunda/feel/impl/builtin/BuiltinContextFunctionsTest.scala b/src/test/scala/org/camunda/feel/impl/builtin/BuiltinContextFunctionsTest.scala index ddd2200a3..4d628666e 100644 --- a/src/test/scala/org/camunda/feel/impl/builtin/BuiltinContextFunctionsTest.scala +++ b/src/test/scala/org/camunda/feel/impl/builtin/BuiltinContextFunctionsTest.scala @@ -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 @@ -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(