Skip to content

Commit 612509a

Browse files
committed
OpenApi: arguments and return types
1 parent fa9d5e6 commit 612509a

27 files changed

+1730
-90
lines changed

jooby/src/main/java/io/jooby/internal/ValueConverters.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,7 @@ public static <T> T convert(ValueNode value, Class type, Router router) {
109109
if (type == byte.class) {
110110
return (T) Byte.valueOf(value.byteValue());
111111
}
112-
if (type.isEnum()) {
112+
if (Enum.class.isAssignableFrom(type)) {
113113
return (T) enumValue(value, type);
114114
}
115115
// Wrapper

modules/jooby-openapi/pom.xml

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,20 @@
4545
<optional>true</optional>
4646
</dependency>
4747

48+
<dependency>
49+
<groupId>io.swagger.core.v3</groupId>
50+
<artifactId>swagger-models</artifactId>
51+
<version>2.1.1</version>
52+
<optional>true</optional>
53+
</dependency>
54+
55+
<dependency>
56+
<groupId>io.swagger.parser.v3</groupId>
57+
<artifactId>swagger-parser</artifactId>
58+
<version>2.0.17</version>
59+
<optional>true</optional>
60+
</dependency>
61+
4862
<!-- Test dependencies -->
4963
<dependency>
5064
<groupId>org.junit.jupiter</groupId>
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
package io.jooby.internal.openapi;
2+
3+
public enum HttpType {
4+
PATH,
5+
QUERY,
6+
FORM;
7+
}

modules/jooby-openapi/src/main/java/io/jooby/internal/openapi/InsnNode.java renamed to modules/jooby-openapi/src/main/java/io/jooby/internal/openapi/InsnSupport.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
import java.util.stream.Stream;
1515
import java.util.stream.StreamSupport;
1616

17-
public class InsnNode {
17+
public class InsnSupport {
1818

1919
private static class NodeIterator implements Iterator<org.objectweb.asm.tree.AbstractInsnNode> {
2020

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
package io.jooby.internal.openapi;
2+
3+
import java.util.Objects;
4+
5+
public class RouteArgument {
6+
private String name;
7+
8+
private String javaType;
9+
10+
private boolean required;
11+
12+
private Object defaultValue;
13+
14+
private HttpType httpType;
15+
16+
private boolean single = true;
17+
18+
public void setName(String name) {
19+
this.name = name;
20+
}
21+
22+
public HttpType getHttpType() {
23+
return httpType;
24+
}
25+
26+
public void setHttpType(HttpType httpType) {
27+
this.httpType = httpType;
28+
}
29+
30+
public void setJavaType(String javaType) {
31+
this.javaType = javaType;
32+
}
33+
34+
public String getName() {
35+
return name;
36+
}
37+
38+
public String getJavaType() {
39+
return javaType;
40+
}
41+
42+
public boolean isRequired() {
43+
return required;
44+
}
45+
46+
public boolean isSingle() {
47+
return single;
48+
}
49+
50+
public void setSingle(boolean single) {
51+
this.single = single;
52+
}
53+
54+
public void setRequired(boolean required) {
55+
this.required = required;
56+
}
57+
58+
public Object getDefaultValue() {
59+
if (defaultValue != null) {
60+
if (javaType.equals(boolean.class.getName())) {
61+
return Objects.equals(defaultValue, 1);
62+
}
63+
}
64+
return defaultValue;
65+
}
66+
67+
public void setDefaultValue(Object defaultValue) {
68+
this.defaultValue = defaultValue;
69+
}
70+
71+
@Override public String toString() {
72+
return javaType + " " + name;
73+
}
74+
}
Lines changed: 263 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,263 @@
1+
package io.jooby.internal.openapi;
2+
3+
import org.objectweb.asm.Handle;
4+
import org.objectweb.asm.Opcodes;
5+
import org.objectweb.asm.Type;
6+
import org.objectweb.asm.tree.AbstractInsnNode;
7+
import org.objectweb.asm.tree.InsnNode;
8+
import org.objectweb.asm.tree.IntInsnNode;
9+
import org.objectweb.asm.tree.InvokeDynamicInsnNode;
10+
import org.objectweb.asm.tree.LdcInsnNode;
11+
import org.objectweb.asm.tree.MethodInsnNode;
12+
import org.objectweb.asm.tree.MethodNode;
13+
14+
import java.util.ArrayList;
15+
import java.util.List;
16+
import java.util.Optional;
17+
import java.util.Set;
18+
import java.util.Spliterator;
19+
import java.util.Spliterators;
20+
import java.util.function.Predicate;
21+
import java.util.stream.Collectors;
22+
import java.util.stream.StreamSupport;
23+
24+
public class RouteArgumentParser {
25+
26+
public static List<RouteArgument> parse(ExecutionContext ctx, MethodNode node) {
27+
List<MethodInsnNode> methodInsnNodes = StreamSupport.stream(
28+
Spliterators.spliteratorUnknownSize(node.instructions.iterator(), Spliterator.ORDERED),
29+
false)
30+
.filter(MethodInsnNode.class::isInstance)
31+
.map(MethodInsnNode.class::cast)
32+
.filter(i -> i.owner.equals("io/jooby/Context"))
33+
.collect(Collectors.toList());
34+
List<RouteArgument> args = new ArrayList<>();
35+
for (MethodInsnNode methodInsnNode : methodInsnNodes) {
36+
Signature signature = Signature.create(methodInsnNode);
37+
RouteArgument argument = new RouteArgument();
38+
if (signature.matches("path")) {
39+
argument.setHttpType(HttpType.PATH);
40+
if (signature.matches(String.class)) {
41+
argument.setName(argumentName(methodInsnNode));
42+
argumentType(argument, methodInsnNode);
43+
} else if (signature.matches(Class.class)) {
44+
argument.setName(signature.getMethod());
45+
argumentContextToType(argument, methodInsnNode);
46+
} else {
47+
// Unsupported query usage
48+
}
49+
} else if (signature.matches("query")) {
50+
argument.setHttpType(HttpType.QUERY);
51+
if (signature.matches(String.class)) {
52+
argument.setName(argumentName(methodInsnNode));
53+
argumentType(argument, methodInsnNode);
54+
} else if (signature.matches(Class.class)) {
55+
argument.setName(signature.getMethod());
56+
argumentContextToType(argument, methodInsnNode);
57+
} else {
58+
// Unsupported query usage
59+
}
60+
} else if (signature.matches("form") || signature.matches("multipart")) {
61+
argument.setHttpType(HttpType.FORM);
62+
if (signature.matches(String.class)) {
63+
argument.setName(argumentName(methodInsnNode));
64+
argumentType(argument, methodInsnNode);
65+
} else if (signature.matches(Class.class)) {
66+
argument.setName(signature.getMethod());
67+
argumentContextToType(argument, methodInsnNode);
68+
} else {
69+
// Unsupported query usage
70+
}
71+
}
72+
73+
if (argument.getJavaType() != null) {
74+
args.add(argument);
75+
} else {
76+
// Unsupported query usage
77+
}
78+
}
79+
return args;
80+
}
81+
82+
private static void argumentContextToType(RouteArgument argument, MethodInsnNode node) {
83+
Type type = InsnSupport.prev(node)
84+
.filter(LdcInsnNode.class::isInstance)
85+
.findFirst()
86+
.map(LdcInsnNode.class::cast)
87+
.filter(i -> i.cst instanceof Type)
88+
.map(i -> (Type) i.cst)
89+
.orElseThrow(() -> new IllegalStateException(
90+
"Parameter type not found, for: " + argument.getName()));
91+
argument.setJavaType(type.getClassName());
92+
argument.setSingle(false);
93+
}
94+
95+
private static void argumentType(RouteArgument argument, MethodInsnNode node) {
96+
MethodInsnNode convertCall = InsnSupport.next(node)
97+
.filter(valueOwner())
98+
.map(MethodInsnNode.class::cast)
99+
.findFirst()
100+
.orElseThrow(() -> new IllegalStateException(
101+
"Parameter type not found, for: " + argument.getName()));
102+
Signature convert = Signature.create(convertCall);
103+
if (convert.matches("value") || convert.matches("valueOrNull") || convert.getMethod()
104+
.endsWith("Value")) {
105+
argument.setJavaType(Type.getReturnType(convertCall.desc).getClassName());
106+
if (convert.matches("valueOrNull")) {
107+
argument.setRequired(false);
108+
} else {
109+
if (convert.getParameterCount() == 0) {
110+
argument.setRequired(true);
111+
} else {
112+
argument.setRequired(false);
113+
argument.setDefaultValue(argumentDefaultValue(convertCall.getPrevious()));
114+
}
115+
}
116+
} else if (convert.matches("toList")) {
117+
argument.setJavaType(toGenericOne(convertCall, convert, List.class));
118+
argument.setRequired(false);
119+
} else if (convert.matches("toSet")) {
120+
argument.setJavaType(toGenericOne(convertCall, convert, Set.class));
121+
argument.setRequired(false);
122+
} else if (convert.matches("toOptional")) {
123+
argument.setJavaType(toGenericOne(convertCall, convert, Optional.class));
124+
argument.setRequired(false);
125+
InsnSupport.next(convertCall)
126+
.filter(optionalOrElse())
127+
.findFirst()
128+
.map(MethodInsnNode.class::cast)
129+
.ifPresent(elseCall -> {
130+
// validate the else branch belong to the same toOptional
131+
InsnSupport.prev(elseCall).filter(valueToOptional())
132+
.findFirst()
133+
.map(MethodInsnNode.class::cast)
134+
.ifPresent(toOptional -> {
135+
if (toOptional.equals(convertCall)) {
136+
argument.setDefaultValue(argumentDefaultValue(elseCall.getPrevious()));
137+
}
138+
});
139+
});
140+
} else if (convert.matches("to")) {
141+
Type toType = InsnSupport.prev(convertCall)
142+
.filter(LdcInsnNode.class::isInstance)
143+
.findFirst()
144+
.map(LdcInsnNode.class::cast)
145+
.map(e -> (Type) e.cst)
146+
.orElseThrow(() -> new IllegalStateException(
147+
"Parameter type not found: " + InsnSupport.toString(convertCall)));
148+
argument.setJavaType(toType.getClassName());
149+
} else if (convert.matches("toEnum")) {
150+
Type toType = InsnSupport.prev(convertCall)
151+
.filter(InvokeDynamicInsnNode.class::isInstance)
152+
.map(InvokeDynamicInsnNode.class::cast)
153+
.filter(i -> i.name.equals("tryApply"))
154+
.findFirst()
155+
.map(i -> (Handle) i.bsmArgs[1])
156+
.map(h -> Type.getObjectType(h.getOwner()))
157+
.orElseThrow(() -> new IllegalStateException(
158+
"Parameter type not found: " + InsnSupport.toString(convertCall)));
159+
argument.setJavaType(toType.getClassName());
160+
argument.setRequired(true);
161+
} else if (convert.matches("toMap")) {
162+
argument.setJavaType("java.util.Map<java.lang.String,java.lang.String>");
163+
argument.setRequired(true);
164+
argument.setSingle(false);
165+
} else if (convert.matches("toMultimap")) {
166+
argument.setJavaType("java.util.Map<java.lang.String,java.util.List<java.lang.String>>");
167+
argument.setRequired(true);
168+
argument.setSingle(false);
169+
}
170+
}
171+
172+
private static Predicate<AbstractInsnNode> valueOwner() {
173+
return e -> {
174+
if (e instanceof MethodInsnNode) {
175+
return ((MethodInsnNode) e).owner.equals("io/jooby/Value") || ((MethodInsnNode) e).owner
176+
.equals("io/jooby/ValueNode");
177+
}
178+
return false;
179+
};
180+
}
181+
182+
private static Predicate<AbstractInsnNode> optionalOrElse() {
183+
return e -> (e instanceof MethodInsnNode && (((MethodInsnNode) e).owner
184+
.equals("java/util/Optional")) && ((MethodInsnNode) e).name.equals("orElse"));
185+
}
186+
187+
private static Predicate<AbstractInsnNode> valueToOptional() {
188+
return valueOwner().and(e -> ((MethodInsnNode) e).name.equals("toOptional"));
189+
}
190+
191+
private static String toGenericOne(MethodInsnNode node, Signature signature, Class collectionType) {
192+
StringBuilder type = new StringBuilder(collectionType.getName());
193+
type.append("<");
194+
if (signature.matches(Class.class)) {
195+
String itemType = InsnSupport.prev(node).filter(LdcInsnNode.class::isInstance)
196+
.findFirst()
197+
.map(e -> ((Type) ((LdcInsnNode) e).cst).getClassName())
198+
.orElse(String.class.getName());
199+
type.append(itemType);
200+
} else {
201+
type.append(String.class.getName());
202+
}
203+
type.append(">");
204+
return type.toString();
205+
}
206+
207+
private static Object argumentDefaultValue(AbstractInsnNode n) {
208+
if (n instanceof LdcInsnNode) {
209+
Object cst = ((LdcInsnNode) n).cst;
210+
if (cst instanceof Type) {
211+
return ((Type) cst).getClassName();
212+
}
213+
return cst;
214+
} else if (n instanceof InsnNode) {
215+
InsnNode insn = (InsnNode) n;
216+
switch (insn.getOpcode()) {
217+
case Opcodes.ICONST_0:
218+
return 0;
219+
case Opcodes.ICONST_1:
220+
return 1;
221+
case Opcodes.ICONST_2:
222+
return 2;
223+
case Opcodes.ICONST_3:
224+
return 3;
225+
case Opcodes.ICONST_4:
226+
return 4;
227+
case Opcodes.ICONST_5:
228+
return 5;
229+
case Opcodes.LCONST_0:
230+
return 0L;
231+
case Opcodes.LCONST_1:
232+
return 1L;
233+
case Opcodes.FCONST_0:
234+
return 0f;
235+
case Opcodes.FCONST_1:
236+
return 1f;
237+
case Opcodes.FCONST_2:
238+
return 2f;
239+
case Opcodes.DCONST_0:
240+
return 0d;
241+
case Opcodes.DCONST_1:
242+
return 1d;
243+
case Opcodes.ICONST_M1:
244+
return -1;
245+
case Opcodes.ACONST_NULL:
246+
return null;
247+
}
248+
} else if (n instanceof IntInsnNode) {
249+
return ((IntInsnNode) n).operand;
250+
}
251+
return null;
252+
}
253+
254+
private static String argumentName(MethodInsnNode node) {
255+
return InsnSupport.prev(node)
256+
.filter(LdcInsnNode.class::isInstance)
257+
.map(it -> ((LdcInsnNode) it).cst.toString())
258+
.findFirst()
259+
.orElseThrow(() -> new IllegalStateException(
260+
"Parameter name not found: " + InsnSupport.toString(node)));
261+
}
262+
263+
}

0 commit comments

Comments
 (0)