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 @@ [![CI](https://github.com/cuzfrog/sharedtype/actions/workflows/ci.yaml/badge.svg)](https://github.com/cuzfrog/sharedtype/actions/workflows/ci.yaml) -![Maven Central Version](https://img.shields.io/maven-central/v/online.sharedtype/sharedtype?style=plastic) +[![Maven Central](https://img.shields.io/maven-central/v/online.sharedtype/sharedtype?style=social)](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. *

* *

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 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 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 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"); + } +}