Skip to content

Commit 4edacc7

Browse files
committed
WIP: openapi: asciidoc output #3820
- setup pebble as template engine/ascii doc pre-processor - start of `snippets` which are going to output/print routes in multiple formats
1 parent 30fa9b3 commit 4edacc7

34 files changed

+1276
-0
lines changed

modules/jooby-openapi/pom.xml

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,17 @@
5151
<version>12.1.1</version>
5252
</dependency>
5353

54+
<dependency>
55+
<groupId>org.asciidoctor</groupId>
56+
<artifactId>asciidoctorj</artifactId>
57+
<version>3.0.1</version>
58+
</dependency>
59+
60+
<dependency>
61+
<groupId>io.pebbletemplates</groupId>
62+
<artifactId>pebble</artifactId>
63+
</dependency>
64+
5465
<dependency>
5566
<groupId>commons-codec</groupId>
5667
<artifactId>commons-codec</artifactId>
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
/*
2+
* Jooby https://jooby.io
3+
* Apache License Version 2.0 https://jooby.io/LICENSE.txt
4+
* Copyright 2014 Edgar Espina
5+
*/
6+
package io.jooby.internal.openapi;
7+
8+
import java.io.IOException;
9+
import java.io.StringWriter;
10+
import java.nio.file.Path;
11+
import java.util.HashMap;
12+
import java.util.List;
13+
import java.util.Map;
14+
15+
import io.jooby.internal.openapi.asciidoc.Filters;
16+
import io.jooby.internal.openapi.asciidoc.Functions;
17+
import io.jooby.internal.openapi.asciidoc.SnippetResolver;
18+
import io.pebbletemplates.pebble.PebbleEngine;
19+
import io.pebbletemplates.pebble.attributes.AttributeResolver;
20+
import io.pebbletemplates.pebble.extension.*;
21+
import io.pebbletemplates.pebble.lexer.Syntax;
22+
import io.pebbletemplates.pebble.operator.BinaryOperator;
23+
import io.pebbletemplates.pebble.operator.UnaryOperator;
24+
import io.pebbletemplates.pebble.tokenParser.TokenParser;
25+
26+
public class AsciiDocGenerator {
27+
28+
public static String generate(OpenAPIExt openAPI, Path index) throws IOException {
29+
var snippetResolver = new SnippetResolver(index.getParent().resolve("snippet"));
30+
var engine =
31+
newEngine(
32+
new OpenApiSupport(Map.of("openapi", openAPI, "snippetResolver", snippetResolver)),
33+
"${",
34+
"}");
35+
snippetResolver.setEngine(engine);
36+
37+
var template = engine.getTemplate(index.toAbsolutePath().toString());
38+
var writer = new StringWriter();
39+
var context = new HashMap<String, Object>();
40+
template.evaluate(writer, context);
41+
return writer.toString();
42+
}
43+
44+
private static PebbleEngine newEngine(OpenApiSupport extension, String start, String end) {
45+
// 1. Define the custom syntax using a builder
46+
return new PebbleEngine.Builder()
47+
.extension(extension)
48+
.autoEscaping(false)
49+
.syntax(
50+
new Syntax.Builder()
51+
.setPrintOpenDelimiter(start)
52+
.setPrintCloseDelimiter(end)
53+
.setEnableNewLineTrimming(false)
54+
.build())
55+
.build();
56+
}
57+
58+
private static class OpenApiSupport implements Extension {
59+
private final Map<String, Object> vars;
60+
61+
public OpenApiSupport(Map<String, Object> vars) {
62+
this.vars = vars;
63+
}
64+
65+
@Override
66+
public Map<String, Filter> getFilters() {
67+
return Filters.fn();
68+
}
69+
70+
@Override
71+
public Map<String, Test> getTests() {
72+
return Map.of();
73+
}
74+
75+
@Override
76+
public Map<String, Function> getFunctions() {
77+
return Functions.fn();
78+
}
79+
80+
@Override
81+
public List<TokenParser> getTokenParsers() {
82+
return List.of();
83+
}
84+
85+
@Override
86+
public List<BinaryOperator> getBinaryOperators() {
87+
return List.of();
88+
}
89+
90+
@Override
91+
public List<UnaryOperator> getUnaryOperators() {
92+
return List.of();
93+
}
94+
95+
@Override
96+
public Map<String, Object> getGlobalVariables() {
97+
return vars;
98+
}
99+
100+
@Override
101+
public List<NodeVisitorFactory> getNodeVisitors() {
102+
return List.of();
103+
}
104+
105+
@Override
106+
public List<AttributeResolver> getAttributeResolver() {
107+
return List.of();
108+
}
109+
}
110+
}

modules/jooby-openapi/src/main/java/io/jooby/internal/openapi/OpenAPIExt.java

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
import java.util.function.BiConsumer;
1010
import java.util.function.Consumer;
1111
import java.util.function.Function;
12+
import java.util.function.Predicate;
1213

1314
import com.fasterxml.jackson.annotation.JsonIgnore;
1415
import io.jooby.Router;
@@ -233,4 +234,25 @@ private <S, V> void setProperty(S src, Function<S, V> getter, S target, BiConsum
233234
}
234235
}
235236
}
237+
238+
public OperationExt findOperationById(String operationId) {
239+
return getOperations().stream()
240+
.filter(it -> it.getOperationId().equals(operationId))
241+
.findFirst()
242+
.orElseThrow(() -> new IllegalArgumentException("Operation not found: " + operationId));
243+
}
244+
245+
public OperationExt findOperation(String method, String pattern) {
246+
Predicate<OperationExt> filter = op -> op.getPattern().equals(pattern);
247+
if (method != null) {
248+
filter = filter.and(op -> op.getMethod().equals(method));
249+
}
250+
return getOperations().stream()
251+
.filter(filter)
252+
.findFirst()
253+
.orElseThrow(
254+
() ->
255+
new IllegalArgumentException(
256+
"Operation not found: " + (method == null ? "" : method + " ") + pattern));
257+
}
236258
}
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
/*
2+
* Jooby https://jooby.io
3+
* Apache License Version 2.0 https://jooby.io/LICENSE.txt
4+
* Copyright 2014 Edgar Espina
5+
*/
6+
package io.jooby.internal.openapi.asciidoc;
7+
8+
import java.util.*;
9+
10+
import io.jooby.internal.openapi.OperationExt;
11+
import io.pebbletemplates.pebble.error.PebbleException;
12+
import io.pebbletemplates.pebble.extension.Filter;
13+
import io.pebbletemplates.pebble.template.EvaluationContext;
14+
import io.pebbletemplates.pebble.template.PebbleTemplate;
15+
import io.swagger.v3.oas.models.Operation;
16+
17+
public enum Filters implements Filter {
18+
curl {
19+
@Override
20+
public Object apply(
21+
Object input,
22+
Map<String, Object> args,
23+
PebbleTemplate self,
24+
EvaluationContext context,
25+
int lineNumber)
26+
throws PebbleException {
27+
try {
28+
if (!(input instanceof OperationExt operation)) {
29+
throw new IllegalArgumentException(
30+
"Argument must be " + Operation.class.getName() + ". Got: " + input);
31+
}
32+
var snippetResolver = (SnippetResolver) context.getVariable("snippetResolver");
33+
34+
var options = new LinkedHashMap<String, Set<String>>();
35+
options.put("-X", Set.of(null));
36+
operation
37+
.getProduces()
38+
.forEach(
39+
produces -> {
40+
options
41+
.computeIfAbsent("-H", (key) -> new LinkedHashSet<>())
42+
.add("'Accept: " + produces + "'");
43+
});
44+
45+
// Convert to map so can override any generated option
46+
var optionList = new ArrayList<>(args.values());
47+
for (int i = 0; i < optionList.size(); ) {
48+
var key = optionList.get(i).toString();
49+
String value = null;
50+
if (i + 1 < optionList.size()) {
51+
var next = optionList.get(i + 1);
52+
if (next.toString().startsWith("-")) {
53+
i += 1;
54+
} else {
55+
value = next.toString();
56+
i += 2;
57+
}
58+
} else {
59+
i += 1;
60+
}
61+
var values = options.computeIfAbsent(key, k -> new LinkedHashSet<>());
62+
if (value != null) {
63+
values.add(value);
64+
}
65+
}
66+
return snippetResolver.apply(
67+
"curl", Map.of("url", operation.getPattern(), "options", toString(options)));
68+
} catch (PebbleException pebbleException) {
69+
throw pebbleException;
70+
} catch (Exception exception) {
71+
throw new PebbleException(exception, name() + " failed to generate output");
72+
}
73+
}
74+
75+
private String toString(Map<String, Set<String>> options) {
76+
if (options.isEmpty()) {
77+
return "";
78+
}
79+
var sb = new StringBuilder();
80+
var separator = " ";
81+
for (var e : options.entrySet()) {
82+
var values = e.getValue();
83+
if (values.isEmpty()) {
84+
sb.append(e.getKey()).append(separator);
85+
} else {
86+
for (var value : e.getValue()) {
87+
sb.append(e.getKey()).append(separator);
88+
sb.append(value).append(separator);
89+
}
90+
}
91+
}
92+
sb.deleteCharAt(sb.length() - separator.length());
93+
return sb.toString();
94+
}
95+
96+
@Override
97+
public List<String> getArgumentNames() {
98+
return null;
99+
}
100+
};
101+
102+
public static Map<String, Filter> fn() {
103+
Map<String, Filter> functions = new HashMap<>();
104+
for (var value : values()) {
105+
functions.put(value.name(), value);
106+
}
107+
return functions;
108+
}
109+
}
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
/*
2+
* Jooby https://jooby.io
3+
* Apache License Version 2.0 https://jooby.io/LICENSE.txt
4+
* Copyright 2014 Edgar Espina
5+
*/
6+
package io.jooby.internal.openapi.asciidoc;
7+
8+
import java.util.HashMap;
9+
import java.util.List;
10+
import java.util.Map;
11+
12+
import io.jooby.internal.openapi.OpenAPIExt;
13+
import io.pebbletemplates.pebble.error.PebbleException;
14+
import io.pebbletemplates.pebble.extension.Function;
15+
import io.pebbletemplates.pebble.template.EvaluationContext;
16+
import io.pebbletemplates.pebble.template.PebbleTemplate;
17+
18+
public enum Functions implements Function {
19+
operation {
20+
@Override
21+
public List<String> getArgumentNames() {
22+
return List.of("identifier", "pattern");
23+
}
24+
25+
@Override
26+
public Object execute(
27+
Map<String, Object> args, PebbleTemplate self, EvaluationContext context, int lineNumber) {
28+
try {
29+
var namedArgs = new HashMap<String, String>();
30+
var value = (String) args.get("identifier");
31+
if (isHTTPMethod(value)) {
32+
namedArgs.put("method", value);
33+
} else if (value.startsWith("/")) {
34+
namedArgs.put("pattern", value);
35+
} else {
36+
namedArgs.put("id", value);
37+
}
38+
namedArgs.putIfAbsent("pattern", (String) args.get("pattern"));
39+
OpenAPIExt openApi = (OpenAPIExt) context.getVariable("openapi");
40+
var operationId = namedArgs.get("id");
41+
if (operationId == null) {
42+
var method = namedArgs.get("method");
43+
var path = namedArgs.get("pattern");
44+
return openApi.findOperation(method, path);
45+
} else {
46+
return openApi.findOperationById(operationId);
47+
}
48+
} catch (Exception cause) {
49+
throw new PebbleException(
50+
cause, name() + " failed to generate output (?:?)", lineNumber, self.getName());
51+
}
52+
}
53+
54+
private boolean isHTTPMethod(String value) {
55+
return switch (value.toUpperCase()) {
56+
case "GET" -> true;
57+
case "POST" -> true;
58+
case "PUT" -> true;
59+
case "DELETE" -> true;
60+
case "HEAD" -> true;
61+
case "OPTIONS" -> true;
62+
case "TRACE" -> true;
63+
case "PATCH" -> true;
64+
default -> false;
65+
};
66+
}
67+
};
68+
69+
public static Map<String, Function> fn() {
70+
Map<String, Function> functions = new HashMap<>();
71+
for (Functions value : values()) {
72+
functions.put(value.name(), value);
73+
}
74+
return functions;
75+
}
76+
}

0 commit comments

Comments
 (0)