Skip to content

Commit a0521b7

Browse files
committed
Add initial openapi module
Signed-off-by: Matheus Cruz <[email protected]>
1 parent 2a50dc4 commit a0521b7

File tree

9 files changed

+1382
-3
lines changed

9 files changed

+1382
-3
lines changed

impl/core/src/main/java/io/serverlessworkflow/impl/WorkflowUtils.java

+14
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
import io.serverlessworkflow.impl.json.JsonUtils;
3131
import io.serverlessworkflow.impl.jsonschema.SchemaValidator;
3232
import io.serverlessworkflow.impl.jsonschema.SchemaValidatorFactory;
33+
import io.serverlessworkflow.impl.resources.ClasspathResource;
3334
import io.serverlessworkflow.impl.resources.ResourceLoader;
3435
import io.serverlessworkflow.impl.resources.StaticResource;
3536
import java.io.IOException;
@@ -67,6 +68,19 @@ private static Optional<JsonNode> schemaToNode(
6768
return Optional.empty();
6869
}
6970

71+
public static Optional<JsonNode> classpathResourceToNode(String resource) {
72+
if (resource != null && !resource.isEmpty()) {
73+
ClasspathResource classpathResource = new ClasspathResource(resource);
74+
ObjectMapper mapper = WorkflowFormat.fromFileName(resource).mapper();
75+
try (InputStream in = classpathResource.open()) {
76+
return Optional.of(mapper.readTree(in));
77+
} catch (IOException io) {
78+
throw new UncheckedIOException(io);
79+
}
80+
}
81+
return Optional.empty();
82+
}
83+
7084
public static Optional<WorkflowFilter> buildWorkflowFilter(
7185
ExpressionFactory exprFactory, InputFrom from) {
7286
return from != null

impl/openapi/pom.xml

+43
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
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>7.0.0-SNAPSHOT</version>
7+
</parent>
8+
<artifactId>serverlessworkflow-impl-openapi</artifactId>
9+
<dependencies>
10+
<dependency>
11+
<groupId>org.glassfish.jersey.core</groupId>
12+
<artifactId>jersey-client</artifactId>
13+
</dependency>
14+
<dependency>
15+
<groupId>org.glassfish.jersey.media</groupId>
16+
<artifactId>jersey-media-json-jackson</artifactId>
17+
</dependency>
18+
<dependency>
19+
<groupId>io.serverlessworkflow</groupId>
20+
<artifactId>serverlessworkflow-impl-core</artifactId>
21+
</dependency>
22+
<dependency>
23+
<groupId>org.junit.jupiter</groupId>
24+
<artifactId>junit-jupiter-api</artifactId>
25+
<scope>test</scope>
26+
</dependency>
27+
<dependency>
28+
<groupId>org.junit.jupiter</groupId>
29+
<artifactId>junit-jupiter-engine</artifactId>
30+
<scope>test</scope>
31+
</dependency>
32+
<dependency>
33+
<groupId>org.junit.jupiter</groupId>
34+
<artifactId>junit-jupiter-params</artifactId>
35+
<scope>test</scope>
36+
</dependency>
37+
<dependency>
38+
<groupId>org.assertj</groupId>
39+
<artifactId>assertj-core</artifactId>
40+
<scope>test</scope>
41+
</dependency>
42+
</dependencies>
43+
</project>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
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 com.fasterxml.jackson.core.type.TypeReference;
19+
import com.fasterxml.jackson.databind.JsonNode;
20+
import io.serverlessworkflow.api.types.CallOpenAPI;
21+
import io.serverlessworkflow.api.types.Endpoint;
22+
import io.serverlessworkflow.api.types.EndpointUri;
23+
import io.serverlessworkflow.api.types.OpenAPIArguments;
24+
import io.serverlessworkflow.api.types.TaskBase;
25+
import io.serverlessworkflow.api.types.UriTemplate;
26+
import io.serverlessworkflow.impl.TaskContext;
27+
import io.serverlessworkflow.impl.WorkflowContext;
28+
import io.serverlessworkflow.impl.WorkflowDefinition;
29+
import io.serverlessworkflow.impl.WorkflowError;
30+
import io.serverlessworkflow.impl.WorkflowException;
31+
import io.serverlessworkflow.impl.WorkflowUtils;
32+
import io.serverlessworkflow.impl.expressions.Expression;
33+
import io.serverlessworkflow.impl.expressions.ExpressionFactory;
34+
import io.serverlessworkflow.impl.json.JsonUtils;
35+
import jakarta.ws.rs.client.Client;
36+
import jakarta.ws.rs.client.ClientBuilder;
37+
import jakarta.ws.rs.client.WebTarget;
38+
import java.util.Map;
39+
import java.util.Optional;
40+
41+
public class OpenAPIExecutor implements CallableTask<CallOpenAPI> {
42+
private static final Client client = ClientBuilder.newClient();
43+
private TargetSupplier targetSupplier;
44+
45+
@FunctionalInterface
46+
private interface TargetSupplier {
47+
WebTarget apply(WorkflowContext workflow, TaskContext<?> task, JsonNode node);
48+
}
49+
50+
@Override
51+
public void init(CallOpenAPI task, WorkflowDefinition definition) {
52+
OpenAPIArguments args = task.getWith();
53+
this.targetSupplier = getTargetSupplier(args, definition.expressionFactory());
54+
}
55+
56+
@Override
57+
public JsonNode apply(
58+
WorkflowContext workflowContext, TaskContext<CallOpenAPI> taskContext, JsonNode input) {
59+
60+
WebTarget target = this.targetSupplier.apply(workflowContext, taskContext, input);
61+
62+
System.out.println("target: " + target.getUri());
63+
64+
return input;
65+
}
66+
67+
@Override
68+
public boolean accept(Class<? extends TaskBase> clazz) {
69+
return clazz.isAssignableFrom(CallOpenAPI.class);
70+
}
71+
72+
private static TargetSupplier getURISupplier(UriTemplate template, String operationId) {
73+
if (template.getLiteralUri() != null) {
74+
75+
Optional<JsonNode> jsonNode =
76+
WorkflowUtils.classpathResourceToNode(template.getLiteralUri().toString());
77+
78+
if (jsonNode.isEmpty()) {
79+
throw new IllegalArgumentException(
80+
"Invalid OpenAPI specification " + template.getLiteralUri().toString());
81+
}
82+
83+
String host = OpenAPIReader.getHost(jsonNode.get());
84+
85+
Optional<JsonNode> possibleOperation =
86+
OpenAPIReader.readOperation(jsonNode.get(), operationId);
87+
88+
if (possibleOperation.isEmpty()) {
89+
throw new WorkflowException(
90+
WorkflowError.error(WorkflowError.RUNTIME_TYPE, 400)
91+
.title("Invalid OpenAPI Specification")
92+
.details("There is no operation ID " + operationId)
93+
.build());
94+
}
95+
96+
return (w, t, n) -> client.target(host);
97+
} else if (template.getLiteralUriTemplate() != null) {
98+
return (w, t, n) ->
99+
client
100+
.target(template.getLiteralUriTemplate())
101+
.resolveTemplates(
102+
JsonUtils.mapper().convertValue(n, new TypeReference<Map<String, Object>>() {}));
103+
}
104+
throw new IllegalArgumentException("Invalid uritemplate definition " + template);
105+
}
106+
107+
private static class ExpressionURISupplier implements TargetSupplier {
108+
private Expression expr;
109+
110+
public ExpressionURISupplier(Expression expr) {
111+
this.expr = expr;
112+
}
113+
114+
@Override
115+
public WebTarget apply(WorkflowContext workflow, TaskContext<?> task, JsonNode node) {
116+
return client.target(expr.eval(workflow, task, node).asText());
117+
}
118+
}
119+
120+
private static TargetSupplier getTargetSupplier(
121+
OpenAPIArguments args, ExpressionFactory expressionFactory) {
122+
123+
Endpoint endpoint = args.getDocument().getEndpoint();
124+
String operationId = args.getOperationId();
125+
126+
if (endpoint.getEndpointConfiguration() != null) {
127+
EndpointUri uri = endpoint.getEndpointConfiguration().getUri();
128+
if (uri.getLiteralEndpointURI() != null) {
129+
return getURISupplier(uri.getLiteralEndpointURI(), operationId);
130+
} else if (uri.getExpressionEndpointURI() != null) {
131+
return new ExpressionURISupplier(
132+
expressionFactory.getExpression(uri.getExpressionEndpointURI()));
133+
}
134+
} else if (endpoint.getRuntimeExpression() != null) {
135+
return new ExpressionURISupplier(
136+
expressionFactory.getExpression(endpoint.getRuntimeExpression()));
137+
} else if (endpoint.getUriTemplate() != null) {
138+
return getURISupplier(endpoint.getUriTemplate(), operationId);
139+
}
140+
throw new IllegalArgumentException("Invalid endpoint definition " + endpoint);
141+
}
142+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
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 com.fasterxml.jackson.databind.JsonNode;
19+
import com.fasterxml.jackson.databind.node.ArrayNode;
20+
import java.util.Map;
21+
import java.util.Optional;
22+
import java.util.Set;
23+
24+
public class OpenAPIReader {
25+
26+
private static final String HTTPS = "https";
27+
private static final String HTTP = "http";
28+
private static final String DEFAULT_SCHEME = HTTPS;
29+
private static final Set<String> ALLOWED_SCHEMES = Set.of(HTTPS, HTTP);
30+
31+
public static String getHost(JsonNode jsonNode) {
32+
JsonNode host = jsonNode.get("host");
33+
if (host == null) {
34+
return null;
35+
}
36+
String scheme = getScheme(jsonNode);
37+
return scheme + "://" + host.asText();
38+
}
39+
40+
private static String getScheme(JsonNode jsonNode) {
41+
ArrayNode array = jsonNode.withArrayProperty("schemes");
42+
if (array != null && !array.isEmpty()) {
43+
String firstScheme = array.get(0).asText();
44+
return ALLOWED_SCHEMES.contains(firstScheme) ? firstScheme : DEFAULT_SCHEME;
45+
}
46+
return DEFAULT_SCHEME;
47+
}
48+
49+
public static Optional<JsonNode> readOperation(JsonNode jsonNode, String operationId) {
50+
JsonNode paths = jsonNode.get("paths");
51+
for (Map.Entry<String, JsonNode> entry : paths.properties()) {
52+
for (Map.Entry<String, JsonNode> httpMethod : entry.getValue().properties()) {
53+
if (httpMethod.getValue().get("operationId").asText().equals(operationId)) {
54+
return Optional.ofNullable(httpMethod.getValue());
55+
}
56+
}
57+
}
58+
return Optional.empty();
59+
}
60+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
io.serverlessworkflow.impl.executors.OpenAPIExecutor
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
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;
17+
18+
import static io.serverlessworkflow.api.WorkflowReader.readWorkflowFromClasspath;
19+
import static org.assertj.core.api.AssertionsForClassTypes.assertThat;
20+
21+
import com.fasterxml.jackson.databind.JsonNode;
22+
import java.io.IOException;
23+
import java.util.Map;
24+
import org.junit.jupiter.api.BeforeAll;
25+
import org.junit.jupiter.api.Test;
26+
27+
public class OpenAPIWorkflowDefinitionTest {
28+
29+
private static WorkflowApplication app;
30+
31+
@BeforeAll
32+
static void init() {
33+
app = WorkflowApplication.builder().build();
34+
}
35+
36+
@Test
37+
void testWorkflowExecution() throws IOException {
38+
Object output =
39+
app.workflowDefinition(readWorkflowFromClasspath("findPetsByStatus.yaml"))
40+
.execute(Map.of("status", "sold"))
41+
.outputAsJsonNode();
42+
assertThat(output)
43+
.isInstanceOf(JsonNode.class)
44+
.satisfies(
45+
(obj) -> {
46+
JsonNode json = (JsonNode) obj;
47+
assertThat(json.get("status").asText()).isEqualTo("sold");
48+
});
49+
}
50+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
document:
2+
dsl: '1.0.0-alpha5'
3+
namespace: test
4+
name: openapi-example
5+
version: '0.1.0'
6+
do:
7+
- findPet:
8+
call: openapi
9+
with:
10+
document:
11+
endpoint: pets.json
12+
operationId: findPetsByStatus
13+
parameters:
14+
status: available

0 commit comments

Comments
 (0)