diff --git a/catalog.settings.gradle.kts b/catalog.settings.gradle.kts index 39a0225..bc87472 100644 --- a/catalog.settings.gradle.kts +++ b/catalog.settings.gradle.kts @@ -6,7 +6,6 @@ dependencyResolutionManagement { version("junit", "5.11.0") version("recordbuilder", "42") version("mapstruct", "1.6.0") - version("ethelred-util", "2.2") library("avaje-json", "io.avaje", "avaje-jsonb").versionRef("avaje-json") library("avaje-json-processor", "io.avaje", "avaje-jsonb-generator").versionRef("avaje-json") @@ -38,7 +37,6 @@ dependencyResolutionManagement { library("compile-testing", "com.google.testing.compile:compile-testing:0.21.0") library("compile-testing-extension", "io.github.kiskae:compile-testing-extension:1.0.2") - library("ethelred-util", "org.ethelred.util", "common").versionRef("ethelred-util") library("yaml", "org.yaml:snakeyaml:2.2") bundle("compile-testing", listOf("guava", "compile-testing", "compile-testing-extension")) diff --git a/processor/build.gradle.kts b/processor/build.gradle.kts index bba5b49..20c9870 100644 --- a/processor/build.gradle.kts +++ b/processor/build.gradle.kts @@ -14,7 +14,6 @@ dependencies { implementation(libs.recordbuilder.core) implementation(libs.mapstruct.processor) implementation(libs.javapoet) - implementation(libs.ethelred.util) testAnnotationProcessor(libs.mapstruct.processor) testImplementation(project(":runtime")) testImplementation(libs.jakarta.inject) diff --git a/processor/src/main/java/org/ethelred/kiwiproc/processor/CoreTypes.java b/processor/src/main/java/org/ethelred/kiwiproc/processor/CoreTypes.java index 9767bce..750764f 100644 --- a/processor/src/main/java/org/ethelred/kiwiproc/processor/CoreTypes.java +++ b/processor/src/main/java/org/ethelred/kiwiproc/processor/CoreTypes.java @@ -1,6 +1,6 @@ package org.ethelred.kiwiproc.processor; -import static org.ethelred.util.collect.BiMap.entry; +import static java.util.Map.entry; import java.math.BigDecimal; import java.math.BigInteger; @@ -18,11 +18,14 @@ import java.util.Set; import java.util.stream.Collectors; import java.util.stream.Stream; -import org.ethelred.util.collect.BiMap; +import org.ethelred.kiwiproc.processor.types.BasicType; +import org.ethelred.kiwiproc.processor.types.KiwiType; +import org.ethelred.kiwiproc.processor.types.PrimitiveKiwiType; import org.jspecify.annotations.Nullable; public class CoreTypes { - public static final SimpleType STRING_TYPE = SimpleType.ofClass(String.class); + public static final BasicType STRING_TYPE = + new BasicType(String.class.getPackageName(), String.class.getSimpleName(), false); public static final Set> BASIC_TYPES = Set.of( String.class, BigInteger.class, @@ -39,7 +42,7 @@ public boolean hasWarning() { } } - public static final BiMap, Class> primitiveToBoxed = BiMap.ofEntries( + public static final Map, Class> primitiveToBoxed = Map.ofEntries( entry(boolean.class, Boolean.class), entry(byte.class, Byte.class), entry(char.class, Character.class), @@ -49,6 +52,10 @@ public boolean hasWarning() { entry(float.class, Float.class), entry(double.class, Double.class)); + public static final Map primitiveToBoxedStrings = primitiveToBoxed.entrySet().stream() + .map(e -> entry(e.getKey().getSimpleName(), e.getValue().getSimpleName())) + .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); + /* The key type can be assigned to any of the value types without casting. Primitive type mappings that are NOT in this map require a cast and a "lossy converson" warning. @@ -68,7 +75,7 @@ private static boolean isAssignable(Class source, Class target) { } private final Conversion invalid = new Conversion(false, null, "invalid"); - Map, SimpleType> coreTypes; + Map, KiwiType> coreTypes; Map coreMappings; public CoreTypes() { @@ -91,20 +98,20 @@ private Map defineMappings() { } private void addPrimitiveParseMappings(Collection> entries) { - primitiveToBoxed.keysA().forEach(target -> { + primitiveToBoxed.keySet().forEach(target -> { String warning = "possible NumberFormatException parsing String to %s".formatted(target.getName()); - Class boxed = primitiveToBoxed.getByA(target).orElseThrow(); + Class boxed = primitiveToBoxed.get(target); // String -> primitive TypeMapping t = new TypeMapping(STRING_TYPE, coreTypes.get(target)); Conversion c = new Conversion( true, warning, "%s.parse%s(%%s)".formatted(boxed.getSimpleName(), Util.capitalizeFirst(target.getSimpleName()))); - entries.add(Map.entry(t, c)); + entries.add(entry(t, c)); // String -> boxed t = new TypeMapping(STRING_TYPE, coreTypes.get(boxed)); c = new Conversion(true, warning, "%s.valueOf(%%s)".formatted(boxed.getSimpleName())); - entries.add(Map.entry(t, c)); + entries.add(entry(t, c)); }); } @@ -116,11 +123,6 @@ private void addBigNumberMappings(Collection> Stream.of(byte.class, short.class, int.class, long.class, float.class, double.class) .forEach(source -> { entries.add(mappingEntry(source, big, null, "%s.valueOf(%%s)".formatted(big.getSimpleName()))); - entries.add(mappingEntry( - primitiveToBoxed.getByA(source).orElseThrow(), - big, - null, - "%s.valueOf(%%s)".formatted(big.getSimpleName()))); }); // String -> Big @@ -132,46 +134,27 @@ private void addBigNumberMappings(Collection> .forEach(target -> { String w = "possible lossy conversion from %s to %s".formatted(big.getName(), target.getName()); entries.add(mappingEntry(big, target, w, "%%s.%sValue()".formatted(target.getName()))); - entries.add(mappingEntry( - big, - primitiveToBoxed.getByA(target).orElseThrow(), - w, - "%%s.%sValue()".formatted(target.getName()))); }); }); } private void addPrimitiveMappings(Collection> entries) { - // boxing - is assignment - primitiveToBoxed.mapByA().forEach((source, target) -> entries.add(mappingEntry(source, target, null, "%s"))); - // primitive safe assignments assignableFrom.forEach((source, targets) -> { targets.forEach(target -> { // primitive entries.add(mappingEntry(source, target, null, "%s")); - // also boxing - entries.add(mappingEntry(source, primitiveToBoxed.getByA(target).orElseThrow(), null, "%s")); - // also boxing both - entries.add(mappingEntry( - primitiveToBoxed.getByA(source).orElseThrow(), - primitiveToBoxed.getByA(target).orElseThrow(), - null, - "%s")); }); }); // primitive lossy assignments - primitiveToBoxed.keysA().forEach(source -> { - primitiveToBoxed.keysA().forEach(target -> { + primitiveToBoxed.keySet().forEach(source -> { + primitiveToBoxed.keySet().forEach(target -> { if (!source.equals(target) && !isAssignable(source, target)) { String warning = "possible lossy conversion from %s to %s".formatted(source.getName(), target.getName()); String conversionFormat = "(%s) %%s".formatted(target.getName()); entries.add(mappingEntry(source, target, warning, conversionFormat)); - // also boxing - entries.add(mappingEntry( - source, primitiveToBoxed.getByA(target).orElseThrow(), warning, conversionFormat)); } }); }); @@ -183,14 +166,16 @@ private Map.Entry mappingEntry( var toType = Objects.requireNonNull(coreTypes.get(target)); var mapping = new TypeMapping(fromType, toType); var lookup = new Conversion(true, warning, conversionFormat); - return Map.entry(mapping, lookup); + return entry(mapping, lookup); } - private Map, SimpleType> defineTypes() { - Map, SimpleType> builder = new LinkedHashMap<>(32); - primitiveToBoxed.keysA().forEach(c -> builder.put(c, SimpleType.ofClass(c))); - primitiveToBoxed.keysB().forEach(c -> builder.put(c, SimpleType.ofClass(c, true))); - BASIC_TYPES.forEach(c -> builder.put(c, SimpleType.ofClass(c))); + private Map, KiwiType> defineTypes() { + Map, KiwiType> builder = new LinkedHashMap<>(32); + primitiveToBoxed.forEach((key, value) -> { + builder.put(key, new PrimitiveKiwiType(key.getSimpleName(), false)); + builder.put(value, new PrimitiveKiwiType(key.getSimpleName(), true)); + }); + BASIC_TYPES.forEach(c -> builder.put(c, new BasicType(c.getPackageName(), c.getSimpleName(), false))); return Map.copyOf(builder); } @@ -202,13 +187,10 @@ public KiwiType type(Class aClass) { } public Conversion lookup(TypeMapping mapper) { - if (mapper.source() instanceof SimpleType source && mapper.target() instanceof SimpleType target) { - return lookup(source, target); - } - return invalid; + return lookup(mapper.source(), mapper.target()); } - public Conversion lookup(SimpleType source, SimpleType target) { + public Conversion lookup(KiwiType source, KiwiType target) { if (source.equals(target) || source.withIsNullable(true).equals(target)) { return new Conversion(true, null, "%s"); } diff --git a/processor/src/main/java/org/ethelred/kiwiproc/processor/DAOMethodInfo.java b/processor/src/main/java/org/ethelred/kiwiproc/processor/DAOMethodInfo.java index ab2111e..9e6aa78 100644 --- a/processor/src/main/java/org/ethelred/kiwiproc/processor/DAOMethodInfo.java +++ b/processor/src/main/java/org/ethelred/kiwiproc/processor/DAOMethodInfo.java @@ -3,6 +3,8 @@ import java.util.List; import javax.lang.model.element.ExecutableElement; import org.ethelred.kiwiproc.meta.ParsedQuery; +import org.ethelred.kiwiproc.processor.types.ContainerType; +import org.ethelred.kiwiproc.processor.types.KiwiType; import org.jspecify.annotations.Nullable; @KiwiRecordBuilder diff --git a/processor/src/main/java/org/ethelred/kiwiproc/processor/DAOResultColumn.java b/processor/src/main/java/org/ethelred/kiwiproc/processor/DAOResultColumn.java index a85129c..d2ad9e6 100644 --- a/processor/src/main/java/org/ethelred/kiwiproc/processor/DAOResultColumn.java +++ b/processor/src/main/java/org/ethelred/kiwiproc/processor/DAOResultColumn.java @@ -1,5 +1,7 @@ package org.ethelred.kiwiproc.processor; +import org.ethelred.kiwiproc.processor.types.KiwiType; + public record DAOResultColumn(String name, SqlTypeMapping sqlTypeMapping, KiwiType targetType) { public TypeMapping asTypeMapping() { return new TypeMapping(sqlTypeMapping.kiwiType(), targetType); diff --git a/processor/src/main/java/org/ethelred/kiwiproc/processor/KiwiProcessor.java b/processor/src/main/java/org/ethelred/kiwiproc/processor/KiwiProcessor.java index c4810cc..87340ae 100644 --- a/processor/src/main/java/org/ethelred/kiwiproc/processor/KiwiProcessor.java +++ b/processor/src/main/java/org/ethelred/kiwiproc/processor/KiwiProcessor.java @@ -23,6 +23,8 @@ import org.ethelred.kiwiproc.meta.DatabaseWrapper; import org.ethelred.kiwiproc.meta.ParsedQuery; import org.ethelred.kiwiproc.processor.generator.PoetDAOGenerator; +import org.ethelred.kiwiproc.processor.types.ContainerType; +import org.ethelred.kiwiproc.processor.types.RecordType; import org.ethelred.kiwiproc.processorconfig.DataSourceConfig; import org.ethelred.kiwiproc.processorconfig.DataSourceConfigJsonAdapter; import org.ethelred.kiwiproc.processorconfig.ProcessorConfig; diff --git a/processor/src/main/java/org/ethelred/kiwiproc/processor/KiwiType.java b/processor/src/main/java/org/ethelred/kiwiproc/processor/KiwiType.java deleted file mode 100644 index 8a77b67..0000000 --- a/processor/src/main/java/org/ethelred/kiwiproc/processor/KiwiType.java +++ /dev/null @@ -1,15 +0,0 @@ -package org.ethelred.kiwiproc.processor; - -/** - * Simplified view of type for parameters and return. - */ -public sealed interface KiwiType - permits ContainerType, RecordType, SimpleType, SqlArrayType, UnsupportedType, VoidType { - static KiwiType unsupported() { - return new UnsupportedType(); - } - - String packageName(); - - String className(); -} diff --git a/processor/src/main/java/org/ethelred/kiwiproc/processor/KiwiTypeVisitor.java b/processor/src/main/java/org/ethelred/kiwiproc/processor/KiwiTypeVisitor.java index cb8a480..eb900f5 100644 --- a/processor/src/main/java/org/ethelred/kiwiproc/processor/KiwiTypeVisitor.java +++ b/processor/src/main/java/org/ethelred/kiwiproc/processor/KiwiTypeVisitor.java @@ -4,6 +4,7 @@ import java.util.List; import javax.lang.model.type.*; import javax.lang.model.util.SimpleTypeVisitor14; +import org.ethelred.kiwiproc.processor.types.*; public class KiwiTypeVisitor extends SimpleTypeVisitor14 { private final TypeUtils utils; @@ -14,7 +15,7 @@ public KiwiTypeVisitor(TypeUtils utils) { @Override public KiwiType visitPrimitive(PrimitiveType t, Void ignore) { - return new SimpleType("", t.toString(), false); + return new PrimitiveKiwiType(t.toString(), false); } @Override @@ -24,11 +25,15 @@ public KiwiType visitArray(ArrayType t, Void ignore) { @Override public KiwiType visitDeclared(DeclaredType t, Void ignore) { - if (utils.isBoxed(t)) { - return new SimpleType(utils.packageName(t), utils.className(t), true); + try { + var primitiveType = utils.unboxedType(t); + return new PrimitiveKiwiType(primitiveType.toString(), true); + + } catch (IllegalArgumentException e) { + // not a boxed type - continue } if (CoreTypes.BASIC_TYPES.stream().anyMatch(bt -> utils.isSameType(t, utils.type(bt)))) { - return new SimpleType(utils.packageName(t), utils.className(t), utils.isNullable(t)); + return new BasicType(utils.packageName(t), utils.className(t), utils.isNullable(t)); } for (var vct : ValidContainerType.values()) { if (utils.isSameType(utils.erasure(t), utils.erasure(vct.javaType()))) { diff --git a/processor/src/main/java/org/ethelred/kiwiproc/processor/MethodParameterInfo.java b/processor/src/main/java/org/ethelred/kiwiproc/processor/MethodParameterInfo.java index a320ca6..0e3fcaf 100644 --- a/processor/src/main/java/org/ethelred/kiwiproc/processor/MethodParameterInfo.java +++ b/processor/src/main/java/org/ethelred/kiwiproc/processor/MethodParameterInfo.java @@ -3,6 +3,7 @@ import java.util.*; import java.util.stream.Collectors; import javax.lang.model.element.VariableElement; +import org.ethelred.kiwiproc.processor.types.KiwiType; import org.jspecify.annotations.Nullable; @KiwiRecordBuilder diff --git a/processor/src/main/java/org/ethelred/kiwiproc/processor/Signature.java b/processor/src/main/java/org/ethelred/kiwiproc/processor/Signature.java index 4efe0f9..703af0f 100644 --- a/processor/src/main/java/org/ethelred/kiwiproc/processor/Signature.java +++ b/processor/src/main/java/org/ethelred/kiwiproc/processor/Signature.java @@ -3,6 +3,7 @@ import java.util.List; import javax.lang.model.element.ExecutableElement; import javax.lang.model.element.VariableElement; +import org.ethelred.kiwiproc.processor.types.KiwiType; @KiwiRecordBuilder public record Signature(KiwiType returnType, String methodName, List paramNames) { diff --git a/processor/src/main/java/org/ethelred/kiwiproc/processor/SimpleType.java b/processor/src/main/java/org/ethelred/kiwiproc/processor/SimpleType.java deleted file mode 100644 index d5d5ed3..0000000 --- a/processor/src/main/java/org/ethelred/kiwiproc/processor/SimpleType.java +++ /dev/null @@ -1,26 +0,0 @@ -package org.ethelred.kiwiproc.processor; - -public record SimpleType(String packageName, String className, boolean isNullable) implements KiwiType { - public static SimpleType ofClass(Class aClass, boolean isNullable) { - var packageName = aClass.getPackageName(); - - if (aClass.isPrimitive()) { - packageName = ""; - } - - return new SimpleType(packageName, aClass.getSimpleName(), isNullable); - } - - public static SimpleType ofClass(Class aClass) { - return ofClass(aClass, false); - } - - @Override - public String toString() { - return className + (isNullable ? "/nullable" : "/non-null"); - } - - public SimpleType withIsNullable(boolean newValue) { - return new SimpleType(packageName, className, newValue); - } -} diff --git a/processor/src/main/java/org/ethelred/kiwiproc/processor/SqlTypeMapping.java b/processor/src/main/java/org/ethelred/kiwiproc/processor/SqlTypeMapping.java index 1b2ad76..2fbd794 100644 --- a/processor/src/main/java/org/ethelred/kiwiproc/processor/SqlTypeMapping.java +++ b/processor/src/main/java/org/ethelred/kiwiproc/processor/SqlTypeMapping.java @@ -7,6 +7,10 @@ import java.util.Map; import java.util.stream.Collectors; import org.ethelred.kiwiproc.meta.ColumnMetaData; +import org.ethelred.kiwiproc.processor.types.BasicType; +import org.ethelred.kiwiproc.processor.types.KiwiType; +import org.ethelred.kiwiproc.processor.types.PrimitiveKiwiType; +import org.ethelred.kiwiproc.processor.types.SqlArrayType; import org.jspecify.annotations.Nullable; @KiwiRecordBuilder @@ -76,13 +80,9 @@ public KiwiType kiwiType() { assert componentType != null; return new SqlArrayType(componentType.kiwiType()); } - var resolvedType = baseType; - if (isNullable) { - var maybeBoxed = CoreTypes.primitiveToBoxed.getByA(baseType); - if (maybeBoxed.isPresent()) { - resolvedType = maybeBoxed.get(); - } + if (CoreTypes.primitiveToBoxed.containsKey(baseType)) { + return new PrimitiveKiwiType(baseType().getSimpleName(), isNullable); } - return SimpleType.ofClass(resolvedType, isNullable); + return new BasicType(baseType.getPackageName(), baseType.getSimpleName(), isNullable); } } diff --git a/processor/src/main/java/org/ethelred/kiwiproc/processor/TypeMapping.java b/processor/src/main/java/org/ethelred/kiwiproc/processor/TypeMapping.java index b35c5b3..4ec70c8 100644 --- a/processor/src/main/java/org/ethelred/kiwiproc/processor/TypeMapping.java +++ b/processor/src/main/java/org/ethelred/kiwiproc/processor/TypeMapping.java @@ -1,3 +1,5 @@ package org.ethelred.kiwiproc.processor; +import org.ethelred.kiwiproc.processor.types.KiwiType; + public record TypeMapping(KiwiType source, KiwiType target) {} diff --git a/processor/src/main/java/org/ethelred/kiwiproc/processor/TypeUtils.java b/processor/src/main/java/org/ethelred/kiwiproc/processor/TypeUtils.java index ab750f2..6ea678c 100644 --- a/processor/src/main/java/org/ethelred/kiwiproc/processor/TypeUtils.java +++ b/processor/src/main/java/org/ethelred/kiwiproc/processor/TypeUtils.java @@ -9,6 +9,8 @@ import javax.lang.model.util.Elements; import javax.lang.model.util.SimpleTypeVisitor14; import javax.lang.model.util.Types; +import org.ethelred.kiwiproc.processor.types.KiwiType; +import org.ethelred.kiwiproc.processor.types.ValidContainerType; import org.jspecify.annotations.Nullable; public class TypeUtils extends TypeMirrors { diff --git a/processor/src/main/java/org/ethelred/kiwiproc/processor/TypeValidator.java b/processor/src/main/java/org/ethelred/kiwiproc/processor/TypeValidator.java index 78a81c1..f3ea92f 100644 --- a/processor/src/main/java/org/ethelred/kiwiproc/processor/TypeValidator.java +++ b/processor/src/main/java/org/ethelred/kiwiproc/processor/TypeValidator.java @@ -4,10 +4,11 @@ import java.util.*; import javax.lang.model.element.Element; import org.ethelred.kiwiproc.meta.ColumnMetaData; +import org.ethelred.kiwiproc.processor.types.*; public record TypeValidator(Logger logger, Element element, CoreTypes coreTypes) { - private static final KiwiType UPDATE_RETURN_TYPE = SimpleType.ofClass(int.class); + private static final KiwiType UPDATE_RETURN_TYPE = new PrimitiveKiwiType("int", false); private static final KiwiType BATCH_RETURN_TYPE = new ContainerType(ValidContainerType.ARRAY, UPDATE_RETURN_TYPE); public TypeValidator { @@ -65,11 +66,11 @@ private boolean validateSingleParameter(KiwiType parameterType, KiwiType columnT } private CharSequence simpleName() { - return element == null ? "unknown element" : element.getSimpleName(); + return element.getSimpleName(); } /** - * Check whether we know how to convert "from" to "to". + * Check whether we know how to convert "source" to "target". * This operation is not necessarily commutative. * @param source * @param target @@ -85,64 +86,61 @@ private boolean validateCompatible(KiwiType source, KiwiType target) { We assume that a container may not contain a null. However, for adding to a target container, pretend that the contained type is nullable. We will skip nulls. */ - if (source instanceof ContainerType fromContainer && target instanceof ContainerType toContainer) { + if (source instanceof ContainerType sourceContainer && target instanceof ContainerType targetContainer) { // we can convert any container into a different one, since they are all effectively equivalent to Iterable. - return validateCompatible(fromContainer.containedType(), toContainer.containedType()); + return validateCompatible(sourceContainer.containedType(), targetContainer.containedType()); } - if (target instanceof ContainerType toContainer) { + if (target instanceof ContainerType targetContainer) { // we can convert a single value to a container by wrapping - return validateCompatible(source, toContainer.containedType()); + return validateCompatible(source, targetContainer.containedType()); } if (source instanceof ContainerType containerType && containerType.type() == ValidContainerType.OPTIONAL - && target instanceof SimpleType simpleType) { + && target.isSimple()) { // an Optional can be converted to a nullable simple type - // TODO how to interact with Record? - return simpleType.isNullable() && validateCompatible(containerType.containedType(), simpleType); + // targetDO how to interact with Record? + return target.isNullable() && validateCompatible(containerType.containedType(), target); } - if (source instanceof RecordType fromRecord && target instanceof RecordType toRecord) { + if (source instanceof RecordType sourceRecord && target instanceof RecordType targetRecord) { // Component names must match, and types must be compatible. Order is not relevant in this context. - var toComponents = toRecord.components(); - return fromRecord.components().stream().allMatch(e -> { - var toComponentType = toComponents.stream() - .filter(toComponent -> e.name().equals(toComponent.name())) + var targetComponents = targetRecord.components(); + return sourceRecord.components().stream().allMatch(e -> { + var targetComponentType = targetComponents.stream() + .filter(targetComponent -> e.name().equals(targetComponent.name())) .findFirst() .orElse(null); - return toComponentType != null && validateCompatible(e.type(), toComponentType.type()); + return targetComponentType != null && validateCompatible(e.type(), targetComponentType.type()); }); } - if (source instanceof SimpleType fromType - && !fromType.isNullable() - && target instanceof SimpleType toType - && toType.isNullable()) { + if (source.isSimple() && !source.isNullable() && target.isSimple() && target.isNullable()) { // non-null can be converted to nullable - return validateCompatible(fromType.withIsNullable(true), toType); + return validateCompatible(source.withIsNullable(true), target); } - if (source instanceof SimpleType fromType && target instanceof SimpleType toType) { - return validateSimpleCompatible(fromType, toType); + if (source.isSimple() && target.isSimple()) { + return validateSimpleCompatible(source, target); } return false; } - private boolean validateSimpleCompatible(SimpleType fromType, SimpleType toType) { - if (fromType.equals(toType)) { + private boolean validateSimpleCompatible(KiwiType source, KiwiType target) { + if (source.equals(target)) { // shortcut return true; } - if (fromType.isNullable() != toType.isNullable()) { + if (source.isNullable() != target.isNullable()) { // nullability must match. (See validateCompatible for non-null -> null) return false; } - if (toType.equals(CoreTypes.STRING_TYPE.withIsNullable(toType.isNullable()))) { + if (target.equals(CoreTypes.STRING_TYPE.withIsNullable(target.isNullable()))) { // anything can be converted to String return true; } - var typeLookup = coreTypes.lookup(fromType, toType); + var typeLookup = coreTypes.lookup(source, target); if (typeLookup.hasWarning()) { logger.warn(element, typeLookup.warning()); } return typeLookup.isValid(); - // TODO user defined mappings + // targetDO user defined mappings } /** @@ -152,17 +150,16 @@ private boolean validateSimpleCompatible(SimpleType fromType, SimpleType toType) */ private boolean validateGeneral(KiwiType type) { // switch record pattern not available in Java 17 :-( - if (type instanceof SimpleType || type instanceof VoidType) { + if (type.isSimple() || type instanceof VoidType) { return true; } if (type instanceof ContainerType ct) { var contained = ct.containedType(); - return ((contained instanceof RecordType) || (contained instanceof SimpleType)) - && validateGeneral(contained); + return ((contained instanceof RecordType) || (contained.isSimple())) && validateGeneral(contained); } if (type instanceof RecordType rt) { var componentTypes = rt.components(); - return componentTypes.stream().allMatch(t -> t.type() instanceof SimpleType); + return componentTypes.stream().allMatch(t -> t.type().isSimple()); } return false; } @@ -198,26 +195,28 @@ public boolean validateReturn(List columnMetaData, KiwiType retu debug("Return type Container %s.%s".formatted(containerType.packageName(), containerType.className())); return validateReturn(columnMetaData, containerType.containedType(), kind); } - if (columnMetaData.size() == 1 && returnType instanceof SimpleType simpleType) { - debug("Return type simple %s.%s".formatted(simpleType.packageName(), simpleType.className())); + if (columnMetaData.size() == 1 && returnType.isSimple()) { + debug("Return type simple %s.%s".formatted(returnType.packageName(), returnType.className())); // a single column result maps to a simple type var first = columnMetaData.get(0); KiwiType columnType = SqlTypeMapping.get(first).kiwiType(); - return validateCompatible(columnType, simpleType); + return validateCompatible(columnType, returnType); } if (returnType instanceof RecordType recordType) { debug("Return type record %s.%s".formatted(recordType.packageName(), recordType.className())); var components = recordType.components(); return columnMetaData.stream().allMatch(cmd -> { var componentType = components.stream() - .filter(toComponent -> cmd.name().equals(toComponent.name())) + .filter(targetComponent -> cmd.name().equals(targetComponent.name())) .findFirst() .orElse(null); KiwiType columnType = SqlTypeMapping.get(cmd).kiwiType(); - return (componentType != null - && componentType.type() instanceof SimpleType simpleType - && validateCompatible(columnType, simpleType)) - || reportError("Missing or incompatible component type %s for column %s type %s" + if (componentType == null) { + return reportError( + "Missing component type for column %s type %s".formatted(cmd.name(), columnType)); + } + return componentType.type().isSimple() && validateCompatible(columnType, componentType.type()) + || reportError("Incompatible component type %s for column %s type %s" .formatted(componentType, cmd.name(), columnType)); }) && components.stream().allMatch(cmp -> { diff --git a/processor/src/main/java/org/ethelred/kiwiproc/processor/generator/InstanceGenerator.java b/processor/src/main/java/org/ethelred/kiwiproc/processor/generator/InstanceGenerator.java index e87cca1..1fac9c4 100644 --- a/processor/src/main/java/org/ethelred/kiwiproc/processor/generator/InstanceGenerator.java +++ b/processor/src/main/java/org/ethelred/kiwiproc/processor/generator/InstanceGenerator.java @@ -8,6 +8,7 @@ import java.util.*; import javax.lang.model.element.Modifier; import org.ethelred.kiwiproc.processor.*; +import org.ethelred.kiwiproc.processor.types.BasicType; public class InstanceGenerator { @@ -84,7 +85,7 @@ private CodeBlock queryMethodBody(DAOMethodInfo methodInfo) { builder.addStatement( "var $L = $L", name, conversion.conversionFormat().formatted(parameterInfo.javaAccessor())); var nullableSource = - parameterInfo.mapper().source() instanceof SimpleType simpleType && simpleType.isNullable(); + parameterInfo.mapper().source() instanceof BasicType simpleType && simpleType.isNullable(); if (nullableSource) { builder.beginControlFlow("if ($L == null)", name) .addStatement("statement.setNull($L, $L)", parameterInfo.index(), parameterInfo.sqlType()) diff --git a/processor/src/main/java/org/ethelred/kiwiproc/processor/generator/KiwiTypeConverter.java b/processor/src/main/java/org/ethelred/kiwiproc/processor/generator/KiwiTypeConverter.java index a274d05..66a9f2d 100644 --- a/processor/src/main/java/org/ethelred/kiwiproc/processor/generator/KiwiTypeConverter.java +++ b/processor/src/main/java/org/ethelred/kiwiproc/processor/generator/KiwiTypeConverter.java @@ -4,8 +4,8 @@ import com.palantir.javapoet.ParameterizedTypeName; import com.palantir.javapoet.TypeName; import java.util.Map; -import org.ethelred.kiwiproc.processor.ContainerType; -import org.ethelred.kiwiproc.processor.KiwiType; +import org.ethelred.kiwiproc.processor.types.ContainerType; +import org.ethelred.kiwiproc.processor.types.KiwiType; import org.ethelred.kiwiproc.processorconfig.DependencyInjectionStyle; public class KiwiTypeConverter { diff --git a/processor/src/main/java/org/ethelred/kiwiproc/processor/generator/ProviderGenerator.java b/processor/src/main/java/org/ethelred/kiwiproc/processor/generator/ProviderGenerator.java index 9dccead..905403f 100644 --- a/processor/src/main/java/org/ethelred/kiwiproc/processor/generator/ProviderGenerator.java +++ b/processor/src/main/java/org/ethelred/kiwiproc/processor/generator/ProviderGenerator.java @@ -8,7 +8,7 @@ import javax.sql.DataSource; import org.ethelred.kiwiproc.processor.DAOClassInfo; import org.ethelred.kiwiproc.processor.DAOMethodInfo; -import org.ethelred.kiwiproc.processor.VoidType; +import org.ethelred.kiwiproc.processor.types.VoidType; import org.ethelred.kiwiproc.processorconfig.DependencyInjectionStyle; public class ProviderGenerator { diff --git a/processor/src/main/java/org/ethelred/kiwiproc/processor/types/BasicType.java b/processor/src/main/java/org/ethelred/kiwiproc/processor/types/BasicType.java new file mode 100644 index 0000000..7d3066a --- /dev/null +++ b/processor/src/main/java/org/ethelred/kiwiproc/processor/types/BasicType.java @@ -0,0 +1,17 @@ +package org.ethelred.kiwiproc.processor.types; + +public record BasicType(String packageName, String className, boolean isNullable) implements KiwiType { + @Override + public String toString() { + return className + (isNullable ? "/nullable" : "/non-null"); + } + + public BasicType withIsNullable(boolean newValue) { + return new BasicType(packageName, className, newValue); + } + + @Override + public boolean isSimple() { + return true; + } +} diff --git a/processor/src/main/java/org/ethelred/kiwiproc/processor/ContainerType.java b/processor/src/main/java/org/ethelred/kiwiproc/processor/types/ContainerType.java similarity index 71% rename from processor/src/main/java/org/ethelred/kiwiproc/processor/ContainerType.java rename to processor/src/main/java/org/ethelred/kiwiproc/processor/types/ContainerType.java index ecfe7fb..51a73ce 100644 --- a/processor/src/main/java/org/ethelred/kiwiproc/processor/ContainerType.java +++ b/processor/src/main/java/org/ethelred/kiwiproc/processor/types/ContainerType.java @@ -1,4 +1,4 @@ -package org.ethelred.kiwiproc.processor; +package org.ethelred.kiwiproc.processor.types; public record ContainerType(ValidContainerType type, KiwiType containedType) implements KiwiType { @@ -11,4 +11,9 @@ public String packageName() { public String className() { return type.javaType().getSimpleName(); } + + @Override + public boolean isSimple() { + return false; + } } diff --git a/processor/src/main/java/org/ethelred/kiwiproc/processor/types/KiwiType.java b/processor/src/main/java/org/ethelred/kiwiproc/processor/types/KiwiType.java new file mode 100644 index 0000000..f3a5d4c --- /dev/null +++ b/processor/src/main/java/org/ethelred/kiwiproc/processor/types/KiwiType.java @@ -0,0 +1,25 @@ +package org.ethelred.kiwiproc.processor.types; + +/** + * Simplified view of type for parameters and return. + */ +public sealed interface KiwiType + permits BasicType, ContainerType, PrimitiveKiwiType, RecordType, SqlArrayType, UnsupportedType, VoidType { + static KiwiType unsupported() { + return new UnsupportedType(); + } + + String packageName(); + + String className(); + + boolean isSimple(); + + default boolean isNullable() { + return false; + } + + default KiwiType withIsNullable(boolean b) { + return this; + } +} diff --git a/processor/src/main/java/org/ethelred/kiwiproc/processor/types/PrimitiveKiwiType.java b/processor/src/main/java/org/ethelred/kiwiproc/processor/types/PrimitiveKiwiType.java new file mode 100644 index 0000000..7665854 --- /dev/null +++ b/processor/src/main/java/org/ethelred/kiwiproc/processor/types/PrimitiveKiwiType.java @@ -0,0 +1,47 @@ +package org.ethelred.kiwiproc.processor.types; + +import org.ethelred.kiwiproc.processor.CoreTypes; + +/** + * Represent the duality of primitive and boxed types. + * @param primitiveName + * @param isNullable + */ +public record PrimitiveKiwiType(String primitiveName, boolean isNullable) implements KiwiType { + public PrimitiveKiwiType { + if (!CoreTypes.primitiveToBoxedStrings.containsKey(primitiveName)) { + throw new IllegalArgumentException("Unknown primitive " + primitiveName); + } + } + + @Override + public String packageName() { + if (isNullable) { + return "java.lang"; + } + return ""; + } + + @Override + public String className() { + if (isNullable) { + return CoreTypes.primitiveToBoxedStrings.get(primitiveName); + } + return primitiveName; + } + + @Override + public boolean isSimple() { + return true; + } + + @Override + public KiwiType withIsNullable(boolean b) { + return new PrimitiveKiwiType(primitiveName, b); + } + + @Override + public String toString() { + return primitiveName + (isNullable ? "/nullable" : "/non-null"); + } +} diff --git a/processor/src/main/java/org/ethelred/kiwiproc/processor/RecordType.java b/processor/src/main/java/org/ethelred/kiwiproc/processor/types/RecordType.java similarity index 65% rename from processor/src/main/java/org/ethelred/kiwiproc/processor/RecordType.java rename to processor/src/main/java/org/ethelred/kiwiproc/processor/types/RecordType.java index 52f463b..f747ae8 100644 --- a/processor/src/main/java/org/ethelred/kiwiproc/processor/RecordType.java +++ b/processor/src/main/java/org/ethelred/kiwiproc/processor/types/RecordType.java @@ -1,8 +1,13 @@ -package org.ethelred.kiwiproc.processor; +package org.ethelred.kiwiproc.processor.types; import java.util.List; public record RecordType(String packageName, String className, List components) implements KiwiType { + @Override + public boolean isSimple() { + return false; + } + public record RecordTypeComponent(String name, KiwiType type) {} } diff --git a/processor/src/main/java/org/ethelred/kiwiproc/processor/SqlArrayType.java b/processor/src/main/java/org/ethelred/kiwiproc/processor/types/SqlArrayType.java similarity index 63% rename from processor/src/main/java/org/ethelred/kiwiproc/processor/SqlArrayType.java rename to processor/src/main/java/org/ethelred/kiwiproc/processor/types/SqlArrayType.java index 6425b78..8814221 100644 --- a/processor/src/main/java/org/ethelred/kiwiproc/processor/SqlArrayType.java +++ b/processor/src/main/java/org/ethelred/kiwiproc/processor/types/SqlArrayType.java @@ -1,4 +1,4 @@ -package org.ethelred.kiwiproc.processor; +package org.ethelred.kiwiproc.processor.types; public record SqlArrayType(KiwiType componentType) implements KiwiType { @Override @@ -10,4 +10,9 @@ public String packageName() { public String className() { return "ARRAY"; } + + @Override + public boolean isSimple() { + return true; // kinda + } } diff --git a/processor/src/main/java/org/ethelred/kiwiproc/processor/UnsupportedType.java b/processor/src/main/java/org/ethelred/kiwiproc/processor/types/UnsupportedType.java similarity index 72% rename from processor/src/main/java/org/ethelred/kiwiproc/processor/UnsupportedType.java rename to processor/src/main/java/org/ethelred/kiwiproc/processor/types/UnsupportedType.java index 425345c..c6c94ee 100644 --- a/processor/src/main/java/org/ethelred/kiwiproc/processor/UnsupportedType.java +++ b/processor/src/main/java/org/ethelred/kiwiproc/processor/types/UnsupportedType.java @@ -1,4 +1,4 @@ -package org.ethelred.kiwiproc.processor; +package org.ethelred.kiwiproc.processor.types; public record UnsupportedType() implements KiwiType { @Override @@ -10,4 +10,9 @@ public String packageName() { public String className() { throw new UnsupportedOperationException("UnsupportedType.className"); } + + @Override + public boolean isSimple() { + return false; + } } diff --git a/processor/src/main/java/org/ethelred/kiwiproc/processor/ValidContainerType.java b/processor/src/main/java/org/ethelred/kiwiproc/processor/types/ValidContainerType.java similarity index 96% rename from processor/src/main/java/org/ethelred/kiwiproc/processor/ValidContainerType.java rename to processor/src/main/java/org/ethelred/kiwiproc/processor/types/ValidContainerType.java index 6c9fc79..d72bd6d 100644 --- a/processor/src/main/java/org/ethelred/kiwiproc/processor/ValidContainerType.java +++ b/processor/src/main/java/org/ethelred/kiwiproc/processor/types/ValidContainerType.java @@ -1,4 +1,4 @@ -package org.ethelred.kiwiproc.processor; +package org.ethelred.kiwiproc.processor.types; import java.lang.reflect.Array; import java.util.Collection; diff --git a/processor/src/main/java/org/ethelred/kiwiproc/processor/VoidType.java b/processor/src/main/java/org/ethelred/kiwiproc/processor/types/VoidType.java similarity index 62% rename from processor/src/main/java/org/ethelred/kiwiproc/processor/VoidType.java rename to processor/src/main/java/org/ethelred/kiwiproc/processor/types/VoidType.java index 91b1a8e..ebd225d 100644 --- a/processor/src/main/java/org/ethelred/kiwiproc/processor/VoidType.java +++ b/processor/src/main/java/org/ethelred/kiwiproc/processor/types/VoidType.java @@ -1,4 +1,4 @@ -package org.ethelred.kiwiproc.processor; +package org.ethelred.kiwiproc.processor.types; public record VoidType() implements KiwiType { @@ -11,4 +11,9 @@ public String packageName() { public String className() { return "void"; } + + @Override + public boolean isSimple() { + return false; + } } diff --git a/processor/src/test/java/org/ethelred/kiwiproc/processor/CoreTypesTest.java b/processor/src/test/java/org/ethelred/kiwiproc/processor/CoreTypesTest.java index 329a7d8..760dbca 100644 --- a/processor/src/test/java/org/ethelred/kiwiproc/processor/CoreTypesTest.java +++ b/processor/src/test/java/org/ethelred/kiwiproc/processor/CoreTypesTest.java @@ -2,13 +2,17 @@ import static com.google.common.truth.Truth.assertThat; import static com.google.common.truth.Truth.assertWithMessage; -import static org.ethelred.kiwiproc.processor.SimpleType.ofClass; +import static org.ethelred.kiwiproc.processor.TestUtils.ofClass; import static org.junit.jupiter.params.provider.Arguments.arguments; import java.io.OutputStream; import java.math.BigDecimal; import java.time.LocalDate; import java.util.stream.Stream; +import org.ethelred.kiwiproc.processor.types.BasicType; +import org.ethelred.kiwiproc.processor.types.KiwiType; +import org.ethelred.kiwiproc.processor.types.PrimitiveKiwiType; +import org.ethelred.kiwiproc.processor.types.UnsupportedType; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; @@ -24,22 +28,22 @@ public class CoreTypesTest { void typeFromClass() { KiwiType type; type = coreTypes.type(LocalDate.class); - assertThat(type).isInstanceOf(SimpleType.class); - assertThat(((SimpleType) type).className()).isEqualTo("LocalDate"); - assertThat(((SimpleType) type).packageName()).isEqualTo("java.time"); - assertThat(((SimpleType) type).isNullable()).isFalse(); + assertThat(type).isInstanceOf(BasicType.class); + assertThat(type.className()).isEqualTo("LocalDate"); + assertThat(type.packageName()).isEqualTo("java.time"); + assertThat(type.isNullable()).isFalse(); type = coreTypes.type(short.class); - assertThat(type).isInstanceOf(SimpleType.class); - assertThat(((SimpleType) type).className()).isEqualTo("short"); - assertThat(((SimpleType) type).packageName()).isEqualTo(""); - assertThat(((SimpleType) type).isNullable()).isFalse(); + assertThat(type).isInstanceOf(PrimitiveKiwiType.class); + assertThat(type.className()).isEqualTo("short"); + assertThat(type.packageName()).isEqualTo(""); + assertThat(type.isNullable()).isFalse(); type = coreTypes.type(Short.class); - assertThat(type).isInstanceOf(SimpleType.class); - assertThat(((SimpleType) type).className()).isEqualTo("Short"); - assertThat(((SimpleType) type).packageName()).isEqualTo("java.lang"); - assertThat(((SimpleType) type).isNullable()).isTrue(); + assertThat(type).isInstanceOf(PrimitiveKiwiType.class); + assertThat(type.className()).isEqualTo("Short"); + assertThat(type.packageName()).isEqualTo("java.lang"); + assertThat(type.isNullable()).isTrue(); type = coreTypes.type(OutputStream.class); assertThat(type).isInstanceOf(UnsupportedType.class); @@ -48,11 +52,15 @@ void typeFromClass() { @ParameterizedTest @MethodSource void testConversions( - SimpleType source, SimpleType target, boolean isValid, boolean isWarning, String conversionFormatContains) { + KiwiType source, KiwiType target, boolean isValid, boolean isWarning, String conversionFormatContains) { var conversion = coreTypes.lookup(source, target); // System.out.println(conversion); - assertWithMessage("is valid").that(conversion.isValid()).isEqualTo(isValid); - assertWithMessage("is warning").that(conversion.hasWarning()).isEqualTo(isWarning); + assertWithMessage("%s to %s is valid", source, target) + .that(conversion.isValid()) + .isEqualTo(isValid); + assertWithMessage("%s to %s is warning", source, target) + .that(conversion.hasWarning()) + .isEqualTo(isWarning); if (conversion.isValid()) { var formatted = conversion.conversionFormat().formatted("value"); assertThat(formatted).isEqualTo(conversionFormatContains); diff --git a/processor/src/test/java/org/ethelred/kiwiproc/processor/SqlTypeMappingTest.java b/processor/src/test/java/org/ethelred/kiwiproc/processor/SqlTypeMappingTest.java index e0ed917..e26af18 100644 --- a/processor/src/test/java/org/ethelred/kiwiproc/processor/SqlTypeMappingTest.java +++ b/processor/src/test/java/org/ethelred/kiwiproc/processor/SqlTypeMappingTest.java @@ -1,10 +1,14 @@ package org.ethelred.kiwiproc.processor; import static com.google.common.truth.Truth.assertThat; +import static org.ethelred.kiwiproc.processor.TestUtils.atLeastOne; import java.sql.JDBCType; import java.util.Set; import org.ethelred.kiwiproc.meta.ColumnMetaData; +import org.ethelred.kiwiproc.processor.types.BasicType; +import org.ethelred.kiwiproc.processor.types.PrimitiveKiwiType; +import org.ethelred.kiwiproc.processor.types.SqlArrayType; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.EnumSource; @@ -38,7 +42,10 @@ public void sqlMappingIsPresentForJDBCType(JDBCType jdbcType) { var columnMetaData = new ColumnMetaData(1, "columnName", false, jdbcType, null); var mapping = SqlTypeMapping.get(columnMetaData); assertThat(mapping).isNotNull(); - assertThat(mapping.kiwiType()).isInstanceOf(SimpleType.class); + atLeastOne( + assertThat(mapping.kiwiType()), + s -> s.isInstanceOf(PrimitiveKiwiType.class), + s -> s.isInstanceOf(BasicType.class)); } @ParameterizedTest diff --git a/processor/src/test/java/org/ethelred/kiwiproc/processor/TestUtils.java b/processor/src/test/java/org/ethelred/kiwiproc/processor/TestUtils.java new file mode 100644 index 0000000..72fe511 --- /dev/null +++ b/processor/src/test/java/org/ethelred/kiwiproc/processor/TestUtils.java @@ -0,0 +1,48 @@ +package org.ethelred.kiwiproc.processor; + +import com.google.common.truth.Subject; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.function.Consumer; +import java.util.stream.Collectors; +import org.ethelred.kiwiproc.processor.types.BasicType; +import org.ethelred.kiwiproc.processor.types.KiwiType; +import org.ethelred.kiwiproc.processor.types.PrimitiveKiwiType; +import org.jspecify.annotations.Nullable; + +public class TestUtils { + static Map, Class> boxedToPrimitive = CoreTypes.primitiveToBoxed.entrySet().stream() + .map(e -> Map.entry(e.getValue(), e.getKey())) + .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); + + static KiwiType ofClass(Class k, @Nullable Boolean nullable) { + if (k.isPrimitive()) { + return new PrimitiveKiwiType(k.getSimpleName(), nullable != null && nullable); + } + if (boxedToPrimitive.containsKey(k)) { + return new PrimitiveKiwiType(boxedToPrimitive.get(k).getSimpleName(), nullable == null || nullable); + } + return new BasicType(k.getPackageName(), k.getSimpleName(), nullable != null && nullable); + } + + static KiwiType ofClass(Class k) { + return ofClass(k, false); + } + + @SafeVarargs + static void atLeastOne(S subject, Consumer... assertions) { + List errors = new ArrayList<>(assertions.length); + for (var assertion : assertions) { + try { + assertion.accept(subject); + return; // no exception if the assertion succeeds + } catch (AssertionError error) { + errors.add(error); + } + } + // no assertion succeeded + var message = errors.stream().map(Throwable::getMessage).collect(Collectors.joining("\n")); + throw new AssertionError(message); + } +} diff --git a/processor/src/test/java/org/ethelred/kiwiproc/processor/TypeValidatorTest.java b/processor/src/test/java/org/ethelred/kiwiproc/processor/TypeValidatorTest.java index c7aabfd..a1c861d 100644 --- a/processor/src/test/java/org/ethelred/kiwiproc/processor/TypeValidatorTest.java +++ b/processor/src/test/java/org/ethelred/kiwiproc/processor/TypeValidatorTest.java @@ -1,7 +1,8 @@ package org.ethelred.kiwiproc.processor; import static com.google.common.truth.Truth.assertThat; -import static org.ethelred.kiwiproc.processor.SimpleType.ofClass; +import static com.google.common.truth.Truth.assertWithMessage; +import static org.ethelred.kiwiproc.processor.TestUtils.ofClass; import static org.junit.jupiter.params.provider.Arguments.arguments; import com.karuslabs.utilitary.Logger; @@ -9,6 +10,7 @@ import java.sql.JDBCType; import java.util.HashSet; import java.util.List; +import java.util.Map; import java.util.Set; import java.util.stream.Stream; import javax.annotation.processing.Messager; @@ -16,6 +18,7 @@ import javax.lang.model.type.TypeMirror; import javax.tools.Diagnostic; import org.ethelred.kiwiproc.meta.ColumnMetaData; +import org.ethelred.kiwiproc.processor.types.*; import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.params.ParameterizedTest; @@ -32,6 +35,48 @@ void reset() { colCount = 1; } + @ParameterizedTest + @MethodSource + void testQueryParameter( + ColumnMetaData columnMetaData, + MethodParameterInfo parameterInfo, + boolean expectedResult, + @Nullable String message) { + var result = validator.validateParameters(Map.of(columnMetaData, parameterInfo), QueryMethodKind.QUERY); + assertWithMessage("testQueryParameter %s -> %s", logKiwiType(columnMetaData), parameterInfo.type()) + .that(result) + .isEqualTo(expectedResult); + if (message == null) { + assertThat(messages).isEmpty(); + } else { + assertThat(messages).contains(message); + } + } + + public static Stream testQueryParameter() { + return Stream.of( + arguments( + col(false, JDBCType.INTEGER), + new MethodParameterInfo(mockVariableElement(), "x", ofClass(int.class), false, null), + true, + null), + arguments( + col(true, JDBCType.INTEGER), + new MethodParameterInfo(mockVariableElement(), "x", ofClass(int.class), false, null), + true, + null), + arguments( + col(false, JDBCType.INTEGER), + new MethodParameterInfo(mockVariableElement(), "x", ofClass(Integer.class, true), false, null), + false, + "Parameter type int/nullable is not compatible with SQL type int/non-null for parameter null"), + arguments( + col(true, JDBCType.INTEGER), + new MethodParameterInfo(mockVariableElement(), "x", ofClass(Integer.class, true), false, null), + true, + null)); + } + @ParameterizedTest @MethodSource void testQueryReturn( @@ -40,7 +85,9 @@ void testQueryReturn( boolean expectedResult, @Nullable String message) { var result = validator.validateReturn(columnMetaData, returnType, QueryMethodKind.QUERY); - assertThat(result).isEqualTo(expectedResult); + assertWithMessage("testQueryReturn %s -> %s", logKiwiType(columnMetaData), returnType) + .that(result) + .isEqualTo(expectedResult); if (message == null) { assertThat(messages).isEmpty(); } else { @@ -48,6 +95,17 @@ void testQueryReturn( } } + private KiwiType logKiwiType(List columnMetaData) { + if (columnMetaData.isEmpty()) { + return KiwiType.unsupported(); + } + return logKiwiType(columnMetaData.get(0)); + } + + private KiwiType logKiwiType(ColumnMetaData columnMetaData) { + return SqlTypeMapping.get(columnMetaData).kiwiType(); + } + public static Stream testQueryReturn() { return Stream.of( testCase(ofClass(int.class), true, null, col(false, JDBCType.INTEGER)), @@ -73,7 +131,7 @@ ValidContainerType.LIST, recordType("TestRecord", "test1", ofClass(int.class))), new ContainerType( ValidContainerType.LIST, recordType("TestRecord", "test1", ofClass(int.class))), false, - "Missing or incompatible component type null for column test2 type String/non-null", + "Missing component type for column test2 type String/non-null", col(false, JDBCType.INTEGER), col(false, JDBCType.VARCHAR))); } @@ -166,6 +224,65 @@ public R accept(ElementVisitor v, P p) { }; } + private static VariableElement mockVariableElement() { + return new VariableElement() { + @Override + public TypeMirror asType() { + return null; + } + + @Override + public Object getConstantValue() { + return null; + } + + @Override + public ElementKind getKind() { + return null; + } + + @Override + public Set getModifiers() { + return Set.of(); + } + + @Override + public Name getSimpleName() { + return null; + } + + @Override + public Element getEnclosingElement() { + return null; + } + + @Override + public List getEnclosedElements() { + return List.of(); + } + + @Override + public List getAnnotationMirrors() { + return List.of(); + } + + @Override + public A getAnnotation(Class annotationType) { + return null; + } + + @Override + public A[] getAnnotationsByType(Class annotationType) { + return null; + } + + @Override + public R accept(ElementVisitor v, P p) { + return null; + } + }; + } + private Logger mockLogger() { return new Logger(mockMessager()); } @@ -174,6 +291,7 @@ private Messager mockMessager() { return new Messager() { @Override public void printMessage(Diagnostic.Kind kind, CharSequence msg) { + System.out.println(msg); if (kind != Diagnostic.Kind.NOTE) { messages.add(msg.toString()); }