Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add new get value function with dynamic path #601

Merged
merged 5 commits into from
Mar 17, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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