Skip to content
Draft
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 @@ -80,6 +80,18 @@ class FeelInterpreter(private val valueMapper: ValueMapper) {

case range: ConstRange => toRange(range)

case Template(parts) =>
ValString(
// evaluate all parts and concatenate them
parts
.map(eval)
.map {
case ValString(value) => value
case other => other.toString
}
.mkString
)

// simple unary tests
case InputEqualTo(x) =>
withVal(getImplicitInputValue, i => checkEquality(i, eval(x)))
Expand Down
65 changes: 61 additions & 4 deletions src/main/scala/org/camunda/feel/impl/parser/FeelParser.scala
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ import org.camunda.feel.syntaxtree.{
ConstNull,
ConstNumber,
ConstRange,
ConstRangeBoundary,
ConstString,
ConstTime,
ConstYearMonthDuration,
Expand Down Expand Up @@ -88,20 +89,20 @@ import org.camunda.feel.syntaxtree.{
PathExpression,
PositionalFunctionParameters,
QualifiedFunctionInvocation,
ConstRangeBoundary,
Ref,
SomeItem,
Subtraction,
Template,
UnaryTestExpression
}
import org.camunda.feel.{
Date,
isDayTimeDuration,
isLocalDateTime,
isOffsetDateTime,
isOffsetTime,
isValidDate,
isYearMonthDuration,
isLocalDateTime,
isDayTimeDuration,
stringToDate,
stringToDateTime,
stringToDayTimeDuration,
Expand Down Expand Up @@ -375,7 +376,7 @@ object FeelParser {
terminalValue.flatMap(optional(chainedValueOp(_)))

private def terminalValue[_: P]: P[Exp] =
temporal | functionInvocation | variableRef | literal | inputValue | functionDefinition | "(" ~ expression ~ ")"
temporal | functionInvocation | variableRef | literal | inputValue | functionDefinition | "(" ~ expression ~ ")" | template

private def literal[_: P]: P[Exp] =
nullLiteral | boolean | string | number | temporal | list | context
Expand Down Expand Up @@ -561,6 +562,62 @@ object FeelParser {
("[" ~ expression ~ "]").rep(1)
).map(ops => ops.foldLeft(base)(Filter))

// --------------- template expressions ---------------

private def template[_: P]: P[Exp] = P(
"```" ~~/ templateContent ~~/ "```"
)

private def templateContent[_: P]: P[Exp] = P(
templateStringPart.? ~~ (
(templatePlaceholder | templateCondition | templateLoop) ~~ templateStringPart.?
).repX(0)
).map { case (prefix, sequence) =>
val start: Seq[Exp] = prefix.map(Seq(_)).getOrElse(Seq.empty)
val parts = sequence.foldLeft(start) {
case (acc, (placeholder, None)) => acc :+ placeholder
case (acc, (placeholder, Some(suffix))) => acc :++ Seq(placeholder, suffix)
}
Template(parts)
}

private def templateStringPart[_: P]: P[Exp] = P(
(!("{{" | "```") ~~ AnyChar).repX(1).!
).map(translateEscapes).map(ConstString)

private def templatePlaceholder[_: P]: P[Exp] = P(
"{{" ~~ !("#" | "/") ~/ expression ~ "}}"
).map(exp =>
FunctionInvocation(
function = "string",
params = PositionalFunctionParameters(List(exp))
)
)

private def templateCondition[_: P]: P[Exp] = P(
"{{#if" ~/ expression ~ "}}" ~~/ templateContent ~~ "{{/if}}"
).map { case (condition, content) =>
If(
condition = condition,
statement = content,
elseStatement = ConstString("")
)
}

private def templateLoop[_: P]: P[Exp] = P(
"{{#loop" ~/ expression ~ "}}" ~~/ templateContent ~~ "{{/loop}}"
).map { case (iterationContext, content) =>
val loop = For(
iterators = List(("this", iterationContext)),
exp = content
)

FunctionInvocation(
function = "string join",
params = PositionalFunctionParameters(List(loop))
)
}

// --------------- unary-tests expressions ---------------

private def unaryTests[_: P]: P[Exp] =
Expand Down
2 changes: 2 additions & 0 deletions src/main/scala/org/camunda/feel/syntaxtree/Exp.scala
Original file line number Diff line number Diff line change
Expand Up @@ -144,3 +144,5 @@ case class Filter(list: Exp, filter: Exp) extends Exp
case class IterationContext(start: Exp, end: Exp) extends Exp

case class UnaryTestExpression(exp: Exp) extends Exp

case class Template(parts: Seq[Exp]) extends Exp
Original file line number Diff line number Diff line change
Expand Up @@ -514,7 +514,7 @@ class InterpreterListExpressionTest

it should "compute a long list" in {
evaluateExpression(
"""count(for x in 0..1000000 return "Hi there")""",
"""count(for x in 0..1000000 return "Hi there")"""
) should returnResult(1000001)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
/*
* Copyright Camunda Services GmbH and/or licensed to Camunda Services GmbH
* under one or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information regarding copyright
* ownership. Camunda licenses this file to you under the Apache License,
* Version 2.0; you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.camunda.feel.impl.interpreter

import org.camunda.feel.impl.{EvaluationResultMatchers, FeelEngineTest}
import org.scalatest.flatspec.AnyFlatSpec
import org.scalatest.matchers.should.Matchers
import org.scalatest.prop.TableDrivenPropertyChecks

class TemplateExpressionTest
extends AnyFlatSpec
with Matchers
with FeelEngineTest
with EvaluationResultMatchers {

"A template" should "be a simple string" in {

evaluateExpression("```Hello friend!```") should returnResult("Hello friend!")
}

it should "contain a variable reference" in {

evaluateExpression(
expression = "```Hello {{name}}```",
variables = Map("name" -> "Zee")
) should returnResult("Hello Zee")

evaluateExpression(
expression = "```Hello {{name}}, nice to meet you.```",
variables = Map("name" -> "Zee")
) should returnResult("Hello Zee, nice to meet you.")
}

it should "contain an expression" in {

evaluateExpression(
expression = """```Good {{if hour < 12 then "morning" else "afternoon"}}```""",
variables = Map("hour" -> 9)
) should returnResult("Good morning")

evaluateExpression(
expression = """```Good {{if hour < 12 then "morning" else "afternoon"}}```""",
variables = Map("hour" -> 15)
) should returnResult("Good afternoon")
}

it should "insert the value as a string" in {

evaluateExpression(
expression = """```Value: {{value}}```""",
variables = Map("value" -> "FEEL")
) should returnResult("Value: FEEL")

evaluateExpression(
expression = """```Value: {{value}}```""",
variables = Map("value" -> 123)
) should returnResult("Value: 123")

evaluateExpression(
expression = """```Value: {{value}}```""",
variables = Map("value" -> true)
) should returnResult("Value: true")

evaluateExpression(
expression = """```Value: {{value}}```""",
variables = Map("value" -> List(1, 2, 3))
) should returnResult("Value: [1, 2, 3]")

evaluateExpression(
expression = """```Value: {{value}}```""",
variables = Map("value" -> Map("a" -> 1, "b" -> 2))
) should returnResult("Value: {a:1, b:2}")
}

it should "contain an conditional section" in {

evaluateExpression(
expression = """```Hello{{#if name = "Zee"}} my friend{{/if}}!```""",
variables = Map("name" -> "Zee")
) should returnResult("Hello my friend!")

evaluateExpression(
expression = """```Hello{{#if name = "Zee"}} my friend{{/if}}!```""",
variables = Map("name" -> "Joe")
) should returnResult("Hello!")
}

it should "contain a loop section" in {

evaluateExpression(
expression = """```Items:\n{{#loop items}}- {{this}}\n{{/loop}}```""",
variables = Map("items" -> List("a", "b", "c"))
) should returnResult("""Items:
|- a
|- b
|- c
|""".stripMargin)
}

}