Repository: serverlessworkflow/sdk-java
Pull request #458 (2024-11-04 19:14:29)

Adding JQ expression support for http call
This completes implemetation of HTTP call by adding jq expression support


Patch:
diff --git a/impl/pom.xml b/impl/pom.xml
index 32ee86a..3907fb7 100644
--- a/impl/pom.xml
+++ b/impl/pom.xml
@@ -8,6 +8,7 @@
   <artifactId>serverlessworkflow-impl</artifactId>
   <properties>
       <version.org.glassfish.jersey>3.1.9</version.org.glassfish.jersey>
+      <version.net.thisptr>1.0.1</version.net.thisptr>
   </properties>
   <dependencies>
     <dependency>
@@ -25,6 +26,11 @@
       <artifactId>jersey-media-json-jackson</artifactId>
       <version>${version.org.glassfish.jersey}</version>
     </dependency>
+    <dependency>
+      <groupId>net.thisptr</groupId>
+      <artifactId>jackson-jq</artifactId>
+      <version>${version.net.thisptr}</version>
+    </dependency>
     <dependency>
       <groupId>org.junit.jupiter</groupId>
       <artifactId>junit-jupiter-api</artifactId>
diff --git a/impl/src/main/java/io/serverlessworkflow/impl/AbstractTaskExecutor.java b/impl/src/main/java/io/serverlessworkflow/impl/AbstractTaskExecutor.java
index f377b3f..1318160 100644
--- a/impl/src/main/java/io/serverlessworkflow/impl/AbstractTaskExecutor.java
+++ b/impl/src/main/java/io/serverlessworkflow/impl/AbstractTaskExecutor.java
@@ -21,9 +21,11 @@ import io.serverlessworkflow.api.types.TaskBase;
 public abstract class AbstractTaskExecutor<T extends TaskBase> implements TaskExecutor<T> {
 
   protected final T task;
+  protected final ExpressionFactory exprFactory;
 
-  protected AbstractTaskExecutor(T task) {
+  protected AbstractTaskExecutor(T task, ExpressionFactory exprFactory) {
     this.task = task;
+    this.exprFactory = exprFactory;
   }
 
   @Override
diff --git a/impl/src/main/java/io/serverlessworkflow/impl/DefaultTaskExecutorFactory.java b/impl/src/main/java/io/serverlessworkflow/impl/DefaultTaskExecutorFactory.java
index 806a320..fab07d8 100644
--- a/impl/src/main/java/io/serverlessworkflow/impl/DefaultTaskExecutorFactory.java
+++ b/impl/src/main/java/io/serverlessworkflow/impl/DefaultTaskExecutorFactory.java
@@ -18,23 +18,32 @@ package io.serverlessworkflow.impl;
 import io.serverlessworkflow.api.types.CallTask;
 import io.serverlessworkflow.api.types.Task;
 import io.serverlessworkflow.api.types.TaskBase;
+import io.serverlessworkflow.impl.jq.JQExpressionFactory;
 
 public class DefaultTaskExecutorFactory implements TaskExecutorFactory {
 
-  protected DefaultTaskExecutorFactory() {}
+  private final ExpressionFactory exprFactory;
 
-  private static TaskExecutorFactory instance = new DefaultTaskExecutorFactory();
+  private static TaskExecutorFactory instance =
+      new DefaultTaskExecutorFactory(JQExpressionFactory.get());
 
   public static TaskExecutorFactory get() {
     return instance;
   }
 
-  public TaskExecutor<? extends TaskBase> getTaskExecutor(Task task) {
+  public static TaskExecutorFactory get(ExpressionFactory factory) {
+    return new DefaultTaskExecutorFactory(factory);
+  }
 
+  protected DefaultTaskExecutorFactory(ExpressionFactory exprFactory) {
+    this.exprFactory = exprFactory;
+  }
+
+  public TaskExecutor<? extends TaskBase> getTaskExecutor(Task task) {
     if (task.getCallTask() != null) {
       CallTask callTask = task.getCallTask();
       if (callTask.getCallHTTP() != null) {
-        return new HttpExecutor(callTask.getCallHTTP());
+        return new HttpExecutor(callTask.getCallHTTP(), exprFactory);
       }
     }
     throw new UnsupportedOperationException(task.get().getClass().getName() + " not supported yet");
diff --git a/impl/src/main/java/io/serverlessworkflow/impl/Expression.java b/impl/src/main/java/io/serverlessworkflow/impl/Expression.java
new file mode 100644
index 0000000..b5bbfc0
--- /dev/null
+++ b/impl/src/main/java/io/serverlessworkflow/impl/Expression.java
@@ -0,0 +1,22 @@
+/*
+ * Copyright 2020-Present The Serverless Workflow Specification Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * 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.serverlessworkflow.impl;
+
+import com.fasterxml.jackson.databind.JsonNode;
+
+public interface Expression {
+  JsonNode eval(JsonNode input);
+}
diff --git a/impl/src/main/java/io/serverlessworkflow/impl/ExpressionFactory.java b/impl/src/main/java/io/serverlessworkflow/impl/ExpressionFactory.java
new file mode 100644
index 0000000..8f9c1dd
--- /dev/null
+++ b/impl/src/main/java/io/serverlessworkflow/impl/ExpressionFactory.java
@@ -0,0 +1,21 @@
+/*
+ * Copyright 2020-Present The Serverless Workflow Specification Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * 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.serverlessworkflow.impl;
+
+public interface ExpressionFactory {
+
+  Expression getExpression(String expression);
+}
diff --git a/impl/src/main/java/io/serverlessworkflow/impl/ExpressionUtils.java b/impl/src/main/java/io/serverlessworkflow/impl/ExpressionUtils.java
new file mode 100644
index 0000000..4500093
--- /dev/null
+++ b/impl/src/main/java/io/serverlessworkflow/impl/ExpressionUtils.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright 2020-Present The Serverless Workflow Specification Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * 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.serverlessworkflow.impl;
+
+public class ExpressionUtils {
+
+  private static final String EXPR_PREFIX = "${";
+  private static final String EXPR_SUFFIX = "}";
+
+  private ExpressionUtils() {}
+
+  public static String trimExpr(String expr) {
+    expr = expr.trim();
+    if (expr.startsWith(EXPR_PREFIX)) {
+      expr = trimExpr(expr, EXPR_PREFIX, EXPR_SUFFIX);
+    }
+    return expr.trim();
+  }
+
+  private static String trimExpr(String expr, String prefix, String suffix) {
+    expr = expr.substring(prefix.length());
+    if (expr.endsWith(suffix)) {
+      expr = expr.substring(0, expr.length() - suffix.length());
+    }
+    return expr;
+  }
+}
diff --git a/impl/src/main/java/io/serverlessworkflow/impl/HttpExecutor.java b/impl/src/main/java/io/serverlessworkflow/impl/HttpExecutor.java
index 203ca15..e2c2c42 100644
--- a/impl/src/main/java/io/serverlessworkflow/impl/HttpExecutor.java
+++ b/impl/src/main/java/io/serverlessworkflow/impl/HttpExecutor.java
@@ -18,7 +18,10 @@ package io.serverlessworkflow.impl;
 import com.fasterxml.jackson.core.type.TypeReference;
 import com.fasterxml.jackson.databind.JsonNode;
 import io.serverlessworkflow.api.types.CallHTTP;
+import io.serverlessworkflow.api.types.Endpoint;
+import io.serverlessworkflow.api.types.EndpointUri;
 import io.serverlessworkflow.api.types.HTTPArguments;
+import io.serverlessworkflow.api.types.UriTemplate;
 import io.serverlessworkflow.api.types.WithHTTPHeaders;
 import io.serverlessworkflow.api.types.WithHTTPQuery;
 import jakarta.ws.rs.HttpMethod;
@@ -27,40 +30,33 @@ import jakarta.ws.rs.client.ClientBuilder;
 import jakarta.ws.rs.client.Entity;
 import jakarta.ws.rs.client.Invocation.Builder;
 import jakarta.ws.rs.client.WebTarget;
+import java.net.URI;
 import java.util.Map;
 import java.util.Map.Entry;
+import java.util.function.Function;
 
 public class HttpExecutor extends AbstractTaskExecutor<CallHTTP> {
 
   private static final Client client = ClientBuilder.newClient();
 
-  public HttpExecutor(CallHTTP task) {
-    super(task);
+  private final Function<JsonNode, WebTarget> targetSupplier;
+
+  public HttpExecutor(CallHTTP task, ExpressionFactory factory) {
+    super(task, factory);
+    this.targetSupplier = getTargetSupplier(task.getWith().getEndpoint());
   }
 
   @Override
   protected JsonNode internalExecute(JsonNode node) {
     HTTPArguments httpArgs = task.getWith();
-    // missing checks
-    String uri =
-        httpArgs
-            .getEndpoint()
-            .getEndpointConfiguration()
-            .getUri()
-            .getLiteralEndpointURI()
-            .getLiteralUriTemplate();
-    WebTarget target = client.target(uri);
     WithHTTPQuery query = httpArgs.getQuery();
+    WebTarget target = targetSupplier.apply(node);
     if (query != null) {
       for (Entry<String, Object> entry : query.getAdditionalProperties().entrySet()) {
         target = target.queryParam(entry.getKey(), entry.getValue());
       }
     }
-    Builder request =
-        target
-            .resolveTemplates(
-                JsonUtils.mapper().convertValue(node, new TypeReference<Map<String, Object>>() {}))
-            .request();
+    Builder request = target.request();
     WithHTTPHeaders headers = httpArgs.getHeaders();
     if (headers != null) {
       headers.getAdditionalProperties().forEach(request::header);
@@ -73,4 +69,71 @@ public class HttpExecutor extends AbstractTaskExecutor<CallHTTP> {
         return request.post(Entity.json(httpArgs.getBody()), JsonNode.class);
     }
   }
+
+  private Function<JsonNode, WebTarget> getTargetSupplier(Endpoint endpoint) {
+    if (endpoint.getEndpointConfiguration() != null) {
+      EndpointUri uri = endpoint.getEndpointConfiguration().getUri();
+      if (uri.getLiteralEndpointURI() != null) {
+        return getURISupplier(uri.getLiteralEndpointURI());
+      } else if (uri.getExpressionEndpointURI() != null) {
+        return new ExpressionURISupplier(uri.getExpressionEndpointURI());
+      }
+    } else if (endpoint.getRuntimeExpression() != null) {
+      return new ExpressionURISupplier(endpoint.getRuntimeExpression());
+    } else if (endpoint.getUriTemplate() != null) {
+      return getURISupplier(endpoint.getUriTemplate());
+    }
+    throw new IllegalArgumentException("Invalid endpoint definition " + endpoint);
+  }
+
+  private Function<JsonNode, WebTarget> getURISupplier(UriTemplate template) {
+    if (template.getLiteralUri() != null) {
+      return new URISupplier(template.getLiteralUri());
+    } else if (template.getLiteralUriTemplate() != null) {
+      return new URITemplateSupplier(template.getLiteralUriTemplate());
+    }
+    throw new IllegalArgumentException("Invalid uritemplate definition " + template);
+  }
+
+  private class URISupplier implements Function<JsonNode, WebTarget> {
+    private final URI uri;
+
+    public URISupplier(URI uri) {
+      this.uri = uri;
+    }
+
+    @Override
+    public WebTarget apply(JsonNode input) {
+      return client.target(uri);
+    }
+  }
+
+  private class URITemplateSupplier implements Function<JsonNode, WebTarget> {
+    private final String uri;
+
+    public URITemplateSupplier(String uri) {
+      this.uri = uri;
+    }
+
+    @Override
+    public WebTarget apply(JsonNode input) {
+      return client
+          .target(uri)
+          .resolveTemplates(
+              JsonUtils.mapper().convertValue(input, new TypeReference<Map<String, Object>>() {}));
+    }
+  }
+
+  private class ExpressionURISupplier implements Function<JsonNode, WebTarget> {
+    private Expression expr;
+
+    public ExpressionURISupplier(String expr) {
+      this.expr = exprFactory.getExpression(expr);
+    }
+
+    @Override
+    public WebTarget apply(JsonNode input) {
+      return client.target(expr.eval(input).asText());
+    }
+  }
 }
diff --git a/impl/src/main/java/io/serverlessworkflow/impl/WorkflowDefinition.java b/impl/src/main/java/io/serverlessworkflow/impl/WorkflowDefinition.java
index 8c8dad4..f926a75 100644
--- a/impl/src/main/java/io/serverlessworkflow/impl/WorkflowDefinition.java
+++ b/impl/src/main/java/io/serverlessworkflow/impl/WorkflowDefinition.java
@@ -33,22 +33,22 @@ public class WorkflowDefinition {
 
   private WorkflowDefinition(
       Workflow workflow,
-      TaskExecutorFactory factory,
+      TaskExecutorFactory taskFactory,
       Collection<WorkflowExecutionListener> listeners) {
     this.workflow = workflow;
-    this.factory = factory;
+    this.taskFactory = taskFactory;
     this.listeners = listeners;
   }
 
   private final Workflow workflow;
   private final Collection<WorkflowExecutionListener> listeners;
-  private final TaskExecutorFactory factory;
+  private final TaskExecutorFactory taskFactory;
   private final Map<JsonPointer, TaskExecutor<? extends TaskBase>> taskExecutors =
       new ConcurrentHashMap<>();
 
   public static class Builder {
     private final Workflow workflow;
-    private TaskExecutorFactory factory = DefaultTaskExecutorFactory.get();
+    private TaskExecutorFactory taskFactory = DefaultTaskExecutorFactory.get();
     private Collection<WorkflowExecutionListener> listeners;
 
     private Builder(Workflow workflow) {
@@ -64,14 +64,14 @@ public class WorkflowDefinition {
     }
 
     public Builder withTaskExecutorFactory(TaskExecutorFactory factory) {
-      this.factory = factory;
+      this.taskFactory = factory;
       return this;
     }
 
     public WorkflowDefinition build() {
       return new WorkflowDefinition(
           workflow,
-          factory,
+          taskFactory,
           listeners == null
               ? Collections.emptySet()
               : Collections.unmodifiableCollection(listeners));
@@ -83,7 +83,7 @@ public class WorkflowDefinition {
   }
 
   public WorkflowInstance execute(Object input) {
-    return new WorkflowInstance(factory, JsonUtils.fromValue(input));
+    return new WorkflowInstance(taskFactory, JsonUtils.fromValue(input));
   }
 
   enum State {
@@ -97,7 +97,6 @@ public class WorkflowDefinition {
     private final JsonNode input;
     private JsonNode output;
     private State state;
-    private TaskExecutorFactory factory;
 
     private JsonPointer currentPos;
 
@@ -106,7 +105,6 @@ public class WorkflowDefinition {
       this.output = object();
       this.state = State.STARTED;
       this.currentPos = JsonPointer.compile("/");
-      this.factory = factory;
       processDo(workflow.getDo());
     }
 
@@ -119,7 +117,7 @@ public class WorkflowDefinition {
         this.output =
             MergeUtils.merge(
                 taskExecutors
-                    .computeIfAbsent(currentPos, k -> factory.getTaskExecutor(task.getTask()))
+                    .computeIfAbsent(currentPos, k -> taskFactory.getTaskExecutor(task.getTask()))
                     .apply(input),
                 output);
         listeners.forEach(l -> l.onTaskEnded(currentPos, task.getTask()));
diff --git a/impl/src/main/java/io/serverlessworkflow/impl/jq/JQExpression.java b/impl/src/main/java/io/serverlessworkflow/impl/jq/JQExpression.java
new file mode 100644
index 0000000..b77f34a
--- /dev/null
+++ b/impl/src/main/java/io/serverlessworkflow/impl/jq/JQExpression.java
@@ -0,0 +1,251 @@
+/*
+ * Copyright 2020-Present The Serverless Workflow Specification Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * 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.serverlessworkflow.impl.jq;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.node.ArrayNode;
+import io.serverlessworkflow.impl.Expression;
+import io.serverlessworkflow.impl.JsonUtils;
+import java.lang.reflect.Field;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Optional;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.function.Supplier;
+import net.thisptr.jackson.jq.Output;
+import net.thisptr.jackson.jq.Scope;
+import net.thisptr.jackson.jq.Version;
+import net.thisptr.jackson.jq.exception.JsonQueryException;
+import net.thisptr.jackson.jq.internal.javacc.ExpressionParser;
+import net.thisptr.jackson.jq.internal.tree.FunctionCall;
+import net.thisptr.jackson.jq.internal.tree.StringInterpolation;
+import net.thisptr.jackson.jq.internal.tree.binaryop.BinaryOperatorExpression;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class JQExpression implements Expression {
+
+  private static final Logger logger = LoggerFactory.getLogger(JQExpression.class);
+  private final Map<Class<? extends net.thisptr.jackson.jq.Expression>, Collection<Field>>
+      declaredFieldsMap = new ConcurrentHashMap<>();
+  private final Map<Class<? extends net.thisptr.jackson.jq.Expression>, Collection<Field>>
+      allFieldsMap = new ConcurrentHashMap<>();
+
+  private final Supplier<Scope> scope;
+  private final String expr;
+
+  private net.thisptr.jackson.jq.Expression internalExpr;
+  private static Field rhsField;
+
+  static {
+    try {
+      rhsField = BinaryOperatorExpression.class.getDeclaredField("rhs");
+      rhsField.setAccessible(true);
+    } catch (ReflectiveOperationException e) {
+      logger.warn("Unexpected exception while resolving rhs field", e);
+    }
+  }
+
+  public JQExpression(Supplier<Scope> scope, String expr, Version version)
+      throws JsonQueryException {
+    this.expr = expr;
+    this.scope = scope;
+    this.internalExpr = compile(version);
+    checkFunctionCall(internalExpr);
+  }
+
+  private net.thisptr.jackson.jq.Expression compile(Version version) throws JsonQueryException {
+    net.thisptr.jackson.jq.Expression expression;
+    try {
+      expression = ExpressionParser.compile(expr, version);
+    } catch (JsonQueryException ex) {
+      expression = handleStringInterpolation(version).orElseThrow(() -> ex);
+    }
+    checkFunctionCall(expression);
+    return expression;
+  }
+
+  private Optional<net.thisptr.jackson.jq.Expression> handleStringInterpolation(Version version) {
+    if (!expr.startsWith("\"")) {
+      try {
+        net.thisptr.jackson.jq.Expression expression =
+            ExpressionParser.compile("\"" + expr + "\"", version);
+        if (expression instanceof StringInterpolation) {
+          return Optional.of(expression);
+        }
+      } catch (JsonQueryException ex) {
+        // ignoring it
+      }
+    }
+    return Optional.empty();
+  }
+
+  private interface TypedOutput<T> extends Output {
+    T getResult();
+  }
+
+  @SuppressWarnings("unchecked")
+  private <T> TypedOutput<T> output(Class<T> returnClass) {
+    TypedOutput<T> out;
+    if (String.class.isAssignableFrom(returnClass)) {
+      out = (TypedOutput<T>) new StringOutput();
+    } else if (Collection.class.isAssignableFrom(returnClass)) {
+      out = (TypedOutput<T>) new CollectionOutput();
+    } else {
+      out = (TypedOutput<T>) new JsonNodeOutput();
+    }
+    return out;
+  }
+
+  private static class StringOutput implements TypedOutput<String> {
+    StringBuilder sb = new StringBuilder();
+
+    @Override
+    public void emit(JsonNode out) throws JsonQueryException {
+      if (sb.length() > 0) {
+        sb.append(' ');
+      }
+      if (!out.isNull() && out.asText() != null) {
+        sb.append(out.asText());
+      }
+    }
+
+    @Override
+    public String getResult() {
+      return sb.toString();
+    }
+  }
+
+  private static class CollectionOutput implements TypedOutput<Collection<Object>> {
+    Collection<Object> result = new ArrayList<>();
+
+    @SuppressWarnings("unchecked")
+    @Override
+    public void emit(JsonNode out) throws JsonQueryException {
+      Object obj = JsonUtils.toJavaValue(out);
+      if (obj instanceof Collection) result.addAll((Collection<Object>) obj);
+      else {
+        result.add(obj);
+      }
+    }
+
+    @Override
+    public Collection<Object> getResult() {
+      return result;
+    }
+  }
+
+  private static class JsonNodeOutput implements TypedOutput<JsonNode> {
+
+    private JsonNode result;
+    private boolean arrayCreated;
+
+    @Override
+    public void emit(JsonNode out) throws JsonQueryException {
+      if (this.result == null) {
+        this.result = out;
+      } else if (!arrayCreated) {
+        ArrayNode newNode = JsonUtils.mapper().createArrayNode();
+        newNode.add(this.result).add(out);
+        this.result = newNode;
+        arrayCreated = true;
+      } else {
+        ((ArrayNode) this.result).add(out);
+      }
+    }
+
+    @Override
+    public JsonNode getResult() {
+      return result;
+    }
+  }
+
+  @Override
+  public JsonNode eval(JsonNode context) {
+    TypedOutput<JsonNode> output = output(JsonNode.class);
+    try {
+      internalExpr.apply(this.scope.get(), context, output);
+      return output.getResult();
+    } catch (JsonQueryException e) {
+      throw new IllegalArgumentException(
+          "Unable to evaluate content " + context + " using expr " + expr, e);
+    }
+  }
+
+  private void checkFunctionCall(net.thisptr.jackson.jq.Expression toCheck)
+      throws JsonQueryException {
+    if (toCheck instanceof FunctionCall) {
+      toCheck.apply(scope.get(), JsonUtils.mapper().createObjectNode(), out -> {});
+    } else if (toCheck instanceof BinaryOperatorExpression) {
+      if (rhsField != null) {
+        try {
+          checkFunctionCall((net.thisptr.jackson.jq.Expression) rhsField.get(toCheck));
+        } catch (ReflectiveOperationException e) {
+          logger.warn(
+              "Ignoring unexpected error {} while accesing field {} for class{} and expression {}",
+              e.getMessage(),
+              rhsField.getName(),
+              toCheck.getClass(),
+              expr);
+        }
+      }
+    } else if (toCheck != null) {
+      for (Field f : getAllExprFields(toCheck))
+        try {
+          checkFunctionCall((net.thisptr.jackson.jq.Expression) f.get(toCheck));
+        } catch (ReflectiveOperationException e) {
+          logger.warn(
+              "Ignoring unexpected error {} while accesing field {} for class{} and expression {}",
+              e.getMessage(),
+              f.getName(),
+              toCheck.getClass(),
+              expr);
+        }
+    }
+  }
+
+  private Collection<Field> getAllExprFields(net.thisptr.jackson.jq.Expression toCheck) {
+    return allFieldsMap.computeIfAbsent(toCheck.getClass(), this::getAllExprFields);
+  }
+
+  private Collection<Field> getAllExprFields(
+      Class<? extends net.thisptr.jackson.jq.Expression> clazz) {
+    Collection<Field> fields = new HashSet<>();
+    Class<?> currentClass = clazz;
+    do {
+      fields.addAll(
+          declaredFieldsMap.computeIfAbsent(
+              currentClass.asSubclass(net.thisptr.jackson.jq.Expression.class),
+              this::getDeclaredExprFields));
+      currentClass = currentClass.getSuperclass();
+    } while (net.thisptr.jackson.jq.Expression.class.isAssignableFrom(currentClass));
+    return fields;
+  }
+
+  private Collection<Field> getDeclaredExprFields(
+      Class<? extends net.thisptr.jackson.jq.Expression> clazz) {
+    Collection<Field> fields = new HashSet<>();
+    for (Field f : clazz.getDeclaredFields()) {
+      if (net.thisptr.jackson.jq.Expression.class.isAssignableFrom(f.getType())) {
+        f.setAccessible(true);
+        fields.add(f);
+      }
+    }
+    return fields;
+  }
+}
diff --git a/impl/src/main/java/io/serverlessworkflow/impl/jq/JQExpressionFactory.java b/impl/src/main/java/io/serverlessworkflow/impl/jq/JQExpressionFactory.java
new file mode 100644
index 0000000..787842d
--- /dev/null
+++ b/impl/src/main/java/io/serverlessworkflow/impl/jq/JQExpressionFactory.java
@@ -0,0 +1,63 @@
+/*
+ * Copyright 2020-Present The Serverless Workflow Specification Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * 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.serverlessworkflow.impl.jq;
+
+import io.serverlessworkflow.impl.Expression;
+import io.serverlessworkflow.impl.ExpressionFactory;
+import io.serverlessworkflow.impl.ExpressionUtils;
+import java.util.function.Supplier;
+import net.thisptr.jackson.jq.BuiltinFunctionLoader;
+import net.thisptr.jackson.jq.Scope;
+import net.thisptr.jackson.jq.Versions;
+import net.thisptr.jackson.jq.exception.JsonQueryException;
+
+public class JQExpressionFactory implements ExpressionFactory {
+
+  private JQExpressionFactory() {}
+
+  private static final JQExpressionFactory instance = new JQExpressionFactory();
+
+  public static JQExpressionFactory get() {
+    return instance;
+  }
+
+  private static Supplier<Scope> scopeSupplier = new DefaultScopeSupplier();
+
+  private static class DefaultScopeSupplier implements Supplier<Scope> {
+    private static class DefaultScope {
+      private static Scope scope;
+
+      static {
+        scope = Scope.newEmptyScope();
+        BuiltinFunctionLoader.getInstance().loadFunctions(Versions.JQ_1_6, scope);
+      }
+    }
+
+    @Override
+    public Scope get() {
+      return DefaultScope.scope;
+    }
+  }
+
+  @Override
+  public Expression getExpression(String expression) {
+    try {
+      return new JQExpression(scopeSupplier, ExpressionUtils.trimExpr(expression), Versions.JQ_1_6);
+    } catch (JsonQueryException e) {
+      throw new IllegalArgumentException(e);
+    }
+  }
+}


Test patch:
diff --git a/impl/src/test/java/io/serverlessworkflow/impl/WorkflowDefinitionTest.java b/impl/src/test/java/io/serverlessworkflow/impl/WorkflowDefinitionTest.java
index f5feb51..66ef5d8 100644
--- a/impl/src/test/java/io/serverlessworkflow/impl/WorkflowDefinitionTest.java
+++ b/impl/src/test/java/io/serverlessworkflow/impl/WorkflowDefinitionTest.java
@@ -41,11 +41,12 @@ public class WorkflowDefinitionTest {
   }
 
   private static Stream<Arguments> provideParameters() {
+    Map<String, Object> petInput = Map.of("petId", 10);
+    Condition<Object> petCondition =
+        new Condition<>(
+            o -> ((Map<String, Object>) o).containsKey("photoUrls"), "callHttpCondition");
     return Stream.of(
-        Arguments.of(
-            "callHttp.yaml",
-            Map.of("petId", 1),
-            new Condition<>(
-                o -> ((Map<String, Object>) o).containsKey("photoUrls"), "callHttpCondition")));
+        Arguments.of("callHttp.yaml", petInput, petCondition),
+        Arguments.of("call-http-endpoint-interpolation.yaml", petInput, petCondition));
   }
 }
diff --git a/impl/src/test/resources/call-http-endpoint-interpolation.yaml b/impl/src/test/resources/call-http-endpoint-interpolation.yaml
new file mode 100644
index 0000000..8380a9a
--- /dev/null
+++ b/impl/src/test/resources/call-http-endpoint-interpolation.yaml
@@ -0,0 +1,13 @@
+document:
+  dsl: '1.0.0-alpha5'
+  namespace: examples
+  name: call-http-shorthand-endpoint
+  version: '0.1.0'
+do:
+  - getPet:
+      call: http
+      with:
+        headers:
+          content-type: application/json
+        method: get
+        endpoint: ${ "https://petstore.swagger.io/v2/pet/\(.petId)" }
\ No newline at end of file