From e827e82990551df9316acfabc6adf08b7b1ea967 Mon Sep 17 00:00:00 2001 From: 1robie <97293924+1robie@users.noreply.github.com> Date: Fri, 27 Mar 2026 18:52:19 +0100 Subject: [PATCH 1/3] fix: support overloaded constructors in createConsumerFromTemplate --- .../maxlego08/sarah/ConsumerConstructor.java | 13 +++--- .../fr/maxlego08/sarah/DTOConsumerTest.java | 40 +++++++++++++++++++ 2 files changed, 46 insertions(+), 7 deletions(-) diff --git a/src/main/java/fr/maxlego08/sarah/ConsumerConstructor.java b/src/main/java/fr/maxlego08/sarah/ConsumerConstructor.java index b02528a..5b26f05 100644 --- a/src/main/java/fr/maxlego08/sarah/ConsumerConstructor.java +++ b/src/main/java/fr/maxlego08/sarah/ConsumerConstructor.java @@ -37,8 +37,6 @@ public class ConsumerConstructor { */ public static Consumer createConsumerFromTemplate(Class template, Object data) { Constructor[] constructors = template.getDeclaredConstructors(); - Constructor firstConstructor = constructors[0]; - firstConstructor.setAccessible(true); Field[] allFields = template.getDeclaredFields(); // Filter out synthetic fields (added by compiler for local/anonymous classes) @@ -46,12 +44,13 @@ public static Consumer createConsumerFromTemplate(Class template, Obj .filter(f -> !f.isSynthetic()) .toArray(Field[]::new); - // For local/anonymous classes, count only non-synthetic constructor parameters - long nonSyntheticParamCount = Arrays.stream(firstConstructor.getParameters()) - .filter(p -> !p.isSynthetic()) - .count(); + boolean hasCompatibleConstructor = Arrays.stream(constructors) + .peek(constructor -> constructor.setAccessible(true)) + .anyMatch(constructor -> Arrays.stream(constructor.getParameters()) + .filter(p -> !p.isSynthetic()) + .count() == fields.length); - if (fields.length != nonSyntheticParamCount) { + if (!hasCompatibleConstructor) { throw new IllegalArgumentException("Fields count does not match constructor parameters count"); } diff --git a/src/test/java/fr/maxlego08/sarah/DTOConsumerTest.java b/src/test/java/fr/maxlego08/sarah/DTOConsumerTest.java index 4330e23..105b2dc 100644 --- a/src/test/java/fr/maxlego08/sarah/DTOConsumerTest.java +++ b/src/test/java/fr/maxlego08/sarah/DTOConsumerTest.java @@ -95,6 +95,34 @@ public ComplexDTO(Long id, String textField, Integer intField, Double doubleFiel public Boolean getBoolField() { return boolField; } } + private static class OverloadedTemplateDTO { + private final String name; + private final Integer quantity; + + public OverloadedTemplateDTO(String name) { + this(name, null); + } + + public OverloadedTemplateDTO(String name, Integer quantity) { + this.name = name; + this.quantity = quantity; + } + } + + private static class IncompatibleTemplateDTO { + private final String code; + private final Integer version; + + public IncompatibleTemplateDTO() { + this(null); + } + + public IncompatibleTemplateDTO(String code) { + this.code = code; + this.version = null; + } + } + @Test public void testCreateTableFromDTOClass() throws Exception { // Create table using DTO class template @@ -304,4 +332,16 @@ public void testDTOWithDifferentTypes() throws Exception { assertEquals(3.14, retrieved.getDoubleField(), 0.001); assertTrue(retrieved.getBoolField()); } + + @Test + public void testCreateConsumerWithOverloadedConstructors() { + assertDoesNotThrow(() -> ConsumerConstructor.createConsumerFromTemplate(OverloadedTemplateDTO.class, null)); + } + + @Test + public void testCreateConsumerThrowsWhenNoCompatibleConstructorExists() { + IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, + () -> ConsumerConstructor.createConsumerFromTemplate(IncompatibleTemplateDTO.class, null)); + assertEquals("Fields count does not match constructor parameters count", exception.getMessage()); + } } \ No newline at end of file From 77711aa9ef551baa9a0f860bb104ac9b9bb924cc Mon Sep 17 00:00:00 2001 From: 1robie <97293924+1robie@users.noreply.github.com> Date: Fri, 3 Apr 2026 10:40:43 +0200 Subject: [PATCH 2/3] fix: enhance type validation in schemaFromType method + fix issue by casting float to double throw ClassCastExeption --- .../maxlego08/sarah/ConsumerConstructor.java | 50 +++++++++++++++---- 1 file changed, 39 insertions(+), 11 deletions(-) diff --git a/src/main/java/fr/maxlego08/sarah/ConsumerConstructor.java b/src/main/java/fr/maxlego08/sarah/ConsumerConstructor.java index 5b26f05..ab79238 100644 --- a/src/main/java/fr/maxlego08/sarah/ConsumerConstructor.java +++ b/src/main/java/fr/maxlego08/sarah/ConsumerConstructor.java @@ -147,13 +147,14 @@ public static Consumer createConsumerFromTemplate(Class template, Obj * *

* If the type is not supported, an {@link IllegalArgumentException} is thrown. + * If the object is not compatible with the type, an {@link IllegalArgumentException} is thrown. *

* @param schema the schema to add the column to * @param type the type of the column * @param name the name of the column * @param object the value of the column, or null if the column should be added without a value */ - private static void schemaFromType(Schema schema, String type, String name, Object object) { + private static void schemaFromType(Schema schema, String type, String name, Object object) throws IllegalArgumentException { switch (type.toLowerCase()) { case "string": if (object == null) { @@ -173,15 +174,29 @@ private static void schemaFromType(Schema schema, String type, String name, Obje schema.bigInt(name); break; } - schema.bigInt(name, Long.parseLong(object.toString())); - break; + if (object instanceof Number) { + schema.bigInt(name, ((Number) object).longValue()); + break; + } + if (object instanceof String) { + try { + schema.bigInt(name, Long.parseLong((String) object)); + break; + } catch (NumberFormatException e) { + throw new IllegalArgumentException("Object is not a valid number for type " + type); + } + } + throw new IllegalArgumentException("Object is not a number for type " + type); case "boolean": if (object == null) { schema.bool(name); break; } - schema.bool(name, (Boolean) object); - break; + if (object instanceof Boolean) { + schema.bool(name, (Boolean) object); + break; + } + throw new IllegalArgumentException("Object is not a boolean for type " + type); case "double": case "float": case "bigdecimal": @@ -189,18 +204,31 @@ private static void schemaFromType(Schema schema, String type, String name, Obje schema.decimal(name); break; } - schema.decimal(name, (Double) object); - break; + if (object instanceof Number) { + schema.decimal(name, ((Number) object)); + break; + } + throw new IllegalArgumentException("Object is not a number for type " + type); case "uuid": if (object == null) { schema.uuid(name); break; } - schema.uuid(name, (UUID) object); - break; + if (object instanceof UUID) { + schema.uuid(name, (UUID) object); + break; + } + throw new IllegalArgumentException("Object is not a UUID for type " + type); case "date": - schema.date(name, (Date) object).nullable(); - break; + if (object == null) { + schema.date(name, null).nullable(); + break; + } + if (object instanceof Date) { + schema.date(name, (Date) object).nullable(); + break; + } + throw new IllegalArgumentException("Object is not a Date for type " + type); case "timestamp": schema.timestamp(name).nullable(); break; From dc45bd701007fabdaee252cd74d13ed65be2137a Mon Sep 17 00:00:00 2001 From: 1robie <97293924+1robie@users.noreply.github.com> Date: Fri, 3 Apr 2026 10:54:51 +0200 Subject: [PATCH 3/3] feat: add TransientField annotation to exclude fields from schema mapping --- .../fr/maxlego08/sarah/ConsumerConstructor.java | 3 ++- .../java/fr/maxlego08/sarah/TransientField.java | 13 +++++++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) create mode 100644 src/main/java/fr/maxlego08/sarah/TransientField.java diff --git a/src/main/java/fr/maxlego08/sarah/ConsumerConstructor.java b/src/main/java/fr/maxlego08/sarah/ConsumerConstructor.java index ab79238..4780dbc 100644 --- a/src/main/java/fr/maxlego08/sarah/ConsumerConstructor.java +++ b/src/main/java/fr/maxlego08/sarah/ConsumerConstructor.java @@ -39,9 +39,10 @@ public static Consumer createConsumerFromTemplate(Class template, Obj Constructor[] constructors = template.getDeclaredConstructors(); Field[] allFields = template.getDeclaredFields(); - // Filter out synthetic fields (added by compiler for local/anonymous classes) + // Filter out synthetic fields and those annotated with @TransientField Field[] fields = Arrays.stream(allFields) .filter(f -> !f.isSynthetic()) + .filter(f -> !f.isAnnotationPresent(TransientField.class)) .toArray(Field[]::new); boolean hasCompatibleConstructor = Arrays.stream(constructors) diff --git a/src/main/java/fr/maxlego08/sarah/TransientField.java b/src/main/java/fr/maxlego08/sarah/TransientField.java new file mode 100644 index 0000000..751f7d7 --- /dev/null +++ b/src/main/java/fr/maxlego08/sarah/TransientField.java @@ -0,0 +1,13 @@ +package fr.maxlego08.sarah; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +/** + * Marks a field as transient for schema/constructor mapping. + * Fields annotated with this will be ignored by schema generation and mapping logic. + */ +@Retention(RetentionPolicy.RUNTIME) +public @interface TransientField { +} +