Skip to content

Commit e825fd0

Browse files
committed
Introduce Run.script task with JavaScript
Signed-off-by: Matheus Cruz <matheuscruz.dev@gmail.com>
1 parent 8a44a70 commit e825fd0

File tree

18 files changed

+819
-1
lines changed

18 files changed

+819
-1
lines changed
Lines changed: 282 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,282 @@
1+
/*
2+
* Copyright 2020-Present The Serverless Workflow Specification Authors
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package io.serverlessworkflow.impl.executors;
17+
18+
import io.serverlessworkflow.api.types.RunScript;
19+
import io.serverlessworkflow.api.types.RunTaskConfiguration;
20+
import io.serverlessworkflow.api.types.Script;
21+
import io.serverlessworkflow.api.types.ScriptUnion;
22+
import io.serverlessworkflow.impl.TaskContext;
23+
import io.serverlessworkflow.impl.WorkflowApplication;
24+
import io.serverlessworkflow.impl.WorkflowContext;
25+
import io.serverlessworkflow.impl.WorkflowDefinition;
26+
import io.serverlessworkflow.impl.WorkflowError;
27+
import io.serverlessworkflow.impl.WorkflowException;
28+
import io.serverlessworkflow.impl.WorkflowModel;
29+
import io.serverlessworkflow.impl.WorkflowUtils;
30+
import io.serverlessworkflow.impl.WorkflowValueResolver;
31+
import io.serverlessworkflow.impl.resources.ResourceLoaderUtils;
32+
import java.util.Arrays;
33+
import java.util.HashMap;
34+
import java.util.Map;
35+
import java.util.ServiceLoader;
36+
import java.util.concurrent.CompletableFuture;
37+
38+
public class RunScriptExecutor implements RunnableTask<RunScript> {
39+
40+
public enum ScriptLanguage {
41+
JS("js"),
42+
PYTHON("python");
43+
44+
private final String lang;
45+
46+
ScriptLanguage(String lang) {
47+
this.lang = lang;
48+
}
49+
50+
public String getLang() {
51+
return lang;
52+
}
53+
54+
public static boolean isSupported(String lang) {
55+
for (ScriptLanguage l : ScriptLanguage.values()) {
56+
if (l.getLang().equalsIgnoreCase(lang)) {
57+
return true;
58+
}
59+
}
60+
return false;
61+
}
62+
}
63+
64+
@FunctionalInterface
65+
private interface CodeSupplier {
66+
String apply(WorkflowContext workflowContext, TaskContext taskContext);
67+
}
68+
69+
@SuppressWarnings("rawtypes")
70+
private Map<String, WorkflowValueResolver> environmentExpr;
71+
@SuppressWarnings("rawtypes")
72+
private Map<String, WorkflowValueResolver> argumentExpr;
73+
private CodeSupplier codeSupplier;
74+
private boolean isAwait;
75+
private RunTaskConfiguration.ProcessReturnType returnType;
76+
private ScriptTaskRunner taskRunner;
77+
78+
@Override
79+
public void init(RunScript taskConfiguration, WorkflowDefinition definition) {
80+
ScriptUnion scriptUnion = taskConfiguration.getScript();
81+
Script script = scriptUnion.get();
82+
String language = scriptUnion.get().getLanguage();
83+
84+
WorkflowApplication application = definition.application();
85+
if (language == null || !ScriptLanguage.isSupported(language)) {
86+
throw new IllegalArgumentException(
87+
"Unsupported script language: "
88+
+ language
89+
+ ". Supported languages are: "
90+
+ Arrays.toString(
91+
Arrays.stream(ScriptLanguage.values()).map(ScriptLanguage::getLang).toArray()));
92+
}
93+
94+
this.taskRunner =
95+
ServiceLoader.load(ScriptTaskRunner.class)
96+
.findFirst()
97+
.orElseThrow(
98+
() -> new IllegalStateException("No implementation found for ScriptTaskRunner"));
99+
100+
this.isAwait = taskConfiguration.isAwait();
101+
102+
this.returnType = taskConfiguration.getReturn();
103+
104+
if (script.getEnvironment() != null
105+
&& script.getEnvironment().getAdditionalProperties() != null) {
106+
this.environmentExpr =
107+
buildMapResolvers(application, script.getEnvironment().getAdditionalProperties());
108+
} else {
109+
this.environmentExpr = Map.of();
110+
}
111+
112+
if (script.getArguments() != null && script.getArguments().getAdditionalProperties() != null) {
113+
this.argumentExpr =
114+
buildMapResolvers(application, script.getArguments().getAdditionalProperties());
115+
} else {
116+
this.argumentExpr = Map.of();
117+
}
118+
119+
this.codeSupplier =
120+
(workflowContext, taskContext) -> {
121+
if (scriptUnion.getInlineScript() != null) {
122+
return scriptUnion.getInlineScript().getCode();
123+
} else if (scriptUnion.getExternalScript() == null) {
124+
throw new WorkflowException(
125+
WorkflowError.runtime(
126+
taskContext, new IllegalStateException("No script source defined."))
127+
.build());
128+
} else {
129+
return definition
130+
.resourceLoader()
131+
.load(
132+
scriptUnion.getExternalScript().getSource(),
133+
ResourceLoaderUtils::readString,
134+
workflowContext,
135+
taskContext,
136+
taskContext.input());
137+
}
138+
};
139+
}
140+
141+
@Override
142+
public CompletableFuture<WorkflowModel> apply(
143+
WorkflowContext workflowContext, TaskContext taskContext, WorkflowModel input) {
144+
145+
RunScriptContext.RunScriptContextBuilder builder =
146+
new RunScriptContext.RunScriptContextBuilder();
147+
148+
Map<String, String> envs = new HashMap<>();
149+
this.environmentExpr.forEach(
150+
(k, v) -> {
151+
Object resolved = v.apply(workflowContext, taskContext, input);
152+
envs.put(k, resolved.toString());
153+
});
154+
155+
Map<String, Object> args = new HashMap<>();
156+
this.argumentExpr.forEach(
157+
(k, v) -> {
158+
Object resolved = v.apply(workflowContext, taskContext, input);
159+
args.put(k, resolved);
160+
});
161+
162+
String code = this.codeSupplier.apply(workflowContext, taskContext);
163+
164+
RunScriptContext ctx =
165+
builder
166+
.withApplication(workflowContext.definition().application())
167+
.withReturnType(returnType)
168+
.withCode(code)
169+
.withArguments(args)
170+
.withEnvironment(envs)
171+
.withAwait(isAwait)
172+
.build();
173+
174+
return CompletableFuture.supplyAsync(() -> taskRunner.buildRun(taskContext).apply(ctx, input));
175+
}
176+
177+
@Override
178+
public boolean accept(Class<? extends RunTaskConfiguration> clazz) {
179+
return RunScript.class.equals(clazz);
180+
}
181+
182+
/** Builds a map of WorkflowValueResolvers from the provided properties. */
183+
@SuppressWarnings("rawtypes")
184+
private Map<String, WorkflowValueResolver> buildMapResolvers(
185+
WorkflowApplication application, Map<String, Object> properties) {
186+
Map<String, WorkflowValueResolver> resolvers = new HashMap<>();
187+
if (properties != null) {
188+
for (Map.Entry<String, Object> entry : properties.entrySet()) {
189+
WorkflowValueResolver<String> valueResolver =
190+
WorkflowUtils.buildStringFilter(application, entry.getValue().toString());
191+
resolvers.put(entry.getKey(), valueResolver);
192+
}
193+
}
194+
return resolvers;
195+
}
196+
197+
public static class RunScriptContext {
198+
private final WorkflowApplication application;
199+
private final Map<String, Object> args;
200+
private final Map<String, String> envs;
201+
private final String code;
202+
private final boolean isAwait;
203+
private final RunTaskConfiguration.ProcessReturnType returnType;
204+
205+
public RunScriptContext(RunScriptContextBuilder builder) {
206+
this.application = builder.application;
207+
this.args = builder.args;
208+
this.envs = builder.envs;
209+
this.code = builder.code;
210+
this.isAwait = builder.awaiting;
211+
this.returnType = builder.returnType;
212+
}
213+
214+
public Map<String, Object> getArgs() {
215+
return args;
216+
}
217+
218+
public Map<String, String> getEnvs() {
219+
return envs;
220+
}
221+
222+
public String getCode() {
223+
return code;
224+
}
225+
226+
public boolean isAwait() {
227+
return isAwait;
228+
}
229+
230+
public WorkflowApplication getApplication() {
231+
return application;
232+
}
233+
234+
public RunTaskConfiguration.ProcessReturnType getReturnType() {
235+
return returnType;
236+
}
237+
238+
public static class RunScriptContextBuilder {
239+
private Map<String, Object> args;
240+
private Map<String, String> envs;
241+
private String code;
242+
private boolean awaiting;
243+
private WorkflowApplication application;
244+
private RunTaskConfiguration.ProcessReturnType returnType;
245+
246+
public RunScriptContextBuilder withArguments(Map<String, Object> args) {
247+
this.args = args;
248+
return this;
249+
}
250+
251+
public RunScriptContextBuilder withEnvironment(Map<String, String> envs) {
252+
this.envs = envs;
253+
return this;
254+
}
255+
256+
public RunScriptContextBuilder withCode(String code) {
257+
this.code = code;
258+
return this;
259+
}
260+
261+
public RunScriptContextBuilder withAwait(boolean awaiting) {
262+
this.awaiting = awaiting;
263+
return this;
264+
}
265+
266+
public RunScriptContextBuilder withApplication(WorkflowApplication application) {
267+
this.application = application;
268+
return this;
269+
}
270+
271+
public RunScriptContextBuilder withReturnType(
272+
RunTaskConfiguration.ProcessReturnType returnType) {
273+
this.returnType = returnType;
274+
return this;
275+
}
276+
277+
public RunScriptContext build() {
278+
return new RunScriptContext(this);
279+
}
280+
}
281+
}
282+
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
/*
2+
* Copyright 2020-Present The Serverless Workflow Specification Authors
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package io.serverlessworkflow.impl.executors;
17+
18+
import io.serverlessworkflow.impl.TaskContext;
19+
import io.serverlessworkflow.impl.WorkflowModel;
20+
import java.util.function.BiFunction;
21+
22+
/** Represents a script task that executes a script in a specific scripting language. */
23+
public interface ScriptTaskRunner {
24+
25+
/**
26+
* The scripting language supported by this script task runner.
27+
*
28+
* @return the scripting language as {@link RunScriptExecutor.ScriptLanguage} enum.
29+
*/
30+
RunScriptExecutor.ScriptLanguage forLanguage();
31+
32+
/**
33+
* Returns a function that executes the script task.
34+
*
35+
* @param taskContext the task context for the script task.
36+
* @return a @{@link BiFunction}} that takes a RunScriptContext and a WorkflowModel as input and
37+
* returns a WorkflowModel as output.
38+
*/
39+
BiFunction<RunScriptExecutor.RunScriptContext, WorkflowModel, WorkflowModel> buildRun(
40+
TaskContext taskContext);
41+
}
Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
io.serverlessworkflow.impl.executors.RunWorkflowExecutor
2-
io.serverlessworkflow.impl.executors.RunShellExecutor
2+
io.serverlessworkflow.impl.executors.RunShellExecutor
3+
io.serverlessworkflow.impl.executors.RunScriptExecutor

impl/pom.xml

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
<version.jakarta.ws.rs>4.0.0</version.jakarta.ws.rs>
1515
<version.net.thisptr>1.6.0</version.net.thisptr>
1616
<version.org.glassfish.jersey>3.1.11</version.org.glassfish.jersey>
17+
<version.org.graalvm.plyglot>23.1.1</version.org.graalvm.plyglot>
1718
</properties>
1819
<dependencyManagement>
1920
<dependencies>
@@ -92,6 +93,22 @@
9293
<artifactId>serverlessworkflow-impl-openapi</artifactId>
9394
<version>${project.version}</version>
9495
</dependency>
96+
<dependency>
97+
<groupId>io.serverlessworkflow</groupId>
98+
<artifactId>serverlessworkflow-impl-script-js</artifactId>
99+
<version>${project.version}</version>
100+
</dependency>
101+
<dependency>
102+
<groupId>org.graalvm.polyglot</groupId>
103+
<artifactId>js</artifactId>
104+
<version>${version.org.graalvm.plyglot}</version>
105+
<type>pom</type>
106+
</dependency>
107+
<dependency>
108+
<groupId>org.graalvm.polyglot</groupId>
109+
<artifactId>polyglot</artifactId>
110+
<version>${version.org.graalvm.plyglot}</version>
111+
</dependency>
95112
<dependency>
96113
<groupId>net.thisptr</groupId>
97114
<artifactId>jackson-jq</artifactId>
@@ -140,5 +157,6 @@
140157
<module>lifecycleevent</module>
141158
<module>validation</module>
142159
<module>test</module>
160+
<module>script-js</module>
143161
</modules>
144162
</project>

impl/script-js/pom.xml

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
2+
<modelVersion>4.0.0</modelVersion>
3+
<parent>
4+
<groupId>io.serverlessworkflow</groupId>
5+
<artifactId>serverlessworkflow-impl</artifactId>
6+
<version>8.0.0-SNAPSHOT</version>
7+
</parent>
8+
<artifactId>serverlessworkflow-impl-script-js</artifactId>
9+
<name>Serverless Workflow :: Impl :: Script JavaScript</name>
10+
<dependencies>
11+
<dependency>
12+
<groupId>io.serverlessworkflow</groupId>
13+
<artifactId>serverlessworkflow-impl-core</artifactId>
14+
</dependency>
15+
<dependency>
16+
<groupId>org.graalvm.polyglot</groupId>
17+
<artifactId>polyglot</artifactId>
18+
</dependency>
19+
<dependency>
20+
<groupId>org.graalvm.polyglot</groupId>
21+
<artifactId>js</artifactId>
22+
<type>pom</type>
23+
</dependency>
24+
</dependencies>
25+
</project>

0 commit comments

Comments
 (0)