diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml
index e18220d8..1b8d0a52 100644
--- a/.github/workflows/ci.yaml
+++ b/.github/workflows/ci.yaml
@@ -106,7 +106,6 @@ jobs:
- uses: actions-rust-lang/setup-rust-toolchain@v1
with:
- profile: minimal
toolchain: 1.83.0
- name: Test Rust
working-directory: client-test/rust
diff --git a/.run/Template JUnit.run.xml b/.run/Template JUnit.run.xml
new file mode 100644
index 00000000..35dcd551
--- /dev/null
+++ b/.run/Template JUnit.run.xml
@@ -0,0 +1,11 @@
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/README.md b/README.md
index e29279d7..4ad69f85 100644
--- a/README.md
+++ b/README.md
@@ -1,5 +1,5 @@
[](https://github.com/cuzfrog/sharedtype/actions/workflows/ci.yaml)
-
+[](https://central.sonatype.com/search?q=g:online.sharedtype++a:sharedtype&smo=true)
# SharedType - Sharing Java Types made easy
From Java:
diff --git a/annotation/src/main/java/online/sharedtype/SharedType.java b/annotation/src/main/java/online/sharedtype/SharedType.java
index 10c31d95..ac07d62f 100644
--- a/annotation/src/main/java/online/sharedtype/SharedType.java
+++ b/annotation/src/main/java/online/sharedtype/SharedType.java
@@ -72,9 +72,9 @@
*
*
* Maps:
- * (Not supported yet.)
+ * Key must be String or numeric types. Enum is support given that its value is a literal.
*
- * - Typescript: {@code [key: string]: T} where {@code T} can be a reified type.
+ * - Typescript: e.g. {@code Record} where {@code T} can be a reified type. If the key is enum, it will be a {@code Partial>}
*
*
* SharedType Website
diff --git a/client-test/rust/src/lib.rs b/client-test/rust/src/lib.rs
index 94180596..607ef1fd 100644
--- a/client-test/rust/src/lib.rs
+++ b/client-test/rust/src/lib.rs
@@ -2,6 +2,8 @@ mod types;
#[cfg(test)]
mod tests {
+ use std::collections::HashMap;
+
use super::types::*;
#[test]
@@ -51,4 +53,25 @@ mod tests {
print!("{}", &json);
assert_eq!(&json, r#"{"directRef":{"directRef":null,"arrayRef":[]},"arrayRef":[{"directRef":null,"arrayRef":[]}]}"#);
}
+
+ #[test]
+ fn map_class() {
+ let mut map_class = MapClass {
+ mapField: HashMap::new(),
+ enumKeyMapField: HashMap::new(),
+ customMapField: HashMap::new(),
+ nestedMapField: HashMap::new(),
+ };
+
+ map_class.mapField.insert(33, String::from("v1"));
+ map_class.nestedMapField.insert(String::from("m1"), HashMap::new());
+
+ let json = serde_json::to_string(&map_class).unwrap();
+
+ let map_class_deser: MapClass = serde_json::from_str(&json).unwrap();
+ assert_eq!(map_class_deser, map_class);
+
+ print!("{}", &json);
+ assert_eq!(&json, r#"{"mapField":{"33":"v1"},"enumKeyMapField":{},"customMapField":{},"nestedMapField":{"m1":{}}}"#);
+ }
}
diff --git a/client-test/typescript/tests/types.java17.test.ts b/client-test/typescript/tests/types.java17.test.ts
index 9b26a2a7..98f0502c 100644
--- a/client-test/typescript/tests/types.java17.test.ts
+++ b/client-test/typescript/tests/types.java17.test.ts
@@ -1,4 +1,4 @@
-import type { DependencyClassA, DependencyClassB, DependencyClassC, EnumGalaxy, EnumSize, EnumTShirt, JavaRecord, AnotherJavaClass, RecursiveClass } from "../src/index.java17.js";
+import type { DependencyClassA, DependencyClassB, DependencyClassC, EnumGalaxy, EnumSize, EnumTShirt, JavaRecord, AnotherJavaClass, RecursiveClass, MapClass } from "../src/index.java17.js";
export const list1: EnumGalaxy[] = ["Andromeda", "MilkyWay", "Triangulum"];
export const record1: Record = {
@@ -68,3 +68,18 @@ export const recursiveClass: RecursiveClass = {
},
arrayRef: [],
}
+
+export const mapClass: MapClass = {
+ mapField: {},
+ enumKeyMapField: {
+ 1: "1",
+ },
+ customMapField: {
+ 55: "abc",
+ },
+ nestedMapField: {
+ m1: {
+ v: 1
+ }
+ }
+}
diff --git a/doc/Development.md b/doc/Development.md
index d9ecb29e..92bdb8f9 100644
--- a/doc/Development.md
+++ b/doc/Development.md
@@ -8,7 +8,7 @@ Internal types also have javadoc for more information.
#### Project structure
* `annotation` contains the annotation type `@SharedType` as client code compile-time dependency.
* `processor` contains annotation processor logic, put on client's annotation processing path.
-* `internal` shared domain types among `processor` and `it`. This is done via build-helper-maven-plugin.
+* `internal` shared domain types among `processor` and `it`. This is done via build-helper-maven-plugin. IDE visibility can be controlled by maven profile.
* `it` contains integration tests, which do metadata verification by deserializing metadata objects.
* `java8` contains major types for tests.
* `java17` uses symlink to reuse types in `java8` then does more type checks, e.g. for Java `record`.
@@ -27,6 +27,13 @@ Optionally mount tmpfs to save your disk by:
```bash
./mount-tmpfs.sh
```
+
+### Maven profiles
+* `dev` and `release` - control whether to include test maven modules during build.
+* `it` - enable integration test profile. `internal` folder is shared source between `processor` and `it`,
+IDE may not able to properly resolve classes in `internal` folder for both modules.
+Enable this profile to enable `it` modules in IDE, and disable it when developing against `processor` module.
+
## Development
### Run test
If you encounter compilation problems with your IDE, delegate compilation to maven.
@@ -58,7 +65,7 @@ Debug annotation processor by run maven build:
```bash
./mvnd
```
-Then attach your debugger on it.
+Then attach your debugger on it. E.g. [IDEA run config](../.run/mvnd.run.xml).
Compile specific classes, e.g.:
```bash
diff --git a/internal/src/main/java/online/sharedtype/processor/domain/ClassDef.java b/internal/src/main/java/online/sharedtype/processor/domain/ClassDef.java
index 3a2cb85f..6f1673f2 100644
--- a/internal/src/main/java/online/sharedtype/processor/domain/ClassDef.java
+++ b/internal/src/main/java/online/sharedtype/processor/domain/ClassDef.java
@@ -53,6 +53,7 @@ public List typeVariables() {
return typeVariables;
}
+ @Override
public List directSupertypes() {
return supertypes;
}
@@ -60,6 +61,11 @@ public List directSupertypes() {
public Set typeInfoSet() {
return typeInfoSet;
}
+
+ public boolean isMapType() {
+ return typeInfoSet.stream().anyMatch(ConcreteTypeInfo::isMapType);
+ }
+
/**
* Register a counterpart typeInfo.
* @see #typeInfoSet
@@ -71,7 +77,7 @@ public void linkTypeInfo(ConcreteTypeInfo typeInfo) {
public ClassDef reify(List extends TypeInfo> typeArgs) {
int l;
if ((l = typeArgs.size()) != typeVariables.size()) {
- throw new IllegalArgumentException(String.format("Cannot reify %s against typeArgs: %s", this, typeArgs));
+ throw new IllegalArgumentException(String.format("Cannot reify %s against typeArgs: %s, type parameter sizes are different.", this, typeArgs));
}
if (l == 0) {
return this;
@@ -115,7 +121,7 @@ public boolean resolved() {
@Override
public String toString() {
List rows = new ArrayList<>(components.size()+2);
- rows.add(String.format("%s%s%s {", simpleName, typeVariablesToString(), supertypesToString()));
+ rows.add(String.format("%s%s%s {", qualifiedName, typeVariablesToString(), supertypesToString()));
rows.addAll(components.stream().map(f -> String.format(" %s", f)).collect(Collectors.toList()));
rows.add("}");
return String.join(System.lineSeparator(), rows);
diff --git a/internal/src/main/java/online/sharedtype/processor/domain/ConcreteTypeInfo.java b/internal/src/main/java/online/sharedtype/processor/domain/ConcreteTypeInfo.java
index bc81475d..5098b719 100644
--- a/internal/src/main/java/online/sharedtype/processor/domain/ConcreteTypeInfo.java
+++ b/internal/src/main/java/online/sharedtype/processor/domain/ConcreteTypeInfo.java
@@ -2,6 +2,7 @@
import lombok.Builder;
import lombok.EqualsAndHashCode;
+import lombok.Getter;
import javax.annotation.Nullable;
import java.util.Collections;
@@ -27,6 +28,15 @@ public final class ConcreteTypeInfo implements TypeInfo {
private final String simpleName;
@Builder.Default
private final List extends TypeInfo> typeArgs = Collections.emptyList();
+ /** If this type is an Enum */
+ @Getter
+ private final boolean enumType;
+ /** If this type is map-like. */
+ @Getter
+ private final boolean mapType;
+ /** If this type is defined in global config as base Map type */
+ @Getter
+ private final boolean baseMapType;
/**
* Qualified names of types from where this typeInfo is strongly referenced, i.e. as a component type.
diff --git a/internal/src/main/java/online/sharedtype/processor/domain/Constants.java b/internal/src/main/java/online/sharedtype/processor/domain/Constants.java
index 96c5f5cb..1774baa6 100644
--- a/internal/src/main/java/online/sharedtype/processor/domain/Constants.java
+++ b/internal/src/main/java/online/sharedtype/processor/domain/Constants.java
@@ -4,7 +4,9 @@
import javax.lang.model.type.TypeKind;
import java.util.HashMap;
+import java.util.HashSet;
import java.util.Map;
+import java.util.Set;
/**
* @author Cause Chung
@@ -35,7 +37,6 @@ public final class Constants {
public static final ConcreteTypeInfo CLASS_TYPE_INFO = ConcreteTypeInfo.ofPredefined("java.lang.Class", "Class");
public static final ConcreteTypeInfo ENUM_TYPE_INFO = ConcreteTypeInfo.ofPredefined("java.lang.Enum", "Enum");
public static final ConcreteTypeInfo OPTIONAL_TYPE_INFO = ConcreteTypeInfo.ofPredefined("java.util.Optional", "Optional");
- public static final ConcreteTypeInfo MAP_TYPE_INFO = ConcreteTypeInfo.ofPredefined("java.util.Map", "Map");
public static final Map PRIMITIVES = new HashMap<>(8);
static {
@@ -65,8 +66,26 @@ public final class Constants {
PREDEFINED_OBJECT_TYPES.put("java.lang.Class", CLASS_TYPE_INFO);
PREDEFINED_OBJECT_TYPES.put("java.lang.Enum", ENUM_TYPE_INFO);
PREDEFINED_OBJECT_TYPES.put("java.util.Optional", OPTIONAL_TYPE_INFO);
- // PREDEFINED_OBJECT_TYPES.put("java.util.Map", MAP_TYPE_INFO); // TODO: Map support
- };
+ }
+
+ public static final Set STRING_AND_NUMBER_TYPES = new HashSet<>(14);
+ static {
+ STRING_AND_NUMBER_TYPES.add(STRING_TYPE_INFO);
+ STRING_AND_NUMBER_TYPES.add(BYTE_TYPE_INFO);
+ STRING_AND_NUMBER_TYPES.add(SHORT_TYPE_INFO);
+ STRING_AND_NUMBER_TYPES.add(INT_TYPE_INFO);
+ STRING_AND_NUMBER_TYPES.add(LONG_TYPE_INFO);
+ STRING_AND_NUMBER_TYPES.add(FLOAT_TYPE_INFO);
+ STRING_AND_NUMBER_TYPES.add(DOUBLE_TYPE_INFO);
+ STRING_AND_NUMBER_TYPES.add(CHAR_TYPE_INFO);
+ STRING_AND_NUMBER_TYPES.add(BOXED_BYTE_TYPE_INFO);
+ STRING_AND_NUMBER_TYPES.add(BOXED_SHORT_TYPE_INFO);
+ STRING_AND_NUMBER_TYPES.add(BOXED_INT_TYPE_INFO);
+ STRING_AND_NUMBER_TYPES.add(BOXED_LONG_TYPE_INFO);
+ STRING_AND_NUMBER_TYPES.add(BOXED_FLOAT_TYPE_INFO);
+ STRING_AND_NUMBER_TYPES.add(BOXED_DOUBLE_TYPE_INFO);
+ STRING_AND_NUMBER_TYPES.add(BOXED_CHAR_TYPE_INFO);
+ }
private Constants() {
}
diff --git a/internal/src/main/java/online/sharedtype/processor/domain/EnumDef.java b/internal/src/main/java/online/sharedtype/processor/domain/EnumDef.java
index 401647dd..3de2e0e7 100644
--- a/internal/src/main/java/online/sharedtype/processor/domain/EnumDef.java
+++ b/internal/src/main/java/online/sharedtype/processor/domain/EnumDef.java
@@ -5,6 +5,7 @@
import lombok.experimental.SuperBuilder;
import java.util.ArrayList;
+import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;
@@ -37,6 +38,11 @@ public List components() {
return enumValueInfos;
}
+ @Override
+ public List directSupertypes() {
+ return Collections.emptyList();
+ }
+
@Override
public boolean resolved() {
return enumValueInfos.stream().allMatch(EnumValueInfo::resolved);
diff --git a/internal/src/main/java/online/sharedtype/processor/domain/TypeDef.java b/internal/src/main/java/online/sharedtype/processor/domain/TypeDef.java
index 82c650d6..740998f6 100644
--- a/internal/src/main/java/online/sharedtype/processor/domain/TypeDef.java
+++ b/internal/src/main/java/online/sharedtype/processor/domain/TypeDef.java
@@ -18,6 +18,8 @@ public interface TypeDef extends Serializable {
List extends ComponentInfo> components();
+ List directSupertypes();
+
/**
* @return true if all required types are resolved.
*/
diff --git a/internal/src/main/java/online/sharedtype/processor/domain/TypeInfo.java b/internal/src/main/java/online/sharedtype/processor/domain/TypeInfo.java
index 139686e0..a30cd181 100644
--- a/internal/src/main/java/online/sharedtype/processor/domain/TypeInfo.java
+++ b/internal/src/main/java/online/sharedtype/processor/domain/TypeInfo.java
@@ -25,6 +25,10 @@ public interface TypeInfo extends Serializable {
*/
boolean resolved();
+ default boolean isEnumType() {
+ return false;
+ }
+
/**
* Replace type variables with type arguments.
* @param mappings key is a type variable e.g. T
diff --git a/it/java17/pom.xml b/it/java17/pom.xml
index 290e1d6e..e5698a6c 100644
--- a/it/java17/pom.xml
+++ b/it/java17/pom.xml
@@ -12,7 +12,7 @@
SharedType Integration Test Java17
- 17
- 17
+ 21
+ 21
diff --git a/it/java8/src/main/java/online/sharedtype/it/java8/CustomMap.java b/it/java8/src/main/java/online/sharedtype/it/java8/CustomMap.java
new file mode 100644
index 00000000..1d26254e
--- /dev/null
+++ b/it/java8/src/main/java/online/sharedtype/it/java8/CustomMap.java
@@ -0,0 +1,7 @@
+package online.sharedtype.it.java8;
+
+import java.util.HashMap;
+
+final class CustomMap extends HashMap {
+
+}
diff --git a/it/java8/src/main/java/online/sharedtype/it/java8/EnumSize.java b/it/java8/src/main/java/online/sharedtype/it/java8/EnumSize.java
index fffa4027..1501c3f5 100644
--- a/it/java8/src/main/java/online/sharedtype/it/java8/EnumSize.java
+++ b/it/java8/src/main/java/online/sharedtype/it/java8/EnumSize.java
@@ -3,6 +3,7 @@
import lombok.RequiredArgsConstructor;
import online.sharedtype.SharedType;
+@SharedType(rustMacroTraits = {"PartialEq", "Eq", "Hash", "serde::Serialize", "serde::Deserialize"})
@RequiredArgsConstructor
public enum EnumSize {
SMALL(1), MEDIUM(2), LARGE(3);
diff --git a/it/java8/src/main/java/online/sharedtype/it/java8/MapClass.java b/it/java8/src/main/java/online/sharedtype/it/java8/MapClass.java
new file mode 100644
index 00000000..39aa73a1
--- /dev/null
+++ b/it/java8/src/main/java/online/sharedtype/it/java8/MapClass.java
@@ -0,0 +1,14 @@
+package online.sharedtype.it.java8;
+
+import online.sharedtype.SharedType;
+
+import java.util.Map;
+import java.util.concurrent.ConcurrentMap;
+
+@SharedType(rustMacroTraits = {"PartialEq", "serde::Serialize", "serde::Deserialize"})
+final class MapClass {
+ private ConcurrentMap mapField;
+ private Map enumKeyMapField;
+ private CustomMap customMapField;
+ private Map> nestedMapField;
+}
diff --git a/it/java8/src/main/java/online/sharedtype/it/java8/TempClass.java b/it/java8/src/main/java/online/sharedtype/it/java8/TempClass.java
index 40f1fe2f..a42db4e1 100644
--- a/it/java8/src/main/java/online/sharedtype/it/java8/TempClass.java
+++ b/it/java8/src/main/java/online/sharedtype/it/java8/TempClass.java
@@ -3,5 +3,5 @@
import online.sharedtype.SharedType;
@SharedType
-public class TempClass extends SuperClassA {
+public class TempClass {
}
diff --git a/it/java8/src/test/java/online/sharedtype/it/MapClassIntegrationTest.java b/it/java8/src/test/java/online/sharedtype/it/MapClassIntegrationTest.java
new file mode 100644
index 00000000..7246e9fd
--- /dev/null
+++ b/it/java8/src/test/java/online/sharedtype/it/MapClassIntegrationTest.java
@@ -0,0 +1,66 @@
+package online.sharedtype.it;
+
+import online.sharedtype.processor.domain.ClassDef;
+import online.sharedtype.processor.domain.ConcreteTypeInfo;
+import online.sharedtype.processor.domain.Constants;
+import org.junit.jupiter.api.Test;
+
+import static online.sharedtype.it.support.TypeDefDeserializer.deserializeTypeDef;
+import static org.assertj.core.api.Assertions.assertThat;
+
+final class MapClassIntegrationTest {
+ @Test
+ void mapClass() {
+ ClassDef classDef = (ClassDef)deserializeTypeDef("online.sharedtype.it.java8.MapClass.ser");
+ assertThat(classDef.components()).satisfiesExactly(
+ mapField -> {
+ assertThat(mapField.name()).isEqualTo("mapField");
+ ConcreteTypeInfo typeInfo = (ConcreteTypeInfo)mapField.type();
+ assertThat(typeInfo.isMapType()).isTrue();
+ assertThat(typeInfo.qualifiedName()).isEqualTo("java.util.concurrent.ConcurrentMap");
+ assertThat(typeInfo.typeArgs()).hasSize(2).satisfiesExactly(
+ keyType -> assertThat(keyType).isEqualTo(Constants.BOXED_INT_TYPE_INFO),
+ valueType -> assertThat(valueType).isEqualTo(Constants.STRING_TYPE_INFO)
+ );
+ },
+ enumKeyMapField -> {
+ assertThat(enumKeyMapField.name()).isEqualTo("enumKeyMapField");
+ ConcreteTypeInfo typeInfo = (ConcreteTypeInfo) enumKeyMapField.type();
+ assertThat(typeInfo.isMapType()).isTrue();
+ assertThat(typeInfo.qualifiedName()).isEqualTo("java.util.Map");
+ assertThat(typeInfo.typeArgs()).hasSize(2).satisfiesExactly(
+ keyType -> {
+ ConcreteTypeInfo keyTypeInfo = (ConcreteTypeInfo) keyType;
+ assertThat(keyTypeInfo.qualifiedName()).isEqualTo("online.sharedtype.it.java8.EnumSize");
+ },
+ valueType -> assertThat(valueType).isEqualTo(Constants.STRING_TYPE_INFO)
+ );
+ },
+ customMapField -> {
+ assertThat(customMapField.name()).isEqualTo("customMapField");
+ ConcreteTypeInfo typeInfo = (ConcreteTypeInfo) customMapField.type();
+ assertThat(typeInfo.isMapType()).isTrue();
+ assertThat(typeInfo.qualifiedName()).isEqualTo("online.sharedtype.it.java8.CustomMap");
+ assertThat(typeInfo.typeArgs()).hasSize(0);
+ },
+ nestedMapField -> {
+ assertThat(nestedMapField.name()).isEqualTo("nestedMapField");
+ ConcreteTypeInfo typeInfo = (ConcreteTypeInfo) nestedMapField.type();
+ assertThat(typeInfo.isMapType()).isTrue();
+ assertThat(typeInfo.qualifiedName()).isEqualTo("java.util.Map");
+ assertThat(typeInfo.typeArgs()).hasSize(2).satisfiesExactly(
+ keyType -> assertThat(keyType).isEqualTo(Constants.STRING_TYPE_INFO),
+ valueType -> {
+ ConcreteTypeInfo valueTypeInfo = (ConcreteTypeInfo) valueType;
+ assertThat(valueTypeInfo.isMapType()).isTrue();
+ assertThat(valueTypeInfo.qualifiedName()).isEqualTo("java.util.Map");
+ assertThat(valueTypeInfo.typeArgs()).hasSize(2).satisfiesExactly(
+ nestedKeyType -> assertThat(nestedKeyType).isEqualTo(Constants.STRING_TYPE_INFO),
+ nestedValueType -> assertThat(nestedValueType).isEqualTo(Constants.BOXED_INT_TYPE_INFO)
+ );
+ }
+ );
+ }
+ );
+ }
+}
diff --git a/pom.xml b/pom.xml
index 3cd22f21..be1c8844 100644
--- a/pom.xml
+++ b/pom.xml
@@ -49,7 +49,7 @@
5.10.2
1.18.34
1.1.1
- 5.14.1
+ 5.15.2
@@ -138,6 +138,12 @@
org.ec4j.maven
editorconfig-maven-plugin
+
+
+ .run/
+ client-test/
+
+
diff --git a/processor/src/main/java/online/sharedtype/processor/context/Context.java b/processor/src/main/java/online/sharedtype/processor/context/Context.java
index f35aaac7..c8893fa4 100644
--- a/processor/src/main/java/online/sharedtype/processor/context/Context.java
+++ b/processor/src/main/java/online/sharedtype/processor/context/Context.java
@@ -5,6 +5,7 @@
import online.sharedtype.SharedType;
import javax.annotation.processing.ProcessingEnvironment;
+import javax.lang.model.element.ElementKind;
import javax.lang.model.element.TypeElement;
import javax.lang.model.type.TypeMirror;
import javax.lang.model.util.Elements;
@@ -35,6 +36,7 @@ public final class Context {
@Getter
private final Trees trees;
private final Set arraylikeTypes;
+ private final Set maplikeTypes;
public Context(ProcessingEnvironment processingEnv, Props props) {
this.processingEnv = processingEnv;
@@ -45,6 +47,9 @@ public Context(ProcessingEnvironment processingEnv, Props props) {
arraylikeTypes = props.getArraylikeTypeQualifiedNames().stream()
.map(qualifiedName -> types.erasure(elements.getTypeElement(qualifiedName).asType()))
.collect(Collectors.toSet());
+ maplikeTypes = props.getMaplikeTypeQualifiedNames().stream()
+ .map(qualifiedName -> types.erasure(elements.getTypeElement(qualifiedName).asType()))
+ .collect(Collectors.toSet());
}
// TODO: optimize by remove varargs
@@ -67,6 +72,19 @@ public boolean isArraylike(TypeMirror typeMirror) {
return false;
}
+ public boolean isMaplike(TypeMirror typeMirror) {
+ for (TypeMirror maplikeType : maplikeTypes) {
+ if (types.isSubtype(types.erasure(typeMirror), maplikeType)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ public boolean isEnumType(TypeMirror typeMirror) {
+ return types.asElement(typeMirror).getKind() == ElementKind.ENUM;
+ }
+
public boolean isTypeIgnored(TypeElement typeElement) {
boolean ignored = typeElement.getAnnotation(SharedType.Ignore.class) != null;
return ignored || props.getIgnoredTypeQualifiedNames().contains(typeElement.getQualifiedName().toString());
diff --git a/processor/src/main/java/online/sharedtype/processor/context/RenderFlags.java b/processor/src/main/java/online/sharedtype/processor/context/RenderFlags.java
index 605fd976..689d0bd6 100644
--- a/processor/src/main/java/online/sharedtype/processor/context/RenderFlags.java
+++ b/processor/src/main/java/online/sharedtype/processor/context/RenderFlags.java
@@ -11,4 +11,5 @@
@Setter
public final class RenderFlags {
private boolean useRustAny = false;
+ private boolean useRustMap = false;
}
diff --git a/processor/src/main/java/online/sharedtype/processor/context/TypeStore.java b/processor/src/main/java/online/sharedtype/processor/context/TypeStore.java
index 0984b79b..399bf11e 100644
--- a/processor/src/main/java/online/sharedtype/processor/context/TypeStore.java
+++ b/processor/src/main/java/online/sharedtype/processor/context/TypeStore.java
@@ -13,7 +13,6 @@
import java.util.HashSet;
import java.util.List;
import java.util.Map;
-import java.util.Set;
import static online.sharedtype.processor.domain.Constants.PREDEFINED_OBJECT_TYPES;
diff --git a/processor/src/main/java/online/sharedtype/processor/parser/type/TypeInfoParserImpl.java b/processor/src/main/java/online/sharedtype/processor/parser/type/TypeInfoParserImpl.java
index 6d30dde4..2cbd3ac6 100644
--- a/processor/src/main/java/online/sharedtype/processor/parser/type/TypeInfoParserImpl.java
+++ b/processor/src/main/java/online/sharedtype/processor/parser/type/TypeInfoParserImpl.java
@@ -97,6 +97,9 @@ private TypeInfo parseDeclared(DeclaredType declaredType, TypeContext typeContex
.qualifiedName(qualifiedName)
.simpleName(simpleName)
.typeArgs(parsedTypeArgs)
+ .enumType(ctx.isEnumType(currentType))
+ .mapType(ctx.isMaplike(currentType))
+ .baseMapType(ctx.getProps().getMaplikeTypeQualifiedNames().contains(qualifiedName))
.resolved(resolved)
.build();
typeStore.saveTypeInfo(qualifiedName, parsedTypeArgs, typeInfo);
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 326d89aa..da2a55db 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
@@ -35,6 +35,9 @@ public boolean shouldAccept(TypeDef typeDef) {
return false;
}
ClassDef classDef = (ClassDef) typeDef;
+ if (classDef.isMapType()) {
+ return false;
+ }
return classDef.isAnnotated() || classDef.isReferencedByAnnotated();
}
@@ -43,7 +46,7 @@ public Tuple convert(TypeDef typeDef) {
ClassDef classDef = (ClassDef) typeDef;
StructExpr value = new StructExpr(
classDef.simpleName(),
- classDef.typeVariables().stream().map(typeExpressionConverter::toTypeExpr).collect(Collectors.toList()),
+ classDef.typeVariables().stream().map(typeInfo -> typeExpressionConverter.toTypeExpr(typeInfo, typeDef)).collect(Collectors.toList()),
gatherProperties(classDef),
macroTraits(classDef)
);
@@ -54,7 +57,7 @@ private List gatherProperties(ClassDef classDef) {
List properties = new ArrayList<>(); // TODO: init cap
Set propertyNames = new HashSet<>();
for (FieldComponentInfo component : classDef.components()) {
- properties.add(toPropertyExpr(component));
+ properties.add(toPropertyExpr(component, classDef));
propertyNames.add(component.name());
}
@@ -68,7 +71,7 @@ private List gatherProperties(ClassDef classDef) {
superTypeDef = superTypeDef.reify(superConcreteTypeInfo.typeArgs());
for (FieldComponentInfo component : superTypeDef.components()) {
if (!propertyNames.contains(component.name())) {
- properties.add(toPropertyExpr(component));
+ properties.add(toPropertyExpr(component, superTypeDef));
propertyNames.add(component.name());
}
}
@@ -79,10 +82,10 @@ private List gatherProperties(ClassDef classDef) {
return properties;
}
- private PropertyExpr toPropertyExpr(FieldComponentInfo field) {
+ private PropertyExpr toPropertyExpr(FieldComponentInfo field, TypeDef contextTypeDef) {
return new PropertyExpr(
ctx.getProps().getRust().isConvertToSnakeCase() ? LiteralUtils.toSnakeCase(field.name()) : field.name(),
- typeExpressionConverter.toTypeExpr(field.type()),
+ typeExpressionConverter.toTypeExpr(field.type(), contextTypeDef),
isOptionalField(field)
);
}
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 5e1e2b65..ab18be74 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
@@ -23,7 +23,11 @@ final class TypescriptInterfaceConverter implements TemplateDataConverter {
@Override
public boolean shouldAccept(TypeDef typeDef) {
- return typeDef instanceof ClassDef;
+ if (typeDef instanceof ClassDef) {
+ ClassDef classDef = (ClassDef) typeDef;
+ return !classDef.isMapType();
+ }
+ return false;
}
@Override
@@ -31,17 +35,17 @@ public Tuple convert(TypeDef typeDef) {
ClassDef classDef = (ClassDef) typeDef;
InterfaceExpr value = new InterfaceExpr(
classDef.simpleName(),
- classDef.typeVariables().stream().map(typeExpressionConverter::toTypeExpr).collect(Collectors.toList()),
- classDef.directSupertypes().stream().map(typeExpressionConverter::toTypeExpr).collect(Collectors.toList()),
- classDef.components().stream().map(this::toPropertyExpr).collect(Collectors.toList())
+ classDef.typeVariables().stream().map(typeInfo -> typeExpressionConverter.toTypeExpr(typeInfo, typeDef)).collect(Collectors.toList()),
+ classDef.directSupertypes().stream().map(typeInfo1 -> typeExpressionConverter.toTypeExpr(typeInfo1, typeDef)).collect(Collectors.toList()),
+ classDef.components().stream().map(field -> toPropertyExpr(field, typeDef)).collect(Collectors.toList())
);
return Tuple.of(Template.TEMPLATE_TYPESCRIPT_INTERFACE, value);
}
- private PropertyExpr toPropertyExpr(FieldComponentInfo field) {
+ private PropertyExpr toPropertyExpr(FieldComponentInfo field, TypeDef contextTypeDef) {
return new PropertyExpr(
field.name(),
- typeExpressionConverter.toTypeExpr(field.type()),
+ typeExpressionConverter.toTypeExpr(field.type(), contextTypeDef),
interfacePropertyDelimiter,
field.optional(),
false,
diff --git a/processor/src/main/java/online/sharedtype/processor/writer/converter/type/AbstractTypeExpressionConverter.java b/processor/src/main/java/online/sharedtype/processor/writer/converter/type/AbstractTypeExpressionConverter.java
index f9133adf..01ab5975 100644
--- a/processor/src/main/java/online/sharedtype/processor/writer/converter/type/AbstractTypeExpressionConverter.java
+++ b/processor/src/main/java/online/sharedtype/processor/writer/converter/type/AbstractTypeExpressionConverter.java
@@ -1,63 +1,121 @@
package online.sharedtype.processor.writer.converter.type;
+import lombok.RequiredArgsConstructor;
+import online.sharedtype.processor.context.Context;
import online.sharedtype.processor.domain.ArrayTypeInfo;
+import online.sharedtype.processor.domain.ClassDef;
import online.sharedtype.processor.domain.ConcreteTypeInfo;
+import online.sharedtype.processor.domain.Constants;
+import online.sharedtype.processor.domain.TypeDef;
import online.sharedtype.processor.domain.TypeInfo;
import online.sharedtype.processor.domain.TypeVariableInfo;
import online.sharedtype.processor.support.annotation.SideEffect;
+import online.sharedtype.processor.support.exception.SharedTypeException;
+import online.sharedtype.processor.support.exception.SharedTypeInternalError;
-import java.util.HashMap;
-import java.util.Map;
+import javax.annotation.Nullable;
+import java.util.ArrayDeque;
+import java.util.Queue;
-abstract class AbstractTypeExpressionConverter implements TypeExpressionConverter {
- private final Map typeNameMappings;
+import static online.sharedtype.processor.support.Preconditions.requireNonNull;
- public AbstractTypeExpressionConverter() {
- typeNameMappings = new HashMap<>(20);
- }
+@RequiredArgsConstructor
+abstract class AbstractTypeExpressionConverter implements TypeExpressionConverter {
+ final Context ctx;
@Override
- public final String toTypeExpr(TypeInfo typeInfo) {
+ public final String toTypeExpr(TypeInfo typeInfo, TypeDef contextTypeDef) {
StringBuilder exprBuilder = new StringBuilder(); // TODO: a better init size
- buildTypeExprRecursively(typeInfo, exprBuilder);
+ buildTypeExprRecursively(typeInfo, exprBuilder, contextTypeDef);
return exprBuilder.toString();
}
- final void addTypeMapping(ConcreteTypeInfo typeInfo, String name) {
- typeNameMappings.put(typeInfo, name);
- }
- void buildArrayExprPrefix(ArrayTypeInfo typeInfo, @SideEffect StringBuilder exprBuilder) {
- }
- void buildArrayExprSuffix(ArrayTypeInfo typeInfo, @SideEffect StringBuilder exprBuilder) {
- }
void beforeVisitTypeInfo(TypeInfo typeInfo) {
}
- String toConcreteTypeExpression(ConcreteTypeInfo concreteTypeInfo) {
- return concreteTypeInfo.simpleName();
- }
+ abstract ArraySpec arraySpec();
+ abstract MapSpec mapSpec(ConcreteTypeInfo typeInfo);
+
+ @Nullable
+ abstract String toTypeExpression(ConcreteTypeInfo typeInfo, @Nullable String defaultExpr);
- private void buildTypeExprRecursively(TypeInfo typeInfo, @SideEffect StringBuilder exprBuilder) {
+ private void buildTypeExprRecursively(TypeInfo typeInfo, @SideEffect StringBuilder exprBuilder, TypeDef contextTypeDef) {
beforeVisitTypeInfo(typeInfo);
if (typeInfo instanceof ConcreteTypeInfo) {
ConcreteTypeInfo concreteTypeInfo = (ConcreteTypeInfo) typeInfo;
- exprBuilder.append(typeNameMappings.getOrDefault(concreteTypeInfo, toConcreteTypeExpression(concreteTypeInfo)));
- if (!concreteTypeInfo.typeArgs().isEmpty()) {
- exprBuilder.append("<");
- for (TypeInfo typeArg : concreteTypeInfo.typeArgs()) {
- buildTypeExprRecursively(typeArg, exprBuilder);
- exprBuilder.append(", ");
+ if (concreteTypeInfo.isMapType()) {
+ buildMapType(concreteTypeInfo, exprBuilder, contextTypeDef);
+ } else {
+ exprBuilder.append(toTypeExpression(concreteTypeInfo, concreteTypeInfo.simpleName()));
+ if (!concreteTypeInfo.typeArgs().isEmpty()) {
+ exprBuilder.append("<");
+ for (TypeInfo typeArg : concreteTypeInfo.typeArgs()) {
+ buildTypeExprRecursively(typeArg, exprBuilder, contextTypeDef);
+ exprBuilder.append(", ");
+ }
+ exprBuilder.setLength(exprBuilder.length() - 2);
+ exprBuilder.append(">");
}
- exprBuilder.setLength(exprBuilder.length() - 2);
- exprBuilder.append(">");
}
} else if (typeInfo instanceof TypeVariableInfo) {
TypeVariableInfo typeVariableInfo = (TypeVariableInfo) typeInfo;
exprBuilder.append(typeVariableInfo.name());
} else if (typeInfo instanceof ArrayTypeInfo) {
ArrayTypeInfo arrayTypeInfo = (ArrayTypeInfo) typeInfo;
- buildArrayExprPrefix(arrayTypeInfo, exprBuilder);
- buildTypeExprRecursively(arrayTypeInfo.component(), exprBuilder);
- buildArrayExprSuffix(arrayTypeInfo, exprBuilder);
+ ArraySpec arraySpec = arraySpec();
+ exprBuilder.append(arraySpec.prefix);
+ buildTypeExprRecursively(arrayTypeInfo.component(), exprBuilder, contextTypeDef);
+ exprBuilder.append(arraySpec.suffix);
+ }
+ }
+
+ private void buildMapType(ConcreteTypeInfo concreteTypeInfo, @SideEffect StringBuilder exprBuilder, TypeDef contextTypeDef) {
+ ConcreteTypeInfo baseMapType = findBaseMapType(concreteTypeInfo);
+ ConcreteTypeInfo keyType = getKeyType(baseMapType, concreteTypeInfo, contextTypeDef);
+ MapSpec mapSpec = mapSpec(keyType);
+ if (mapSpec == null) {
+ return;
+ }
+ String keyTypeExpr = toTypeExpression(keyType, keyType.simpleName());
+ if (keyTypeExpr == null) {
+ throw new SharedTypeInternalError(String.format(
+ "Valid keyType should not be null, probably because type mapping is not provided, keyType: %s, baseMapType: %s" +
+ "When trying to build expression for concrete type: %s. Context type: %s.", keyType, baseMapType, concreteTypeInfo, contextTypeDef));
+ }
+ exprBuilder.append(mapSpec.prefix);
+ exprBuilder.append(keyTypeExpr);
+ exprBuilder.append(mapSpec.delimiter);
+ buildTypeExprRecursively(baseMapType.typeArgs().get(1), exprBuilder, contextTypeDef);
+ exprBuilder.append(mapSpec.suffix);
+ }
+
+ private static ConcreteTypeInfo getKeyType(ConcreteTypeInfo baseMapType, ConcreteTypeInfo concreteTypeInfo, TypeDef contextTypeDef) {
+ TypeInfo keyType = baseMapType.typeArgs().get(0);
+ if (!(keyType instanceof ConcreteTypeInfo) || (!Constants.STRING_AND_NUMBER_TYPES.contains(keyType) && !keyType.isEnumType())) {
+ throw new SharedTypeException(String.format(
+ "Key type of %s must be string or numbers or enum (with EnumValue being string or numbers), but is %s, " +
+ "when trying to build expression for concrete type: %s, context type: %s.",
+ baseMapType.qualifiedName(), keyType, concreteTypeInfo, contextTypeDef));
+ }
+ return (ConcreteTypeInfo) keyType;
+ }
+
+ private ConcreteTypeInfo findBaseMapType(ConcreteTypeInfo concreteTypeInfo) {
+ Queue queue = new ArrayDeque<>();
+ ConcreteTypeInfo baseMapType = concreteTypeInfo;
+ while (!ctx.getProps().getMaplikeTypeQualifiedNames().contains(baseMapType.qualifiedName())) {
+ ClassDef typeDef = (ClassDef)requireNonNull(baseMapType.typeDef(), "Custom Map type must have a type definition, concrete type: %s", concreteTypeInfo);
+ typeDef = typeDef.reify(baseMapType.typeArgs());
+ for (TypeInfo supertype : typeDef.directSupertypes()) {
+ if (supertype instanceof ConcreteTypeInfo) {
+ queue.add((ConcreteTypeInfo) supertype);
+ }
+ }
+ baseMapType = requireNonNull(queue.poll(), "Cannot find a qualified type name of a map-like type, concrete type: %s", concreteTypeInfo);
+ }
+ if (baseMapType.typeArgs().size() != 2) {
+ throw new SharedTypeException(String.format("Base Map type must have 2 type arguments, with first as the key type and the second as the value type," +
+ "but is %s, when trying to build expression for concrete type: %s", baseMapType, concreteTypeInfo));
}
+ return baseMapType;
}
}
diff --git a/processor/src/main/java/online/sharedtype/processor/writer/converter/type/RustTypeExpressionConverter.java b/processor/src/main/java/online/sharedtype/processor/writer/converter/type/RustTypeExpressionConverter.java
index 6ee99b7b..88e22802 100644
--- a/processor/src/main/java/online/sharedtype/processor/writer/converter/type/RustTypeExpressionConverter.java
+++ b/processor/src/main/java/online/sharedtype/processor/writer/converter/type/RustTypeExpressionConverter.java
@@ -2,61 +2,73 @@
import online.sharedtype.processor.context.Context;
import online.sharedtype.processor.context.RenderFlags;
-import online.sharedtype.processor.domain.ArrayTypeInfo;
import online.sharedtype.processor.domain.ConcreteTypeInfo;
import online.sharedtype.processor.domain.Constants;
import online.sharedtype.processor.domain.TypeInfo;
+import javax.annotation.Nullable;
+import java.util.HashMap;
+import java.util.Map;
+
final class RustTypeExpressionConverter extends AbstractTypeExpressionConverter {
+ private static final ArraySpec ARRAY_SPEC = new ArraySpec("Vec<", ">");
+ private static final MapSpec DEFAULT_MAP_SPEC = new MapSpec("HashMap<", ", ", ">");
+ private final Map typeNameMappings = new HashMap<>(20);
private final RenderFlags renderFlags;
RustTypeExpressionConverter(Context ctx) {
+ super(ctx);
this.renderFlags = ctx.getRenderFlags();
- addTypeMapping(Constants.BOOLEAN_TYPE_INFO, "bool");
- addTypeMapping(Constants.BYTE_TYPE_INFO, "i8");
- addTypeMapping(Constants.CHAR_TYPE_INFO, "char");
- addTypeMapping(Constants.DOUBLE_TYPE_INFO, "f64");
- addTypeMapping(Constants.FLOAT_TYPE_INFO, "f32");
- addTypeMapping(Constants.INT_TYPE_INFO, "i32");
- addTypeMapping(Constants.LONG_TYPE_INFO, "i64");
- addTypeMapping(Constants.SHORT_TYPE_INFO, "i16");
-
- addTypeMapping(Constants.BOXED_BOOLEAN_TYPE_INFO, "bool");
- addTypeMapping(Constants.BOXED_BYTE_TYPE_INFO, "i8");
- addTypeMapping(Constants.BOXED_CHAR_TYPE_INFO, "char");
- addTypeMapping(Constants.BOXED_DOUBLE_TYPE_INFO, "f64");
- addTypeMapping(Constants.BOXED_FLOAT_TYPE_INFO, "f32");
- addTypeMapping(Constants.BOXED_INT_TYPE_INFO, "i32");
- addTypeMapping(Constants.BOXED_LONG_TYPE_INFO, "i64");
- addTypeMapping(Constants.BOXED_SHORT_TYPE_INFO, "i16");
-
- addTypeMapping(Constants.STRING_TYPE_INFO, "String");
- addTypeMapping(Constants.VOID_TYPE_INFO, "!");
- addTypeMapping(Constants.OBJECT_TYPE_INFO, "Box");
- }
+ typeNameMappings.put(Constants.BOOLEAN_TYPE_INFO, "bool");
+ typeNameMappings.put(Constants.BYTE_TYPE_INFO, "i8");
+ typeNameMappings.put(Constants.CHAR_TYPE_INFO, "char");
+ typeNameMappings.put(Constants.DOUBLE_TYPE_INFO, "f64");
+ typeNameMappings.put(Constants.FLOAT_TYPE_INFO, "f32");
+ typeNameMappings.put(Constants.INT_TYPE_INFO, "i32");
+ typeNameMappings.put(Constants.LONG_TYPE_INFO, "i64");
+ typeNameMappings.put(Constants.SHORT_TYPE_INFO, "i16");
- @Override
- public void buildArrayExprPrefix(ArrayTypeInfo typeInfo, StringBuilder exprBuilder) {
- exprBuilder.append("Vec<");
- }
+ typeNameMappings.put(Constants.BOXED_BOOLEAN_TYPE_INFO, "bool");
+ typeNameMappings.put(Constants.BOXED_BYTE_TYPE_INFO, "i8");
+ typeNameMappings.put(Constants.BOXED_CHAR_TYPE_INFO, "char");
+ typeNameMappings.put(Constants.BOXED_DOUBLE_TYPE_INFO, "f64");
+ typeNameMappings.put(Constants.BOXED_FLOAT_TYPE_INFO, "f32");
+ typeNameMappings.put(Constants.BOXED_INT_TYPE_INFO, "i32");
+ typeNameMappings.put(Constants.BOXED_LONG_TYPE_INFO, "i64");
+ typeNameMappings.put(Constants.BOXED_SHORT_TYPE_INFO, "i16");
- @Override
- public void buildArrayExprSuffix(ArrayTypeInfo typeInfo, StringBuilder exprBuilder) {
- exprBuilder.append(">");
+ typeNameMappings.put(Constants.STRING_TYPE_INFO, "String");
+ typeNameMappings.put(Constants.VOID_TYPE_INFO, "!");
+ typeNameMappings.put(Constants.OBJECT_TYPE_INFO, "Box");
}
@Override
void beforeVisitTypeInfo(TypeInfo typeInfo) {
if (typeInfo.equals(Constants.OBJECT_TYPE_INFO)) {
renderFlags.setUseRustAny(true);
+ } else if (typeInfo instanceof ConcreteTypeInfo && ((ConcreteTypeInfo) typeInfo).isMapType()) {
+ renderFlags.setUseRustMap(true);
}
}
@Override
- String toConcreteTypeExpression(ConcreteTypeInfo concreteTypeInfo) {
- String expr = super.toConcreteTypeExpression(concreteTypeInfo);
- if (concreteTypeInfo.typeDef() != null && concreteTypeInfo.typeDef().isCyclicReferenced()) {
- return String.format("Box<%s>", expr);
+ ArraySpec arraySpec() {
+ return ARRAY_SPEC;
+ }
+
+ @Override
+ MapSpec mapSpec(ConcreteTypeInfo typeInfo) {
+ return DEFAULT_MAP_SPEC;
+ }
+
+ @Override
+ @Nullable
+ String toTypeExpression(ConcreteTypeInfo typeInfo, @Nullable String defaultExpr) {
+ String expr = typeNameMappings.getOrDefault(typeInfo, defaultExpr);
+ if (typeInfo != null && expr != null) {
+ if (typeInfo.typeDef() != null && typeInfo.typeDef().isCyclicReferenced()) {
+ expr = String.format("Box<%s>", expr);
+ }
}
return expr;
}
diff --git a/processor/src/main/java/online/sharedtype/processor/writer/converter/type/TypeExpressionConverter.java b/processor/src/main/java/online/sharedtype/processor/writer/converter/type/TypeExpressionConverter.java
index 66f890d3..30713074 100644
--- a/processor/src/main/java/online/sharedtype/processor/writer/converter/type/TypeExpressionConverter.java
+++ b/processor/src/main/java/online/sharedtype/processor/writer/converter/type/TypeExpressionConverter.java
@@ -1,10 +1,12 @@
package online.sharedtype.processor.writer.converter.type;
+import lombok.RequiredArgsConstructor;
import online.sharedtype.processor.context.Context;
+import online.sharedtype.processor.domain.TypeDef;
import online.sharedtype.processor.domain.TypeInfo;
public interface TypeExpressionConverter {
- String toTypeExpr(TypeInfo typeInfo);
+ String toTypeExpr(TypeInfo typeInfo, TypeDef contextTypeDef);
static TypeExpressionConverter typescript(Context ctx) {
return new TypescriptTypeExpressionConverter(ctx);
@@ -13,4 +15,17 @@ static TypeExpressionConverter typescript(Context ctx) {
static TypeExpressionConverter rust(Context ctx) {
return new RustTypeExpressionConverter(ctx);
}
+
+ @RequiredArgsConstructor
+ final class ArraySpec {
+ final String prefix;
+ final String suffix;
+ }
+
+ @RequiredArgsConstructor
+ final class MapSpec {
+ final String prefix;
+ final String delimiter;
+ final String suffix;
+ }
}
diff --git a/processor/src/main/java/online/sharedtype/processor/writer/converter/type/TypescriptTypeExpressionConverter.java b/processor/src/main/java/online/sharedtype/processor/writer/converter/type/TypescriptTypeExpressionConverter.java
index 16ffb5e9..302bf8ad 100644
--- a/processor/src/main/java/online/sharedtype/processor/writer/converter/type/TypescriptTypeExpressionConverter.java
+++ b/processor/src/main/java/online/sharedtype/processor/writer/converter/type/TypescriptTypeExpressionConverter.java
@@ -1,36 +1,61 @@
package online.sharedtype.processor.writer.converter.type;
import online.sharedtype.processor.context.Context;
-import online.sharedtype.processor.domain.ArrayTypeInfo;
+import online.sharedtype.processor.domain.ConcreteTypeInfo;
import online.sharedtype.processor.domain.Constants;
+import online.sharedtype.processor.domain.TypeInfo;
+
+import javax.annotation.Nullable;
+import java.util.HashMap;
+import java.util.Map;
final class TypescriptTypeExpressionConverter extends AbstractTypeExpressionConverter {
+ private static final ArraySpec ARRAY_SPEC = new ArraySpec("", "[]");
+ private static final MapSpec DEFAULT_MAP_SPEC = new MapSpec("Record<", ", ", ">");
+ private static final MapSpec ENUM_KEY_MAP_SPEC = new MapSpec("Partial>");
+ final Map typeNameMappings = new HashMap<>(20);
+
TypescriptTypeExpressionConverter(Context ctx) {
- addTypeMapping(Constants.BOOLEAN_TYPE_INFO, "boolean");
- addTypeMapping(Constants.BYTE_TYPE_INFO, "number");
- addTypeMapping(Constants.CHAR_TYPE_INFO, "string");
- addTypeMapping(Constants.DOUBLE_TYPE_INFO, "number");
- addTypeMapping(Constants.FLOAT_TYPE_INFO, "number");
- addTypeMapping(Constants.INT_TYPE_INFO, "number");
- addTypeMapping(Constants.LONG_TYPE_INFO, "number");
- addTypeMapping(Constants.SHORT_TYPE_INFO, "number");
-
- addTypeMapping(Constants.BOXED_BOOLEAN_TYPE_INFO, "boolean");
- addTypeMapping(Constants.BOXED_BYTE_TYPE_INFO, "number");
- addTypeMapping(Constants.BOXED_CHAR_TYPE_INFO, "string");
- addTypeMapping(Constants.BOXED_DOUBLE_TYPE_INFO, "number");
- addTypeMapping(Constants.BOXED_FLOAT_TYPE_INFO, "number");
- addTypeMapping(Constants.BOXED_INT_TYPE_INFO, "number");
- addTypeMapping(Constants.BOXED_LONG_TYPE_INFO, "number");
- addTypeMapping(Constants.BOXED_SHORT_TYPE_INFO, "number");
-
- addTypeMapping(Constants.STRING_TYPE_INFO, "string");
- addTypeMapping(Constants.VOID_TYPE_INFO, "never");
- addTypeMapping(Constants.OBJECT_TYPE_INFO, ctx.getProps().getTypescript().getJavaObjectMapType());
+ super(ctx);
+ typeNameMappings.put(Constants.BOOLEAN_TYPE_INFO, "boolean");
+ typeNameMappings.put(Constants.BYTE_TYPE_INFO, "number");
+ typeNameMappings.put(Constants.CHAR_TYPE_INFO, "string");
+ typeNameMappings.put(Constants.DOUBLE_TYPE_INFO, "number");
+ typeNameMappings.put(Constants.FLOAT_TYPE_INFO, "number");
+ typeNameMappings.put(Constants.INT_TYPE_INFO, "number");
+ typeNameMappings.put(Constants.LONG_TYPE_INFO, "number");
+ typeNameMappings.put(Constants.SHORT_TYPE_INFO, "number");
+
+ typeNameMappings.put(Constants.BOXED_BOOLEAN_TYPE_INFO, "boolean");
+ typeNameMappings.put(Constants.BOXED_BYTE_TYPE_INFO, "number");
+ typeNameMappings.put(Constants.BOXED_CHAR_TYPE_INFO, "string");
+ typeNameMappings.put(Constants.BOXED_DOUBLE_TYPE_INFO, "number");
+ typeNameMappings.put(Constants.BOXED_FLOAT_TYPE_INFO, "number");
+ typeNameMappings.put(Constants.BOXED_INT_TYPE_INFO, "number");
+ typeNameMappings.put(Constants.BOXED_LONG_TYPE_INFO, "number");
+ typeNameMappings.put(Constants.BOXED_SHORT_TYPE_INFO, "number");
+
+ typeNameMappings.put(Constants.STRING_TYPE_INFO, "string");
+ typeNameMappings.put(Constants.VOID_TYPE_INFO, "never");
+ typeNameMappings.put(Constants.OBJECT_TYPE_INFO, ctx.getProps().getTypescript().getJavaObjectMapType());
+ }
+
+ @Override
+ ArraySpec arraySpec() {
+ return ARRAY_SPEC;
+ }
+
+ @Override
+ MapSpec mapSpec(ConcreteTypeInfo typeInfo) {
+ if (typeInfo.isEnumType()) {
+ return ENUM_KEY_MAP_SPEC;
+ }
+ return DEFAULT_MAP_SPEC;
}
@Override
- void buildArrayExprSuffix(ArrayTypeInfo typeInfo, StringBuilder exprBuilder) {
- exprBuilder.append("[]");
+ @Nullable
+ String toTypeExpression(ConcreteTypeInfo typeInfo, String defaultExpr) {
+ return typeNameMappings.getOrDefault(typeInfo, defaultExpr);
}
}
diff --git a/processor/src/main/resources/sharedtype-default.properties b/processor/src/main/resources/sharedtype-default.properties
index d05771c7..1b0ab36a 100644
--- a/processor/src/main/resources/sharedtype-default.properties
+++ b/processor/src/main/resources/sharedtype-default.properties
@@ -17,6 +17,8 @@ sharedtype.accessor.getter-prefixes=get,is
sharedtype.array-like-types=java.lang.Iterable
## a set of type qualified names to be treated as map during type parsing, comma separated.
+## Currently, the type must have exactly two type parameters, the first one is the key type and the second one is the value type.
+## Later version of SharedType will support more flexible map types, by providing configuration options.
sharedtype.map-like-types=java.util.Map
## a set of type qualified names to be ignored during type parsing, comma separated.
diff --git a/processor/src/main/resources/templates/rust/header.mustache b/processor/src/main/resources/templates/rust/header.mustache
index 51323538..bf2a9925 100644
--- a/processor/src/main/resources/templates/rust/header.mustache
+++ b/processor/src/main/resources/templates/rust/header.mustache
@@ -1,4 +1,5 @@
// Code generated by https://github.com/SharedType/sharedtype
{{allowExpr}}
{{#renderFlags.useRustAny}}use std::any::Any;{{/renderFlags.useRustAny}}
+{{#renderFlags.useRustMap}}use std::collections::HashMap;{{/renderFlags.useRustMap}}
diff --git a/processor/src/test/java/online/sharedtype/processor/parser/type/TypeInfoParserImplTest.java b/processor/src/test/java/online/sharedtype/processor/parser/type/TypeInfoParserImplTest.java
index fef67100..e57636e0 100644
--- a/processor/src/test/java/online/sharedtype/processor/parser/type/TypeInfoParserImplTest.java
+++ b/processor/src/test/java/online/sharedtype/processor/parser/type/TypeInfoParserImplTest.java
@@ -158,6 +158,24 @@ void parseObject() {
});
}
+ @ParameterizedTest
+ @CsvSource({
+ "false, false",
+ "true, false",
+ "false, true",
+ })
+ void setTypeFlags(boolean isEnum, boolean isMaplike) {
+ var type = ctxMocks.declaredTypeVariable("field1", ctxMocks.typeElement("com.github.cuzfrog.SomeType").type())
+ .withTypeKind(TypeKind.DECLARED)
+ .type();
+
+ when(ctxMocks.getContext().isEnumType(type)).thenReturn(isEnum);
+ when(ctxMocks.getContext().isMaplike(type)).thenReturn(isMaplike);
+ var typeInfo = (ConcreteTypeInfo) parser.parse(type, typeContextOuter);
+ assertThat(typeInfo.isEnumType()).isEqualTo(isEnum);
+ assertThat(typeInfo.isMapType()).isEqualTo(isMaplike);
+ }
+
@Test
void parseGenericObjectWithKnownTypeArgs() {
var type = ctxMocks.declaredTypeVariable("field1", ctxMocks.typeElement("com.github.cuzfrog.Tuple").type())
@@ -245,7 +263,7 @@ void shouldCacheTypeInfoWithTypeArgsIfIsGeneric() {
var typeInfo = (ConcreteTypeInfo) parser.parse(type, TypeContext.builder().typeDef(ClassDef.builder().qualifiedName("com.github.cuzfrog.Container").build()).build());
assertThat(typeInfo.qualifiedName()).isEqualTo("com.github.cuzfrog.Container");
assertThat(typeInfo.typeArgs()).hasSize(1);
- var typeArg = (ConcreteTypeInfo)typeInfo.typeArgs().get(0);
+ var typeArg = (ConcreteTypeInfo)typeInfo.typeArgs().getFirst();
assertThat(typeArg.qualifiedName()).isEqualTo("java.lang.Integer");
verify(ctxMocks.getTypeStore()).getTypeInfo("com.github.cuzfrog.Container", Collections.singletonList(typeArg));
diff --git a/processor/src/test/java/online/sharedtype/processor/writer/converter/RustStructConverterIntegrationTest.java b/processor/src/test/java/online/sharedtype/processor/writer/converter/RustStructConverterIntegrationTest.java
index 2727721e..155745ed 100644
--- a/processor/src/test/java/online/sharedtype/processor/writer/converter/RustStructConverterIntegrationTest.java
+++ b/processor/src/test/java/online/sharedtype/processor/writer/converter/RustStructConverterIntegrationTest.java
@@ -25,6 +25,14 @@ void skipNonClassDef() {
assertThat(converter.shouldAccept(EnumDef.builder().build())).isFalse();
}
+ @Test
+ void skipMapClassDef() {
+ ClassDef classDef = ClassDef.builder()
+ .build();
+ classDef.linkTypeInfo(ConcreteTypeInfo.builder().mapType(true).build());
+ assertThat(converter.shouldAccept(classDef)).isFalse();
+ }
+
@Test
void shouldAcceptClassDefAnnotated() {
assertThat(converter.shouldAccept(ClassDef.builder().build())).isFalse();
@@ -70,6 +78,19 @@ void convert() {
FieldComponentInfo.builder()
.name("field3")
.type(recursiveTypeInfo)
+ .build(),
+ FieldComponentInfo.builder()
+ .name("mapField")
+ .type(ConcreteTypeInfo.builder()
+ .qualifiedName("java.util.Map")
+ .simpleName("Map")
+ .mapType(true)
+ .typeArgs(List.of(
+ Constants.STRING_TYPE_INFO,
+ Constants.INT_TYPE_INFO
+ ))
+ .build()
+ )
.build()
))
.supertypes(List.of(
@@ -103,7 +124,7 @@ void convert() {
assertThat(model.name).isEqualTo("ClassA");
assertThat(model.typeParameters).containsExactly("T");
- assertThat(model.properties).hasSize(4);
+ assertThat(model.properties).hasSize(5);
RustStructConverter.PropertyExpr prop1 = model.properties.get(0);
assertThat(prop1.name).isEqualTo("field1");
assertThat(prop1.type).isEqualTo("i32");
@@ -121,7 +142,11 @@ void convert() {
assertThat(prop3.optional).isTrue();
assertThat(prop3.typeExpr()).isEqualTo("Option>");
- RustStructConverter.PropertyExpr prop4 = model.properties.get(3);
+ RustStructConverter.PropertyExpr prop5 = model.properties.get(3);
+ assertThat(prop5.name).isEqualTo("mapField");
+ assertThat(prop5.type).isEqualTo("HashMap");
+
+ RustStructConverter.PropertyExpr prop4 = model.properties.get(4);
assertThat(prop4.name).isEqualTo("superField1");
assertThat(prop4.type).isEqualTo("String");
assertThat(prop4.optional).isFalse();
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 d93fa8d5..b5cb1778 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
@@ -4,6 +4,7 @@
import online.sharedtype.processor.domain.ArrayTypeInfo;
import online.sharedtype.processor.domain.ClassDef;
import online.sharedtype.processor.domain.ConcreteTypeInfo;
+import online.sharedtype.processor.domain.Constants;
import online.sharedtype.processor.domain.FieldComponentInfo;
import online.sharedtype.processor.domain.TypeVariableInfo;
import online.sharedtype.processor.writer.converter.type.TypeExpressionConverter;
@@ -12,6 +13,7 @@
import java.util.Arrays;
import java.util.Collections;
+import java.util.List;
import static online.sharedtype.processor.domain.Constants.INT_TYPE_INFO;
import static online.sharedtype.processor.domain.Constants.STRING_TYPE_INFO;
@@ -22,6 +24,14 @@ final class TypescriptInterfaceConverterIntegrationTest {
private final TypescriptInterfaceConverter converter = new TypescriptInterfaceConverter(
ctxMocks.getContext(), TypeExpressionConverter.typescript(ctxMocks.getContext()));
+ @Test
+ void skipMapClassDef() {
+ ClassDef classDef = ClassDef.builder()
+ .build();
+ classDef.linkTypeInfo(ConcreteTypeInfo.builder().mapType(true).build());
+ assertThat(converter.shouldAccept(classDef)).isFalse();
+ }
+
@Test
void writeInterface() {
ClassDef classDef = ClassDef.builder()
@@ -56,6 +66,25 @@ void writeInterface() {
.build(),
FieldComponentInfo.builder().name("field4")
.type(new ArrayTypeInfo(TypeVariableInfo.builder().name("T").build()))
+ .build(),
+ FieldComponentInfo.builder().name("mapField")
+ .type(
+ ConcreteTypeInfo.builder()
+ .qualifiedName("java.util.Map").simpleName("Map").mapType(true)
+ .typeArgs(Arrays.asList(STRING_TYPE_INFO, INT_TYPE_INFO))
+ .build()
+ )
+ .build(),
+ FieldComponentInfo.builder().name("mapFieldEnumKey")
+ .type(
+ ConcreteTypeInfo.builder()
+ .qualifiedName("java.util.Map").simpleName("Map").mapType(true)
+ .typeArgs(List.of(
+ ConcreteTypeInfo.builder().simpleName("MyEnum").enumType(true).build(),
+ INT_TYPE_INFO
+ ))
+ .build()
+ )
.build()
))
.build();
@@ -66,7 +95,7 @@ void writeInterface() {
assertThat(model.name).isEqualTo("ClassA");
assertThat(model.typeParameters).containsExactly("T", "U");
assertThat(model.supertypes).containsExactly("SuperClassA");
- assertThat(model.properties).hasSize(4);
+ assertThat(model.properties).hasSize(6);
TypescriptInterfaceConverter.PropertyExpr prop1 = model.properties.get(0);
assertThat(prop1.name).isEqualTo("field1");
assertThat(prop1.type).isEqualTo("number");
@@ -86,5 +115,13 @@ void writeInterface() {
assertThat(prop4.name).isEqualTo("field4");
assertThat(prop4.type).isEqualTo("T[]");
assertThat(prop4.optional).isFalse();
+
+ TypescriptInterfaceConverter.PropertyExpr prop5 = model.properties.get(4);
+ assertThat(prop5.name).isEqualTo("mapField");
+ assertThat(prop5.type).isEqualTo("Record");
+
+ TypescriptInterfaceConverter.PropertyExpr prop6 = model.properties.get(5);
+ assertThat(prop6.name).isEqualTo("mapFieldEnumKey");
+ assertThat(prop6.type).isEqualTo("Partial>");
}
}
diff --git a/processor/src/test/java/online/sharedtype/processor/writer/converter/type/RustTypeExpressionConverterTest.java b/processor/src/test/java/online/sharedtype/processor/writer/converter/type/RustTypeExpressionConverterTest.java
index 131693ff..4cb5463c 100644
--- a/processor/src/test/java/online/sharedtype/processor/writer/converter/type/RustTypeExpressionConverterTest.java
+++ b/processor/src/test/java/online/sharedtype/processor/writer/converter/type/RustTypeExpressionConverterTest.java
@@ -19,15 +19,17 @@ final class RustTypeExpressionConverterTest {
private final ContextMocks contextMocks = new ContextMocks();
private final RustTypeExpressionConverter converter = new RustTypeExpressionConverter(contextMocks.getContext());
+ private final ClassDef contextTypeDef = ClassDef.builder().simpleName("Abc").build();
+
@Test
void convertArrayType() {
- String expr = converter.toTypeExpr(new ArrayTypeInfo(Constants.INT_TYPE_INFO));
+ String expr = converter.toTypeExpr(new ArrayTypeInfo(Constants.INT_TYPE_INFO), contextTypeDef);
assertThat(expr).isEqualTo("Vec");
}
@Test
void convertObjectType() {
- assertThat(converter.toTypeExpr(Constants.OBJECT_TYPE_INFO)).isEqualTo("Box");
+ assertThat(converter.toTypeExpr(Constants.OBJECT_TYPE_INFO, contextTypeDef)).isEqualTo("Box");
}
@Test
@@ -39,12 +41,16 @@ void flagToRenderObjectType() {
converter.beforeVisitTypeInfo(Constants.OBJECT_TYPE_INFO);
verify(renderFlags).setUseRustAny(true);
+
+ verify(renderFlags, never()).setUseRustMap(anyBoolean());
+ converter.beforeVisitTypeInfo(ConcreteTypeInfo.builder().mapType(true).build());
+ verify(renderFlags).setUseRustMap(true);
}
@Test
void addSmartPointerBoxToCyclicReferencedType() {
var classDef = ClassDef.builder().cyclicReferenced(true).build();
- var expr = converter.toConcreteTypeExpression(ConcreteTypeInfo.builder().simpleName("Abc").typeDef(classDef).build());
+ var expr = converter.toTypeExpression(ConcreteTypeInfo.builder().simpleName("Abc").typeDef(classDef).build(), "Abc");
assertThat(expr).isEqualTo("Box");
}
}
diff --git a/processor/src/test/java/online/sharedtype/processor/writer/converter/type/TypescriptTypeExpressionConverterTest.java b/processor/src/test/java/online/sharedtype/processor/writer/converter/type/TypescriptTypeExpressionConverterTest.java
new file mode 100644
index 00000000..2303ae0a
--- /dev/null
+++ b/processor/src/test/java/online/sharedtype/processor/writer/converter/type/TypescriptTypeExpressionConverterTest.java
@@ -0,0 +1,72 @@
+package online.sharedtype.processor.writer.converter.type;
+
+import online.sharedtype.processor.context.ContextMocks;
+import online.sharedtype.processor.domain.ClassDef;
+import online.sharedtype.processor.domain.ConcreteTypeInfo;
+import online.sharedtype.processor.domain.Constants;
+import online.sharedtype.processor.domain.TypeVariableInfo;
+import org.junit.jupiter.api.Test;
+
+import java.util.List;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+final class TypescriptTypeExpressionConverterTest {
+ private final ContextMocks ctxMocks = new ContextMocks();
+ private final TypescriptTypeExpressionConverter converter = new TypescriptTypeExpressionConverter(ctxMocks.getContext());
+
+ @Test
+ void typeContract() {
+ assertThat(converter.typeNameMappings.keySet()).containsAll(Constants.STRING_AND_NUMBER_TYPES);
+ }
+
+ @Test
+ void invalidKeyType() {
+ ClassDef contextTypeDef = ClassDef.builder().qualifiedName("a.b.Abc").simpleName("Abc").build();
+ ConcreteTypeInfo invalidKeyTypeInfo = ConcreteTypeInfo.builder()
+ .qualifiedName("java.util.Map").simpleName("Map")
+ .mapType(true)
+ .typeArgs(List.of(
+ ConcreteTypeInfo.builder().qualifiedName("a.b.Foo").simpleName("Foo").build(),
+ Constants.INT_TYPE_INFO
+ ))
+ .typeDef(
+ ClassDef.builder().qualifiedName("java.util.Map").simpleName("Map")
+ .typeVariables(List.of(
+ TypeVariableInfo.builder().name("K").contextTypeQualifiedName("java.util.Map").build(),
+ TypeVariableInfo.builder().name("V").contextTypeQualifiedName("java.util.Map").build()
+ ))
+ .build()
+ )
+ .build();
+ assertThatThrownBy(() -> converter.toTypeExpr(invalidKeyTypeInfo,contextTypeDef))
+ .hasMessageContaining("Key type of java.util.Map must be string or numbers or enum")
+ .hasMessageContaining("context type: a.b.Abc");
+ }
+
+ @Test
+ void mapTypeHasWrongNumberOfTypeParameters() {
+ ClassDef contextTypeDef = ClassDef.builder().qualifiedName("a.b.Abc").simpleName("Abc").build();
+ ConcreteTypeInfo invalidMapTypeInfo = ConcreteTypeInfo.builder()
+ .qualifiedName("java.util.Map").simpleName("Map")
+ .mapType(true)
+ .typeArgs(List.of(
+ Constants.INT_TYPE_INFO,
+ Constants.STRING_TYPE_INFO,
+ Constants.BOXED_LONG_TYPE_INFO
+ ))
+ .typeDef(
+ ClassDef.builder().qualifiedName("java.util.Map").simpleName("Map")
+ .typeVariables(List.of(
+ TypeVariableInfo.builder().name("K").contextTypeQualifiedName("java.util.Map").build(),
+ TypeVariableInfo.builder().name("V").contextTypeQualifiedName("java.util.Map").build(),
+ TypeVariableInfo.builder().name("W").contextTypeQualifiedName("java.util.Map").build()
+ ))
+ .build()
+ )
+ .build();
+ assertThatThrownBy(() -> converter.toTypeExpr(invalidMapTypeInfo,contextTypeDef))
+ .hasMessageContaining("Base Map type must have 2 type arguments");
+ }
+}