Skip to content
This repository was archived by the owner on Mar 3, 2026. It is now read-only.

Commit f759e98

Browse files
committed
Kotlin: API tool does not detect required model properties fix jooby-project#1072
1 parent 09c46d6 commit f759e98

File tree

10 files changed

+176
-8
lines changed

10 files changed

+176
-8
lines changed

modules/jooby-apitool/pom.xml

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -203,6 +203,17 @@
203203
<artifactId>jackson-dataformat-yaml</artifactId>
204204
</dependency>
205205

206+
<dependency>
207+
<groupId>com.fasterxml.jackson.module</groupId>
208+
<artifactId>jackson-module-kotlin</artifactId>
209+
<optional>true</optional>
210+
</dependency>
211+
212+
<dependency>
213+
<groupId>com.fasterxml.jackson.datatype</groupId>
214+
<artifactId>jackson-datatype-jdk8</artifactId>
215+
</dependency>
216+
206217
<!-- commons-lang3 -->
207218
<dependency>
208219
<groupId>org.apache.commons</groupId>

modules/jooby-apitool/src/main/java/org/jooby/apitool/RouteResponse.java

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -211,6 +211,7 @@
211211
import java.util.Map;
212212
import java.util.Objects;
213213
import java.util.Optional;
214+
import java.util.stream.Stream;
214215

215216
/**
216217
* Represent a route response type.
@@ -262,7 +263,18 @@ public RouteResponse type(Type type) {
262263
* @return Response description.
263264
*/
264265
public Optional<String> description() {
265-
return Optional.ofNullable(description);
266+
if (description == null) {
267+
String typename;
268+
if (type instanceof Class) {
269+
typename = ((Class) type).getCanonicalName();
270+
} else {
271+
typename = type.getTypeName();
272+
}
273+
return Stream.of(typename.split("\\."))
274+
.filter(name -> name.length() > 0)
275+
.reduce((it, next) -> next);
276+
}
277+
return Optional.of(description);
266278
}
267279

268280
/**

modules/jooby-apitool/src/main/java/org/jooby/internal/apitool/BytecodeRouteParser.java

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -435,9 +435,18 @@ public List<RouteMethod> parse(String classname) throws Exception {
435435
}
436436
}
437437
}
438+
return typeAnalizer(methods);
439+
}
440+
441+
private List<RouteMethod> typeAnalizer(List<RouteMethod> methods) {
442+
methods.forEach(this::typeAnalizer);
438443
return methods;
439444
}
440445

446+
private void typeAnalizer(RouteMethod route) {
447+
route.parameters().forEach(p -> p.type());
448+
}
449+
441450
private RouteMethod javadoc(final RouteMethod method, final Optional<DocItem> doc) {
442451
doc.ifPresent(it -> {
443452
method.description(it.text);

modules/jooby-apitool/src/main/java/org/jooby/internal/apitool/SwaggerBuilder.java

Lines changed: 48 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -203,6 +203,10 @@
203203
*/
204204
package org.jooby.internal.apitool;
205205

206+
import com.fasterxml.jackson.databind.BeanDescription;
207+
import com.fasterxml.jackson.databind.Module;
208+
import com.fasterxml.jackson.databind.introspect.BeanPropertyDefinition;
209+
import com.fasterxml.jackson.datatype.jdk8.Jdk8Module;
206210
import static com.google.common.base.CaseFormat.LOWER_CAMEL;
207211
import static com.google.common.base.CaseFormat.UPPER_CAMEL;
208212
import com.google.common.base.Splitter;
@@ -233,6 +237,7 @@
233237
import io.swagger.models.properties.PropertyBuilder;
234238
import io.swagger.models.properties.PropertyBuilder.PropertyId;
235239
import io.swagger.util.Json;
240+
import io.swagger.util.Yaml;
236241
import org.jooby.MediaType;
237242
import org.jooby.Upload;
238243
import org.jooby.apitool.RouteMethod;
@@ -259,7 +264,7 @@ public class SwaggerBuilder {
259264
.omitEmptyStrings()
260265
.split(r.pattern())
261266
.iterator();
262-
return segments.hasNext() ? segments.next() : "";
267+
return segments.hasNext() ? segments.next() : r.pattern();
263268
};
264269
private Function<RouteMethod, String> tagger = TAG_PROVIDER;
265270

@@ -285,6 +290,19 @@ public class SwaggerBuilder {
285290
}
286291
}
287292
});
293+
/** Kotlin module? */
294+
try {
295+
Module module = (Module) SwaggerBuilder.class.getClassLoader()
296+
.loadClass("com.fasterxml.jackson.module.kotlin.KotlinModule").newInstance();
297+
Json.mapper().registerModule(module);
298+
Yaml.mapper().registerModule(module);
299+
} catch (ClassNotFoundException | InstantiationException | IllegalAccessException x) {
300+
// Nop
301+
}
302+
/** Java8/Optional: */
303+
Jdk8Module jdk8 = new Jdk8Module();
304+
Json.mapper().registerModule(jdk8);
305+
Yaml.mapper().registerModule(jdk8);
288306
}
289307

290308
public SwaggerBuilder() {
@@ -305,6 +323,7 @@ public Swagger build(Swagger base, final List<RouteMethod> routes) throws Except
305323
Tag tag = new Tag();
306324
if (value.length() > 0) {
307325
tag.name(Character.toUpperCase(value.charAt(0)) + value.substring(1));
326+
swagger.addTag(tag);
308327
} else {
309328
tag.name(value);
310329
}
@@ -324,12 +343,12 @@ public Swagger build(Swagger base, final List<RouteMethod> routes) throws Except
324343
ModelConverters converter = ModelConverters.getInstance();
325344
/** Model factory: */
326345
Function<Type, Model> modelFactory = type -> {
327-
Property property = converter.readAsProperty(type);
328-
329-
Map<PropertyId, Object> args = new EnumMap<>(PropertyId.class);
330346
for (Map.Entry<String, Model> entry : converter.readAll(type).entrySet()) {
331-
swagger.addDefinition(entry.getKey(), entry.getValue());
347+
swagger.addDefinition(entry.getKey(), doModel(type, entry.getValue()));
332348
}
349+
// reference:
350+
Property property = converter.readAsProperty(type);
351+
Map<PropertyId, Object> args = new EnumMap<>(PropertyId.class);
333352
return PropertyBuilder.toModel(PropertyBuilder.merge(property, args));
334353
};
335354

@@ -413,7 +432,8 @@ public Swagger build(Swagger base, final List<RouteMethod> routes) throws Except
413432
Map<Integer, String> status = returns.status();
414433
Integer statusCode = returns.statusCode();
415434
Response response = new Response();
416-
String doc = returns.description().orElse(status.getOrDefault(statusCode, statusCode.toString()));
435+
String doc = returns.description()
436+
.orElse(status.getOrDefault(statusCode, statusCode.toString()));
417437
response.description(doc);
418438
if (!"void".equals(returns.type().getTypeName())) {
419439
// make sure type definition gets in
@@ -447,6 +467,28 @@ public Swagger build(Swagger base, final List<RouteMethod> routes) throws Except
447467
return swagger;
448468
}
449469

470+
/**
471+
* Mostly for kotlin null safe operator and immutable properties.
472+
*
473+
* @param type Target type.
474+
* @param model Model.
475+
* @return Input model.
476+
*/
477+
private Model doModel(Type type, Model model) {
478+
Map<String, Property> properties = model.getProperties();
479+
if (properties != null) {
480+
BeanDescription desc = Json.mapper().getSerializationConfig()
481+
.introspect(Json.mapper().constructType(type));
482+
for (BeanPropertyDefinition beanProperty : desc.findProperties()) {
483+
Property property = properties.get(beanProperty.getName());
484+
if (property != null) {
485+
property.setRequired(beanProperty.isRequired());
486+
}
487+
}
488+
}
489+
return model;
490+
}
491+
450492
private SerializableParameter complement(Property property, RouteParameter source,
451493
SerializableParameter param) {
452494
param.setType(property.getType());

modules/jooby-apitool/src/test/java/issues/Issue1050.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ public void mediaTypeOnControllers() throws IOException {
3131
+ " get:\n"
3232
+ " responses:\n"
3333
+ " 200:\n"
34+
+ " description: String\n"
3435
+ " body:\n"
3536
+ " application/json:\n"
3637
+ " type: string\n"
@@ -41,6 +42,7 @@ public void mediaTypeOnControllers() throws IOException {
4142
+ " get:\n"
4243
+ " responses:\n"
4344
+ " 200:\n"
45+
+ " description: String\n"
4446
+ " body:\n"
4547
+ " application/json:\n"
4648
+ " type: string\n"

modules/jooby-apitool/src/test/java/issues/Issue1070.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -128,7 +128,7 @@ public void shouldContainsSwaggerResponseDescription() throws Exception {
128128
+ " \"parameters\" : [ ],\n"
129129
+ " \"responses\" : {\n"
130130
+ " \"200\" : {\n"
131-
+ " \"description\" : \"200\",\n"
131+
+ " \"description\" : \"String\",\n"
132132
+ " \"schema\" : {\n"
133133
+ " \"type\" : \"string\"\n"
134134
+ " }\n"
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
package issues;
2+
3+
import io.swagger.util.Yaml;
4+
import kt.App1072;
5+
import org.jooby.apitool.ApiParser;
6+
import org.jooby.apitool.RouteMethod;
7+
import org.jooby.apitool.RouteMethodAssert;
8+
import org.jooby.internal.apitool.SwaggerBuilder;
9+
import static org.junit.Assert.assertEquals;
10+
import org.junit.Test;
11+
12+
import java.nio.file.Path;
13+
import java.nio.file.Paths;
14+
import java.util.List;
15+
16+
public class Issue1072 {
17+
18+
@Test
19+
public void shouldContainsSwaggerResponseDescription() throws Exception {
20+
List<RouteMethod> routes = new ApiParser(dir()).parseFully(new App1072());
21+
new RouteMethodAssert(routes)
22+
.next(r -> {
23+
r.returnType("kt.Person");
24+
r.method("GET");
25+
r.pattern("/");
26+
r.description(null);
27+
r.summary(null);
28+
r.returns("Person");
29+
})
30+
.done();
31+
assertEquals("---\n"
32+
+ "swagger: \"2.0\"\n"
33+
+ "tags:\n"
34+
+ "- name: \"/\"\n"
35+
+ "consumes:\n"
36+
+ "- \"application/json\"\n"
37+
+ "produces:\n"
38+
+ "- \"application/json\"\n"
39+
+ "paths:\n"
40+
+ " /:\n"
41+
+ " get:\n"
42+
+ " tags:\n"
43+
+ " - \"/\"\n"
44+
+ " operationId: \"get/\"\n"
45+
+ " parameters: []\n"
46+
+ " responses:\n"
47+
+ " 200:\n"
48+
+ " description: \"Person\"\n"
49+
+ " schema:\n"
50+
+ " $ref: \"#/definitions/Person\"\n"
51+
+ "definitions:\n"
52+
+ " Person:\n"
53+
+ " type: \"object\"\n"
54+
+ " required:\n"
55+
+ " - \"name\"\n"
56+
+ " properties:\n"
57+
+ " name:\n"
58+
+ " type: \"string\"\n"
59+
+ " firstname:\n"
60+
+ " type: \"string\"\n", Yaml
61+
.mapper().writer().withDefaultPrettyPrinter().writeValueAsString(new SwaggerBuilder()
62+
.build(null, routes)));
63+
}
64+
65+
private Path dir() {
66+
Path userdir = Paths.get(System.getProperty("user.dir"));
67+
if (!userdir.toString().endsWith("jooby-apitool")) {
68+
userdir = userdir.resolve("modules").resolve("jooby-apitool");
69+
}
70+
return userdir;
71+
}
72+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
package kt
2+
3+
import org.jooby.Kooby
4+
5+
data class Person(
6+
val name: String,
7+
val firstname: String?)
8+
9+
class App1072 : Kooby({
10+
get {
11+
Person("John Doe", null)
12+
}
13+
})

modules/jooby-apitool/src/test/java/org/jooby/apitool/Cat.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,10 @@ public class Cat {
44

55
private String name;
66

7+
public String getName() {
8+
return name;
9+
}
10+
711
public void setName(final String name) {
812
this.name = name;
913
}

modules/jooby-apitool/src/test/java/org/jooby/apitool/RamlTest.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ public void shouldWorkWithUUID() throws Exception {
7070
+ " get:\n"
7171
+ " responses:\n"
7272
+ " 200:\n"
73+
+ " description: Client\n"
7374
+ " body:\n"
7475
+ " application/json:\n"
7576
+ " type: Client\n", yaml);
@@ -112,6 +113,7 @@ public void shouldUseCustomType() throws Exception {
112113
+ " get:\n"
113114
+ " responses:\n"
114115
+ " 200:\n"
116+
+ " description: Foo\n"
115117
+ " body:\n"
116118
+ " application/json:\n"
117119
+ " type: Foo\n", yaml);
@@ -167,6 +169,7 @@ public void shouldExportToRaml() throws Exception {
167169
+ " description: Home page.\n"
168170
+ " responses:\n"
169171
+ " 200:\n"
172+
+ " description: String\n"
170173
+ " body:\n"
171174
+ " application/json:\n"
172175
+ " type: string\n"

0 commit comments

Comments
 (0)