Skip to content

Commit 50aedb5

Browse files
committed
Added OpenAPI call task support
Signed-off-by: Dmitrii Tikhomirov <chani.liet@gmail.com>
1 parent 3f6a59d commit 50aedb5

File tree

9 files changed

+665
-2
lines changed

9 files changed

+665
-2
lines changed

impl/openapi/pom.xml

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
2+
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
3+
<modelVersion>4.0.0</modelVersion>
4+
<parent>
5+
<groupId>io.serverlessworkflow</groupId>
6+
<artifactId>serverlessworkflow-impl</artifactId>
7+
<version>8.0.0-SNAPSHOT</version>
8+
</parent>
9+
<artifactId>serverlessworkflow-impl-openapi</artifactId>
10+
<name>Serverless Workflow :: Impl :: OpenAPI</name>
11+
<dependencies>
12+
<dependency>
13+
<groupId>jakarta.ws.rs</groupId>
14+
<artifactId>jakarta.ws.rs-api</artifactId>
15+
</dependency>
16+
<dependency>
17+
<groupId>io.serverlessworkflow</groupId>
18+
<artifactId>serverlessworkflow-impl-core</artifactId>
19+
</dependency>
20+
<dependency>
21+
<groupId>io.serverlessworkflow</groupId>
22+
<artifactId>serverlessworkflow-impl-http</artifactId>
23+
</dependency>
24+
<dependency>
25+
<groupId>io.swagger.parser.v3</groupId>
26+
<artifactId>swagger-parser</artifactId>
27+
<version>${version.io.swagger.parser.v3}</version>
28+
</dependency>
29+
</dependencies>
30+
</project>
Lines changed: 195 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,195 @@
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.openapi;
17+
18+
import io.serverlessworkflow.api.types.CallHTTP;
19+
import io.serverlessworkflow.api.types.Endpoint;
20+
import io.serverlessworkflow.api.types.EndpointConfiguration;
21+
import io.serverlessworkflow.api.types.HTTPArguments;
22+
import io.serverlessworkflow.api.types.HTTPHeaders;
23+
import io.serverlessworkflow.api.types.HTTPQuery;
24+
import io.serverlessworkflow.api.types.Headers;
25+
import io.serverlessworkflow.api.types.Query;
26+
import io.serverlessworkflow.api.types.ReferenceableAuthenticationPolicy;
27+
import io.serverlessworkflow.api.types.UriTemplate;
28+
import io.swagger.v3.oas.models.media.Schema;
29+
import io.swagger.v3.oas.models.parameters.Parameter;
30+
import java.net.URI;
31+
import java.util.Collection;
32+
import java.util.LinkedHashMap;
33+
import java.util.Map;
34+
35+
@SuppressWarnings("rawtypes")
36+
public class HttpCallAdapter {
37+
38+
private ReferenceableAuthenticationPolicy auth;
39+
private Map<String, Schema> body;
40+
private String contentType;
41+
private Collection<Parameter> headers;
42+
private String method;
43+
private Collection<Parameter> query;
44+
private boolean redirect;
45+
private URI server;
46+
private URI target;
47+
private Map<String, Object> workflowParams;
48+
49+
public HttpCallAdapter auth(ReferenceableAuthenticationPolicy policy) {
50+
if (policy != null) {
51+
this.auth = policy;
52+
}
53+
return this;
54+
}
55+
56+
public HttpCallAdapter body(Map<String, Schema> body) {
57+
this.body = body;
58+
return this;
59+
}
60+
61+
public CallHTTP build() {
62+
CallHTTP callHTTP = new CallHTTP();
63+
64+
HTTPArguments httpArgs = new HTTPArguments();
65+
callHTTP.withWith(httpArgs);
66+
67+
Endpoint endpoint = new Endpoint();
68+
httpArgs.withEndpoint(endpoint);
69+
70+
if (this.auth != null) {
71+
EndpointConfiguration endPointConfig = new EndpointConfiguration();
72+
endPointConfig.setAuthentication(this.auth);
73+
endpoint.setEndpointConfiguration(endPointConfig);
74+
}
75+
76+
httpArgs.setRedirect(this.redirect);
77+
httpArgs.setMethod(this.method);
78+
79+
addHttpHeaders(httpArgs);
80+
addQueryParams(httpArgs);
81+
addBody(httpArgs);
82+
83+
addTarget(endpoint);
84+
85+
return callHTTP;
86+
}
87+
88+
public HttpCallAdapter contentType(String contentType) {
89+
this.contentType = contentType;
90+
return this;
91+
}
92+
93+
public HttpCallAdapter headers(
94+
Collection<io.swagger.v3.oas.models.parameters.Parameter> headers) {
95+
this.headers = headers;
96+
return this;
97+
}
98+
99+
public HttpCallAdapter method(String method) {
100+
this.method = method;
101+
return this;
102+
}
103+
104+
public HttpCallAdapter query(Collection<io.swagger.v3.oas.models.parameters.Parameter> query) {
105+
this.query = query;
106+
return this;
107+
}
108+
109+
public HttpCallAdapter redirect(boolean redirect) {
110+
this.redirect = redirect;
111+
return this;
112+
}
113+
114+
public HttpCallAdapter server(String server) {
115+
this.server = URI.create(server);
116+
return this;
117+
}
118+
119+
public HttpCallAdapter target(URI target) {
120+
this.target = target;
121+
return this;
122+
}
123+
124+
public HttpCallAdapter workflowParams(Map<String, Object> workflowParams) {
125+
this.workflowParams = workflowParams;
126+
return this;
127+
}
128+
129+
private void addBody(HTTPArguments httpArgs) {
130+
Map<String, Object> bodyContent = new LinkedHashMap<>();
131+
if (!(body == null || body.isEmpty())) {
132+
for (Map.Entry<String, Schema> entry : body.entrySet()) {
133+
String name = entry.getKey();
134+
if (workflowParams.containsKey(name)) {
135+
Object value = workflowParams.get(name);
136+
bodyContent.put(name, value);
137+
}
138+
}
139+
if (!bodyContent.isEmpty()) {
140+
httpArgs.setBody(bodyContent);
141+
}
142+
}
143+
}
144+
145+
private void addHttpHeaders(HTTPArguments httpArgs) {
146+
if (!(headers == null || headers.isEmpty())) {
147+
Headers hdrs = new Headers();
148+
HTTPHeaders httpHeaders = new HTTPHeaders();
149+
hdrs.setHTTPHeaders(httpHeaders);
150+
httpArgs.setHeaders(hdrs);
151+
152+
for (Parameter p : headers) {
153+
String name = p.getName();
154+
if (workflowParams.containsKey(name)) {
155+
Object value = workflowParams.get(name);
156+
if (value instanceof String asString) {
157+
httpHeaders.setAdditionalProperty(name, asString);
158+
} else {
159+
throw new IllegalArgumentException("Header parameter " + name + " must be a String");
160+
}
161+
}
162+
}
163+
}
164+
}
165+
166+
private void addQueryParams(HTTPArguments httpArgs) {
167+
if (!(query == null || query.isEmpty())) {
168+
Query queryParams = new Query();
169+
httpArgs.setQuery(queryParams);
170+
HTTPQuery httpQuery = new HTTPQuery();
171+
queryParams.setHTTPQuery(httpQuery);
172+
173+
for (Parameter p : query) {
174+
String name = p.getName();
175+
if (workflowParams.containsKey(name)) {
176+
Object value = workflowParams.get(name);
177+
if (value instanceof String asString) {
178+
httpQuery.setAdditionalProperty(name, asString);
179+
} else {
180+
throw new IllegalArgumentException("Query parameter " + name + " must be a String");
181+
}
182+
}
183+
}
184+
}
185+
}
186+
187+
private void addTarget(Endpoint endpoint) {
188+
if (this.target == null) {
189+
throw new IllegalArgumentException("No Server defined for the OpenAPI operation");
190+
}
191+
UriTemplate uriTemplate = new UriTemplate();
192+
uriTemplate.withLiteralUri(this.server.resolve(this.target.getPath()));
193+
endpoint.setUriTemplate(uriTemplate);
194+
}
195+
}
Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
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.openapi;
17+
18+
import io.serverlessworkflow.api.types.CallHTTP;
19+
import io.serverlessworkflow.api.types.CallOpenAPI;
20+
import io.serverlessworkflow.api.types.Endpoint;
21+
import io.serverlessworkflow.api.types.EndpointUri;
22+
import io.serverlessworkflow.api.types.TaskBase;
23+
import io.serverlessworkflow.api.types.UriTemplate;
24+
import io.serverlessworkflow.api.types.Workflow;
25+
import io.serverlessworkflow.impl.TaskContext;
26+
import io.serverlessworkflow.impl.WorkflowApplication;
27+
import io.serverlessworkflow.impl.WorkflowContext;
28+
import io.serverlessworkflow.impl.WorkflowDefinition;
29+
import io.serverlessworkflow.impl.WorkflowModel;
30+
import io.serverlessworkflow.impl.WorkflowValueResolver;
31+
import io.serverlessworkflow.impl.executors.CallableTask;
32+
import io.serverlessworkflow.impl.executors.http.HttpExecutor;
33+
import io.serverlessworkflow.impl.expressions.ExpressionDescriptor;
34+
import io.serverlessworkflow.impl.expressions.ExpressionFactory;
35+
import io.serverlessworkflow.impl.resources.ResourceLoader;
36+
import jakarta.ws.rs.core.UriBuilder;
37+
import java.net.URI;
38+
import java.util.Objects;
39+
import java.util.concurrent.CompletableFuture;
40+
import java.util.concurrent.ExecutionException;
41+
import java.util.stream.Collectors;
42+
43+
public class OpenAPIExecutor implements CallableTask<CallOpenAPI> {
44+
45+
private CallOpenAPI task;
46+
private Workflow workflow;
47+
private WorkflowDefinition definition;
48+
private WorkflowApplication application;
49+
private TargetSupplier targetSupplier;
50+
51+
private ResourceLoader resourceLoader;
52+
53+
private static TargetSupplier getTargetSupplier(
54+
Endpoint endpoint, ExpressionFactory expressionFactory) {
55+
if (endpoint.getEndpointConfiguration() != null) {
56+
EndpointUri uri = endpoint.getEndpointConfiguration().getUri();
57+
if (uri.getLiteralEndpointURI() != null) {
58+
return getURISupplier(uri.getLiteralEndpointURI());
59+
} else if (uri.getExpressionEndpointURI() != null) {
60+
return new ExpressionURISupplier(
61+
expressionFactory.resolveString(
62+
ExpressionDescriptor.from(uri.getExpressionEndpointURI())));
63+
}
64+
} else if (endpoint.getRuntimeExpression() != null) {
65+
return new ExpressionURISupplier(
66+
expressionFactory.resolveString(
67+
ExpressionDescriptor.from(endpoint.getRuntimeExpression())));
68+
} else if (endpoint.getUriTemplate() != null) {
69+
return getURISupplier(endpoint.getUriTemplate());
70+
}
71+
throw new IllegalArgumentException("Invalid endpoint definition " + endpoint);
72+
}
73+
74+
private static TargetSupplier getURISupplier(UriTemplate template) {
75+
if (template.getLiteralUri() != null) {
76+
return (w, t, n) -> template.getLiteralUri();
77+
} else if (template.getLiteralUriTemplate() != null) {
78+
return (w, t, n) ->
79+
UriBuilder.fromUri(template.getLiteralUriTemplate())
80+
.resolveTemplates(n.asMap().orElseThrow(), false)
81+
.build();
82+
}
83+
throw new IllegalArgumentException("Invalid uritemplate definition " + template);
84+
}
85+
86+
@Override
87+
public boolean accept(Class<? extends TaskBase> clazz) {
88+
return clazz.equals(CallOpenAPI.class);
89+
}
90+
91+
@Override
92+
public CompletableFuture<WorkflowModel> apply(
93+
WorkflowContext workflowContext, TaskContext taskContext, WorkflowModel input) {
94+
95+
String operationId = task.getWith().getOperationId();
96+
URI openAPIEndpoint = targetSupplier.apply(workflowContext, taskContext, input);
97+
OpenAPIProcessor processor = new OpenAPIProcessor(operationId, openAPIEndpoint);
98+
OperationDefinition operation = processor.parse();
99+
100+
OperationPathResolver pathResolver =
101+
new OperationPathResolver(operation.getPath(), input.asMap().orElseThrow());
102+
URI resolvedPath = pathResolver.passPathParams().apply(workflowContext, taskContext, input);
103+
104+
HttpCallAdapter httpCallAdapter =
105+
new HttpCallAdapter()
106+
.auth(task.getWith().getAuthentication())
107+
.body(operation.getBody())
108+
.contentType(operation.getContentType())
109+
.headers(
110+
operation.getParameters().stream()
111+
.filter(p -> "header".equals(p.getIn()))
112+
.collect(Collectors.toUnmodifiableSet()))
113+
.method(operation.getMethod())
114+
.query(
115+
operation.getParameters().stream()
116+
.filter(p -> "query".equals(p.getIn()))
117+
.collect(Collectors.toUnmodifiableSet()))
118+
.redirect(task.getWith().isRedirect())
119+
.target(resolvedPath)
120+
.workflowParams(task.getWith().getParameters().getAdditionalProperties());
121+
122+
return CompletableFuture.supplyAsync(
123+
() -> {
124+
RuntimeException ex = null;
125+
for (var server : operation.getServers()) {
126+
CallHTTP callHTTP = httpCallAdapter.server(server).build();
127+
HttpExecutor executor = new HttpExecutor();
128+
executor.init(callHTTP, definition);
129+
130+
try {
131+
return executor.apply(workflowContext, taskContext, input).get();
132+
} catch (InterruptedException | RuntimeException | ExecutionException e) {
133+
ex = new RuntimeException(e);
134+
}
135+
}
136+
Objects.requireNonNull(ex, "Should have at least one exception");
137+
throw ex; // if we there, we failed all servers and ex is not null
138+
},
139+
workflowContext.definition().application().executorService());
140+
}
141+
142+
@Override
143+
public void init(CallOpenAPI task, WorkflowDefinition definition) {
144+
this.task = task;
145+
this.definition = definition;
146+
this.workflow = definition.workflow();
147+
this.application = definition.application();
148+
this.resourceLoader = definition.resourceLoader();
149+
150+
this.targetSupplier =
151+
getTargetSupplier(
152+
task.getWith().getDocument().getEndpoint(), application.expressionFactory());
153+
}
154+
155+
public interface TargetSupplier {
156+
URI apply(WorkflowContext workflow, TaskContext taskContext, WorkflowModel input);
157+
}
158+
159+
private static class ExpressionURISupplier implements TargetSupplier {
160+
private WorkflowValueResolver<String> expr;
161+
162+
public ExpressionURISupplier(WorkflowValueResolver<String> expr) {
163+
this.expr = expr;
164+
}
165+
166+
@Override
167+
public URI apply(WorkflowContext workflow, TaskContext task, WorkflowModel node) {
168+
return URI.create(expr.apply(workflow, task, node));
169+
}
170+
}
171+
}

0 commit comments

Comments
 (0)