Skip to content

Commit

Permalink
feat: add jackson deserializer for FEEL expressions (#525)
Browse files Browse the repository at this point in the history
  • Loading branch information
chillleader committed Jul 26, 2023
1 parent 82acf7e commit d8f50b8
Show file tree
Hide file tree
Showing 10 changed files with 741 additions and 0 deletions.
24 changes: 24 additions & 0 deletions core/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,24 @@
<scope>provided</scope>
</dependency>

<dependency>
<groupId>org.camunda.feel</groupId>
<artifactId>feel-engine</artifactId>
<scope>provided</scope>
</dependency>

<dependency>
<groupId>com.fasterxml.jackson.module</groupId>
<artifactId>jackson-module-scala_3</artifactId>
<scope>provided</scope>
</dependency>

<dependency>
<groupId>com.fasterxml.jackson.datatype</groupId>
<artifactId>jackson-datatype-jsr310</artifactId>
<scope>provided</scope>
</dependency>

<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
Expand All @@ -43,6 +61,12 @@
<artifactId>assertj-core</artifactId>
<scope>test</scope>
</dependency>

<dependency>
<groupId>org.skyscreamer</groupId>
<artifactId>jsonassert</artifactId>
<scope>test</scope>
</dependency>
</dependencies>

</project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
/*
* 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 io.camunda.connector.impl.feel;

import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.deser.ContextualDeserializer;
import com.fasterxml.jackson.databind.deser.std.StdDeserializer;
import java.io.IOException;

public abstract class AbstractFeelDeserializer<T> extends StdDeserializer<T>
implements ContextualDeserializer {
protected FeelEngineWrapper feelEngineWrapper;

protected AbstractFeelDeserializer(FeelEngineWrapper feelEngineWrapper) {
super(String.class);
this.feelEngineWrapper = feelEngineWrapper;
}

@Override
public T deserialize(JsonParser parser, DeserializationContext context) throws IOException {
JsonNode node = parser.getCodec().readTree(parser);

if (node != null && node.isTextual()) {
String value = node.textValue();
if (isFeelExpression(value)) {
return doDeserialize(value);
}
}
throw new IOException(
"Invalid input: expected a FEEL expression, but got '" + node + "' instead.");
}

private boolean isFeelExpression(String value) {
return value.startsWith("=");
}

protected abstract T doDeserialize(String expression);
}
28 changes: 28 additions & 0 deletions core/src/main/java/io/camunda/connector/impl/feel/FEEL.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
/*
* 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 io.camunda.connector.impl.feel;

import com.fasterxml.jackson.annotation.JacksonAnnotationsInside;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;

/** A shortcut for {@link JsonDeserialize} with {@link FeelDeserializer}. */
@Retention(RetentionPolicy.RUNTIME)
@JacksonAnnotationsInside
@JsonDeserialize(using = FeelDeserializer.class)
public @interface FEEL {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
/*
* 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 io.camunda.connector.impl.feel;

import java.util.Collection;
import java.util.List;
import java.util.Optional;
import org.camunda.feel.context.Context;
import org.camunda.feel.context.JavaFunction;
import org.camunda.feel.context.JavaFunctionProvider;
import org.camunda.feel.syntaxtree.Val;
import org.camunda.feel.syntaxtree.ValContext;
import org.camunda.feel.syntaxtree.ValString;
import scala.collection.immutable.Map;
import scala.collection.immutable.Map$;

/** Provider of Connector-related FEEL functions like 'bpmnError'. */
public class FeelConnectorFunctionProvider extends JavaFunctionProvider {

private static final String BPMN_ERROR_FUNCTION_NAME = "bpmnError";
private static final List<String> BPMN_ERROR_ARGUMENTS = List.of("code", "message");
private static final JavaFunction BPMN_ERROR_FUNCTION =
new JavaFunction(
BPMN_ERROR_ARGUMENTS,
args ->
new ValContext(
new Context.StaticContext(
new Map.Map2<>(
BPMN_ERROR_ARGUMENTS.get(0),
toString(args, 0),
BPMN_ERROR_ARGUMENTS.get(1),
toString(args, 1)),
Map$.MODULE$.empty())));

private static final java.util.Map<String, JavaFunction> functions =
java.util.Map.of(BPMN_ERROR_FUNCTION_NAME, BPMN_ERROR_FUNCTION);

@Override
public Optional<JavaFunction> resolveFunction(String functionName) {
return Optional.ofNullable(functions.get(functionName));
}

@Override
public Collection<String> getFunctionNames() {
return List.of(BPMN_ERROR_FUNCTION_NAME);
}

private static String toString(List<Val> arguments, int index) {
Val value = arguments.get(index);
if (value instanceof ValString) {
return ((ValString) value).value();
}
throw new IllegalArgumentException(
String.format(
"Parameter '%s' of function '%s' must be a String",
BPMN_ERROR_ARGUMENTS.get(index), BPMN_ERROR_FUNCTION_NAME));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
/*
* 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 io.camunda.connector.impl.feel;

import com.fasterxml.jackson.databind.BeanProperty;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonDeserializer;
import java.util.Map;

/**
* A Jackson deserializer for FEEL expressions. It can be used to deserialize a string that contains
* a FEEL expression, e.g. in inbound connector properties. NB: for outbound connectors, FEEL
* expressions in connector variables are evaluated by Zeebe, so this deserializer is not needed.
*/
public class FeelDeserializer extends AbstractFeelDeserializer<Object> {

private final Class<?> outputType;
private static final FeelEngineWrapper FEEL_ENGINE_WRAPPER = new FeelEngineWrapper();

public FeelDeserializer() { // needed for references in @JsonDeserialize
this(FEEL_ENGINE_WRAPPER, Object.class);
}

protected FeelDeserializer(FeelEngineWrapper feelEngineWrapper, Class<?> outputType) {
super(feelEngineWrapper);
this.outputType = outputType;
}

@Override
protected Object doDeserialize(String expression) {
return FEEL_ENGINE_WRAPPER.evaluate(expression, Map.of(), outputType);
}

@Override
public JsonDeserializer<?> createContextual(DeserializationContext ctxt, BeanProperty property) {
return new FeelDeserializer(FEEL_ENGINE_WRAPPER, property.getType().getRawClass());
}
}
Loading

0 comments on commit d8f50b8

Please sign in to comment.