Skip to content

Commit bc0ac2e

Browse files
committed
filter: curl
- more advanced implementation - unit tests - ref #3820
1 parent 0c46eff commit bc0ac2e

File tree

16 files changed

+378
-338
lines changed

16 files changed

+378
-338
lines changed

modules/jooby-openapi/pom.xml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,11 @@
7373
<artifactId>swagger-parser</artifactId>
7474
</dependency>
7575

76+
<dependency>
77+
<groupId>com.google.guava</groupId>
78+
<artifactId>guava</artifactId>
79+
</dependency>
80+
7681
<!-- Test dependencies -->
7782
<dependency>
7883
<groupId>org.junit.jupiter</groupId>
@@ -139,6 +144,11 @@
139144
<version>1.18.1</version>
140145
<scope>test</scope>
141146
</dependency>
147+
<dependency>
148+
<groupId>org.mockito</groupId>
149+
<artifactId>mockito-core</artifactId>
150+
<scope>test</scope>
151+
</dependency>
142152
</dependencies>
143153

144154
<build>

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

Lines changed: 32 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -21,14 +21,7 @@
2121
import org.objectweb.asm.tree.*;
2222

2323
import io.jooby.*;
24-
import io.jooby.annotation.ContextParam;
25-
import io.jooby.annotation.CookieParam;
26-
import io.jooby.annotation.FormParam;
27-
import io.jooby.annotation.GET;
28-
import io.jooby.annotation.HeaderParam;
29-
import io.jooby.annotation.Path;
30-
import io.jooby.annotation.PathParam;
31-
import io.jooby.annotation.QueryParam;
24+
import io.jooby.annotation.*;
3225
import io.swagger.v3.oas.models.media.Content;
3326
import io.swagger.v3.oas.models.media.ObjectSchema;
3427
import io.swagger.v3.oas.models.media.Schema;
@@ -314,6 +307,7 @@ private static Map<String, MethodNode> methods(ParserContext ctx, ClassNode node
314307
return methods;
315308
}
316309

310+
@SuppressWarnings("unchecked")
317311
private static List<OperationExt> routerMethod(
318312
ParserContext ctx, String prefix, ClassNode classNode, MethodNode method) {
319313

@@ -330,13 +324,35 @@ private static List<OperationExt> routerMethod(
330324
operation.setOperationId(method.name);
331325
Optional.ofNullable(requestBody.get()).ifPresent(operation::setRequestBody);
332326

327+
mediaType(classNode, method, produces(), operation::addProduces);
328+
mediaType(classNode, method, consumes(), operation::addConsumes);
329+
333330
result.add(operation);
334331
}
335332
}
336333

337334
return result;
338335
}
339336

337+
@SuppressWarnings("unchecked")
338+
public static void mediaType(
339+
ClassNode classNode, MethodNode method, List<String> types, Consumer<String> consumer) {
340+
mediaType(classNode, method, types).stream()
341+
.map(AsmUtils::toMap)
342+
.map(it -> it.get("value"))
343+
.filter(Objects::nonNull)
344+
.map(List.class::cast)
345+
.flatMap(List::stream)
346+
.distinct()
347+
.forEach(it -> consumer.accept(it.toString()));
348+
}
349+
350+
public static List<AnnotationNode> mediaType(
351+
ClassNode classNode, MethodNode method, List<String> types) {
352+
var result = findAnnotationByType(method.visibleAnnotations, types);
353+
return result.isEmpty() ? findAnnotationByType(classNode.visibleAnnotations, types) : result;
354+
}
355+
340356
private static ResponseExt returnTypes(MethodNode method) {
341357
Signature signature = Signature.create(method);
342358
String desc = Optional.ofNullable(method.signature).orElse(method.desc);
@@ -604,6 +620,14 @@ private static List<String> httpMethods() {
604620
return annotationTypes;
605621
}
606622

623+
private static List<String> produces() {
624+
return List.of(Produces.class.getName(), jakarta.ws.rs.Produces.class.getName());
625+
}
626+
627+
private static List<String> consumes() {
628+
return List.of(Consumes.class.getName(), jakarta.ws.rs.Consumes.class.getName());
629+
}
630+
607631
private static List<String> httpMethod(String pkg, Class pathType) {
608632
List<String> annotationTypes =
609633
Router.METHODS.stream().map(m -> pkg + "." + m).collect(Collectors.toList());

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

Lines changed: 14 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -26,30 +26,33 @@
2626
public class AsciiDocGenerator {
2727

2828
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-
29+
var engine = newEngine(openAPI, index.getParent());
3730
var template = engine.getTemplate(index.toAbsolutePath().toString());
3831
var writer = new StringWriter();
3932
var context = new HashMap<String, Object>();
4033
template.evaluate(writer, context);
4134
return writer.toString();
4235
}
4336

44-
private static PebbleEngine newEngine(OpenApiSupport extension, String start, String end) {
37+
private static PebbleEngine newEngine(OpenAPIExt openAPI, Path baseDir) {
38+
var snippetResolver = new SnippetResolver(baseDir.resolve("snippet"));
39+
var engine =
40+
newEngine(
41+
new OpenApiSupport(Map.of("openapi", openAPI, "snippetResolver", snippetResolver)));
42+
snippetResolver.setEngine(engine);
43+
return newEngine(
44+
new OpenApiSupport(Map.of("openapi", openAPI, "snippetResolver", snippetResolver)));
45+
}
46+
47+
private static PebbleEngine newEngine(OpenApiSupport extension) {
4548
// 1. Define the custom syntax using a builder
4649
return new PebbleEngine.Builder()
4750
.extension(extension)
4851
.autoEscaping(false)
4952
.syntax(
5053
new Syntax.Builder()
51-
.setPrintOpenDelimiter(start)
52-
.setPrintCloseDelimiter(end)
54+
.setPrintOpenDelimiter("${")
55+
.setPrintCloseDelimiter("}")
5356
.setEnableNewLineTrimming(false)
5457
.build())
5558
.build();

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -604,7 +604,7 @@ private static void operationResponse(
604604

605605
String name = stringValue(value, "name");
606606
stringValue(value, "description", header::setDescription);
607-
io.swagger.v3.oas.models.media.Schema schema =
607+
var schema =
608608
annotationValue(value, "schema")
609609
.map(schemaMap -> toSchema(ctx, schemaMap).orElseGet(StringSchema::new))
610610
.orElseGet(StringSchema::new);

modules/jooby-openapi/src/main/java/io/jooby/internal/openapi/asciidoc/Filters.java

Lines changed: 123 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@
77

88
import java.util.*;
99

10+
import com.google.common.base.Splitter;
11+
import com.google.common.collect.*;
12+
import edu.umd.cs.findbugs.annotations.NonNull;
1013
import io.jooby.internal.openapi.OperationExt;
1114
import io.pebbletemplates.pebble.error.PebbleException;
1215
import io.pebbletemplates.pebble.extension.Filter;
@@ -16,6 +19,9 @@
1619

1720
public enum Filters implements Filter {
1821
curl {
22+
private static final CharSequence Accept = new HeaderName("Accept");
23+
private static final CharSequence ContentType = new HeaderName("Content-Type");
24+
1925
@Override
2026
public Object apply(
2127
Object input,
@@ -30,80 +36,143 @@ public Object apply(
3036
"Argument must be " + Operation.class.getName() + ". Got: " + input);
3137
}
3238
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-
}
39+
var options = args(args);
40+
var method =
41+
Optional.of(options.removeAll("-X"))
42+
.map(Collection::iterator)
43+
.filter(Iterator::hasNext)
44+
.map(Iterator::next)
45+
.orElse(operation.getMethod())
46+
.toUpperCase();
47+
/* Accept/Content-Type: */
48+
var addAccept = true;
49+
var addContentType = true;
50+
if (options.containsKey("-H")) {
51+
var headers = parseHeaders(options.get("-H"));
52+
addAccept = !headers.containsKey(Accept);
53+
addContentType = !headers.containsKey(ContentType);
54+
}
55+
if (addAccept) {
56+
operation.getProduces().forEach(value -> options.put("-H", "'Accept: " + value + "'"));
57+
}
58+
if (addContentType && !READ_METHODS.contains(method)) {
59+
operation
60+
.getConsumes()
61+
.forEach(value -> options.put("-H", "'Content-Type: " + value + "'"));
62+
}
63+
/* Method */
64+
if (!options.containsKey("-X")) {
65+
options.put("-X", method);
6566
}
6667
return snippetResolver.apply(
67-
"curl", Map.of("url", operation.getPattern(), "options", toString(options)));
68+
name(), Map.of("url", operation.getPattern(), "options", toString(options)));
6869
} catch (PebbleException pebbleException) {
6970
throw pebbleException;
7071
} catch (Exception exception) {
7172
throw new PebbleException(exception, name() + " failed to generate output");
7273
}
7374
}
7475

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-
9676
@Override
9777
public List<String> getArgumentNames() {
9878
return null;
9979
}
10080
};
10181

82+
protected Multimap<CharSequence, String> parseHeaders(Collection<String> headers) {
83+
Multimap<CharSequence, String> result = LinkedHashMultimap.create();
84+
for (var line : headers) {
85+
if (line.startsWith("'") && line.endsWith("'")) {
86+
line = line.substring(1, line.length() - 1);
87+
}
88+
var header = Splitter.on(':').trimResults().omitEmptyStrings().splitToList(line);
89+
if (header.size() != 2) {
90+
throw new IllegalArgumentException("Invalid header: " + line);
91+
}
92+
result.put(new HeaderName(header.get(0)), header.get(1));
93+
}
94+
return result;
95+
}
96+
97+
protected static final Set<String> READ_METHODS = Set.of("GET", "HEAD");
98+
99+
protected String toString(Multimap<String, String> options) {
100+
if (options.isEmpty()) {
101+
return "";
102+
}
103+
var sb = new StringBuilder();
104+
var separator = " ";
105+
options.forEach(
106+
(k, v) -> {
107+
sb.append(k).append(separator);
108+
if (v != null && !v.isEmpty()) {
109+
sb.append(v).append(separator);
110+
}
111+
});
112+
sb.deleteCharAt(sb.length() - separator.length());
113+
return sb.toString();
114+
}
115+
116+
protected Multimap<String, String> args(Map<String, Object> args) {
117+
Multimap<String, String> result = LinkedHashMultimap.create();
118+
var optionList = new ArrayList<>(args.values());
119+
for (int i = 0; i < optionList.size(); ) {
120+
var key = optionList.get(i).toString();
121+
String value = null;
122+
if (i + 1 < optionList.size()) {
123+
var next = optionList.get(i + 1);
124+
if (next.toString().startsWith("-")) {
125+
i += 1;
126+
} else {
127+
value = next.toString();
128+
i += 2;
129+
}
130+
} else {
131+
i += 1;
132+
}
133+
result.put(key, value == null ? "" : value);
134+
}
135+
return result;
136+
}
137+
102138
public static Map<String, Filter> fn() {
103139
Map<String, Filter> functions = new HashMap<>();
104140
for (var value : values()) {
105141
functions.put(value.name(), value);
106142
}
107143
return functions;
108144
}
145+
146+
protected record HeaderName(String value) implements CharSequence {
147+
148+
@Override
149+
public int length() {
150+
return value.length();
151+
}
152+
153+
@Override
154+
public boolean equals(Object obj) {
155+
return value.equalsIgnoreCase(obj.toString());
156+
}
157+
158+
@Override
159+
public int hashCode() {
160+
return value.toLowerCase().hashCode();
161+
}
162+
163+
@Override
164+
public char charAt(int index) {
165+
return value.charAt(index);
166+
}
167+
168+
@NonNull @Override
169+
public CharSequence subSequence(int start, int end) {
170+
return value.subSequence(start, end);
171+
}
172+
173+
@Override
174+
public String toString() {
175+
return value;
176+
}
177+
}
109178
}

modules/jooby-openapi/src/main/java/module-info.java

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
/** Open API module. */
22
module io.jooby.openapi {
33
exports io.jooby.openapi;
4-
exports io.jooby.internal.openapi to
5-
com.fasterxml.jackson.databind;
64

75
requires io.jooby;
86
requires static com.github.spotbugs.annotations;
@@ -21,4 +19,5 @@
2119
requires org.objectweb.asm.util;
2220
requires io.pebbletemplates;
2321
requires jdk.jshell;
22+
requires com.google.common;
2423
}
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
[source,bash]
22
----
3-
$ curl ${options} '${url}'
3+
$ curl ${options | raw} '${url}'
44
----

0 commit comments

Comments
 (0)