Skip to content

Commit ee654cf

Browse files
committed
WIP: Ascii doc open api
1 parent 34d1566 commit ee654cf

File tree

8 files changed

+1240
-0
lines changed

8 files changed

+1240
-0
lines changed

modules/jooby-openapi/pom.xml

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,18 @@
3636
<artifactId>jakarta.ws.rs-api</artifactId>
3737
</dependency>
3838

39+
<dependency>
40+
<groupId>org.asciidoctor</groupId>
41+
<artifactId>asciidoctorj</artifactId>
42+
<version>3.0.0</version>
43+
</dependency>
44+
<!-- https://mvnrepository.com/artifact/org.asciidoctor/asciidoctorj-pdf -->
45+
<dependency>
46+
<groupId>org.asciidoctor</groupId>
47+
<artifactId>asciidoctorj-pdf</artifactId>
48+
<version>2.3.19</version>
49+
</dependency>
50+
3951
<!-- ASM -->
4052
<dependency>
4153
<groupId>org.ow2.asm</groupId>
Lines changed: 280 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,280 @@
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.nio.charset.StandardCharsets;
10+
import java.nio.file.Files;
11+
import java.nio.file.Paths;
12+
import java.nio.file.StandardOpenOption;
13+
import java.time.Instant;
14+
import java.util.*;
15+
import java.util.function.BiConsumer;
16+
import java.util.function.Consumer;
17+
import java.util.function.Function;
18+
import java.util.stream.Stream;
19+
20+
import org.asciidoctor.Asciidoctor;
21+
import org.asciidoctor.Attributes;
22+
import org.asciidoctor.Options;
23+
import org.asciidoctor.SafeMode;
24+
25+
import com.fasterxml.jackson.databind.JsonNode;
26+
import com.fasterxml.jackson.databind.node.ObjectNode;
27+
import io.swagger.v3.oas.models.OpenAPI;
28+
import io.swagger.v3.oas.models.Operation;
29+
import io.swagger.v3.oas.models.info.Contact;
30+
import io.swagger.v3.oas.models.info.Info;
31+
import io.swagger.v3.oas.models.parameters.Parameter;
32+
import io.swagger.v3.oas.models.tags.Tag;
33+
34+
public class AsciiDocGenerator {
35+
public static String generate(OpenAPI openAPI) throws IOException {
36+
var sb = new StringBuilder();
37+
/* Info: */
38+
intro(openAPI, sb);
39+
paths(openAPI, sb);
40+
Files.write(Paths.get("target", "out.adoc"), sb.toString().getBytes(StandardCharsets.UTF_8));
41+
Asciidoctor asciidoctor = Asciidoctor.Factory.create();
42+
var css = Paths.get(System.getProperty("java.io.tmpdir"), "openapi-asciidoc.css");
43+
44+
Files.write(css, css(), StandardOpenOption.CREATE);
45+
Attributes attributes =
46+
Attributes.builder()
47+
// .backend("pdf")
48+
.attribute("toc", "numbered")
49+
.attribute("stylesheet", css.toAbsolutePath().toString())
50+
.experimental(true)
51+
// .backend("pdf")
52+
.build();
53+
asciidoctor.convertFile(
54+
Paths.get("target", "out.adoc").toFile(),
55+
Options.builder().toFile(true).attributes(attributes).safe(SafeMode.UNSAFE).build());
56+
return sb.toString();
57+
}
58+
59+
private static byte[] css() throws IOException {
60+
try (var in = AsciiDocGenerator.class.getResourceAsStream("/asciidoc.css")) {
61+
return in.readAllBytes();
62+
}
63+
}
64+
65+
record Route(String method, String pattern, Operation operation) {}
66+
67+
private static void paths(OpenAPI node, StringBuilder out) {
68+
out.append("== ")
69+
.append("Operations")
70+
.append(System.lineSeparator())
71+
.append(System.lineSeparator());
72+
var paths = node.getPaths();
73+
Map<String, List<Route>> tags = new LinkedHashMap<>();
74+
paths.forEach(
75+
(pattern, path) -> {
76+
Map<String, Operation> routes = new LinkedHashMap<>();
77+
routes.put("GET", path.getGet());
78+
routes.put("POST", path.getPost());
79+
routes.put("PUT", path.getPut());
80+
routes.put("PATCH", path.getPatch());
81+
routes.put("DELETE", path.getDelete());
82+
routes.put("HEAD", path.getHead());
83+
routes.put("OPTIONS", path.getOptions());
84+
routes.put("TRACE", path.getTrace());
85+
routes.forEach(
86+
(method, operation) -> {
87+
nonnullOp(method, pattern, operation)
88+
.ifPresent(
89+
route -> {
90+
Optional.ofNullable(route.operation().getTags())
91+
.filter(it -> !it.isEmpty())
92+
.orElse(List.of("*"))
93+
.forEach(
94+
tag -> {
95+
tags.computeIfAbsent(tag, key -> new ArrayList<>()).add(route);
96+
});
97+
});
98+
});
99+
});
100+
tags.entrySet().stream()
101+
.filter(it -> !it.equals("*"))
102+
.forEach(
103+
e -> {
104+
tag(node, e.getKey(), e.getValue(), out);
105+
});
106+
}
107+
108+
private static void tag(OpenAPI api, String key, List<Route> routes, StringBuilder out) {
109+
var tag =
110+
api.getTags().stream()
111+
.filter(it -> it.getName().equalsIgnoreCase(key))
112+
.findFirst()
113+
.orElseGet(() -> new Tag().name(key));
114+
out.append("=== ")
115+
.append(tag.getName())
116+
.append(System.lineSeparator())
117+
.append(System.lineSeparator());
118+
119+
withProperty(
120+
tag,
121+
Tag::getDescription,
122+
description -> out.append(description).append(System.lineSeparator()));
123+
for (Route route : routes) {
124+
var deprecated = route.operation().getDeprecated();
125+
var style = "[.method";
126+
if (deprecated == Boolean.TRUE) {
127+
style += " .deprecated";
128+
}
129+
style += "]";
130+
out.append(System.lineSeparator());
131+
out.append("[TIP.")
132+
.append(route.method.toLowerCase())
133+
.append(", caption=")
134+
.append(route.method)
135+
.append("]")
136+
.append(System.lineSeparator());
137+
out.append(".")
138+
.append(style)
139+
.append("#")
140+
.append(route.pattern)
141+
.append("# ")
142+
.append(route.operation.getSummary())
143+
.append(System.lineSeparator());
144+
var description = route.operation().getDescription();
145+
out.append(description == null ? " " : description)
146+
.append("&nbsp;")
147+
.append(System.lineSeparator());
148+
if (route.operation.getParameters() != null) {
149+
out.append(System.lineSeparator()).append("* Parameters").append(System.lineSeparator());
150+
for (Parameter parameter : route.operation.getParameters()) {
151+
out.append("** ")
152+
.append(parameter.getName())
153+
.append(": ")
154+
.append(parameter.getDescription())
155+
.append(System.lineSeparator());
156+
var schema = parameter.getSchema();
157+
var schemaString = schema.getType();
158+
if (schema.getType().equals("array")) {
159+
schemaString += " of " + schema.getItems().getType();
160+
}
161+
out.append("*** ").append("type: ").append(schemaString).append(System.lineSeparator());
162+
out.append("*** ")
163+
.append("in: ")
164+
.append(parameter.getIn())
165+
.append(System.lineSeparator());
166+
out.append("*** ")
167+
.append("required: ")
168+
.append(parameter.getRequired())
169+
.append(System.lineSeparator());
170+
}
171+
}
172+
}
173+
}
174+
175+
private static Optional<Route> nonnullOp(String method, String pattern, Operation operation) {
176+
if (operation != null) {
177+
return Optional.of(new Route(method, pattern, operation));
178+
}
179+
return Optional.empty();
180+
}
181+
182+
private static void operation(
183+
String method, String pattern, Operation operation, StringBuilder out) {
184+
if (operation != null) {
185+
out.append("=== ")
186+
.append(method)
187+
.append(" ")
188+
.append(pattern)
189+
.append(System.lineSeparator())
190+
.append(System.lineSeparator());
191+
if (operation.getSummary() != null) {
192+
out.append(operation.getSummary()).append(System.lineSeparator());
193+
}
194+
if (operation.getDescription() != null) {
195+
out.append(operation.getDescription()).append(System.lineSeparator());
196+
}
197+
}
198+
}
199+
200+
private static void intro(OpenAPI openApi, StringBuilder out) {
201+
var info = openApi.getInfo();
202+
var title = info.getTitle();
203+
var version = info.getVersion();
204+
205+
out.append("= ").append(title).append(System.lineSeparator());
206+
out.append("v")
207+
.append(version)
208+
.append(", ")
209+
.append(Instant.now())
210+
.append(System.lineSeparator());
211+
out.append(System.lineSeparator());
212+
withProperty(info, Info::getSummary, value -> out.append(value).append(System.lineSeparator()));
213+
withProperty(
214+
info, Info::getDescription, value -> out.append(value).append(System.lineSeparator()));
215+
withProperty(
216+
info,
217+
Info::getTermsOfService,
218+
value ->
219+
out.append(System.lineSeparator())
220+
.append("=== Term of Service")
221+
.append(System.lineSeparator())
222+
.append(value)
223+
.append(System.lineSeparator()));
224+
withProperty(
225+
info,
226+
Info::getContact,
227+
value -> {
228+
out.append(System.lineSeparator()).append("=== Contact").append(System.lineSeparator());
229+
var contact = new StringBuilder();
230+
withProperty(
231+
value,
232+
Contact::getName,
233+
name -> {
234+
withProperty(value, Contact::getUrl, contact::append);
235+
if (contact.isEmpty()) {
236+
contact.append(name);
237+
} else {
238+
contact.append("[").append(name).append("]");
239+
}
240+
withProperty(
241+
value,
242+
Contact::getEmail,
243+
email ->
244+
contact
245+
.append(". ")
246+
.append("mailto:")
247+
.append(email)
248+
.append("[Contact Support]"));
249+
contact.append(System.lineSeparator());
250+
});
251+
out.append(contact).append(System.lineSeparator());
252+
});
253+
}
254+
255+
private static <S, T> void withProperty(S node, Function<S, T> mapper, Consumer<T> consumer) {
256+
var value = mapper.apply(node);
257+
if (value != null) {
258+
consumer.accept(value);
259+
}
260+
;
261+
}
262+
263+
private static void walkTree(
264+
List<String> path, JsonNode node, BiConsumer<List<String>, String> consumer) {
265+
if (node instanceof ObjectNode object) {
266+
object
267+
.fields()
268+
.forEachRemaining(
269+
e ->
270+
walkTree(
271+
Stream.concat(path.stream(), Stream.of(e.getKey())).toList(),
272+
e.getValue(),
273+
consumer));
274+
} else if (node.isArray()) {
275+
node.elements().forEachRemaining(e -> walkTree(path, e, consumer));
276+
} else if (node.isValueNode() && !node.isNull()) {
277+
consumer.accept(path, node.asText());
278+
}
279+
}
280+
}

modules/jooby-openapi/src/main/java/io/jooby/openapi/OpenAPIGenerator.java

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,13 @@ public String toString(OpenAPIGenerator tool, OpenAPI result) {
6262
public String toString(OpenAPIGenerator tool, OpenAPI result) {
6363
return tool.toYaml(result);
6464
}
65+
},
66+
67+
ASCIIDOC {
68+
@NonNull @Override
69+
public String toString(@NonNull OpenAPIGenerator tool, @NonNull OpenAPI result) {
70+
return tool.toAsciiDoc(result);
71+
}
6572
};
6673

6774
/**
@@ -296,6 +303,14 @@ private void defaults(String classname, String contextPath, OpenAPIExt openapi)
296303
}
297304
}
298305

306+
public @NonNull String toAsciiDoc(OpenAPI openAPI) {
307+
try {
308+
return AsciiDocGenerator.generate(openAPI);
309+
} catch (IOException e) {
310+
throw new RuntimeException(e);
311+
}
312+
}
313+
299314
/**
300315
* Use a custom classloader for resolving class files.
301316
*
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
@import "https://fonts.googleapis.com/css?family=Open+Sans:300,300italic,400,400italic,600,600italic%7CNoto+Serif:400,400italic,700,700italic%7CDroid+Sans+Mono:400,700";
2+
@import "https://cdn.jsdelivr.net/gh/asciidoctor/asciidoctor@2.0/data/stylesheets/asciidoctor-default.css";
3+
4+
.admonitionblock.tip.put table td.icon .title {
5+
background-color: #f70;
6+
color: #fff;
7+
}
8+
9+
.admonitionblock.tip.patch table td.icon .title {
10+
background-color: #802392;
11+
color: #fff;
12+
}
13+
14+
.admonitionblock.tip.get table td.icon .title {
15+
background-color: #43b929;
16+
color: #fff;
17+
}
18+
19+
.admonitionblock.tip.delete table td.icon .title {
20+
background-color: #e40046;
21+
color: #fff;
22+
}
23+
24+
.admonitionblock.tip.post table td.icon .title {
25+
background-color: #2d7dd2;
26+
color: #fff;
27+
}
28+
29+
.admonitionblock.tip.head table td.icon .title {
30+
background-color: #43b929;
31+
color: #fff;
32+
}
33+
34+
.admonitionblock.tip.trace table td.icon .title {
35+
background-color: #43b929;
36+
color: #fff;
37+
}
38+
39+
.admonitionblock.tip.options table td.icon .title {
40+
background-color: #43b929;
41+
color: #fff;
42+
}
43+
44+
.admonitionblock td.content>.title {
45+
font-style: normal;
46+
color: black;
47+
}
48+
49+
.admonitionblock td.content>.title .method {
50+
font-style: normal;
51+
font-family: monospace;
52+
font-size: 16px;
53+
font-weight: 600;
54+
word-break: break-word;
55+
color: black;
56+
}
57+
58+
.admonitionblock td.content>.title .method.deprecated {
59+
text-decoration: line-through;
60+
color: grey;
61+
}
62+

0 commit comments

Comments
 (0)