Skip to content

Commit a1ce320

Browse files
Nullability in Kotlin and Java (#448)
- new parameters: nullabilityDefinition, nullableAnnotations - added JTypeWithNullability and TsType.NullableType - TypeParser for nullability (Kotlin reflection, Java type annotations) - support in JAX-RS and Spring
1 parent 6961b2e commit a1ce320

File tree

36 files changed

+1392
-229
lines changed

36 files changed

+1392
-229
lines changed

pom.xml

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,8 @@
5454

5555
<properties>
5656
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
57+
<java.version>1.8</java.version>
58+
<kotlin.version>1.3.61</kotlin.version>
5759
<junit.version>4.12</junit.version>
5860
<github.global.server>github</github.global.server>
5961
</properties>
@@ -66,8 +68,8 @@
6668
<artifactId>maven-compiler-plugin</artifactId>
6769
<version>3.8.0</version>
6870
<configuration>
69-
<source>1.8</source>
70-
<target>1.8</target>
71+
<source>${java.version}</source>
72+
<target>${java.version}</target>
7173
<showWarnings>true</showWarnings>
7274
<showDeprecation>true</showDeprecation>
7375
<compilerArgs>
@@ -76,6 +78,30 @@
7678
</compilerArgs>
7779
</configuration>
7880
</plugin>
81+
<plugin>
82+
<groupId>org.jetbrains.kotlin</groupId>
83+
<artifactId>kotlin-maven-plugin</artifactId>
84+
<version>${kotlin.version}</version>
85+
<executions>
86+
<!-- <execution>
87+
<id>compile</id>
88+
<phase>compile</phase>
89+
<goals>
90+
<goal>compile</goal>
91+
</goals>
92+
</execution> -->
93+
<execution>
94+
<id>test-compile</id>
95+
<phase>test-compile</phase>
96+
<goals>
97+
<goal>test-compile</goal>
98+
</goals>
99+
</execution>
100+
</executions>
101+
<configuration>
102+
<jvmTarget>${java.version}</jvmTarget>
103+
</configuration>
104+
</plugin>
79105
<plugin>
80106
<groupId>org.apache.maven.plugins</groupId>
81107
<artifactId>maven-surefire-plugin</artifactId>

typescript-generator-core/pom.xml

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,16 @@
6767
<artifactId>gson</artifactId>
6868
<version>2.8.5</version>
6969
</dependency>
70+
<dependency>
71+
<groupId>org.jetbrains.kotlin</groupId>
72+
<artifactId>kotlin-stdlib</artifactId>
73+
<version>${kotlin.version}</version>
74+
</dependency>
75+
<dependency>
76+
<groupId>org.jetbrains.kotlin</groupId>
77+
<artifactId>kotlin-reflect</artifactId>
78+
<version>${kotlin.version}</version>
79+
</dependency>
7080
<!--test dependencies-->
7181
<dependency>
7282
<groupId>junit</groupId>
@@ -122,6 +132,12 @@
122132
<version>2.10.1</version>
123133
<scope>test</scope>
124134
</dependency>
135+
<dependency>
136+
<groupId>org.checkerframework</groupId>
137+
<artifactId>checker-qual</artifactId>
138+
<version>3.0.1</version>
139+
<scope>test</scope>
140+
</dependency>
125141
</dependencies>
126142

127143
<build>

typescript-generator-core/src/main/java/cz/habarta/typescript/generator/DefaultTypeProcessor.java

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11

22
package cz.habarta.typescript.generator;
33

4-
import cz.habarta.typescript.generator.util.UnionType;
4+
import cz.habarta.typescript.generator.type.JTypeWithNullability;
5+
import cz.habarta.typescript.generator.type.JUnionType;
56
import cz.habarta.typescript.generator.util.Utils;
67
import java.lang.reflect.GenericArrayType;
78
import java.lang.reflect.Method;
@@ -137,9 +138,9 @@ public Result processType(Type javaType, Context context) {
137138
? context.processType(upperBounds[0])
138139
: new Result(TsType.Any);
139140
}
140-
if (javaType instanceof UnionType) {
141-
final UnionType unionType = (UnionType) javaType;
142-
final List<Result> results = unionType.types.stream()
141+
if (javaType instanceof JUnionType) {
142+
final JUnionType unionType = (JUnionType) javaType;
143+
final List<Result> results = unionType.getTypes().stream()
143144
.map(type -> context.processType(type))
144145
.collect(Collectors.toList());
145146
return new Result(
@@ -151,6 +152,14 @@ public Result processType(Type javaType, Context context) {
151152
.collect(Collectors.toList())
152153
);
153154
}
155+
if (javaType instanceof JTypeWithNullability) {
156+
final JTypeWithNullability typeWithNullability = (JTypeWithNullability) javaType;
157+
final Result result = context.processType(typeWithNullability.getType());
158+
return new Result(
159+
typeWithNullability.isNullable() ? new TsType.NullableType(result.getTsType()) : result.getTsType(),
160+
result.getDiscoveredClasses()
161+
);
162+
}
154163
return null;
155164
}
156165

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
2+
package cz.habarta.typescript.generator;
3+
4+
import java.util.Arrays;
5+
import java.util.List;
6+
7+
8+
public enum NullabilityDefinition {
9+
10+
nullAndUndefinedUnion (false, TsType.Null, TsType.Undefined),
11+
nullUnion (false, TsType.Null),
12+
undefinedUnion (false, TsType.Undefined),
13+
nullAndUndefinedInlineUnion (true, TsType.Null, TsType.Undefined),
14+
nullInlineUnion (true, TsType.Null),
15+
undefinedInlineUnion (true, TsType.Undefined);
16+
17+
private final boolean isInline;
18+
private final List<TsType> types;
19+
20+
private NullabilityDefinition(boolean isInline, TsType... types) {
21+
this.isInline = isInline;
22+
this.types = Arrays.asList(types);
23+
}
24+
25+
public boolean isInline() {
26+
return isInline;
27+
}
28+
29+
public List<TsType> getTypes() {
30+
return types;
31+
}
32+
33+
public boolean containsUndefined() {
34+
return types.contains(TsType.Undefined);
35+
}
36+
37+
public boolean containsNull() {
38+
return types.contains(TsType.Null);
39+
}
40+
41+
}

typescript-generator-core/src/main/java/cz/habarta/typescript/generator/Settings.java

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,18 @@
88
import cz.habarta.typescript.generator.emitter.EmitterExtensionFeatures;
99
import cz.habarta.typescript.generator.parser.JaxrsApplicationParser;
1010
import cz.habarta.typescript.generator.parser.RestApplicationParser;
11+
import cz.habarta.typescript.generator.parser.TypeParser;
1112
import cz.habarta.typescript.generator.util.Pair;
1213
import cz.habarta.typescript.generator.util.Utils;
1314
import java.io.File;
1415
import java.lang.annotation.Annotation;
16+
import java.lang.annotation.ElementType;
17+
import java.lang.annotation.Target;
1518
import java.lang.reflect.TypeVariable;
1619
import java.net.URL;
1720
import java.net.URLClassLoader;
1821
import java.util.ArrayList;
22+
import java.util.Arrays;
1923
import java.util.Collections;
2024
import java.util.LinkedHashMap;
2125
import java.util.LinkedHashSet;
@@ -53,6 +57,8 @@ public class Settings {
5357
@Deprecated public boolean declarePropertiesAsOptional = false;
5458
public OptionalProperties optionalProperties; // default is OptionalProperties.useSpecifiedAnnotations
5559
public OptionalPropertiesDeclaration optionalPropertiesDeclaration; // default is OptionalPropertiesDeclaration.questionMark
60+
public NullabilityDefinition nullabilityDefinition; // default is NullabilityDefinition.nullInlineUnion
61+
private TypeParser typeParser = null;
5662
public boolean declarePropertiesAsReadOnly = false;
5763
public String removeTypeNamePrefix = null;
5864
public String removeTypeNameSuffix = null;
@@ -102,6 +108,7 @@ public class Settings {
102108
public List<Class<? extends Annotation>> includePropertyAnnotations = new ArrayList<>();
103109
public List<Class<? extends Annotation>> excludePropertyAnnotations = new ArrayList<>();
104110
public List<Class<? extends Annotation>> optionalAnnotations = new ArrayList<>();
111+
public List<Class<? extends Annotation>> nullableAnnotations = new ArrayList<>();
105112
public boolean generateInfoJson = false;
106113
public boolean generateNpmPackageJson = false;
107114
public String npmName = null;
@@ -228,6 +235,10 @@ public void loadOptionalAnnotations(ClassLoader classLoader, List<String> option
228235
this.optionalAnnotations = loadClasses(classLoader, optionalAnnotations, Annotation.class);
229236
}
230237

238+
public void loadNullableAnnotations(ClassLoader classLoader, List<String> nullableAnnotations) {
239+
this.nullableAnnotations = loadClasses(classLoader, nullableAnnotations, Annotation.class);
240+
}
241+
231242
public void loadJackson2Modules(ClassLoader classLoader, List<String> jackson2Modules) {
232243
this.jackson2Modules = loadClasses(classLoader, jackson2Modules, Module.class);
233244
}
@@ -320,6 +331,24 @@ public void validate() {
320331
if (mapClassesAsClassesPatterns != null && mapClasses != ClassMapping.asClasses) {
321332
throw new RuntimeException("'mapClassesAsClassesPatterns' parameter can only be used when 'mapClasses' parameter is set to 'asClasses'.");
322333
}
334+
for (Class<? extends Annotation> annotation : optionalAnnotations) {
335+
final Target target = annotation.getAnnotation(Target.class);
336+
final List<ElementType> elementTypes = target != null ? Arrays.asList(target.value()) : Arrays.asList();
337+
if (elementTypes.contains(ElementType.TYPE_PARAMETER) || elementTypes.contains(ElementType.TYPE_USE)) {
338+
TypeScriptGenerator.getLogger().info(String.format(
339+
"Suggestion: annotation '%s' supports 'TYPE_PARAMETER' or 'TYPE_USE' target. Consider using 'nullableAnnotations' parameter instead of 'optionalAnnotations'.",
340+
annotation.getName()));
341+
}
342+
}
343+
for (Class<? extends Annotation> annotation : nullableAnnotations) {
344+
final Target target = annotation.getAnnotation(Target.class);
345+
final List<ElementType> elementTypes = target != null ? Arrays.asList(target.value()) : Arrays.asList();
346+
if (!elementTypes.contains(ElementType.TYPE_PARAMETER) && !elementTypes.contains(ElementType.TYPE_USE)) {
347+
throw new RuntimeException(String.format(
348+
"'%s' annotation cannot be used as nullable annotation because it doesn't have 'TYPE_PARAMETER' or 'TYPE_USE' target.",
349+
annotation.getName()));
350+
}
351+
}
323352
if (generateJaxrsApplicationClient && outputFileType != TypeScriptFileType.implementationFile) {
324353
throw new RuntimeException("'generateJaxrsApplicationClient' can only be used when generating implementation file ('outputFileType' parameter is 'implementationFile').");
325354
}
@@ -394,6 +423,17 @@ public void validate() {
394423
}
395424
}
396425

426+
public NullabilityDefinition getNullabilityDefinition() {
427+
return nullabilityDefinition != null ? nullabilityDefinition : NullabilityDefinition.nullInlineUnion;
428+
}
429+
430+
public TypeParser getTypeParser() {
431+
if (typeParser == null) {
432+
typeParser = new TypeParser(nullableAnnotations);
433+
}
434+
return typeParser;
435+
}
436+
397437
public List<CustomTypeMapping> getValidatedCustomTypeMappings() {
398438
if (validatedCustomTypeMappings == null) {
399439
validatedCustomTypeMappings = new ArrayList<>();

typescript-generator-core/src/main/java/cz/habarta/typescript/generator/TsType.java

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@
99
import java.util.Arrays;
1010
import java.util.LinkedHashSet;
1111
import java.util.List;
12+
import java.util.stream.Collectors;
13+
import java.util.stream.Stream;
1214

1315

1416
/**
@@ -238,6 +240,28 @@ public UnionType(List<? extends TsType> types) {
238240
this.types = new ArrayList<>(new LinkedHashSet<>(types));
239241
}
240242

243+
public static UnionType combine(List<? extends TsType> types) {
244+
return new UnionType(types.stream()
245+
.flatMap(type -> {
246+
if (type instanceof UnionType) {
247+
final UnionType unionType = (UnionType) type;
248+
return unionType.types.stream();
249+
} else {
250+
return Stream.of(type);
251+
}
252+
})
253+
.collect(Collectors.toList())
254+
);
255+
}
256+
257+
public UnionType add(List<TsType> types) {
258+
return new UnionType(Utils.concat(this.types, types));
259+
}
260+
261+
public UnionType remove(List<TsType> types) {
262+
return new UnionType(Utils.removeAll(this.types, types));
263+
}
264+
241265
@Override
242266
public String format(Settings settings) {
243267
return types.isEmpty()
@@ -298,6 +322,7 @@ public String format(Settings settings) {
298322

299323
}
300324

325+
// optionality should have been represented as attribute of properties and parameters
301326
public static class OptionalType extends TsType {
302327

303328
public final TsType type;
@@ -313,6 +338,23 @@ public String format(Settings settings) {
313338

314339
}
315340

341+
public static class NullableType extends TsType {
342+
343+
public static final String AliasName = "Nullable";
344+
345+
public final TsType type;
346+
347+
public NullableType(TsType type) {
348+
this.type = type;
349+
}
350+
351+
@Override
352+
public String format(Settings settings) {
353+
return AliasName + "<" + type.format(settings) + ">";
354+
}
355+
356+
}
357+
316358
public static class ObjectType extends TsType {
317359

318360
public final List<TsProperty> properties;

0 commit comments

Comments
 (0)