diff --git a/.gemini/GEMINI.md b/.gemini/GEMINI.md
new file mode 100644
index 00000000..e5e0d1af
--- /dev/null
+++ b/.gemini/GEMINI.md
@@ -0,0 +1 @@
+# Project Instructions
diff --git a/internal/src/main/java/online/sharedtype/processor/domain/type/TypeVariableInfo.java b/internal/src/main/java/online/sharedtype/processor/domain/type/TypeVariableInfo.java
index bd66e05d..d8bd0484 100644
--- a/internal/src/main/java/online/sharedtype/processor/domain/type/TypeVariableInfo.java
+++ b/internal/src/main/java/online/sharedtype/processor/domain/type/TypeVariableInfo.java
@@ -3,8 +3,11 @@
import lombok.Builder;
import lombok.EqualsAndHashCode;
+import java.util.Collections;
+import java.util.List;
import java.util.Map;
+
/**
* Represents a generic type variable.
*
@@ -21,7 +24,8 @@ public final class TypeVariableInfo extends ReferableTypeInfo {
private final String contextTypeQualifiedName; // TODO: reference to TypeDef to avoid string
private final String name;
private String qualifiedName;
- // TODO: support generic bounds
+ @Builder.Default
+ private final List bounds = Collections.emptyList();
public static String concatQualifiedName(String contextTypeQualifiedName, String name) {
return contextTypeQualifiedName + "@" + name;
@@ -35,6 +39,10 @@ public String name() {
return name;
}
+ public List bounds() {
+ return bounds;
+ }
+
public String qualifiedName() {
if (qualifiedName == null) {
qualifiedName = concatQualifiedName(contextTypeQualifiedName, name);
diff --git a/it/java8/src/main/java/online/sharedtype/it/java8/TypeBoundsIssue7.java b/it/java8/src/main/java/online/sharedtype/it/java8/TypeBoundsIssue7.java
new file mode 100644
index 00000000..1a040433
--- /dev/null
+++ b/it/java8/src/main/java/online/sharedtype/it/java8/TypeBoundsIssue7.java
@@ -0,0 +1,31 @@
+package online.sharedtype.it.java8;
+
+import online.sharedtype.SharedType;
+
+@SharedType
+public class TypeBoundsIssue7 {
+ @SharedType
+ public interface Shape {
+ double area();
+ }
+
+ @SharedType
+ public static class Circle implements Shape {
+ public double radius;
+
+ @Override
+ public double area() {
+ return 3.14 * radius * radius;
+ }
+ }
+
+ @SharedType
+ public static class ContainerBounds {
+ public T shape;
+ }
+
+ @SharedType
+ public static class MultiBoundContainer {
+ public T shape;
+ }
+}
diff --git a/processor/src/main/java/online/sharedtype/processor/parser/ClassTypeDefParser.java b/processor/src/main/java/online/sharedtype/processor/parser/ClassTypeDefParser.java
index adb51523..5d33525a 100644
--- a/processor/src/main/java/online/sharedtype/processor/parser/ClassTypeDefParser.java
+++ b/processor/src/main/java/online/sharedtype/processor/parser/ClassTypeDefParser.java
@@ -97,9 +97,28 @@ private List parseTypeVariables(TypeElement typeElement) {
TypeVariableInfo.builder()
.contextTypeQualifiedName(typeElement.getQualifiedName().toString())
.name(typeParameterElement.getSimpleName().toString())
+ .bounds(parseBounds(typeParameterElement, typeElement))
.build()
)
- .collect(Collectors.toList()); // TODO: type bounds
+ .collect(Collectors.toList());
+ }
+
+ private List parseBounds(TypeParameterElement typeParameterElement, TypeElement typeElement) {
+ return typeParameterElement.getBounds().stream()
+ .filter(b -> !"java.lang.Object".equals(b.toString()))
+ .filter(b -> {
+ Element e = ctx.getProcessingEnv().getTypeUtils().asElement(b);
+ if (e == null || ctx.isIgnored(e)) {
+ return false;
+ }
+ if (e instanceof TypeElement) {
+ TypeElement te = (TypeElement) e;
+ return !ctx.isOptionalType(te.getQualifiedName().toString());
+ }
+ return true;
+ })
+ .map(b -> typeInfoParser.parse(b, typeElement))
+ .collect(Collectors.toList());
}
private List parseSupertypes(TypeElement typeElement) {
diff --git a/processor/src/main/java/online/sharedtype/processor/writer/converter/GoStructConverter.java b/processor/src/main/java/online/sharedtype/processor/writer/converter/GoStructConverter.java
index 9638f889..21ad4c15 100644
--- a/processor/src/main/java/online/sharedtype/processor/writer/converter/GoStructConverter.java
+++ b/processor/src/main/java/online/sharedtype/processor/writer/converter/GoStructConverter.java
@@ -29,7 +29,20 @@ public Tuple convert(TypeDef typeDef) {
ClassDef classDef = (ClassDef) typeDef;
StructExpr value = new StructExpr(
classDef.simpleName(),
- classDef.typeVariables().stream().map(typeInfo -> typeExpressionConverter.toTypeExpr(typeInfo, typeDef)).collect(Collectors.toList()),
+ classDef.typeVariables().stream().map(typeVar -> {
+ String name = typeVar.name();
+ if (typeVar.bounds().isEmpty()) {
+ return name + " any";
+ }
+ String bounds = typeVar.bounds().stream()
+ .map(b -> typeExpressionConverter.toTypeExpr(b, typeDef))
+ .collect(Collectors.joining("; "));
+
+ if (typeVar.bounds().size() > 1) {
+ return name + " interface{ " + bounds + " }";
+ }
+ return name + " " + bounds;
+ }).collect(Collectors.toList()),
classDef.directSupertypes().stream().map(typeInfo1 -> typeExpressionConverter.toTypeExpr(typeInfo1, typeDef)).collect(Collectors.toList()),
gatherProperties(classDef)
);
@@ -64,7 +77,7 @@ String typeParametersExpr() {
if (typeParameters.isEmpty()) {
return null;
}
- return String.format("[%s any]", String.join(", ", typeParameters));
+ return String.format("[%s]", String.join(", ", typeParameters));
}
}
diff --git a/processor/src/main/java/online/sharedtype/processor/writer/converter/RustStructConverter.java b/processor/src/main/java/online/sharedtype/processor/writer/converter/RustStructConverter.java
index 858c6e33..4b9fb07f 100644
--- a/processor/src/main/java/online/sharedtype/processor/writer/converter/RustStructConverter.java
+++ b/processor/src/main/java/online/sharedtype/processor/writer/converter/RustStructConverter.java
@@ -47,7 +47,16 @@ public Tuple convert(TypeDef typeDef) {
ClassDef classDef = (ClassDef) typeDef;
StructExpr value = new StructExpr(
classDef.simpleName(),
- classDef.typeVariables().stream().map(typeInfo -> typeExpressionConverter.toTypeExpr(typeInfo, typeDef)).collect(Collectors.toList()),
+ classDef.typeVariables().stream().map(typeVar -> {
+ String name = typeVar.name();
+ if (typeVar.bounds().isEmpty()) {
+ return name;
+ }
+ String bounds = typeVar.bounds().stream()
+ .map(b -> typeExpressionConverter.toTypeExpr(b, typeDef))
+ .collect(Collectors.joining(" + "));
+ return name + ": " + bounds;
+ }).collect(Collectors.toList()),
gatherProperties(classDef),
rustMacroTraitsGenerator.generate(classDef)
);
diff --git a/processor/src/main/java/online/sharedtype/processor/writer/converter/TypescriptInterfaceConverter.java b/processor/src/main/java/online/sharedtype/processor/writer/converter/TypescriptInterfaceConverter.java
index c8885054..9e944368 100644
--- a/processor/src/main/java/online/sharedtype/processor/writer/converter/TypescriptInterfaceConverter.java
+++ b/processor/src/main/java/online/sharedtype/processor/writer/converter/TypescriptInterfaceConverter.java
@@ -38,7 +38,16 @@ public Tuple convert(TypeDef typeDef) {
Config config = ctx.getTypeStore().getConfig(typeDef);
InterfaceExpr value = new InterfaceExpr(
classDef.simpleName(),
- classDef.typeVariables().stream().map(typeInfo -> typeExpressionConverter.toTypeExpr(typeInfo, typeDef)).collect(Collectors.toList()),
+ classDef.typeVariables().stream().map(typeVar -> {
+ String name = typeVar.name();
+ if (typeVar.bounds().isEmpty()) {
+ return name;
+ }
+ String bounds = typeVar.bounds().stream()
+ .map(b -> typeExpressionConverter.toTypeExpr(b, typeDef))
+ .collect(Collectors.joining(" & "));
+ return name + " extends " + bounds;
+ }).collect(Collectors.toList()),
classDef.directSupertypes().stream().map(typeInfo1 -> typeExpressionConverter.toTypeExpr(typeInfo1, typeDef)).collect(Collectors.toList()),
classDef.components().stream().map(field -> toPropertyExpr(field, typeDef, config)).collect(Collectors.toList())
);
diff --git a/processor/src/test/java/online/sharedtype/processor/parser/ClassTypeDefParserTest.java b/processor/src/test/java/online/sharedtype/processor/parser/ClassTypeDefParserTest.java
index 52ea7a31..1423e634 100644
--- a/processor/src/test/java/online/sharedtype/processor/parser/ClassTypeDefParserTest.java
+++ b/processor/src/test/java/online/sharedtype/processor/parser/ClassTypeDefParserTest.java
@@ -156,4 +156,60 @@ void ignoreGlobalConfiguredField() {
var classDef = parser.parse(typeElement).get(0);
assertThat(classDef.components()).isEmpty();
}
+
+ @Test
+ void parseTypeBounds() {
+ var boundTypeMock = ctxMocks.typeElement("com.example.Bound");
+ var boundType = boundTypeMock.type();
+ var typeParam = ctxMocks.typeParameter("T");
+
+ java.util.List bounds = new java.util.ArrayList<>();
+ bounds.add(boundType);
+ org.mockito.Mockito.doReturn(bounds).when(typeParam.element()).getBounds();
+
+ var clazz = ctxMocks.typeElement("com.example.MyClass")
+ .withTypeParameters(typeParam.element())
+ .element();
+
+ var parsedBoundType = ConcreteTypeInfo.builder().qualifiedName("com.example.Bound").build();
+ when(typeInfoParser.parse(boundType, clazz)).thenReturn(parsedBoundType);
+
+ var parsedSelfTypeInfo = ConcreteTypeInfo.builder().qualifiedName("com.example.MyClass").build();
+ when(typeInfoParser.parse(clazz.asType(), clazz)).thenReturn(parsedSelfTypeInfo);
+
+ var classDefs = parser.parse(clazz);
+ var classDef = (ClassDef)classDefs.get(0);
+
+ assertThat(classDef.typeVariables()).hasSize(1);
+ var typeVar = classDef.typeVariables().get(0);
+ assertThat(typeVar.name()).isEqualTo("T");
+ assertThat(typeVar.bounds()).containsExactly(parsedBoundType);
+ }
+
+ @Test
+ void parseTypeBoundsWithObject() {
+ var objectTypeMock = ctxMocks.typeElement("java.lang.Object");
+ var objectType = objectTypeMock.type();
+ var typeParam = ctxMocks.typeParameter("T");
+
+ java.util.List bounds = new java.util.ArrayList<>();
+ bounds.add(objectType);
+ org.mockito.Mockito.doReturn(bounds).when(typeParam.element()).getBounds();
+ when(objectType.toString()).thenReturn("java.lang.Object");
+
+ var clazz = ctxMocks.typeElement("com.example.MyClass")
+ .withTypeParameters(typeParam.element())
+ .element();
+
+ var parsedSelfTypeInfo = ConcreteTypeInfo.builder().qualifiedName("com.example.MyClass").build();
+ when(typeInfoParser.parse(clazz.asType(), clazz)).thenReturn(parsedSelfTypeInfo);
+
+ var classDefs = parser.parse(clazz);
+ var classDef = (ClassDef)classDefs.get(0);
+
+ assertThat(classDef.typeVariables()).hasSize(1);
+ var typeVar = classDef.typeVariables().get(0);
+ assertThat(typeVar.name()).isEqualTo("T");
+ assertThat(typeVar.bounds()).isEmpty();
+ }
}
diff --git a/processor/src/test/java/online/sharedtype/processor/writer/converter/GoStructConverterTest.java b/processor/src/test/java/online/sharedtype/processor/writer/converter/GoStructConverterTest.java
index f2e2bfe5..252daf0d 100644
--- a/processor/src/test/java/online/sharedtype/processor/writer/converter/GoStructConverterTest.java
+++ b/processor/src/test/java/online/sharedtype/processor/writer/converter/GoStructConverterTest.java
@@ -85,7 +85,7 @@ void convert() {
assertThat(data).isNotNull();
var model = (GoStructConverter.StructExpr) data.b();
assertThat(model.name).isEqualTo("ClassA");
- assertThat(model.typeParameters).containsExactly("T");
+ assertThat(model.typeParameters).containsExactly("T any");
assertThat(model.typeParametersExpr()).isEqualTo("[T any]");
assertThat(model.supertypes).containsExactly("SuperClassA[string]");
@@ -115,6 +115,33 @@ void convert() {
assertThat(prop5.type).isEqualTo("map[string]int32");
}
+ @Test
+ void convertTypeWithBounds() {
+ ClassDef classDef = ClassDef.builder()
+ .simpleName("ClassA")
+ .qualifiedName("com.github.cuzfrog.ClassA")
+ .typeVariables(List.of(
+ TypeVariableInfo.builder()
+ .name("T")
+ .bounds(List.of(
+ ConcreteTypeInfo.builder()
+ .qualifiedName("com.github.cuzfrog.Shape")
+ .simpleName("Shape")
+ .build()
+ ))
+ .build()
+ ))
+ .components(List.of())
+ .build();
+
+ var data = converter.convert(classDef);
+ var model = (GoStructConverter.StructExpr) data.b();
+
+ assertThat(model.name).isEqualTo("ClassA");
+ assertThat(model.typeParameters).containsExactly("T Shape");
+ assertThat(model.typeParametersExpr()).isEqualTo("[T Shape]");
+ }
+
@Test
void inlineTagsOverrideDefaultTags() {
var propertyExpr = new GoStructConverter.PropertyExpr(
diff --git a/processor/src/test/java/online/sharedtype/processor/writer/converter/RustStructConverterTest.java b/processor/src/test/java/online/sharedtype/processor/writer/converter/RustStructConverterTest.java
index 7ed81994..dd7e2bff 100644
--- a/processor/src/test/java/online/sharedtype/processor/writer/converter/RustStructConverterTest.java
+++ b/processor/src/test/java/online/sharedtype/processor/writer/converter/RustStructConverterTest.java
@@ -219,4 +219,30 @@ void convertComplexType() {
assertThat(prop4.optional).isFalse();
assertThat(prop4.typeExpr()).isEqualTo("String");
}
+
+ @Test
+ void convertTypeWithBounds() {
+ ClassDef classDef = ClassDef.builder()
+ .simpleName("ClassA")
+ .qualifiedName("com.github.cuzfrog.ClassA")
+ .typeVariables(List.of(
+ TypeVariableInfo.builder()
+ .name("T")
+ .bounds(List.of(
+ ConcreteTypeInfo.builder()
+ .qualifiedName("com.github.cuzfrog.Shape")
+ .simpleName("Shape")
+ .build()
+ ))
+ .build()
+ ))
+ .components(List.of())
+ .build();
+
+ var data = converter.convert(classDef);
+ var model = (RustStructConverter.StructExpr) data.b();
+
+ assertThat(model.name).isEqualTo("ClassA");
+ assertThat(model.typeParameters).containsExactly("T: Shape");
+ }
}
diff --git a/processor/src/test/java/online/sharedtype/processor/writer/converter/TypescriptInterfaceConverterIntegrationTest.java b/processor/src/test/java/online/sharedtype/processor/writer/converter/TypescriptInterfaceConverterIntegrationTest.java
index e7401013..1bd72843 100644
--- a/processor/src/test/java/online/sharedtype/processor/writer/converter/TypescriptInterfaceConverterIntegrationTest.java
+++ b/processor/src/test/java/online/sharedtype/processor/writer/converter/TypescriptInterfaceConverterIntegrationTest.java
@@ -181,4 +181,33 @@ void optionalFieldUnionNullAndUndefined() {
assertThat(prop1.unionUndefined).isTrue();
assertThat(prop1.readonly).isFalse();
}
+
+ @Test
+ void writeInterfaceWithBounds() {
+ ClassDef classDef = ClassDef.builder()
+ .qualifiedName("com.github.cuzfrog.ClassA")
+ .simpleName("ClassA")
+ .typeVariables(Collections.singletonList(
+ TypeVariableInfo.builder()
+ .name("T")
+ .bounds(Collections.singletonList(
+ ConcreteTypeInfo.builder()
+ .qualifiedName("com.github.cuzfrog.Shape")
+ .simpleName("Shape")
+ .build()
+ ))
+ .build()
+ ))
+ .components(Collections.emptyList())
+ .build();
+
+ when(ctxMocks.getContext().getTypeStore().getConfig(classDef)).thenReturn(config);
+ when(config.getTypescriptFieldReadonly()).thenReturn(Props.Typescript.FieldReadonlyType.NONE);
+
+ var tuple = converter.convert(classDef);
+ assertThat(tuple).isNotNull();
+ TypescriptInterfaceConverter.InterfaceExpr model = (TypescriptInterfaceConverter.InterfaceExpr) tuple.b();
+ assertThat(model.name).isEqualTo("ClassA");
+ assertThat(model.typeParameters).containsExactly("T extends Shape");
+ }
}