From 7b0e592f4f58931cc7a65693429a917c55580073 Mon Sep 17 00:00:00 2001 From: Reed von Redwitz Date: Fri, 10 Jul 2026 06:47:50 +0200 Subject: [PATCH] fix(codemodel-foundation): marshal AnnotationValue.Value variants as real registered types --- .../foundation/usage/AnnotationValue.java | 115 ++++++++++++------ .../foundation/usage/MarshallingTests.java | 86 +++++++++++++ 2 files changed, 163 insertions(+), 38 deletions(-) diff --git a/codemodel-foundation/src/main/java/build/codemodel/foundation/usage/AnnotationValue.java b/codemodel-foundation/src/main/java/build/codemodel/foundation/usage/AnnotationValue.java index 0c9b9eb..94d02aa 100644 --- a/codemodel-foundation/src/main/java/build/codemodel/foundation/usage/AnnotationValue.java +++ b/codemodel-foundation/src/main/java/build/codemodel/foundation/usage/AnnotationValue.java @@ -63,10 +63,24 @@ public sealed interface Value * A primitive or {@link String} literal. */ record Literal(Object value) implements Value { + + @Unmarshal + public Literal { + } + + @Marshal + public void destructor(final Out value) { + value.set(this.value); + } + @Override public String toString() { return value.toString(); } + + static { + Marshalling.register(Literal.class, MethodHandles.lookup()); + } } /** @@ -75,10 +89,24 @@ public String toString() { * @param typeName the codemodel {@link TypeName} for the referenced class */ record ClassRef(TypeName typeName) implements Value { + + @Unmarshal + public ClassRef { + } + + @Marshal + public void destructor(final Out typeName) { + typeName.set(this.typeName); + } + @Override public String toString() { return typeName.canonicalName() + ".class"; } + + static { + Marshalling.register(ClassRef.class, MethodHandles.lookup()); + } } /** @@ -88,10 +116,25 @@ public String toString() { * @param constantName the constant name */ record EnumConstant(TypeName typeName, String constantName) implements Value { + + @Unmarshal + public EnumConstant { + } + + @Marshal + public void destructor(final Out typeName, final Out constantName) { + typeName.set(this.typeName); + constantName.set(this.constantName); + } + @Override public String toString() { return typeName.canonicalName() + "." + constantName; } + + static { + Marshalling.register(EnumConstant.class, MethodHandles.lookup()); + } } /** @@ -100,10 +143,25 @@ public String toString() { * @param annotation the {@link AnnotationTypeUsage} representing the nested annotation */ record Nested(AnnotationTypeUsage annotation) implements Value { + + @Unmarshal + public Nested(final Marshaller marshaller, final Marshalled annotation) { + this(marshaller.unmarshal(annotation)); + } + + @Marshal + public void destructor(final Marshaller marshaller, final Out> annotation) { + annotation.set(marshaller.marshal(this.annotation)); + } + @Override public String toString() { return annotation.toString(); } + + static { + Marshalling.register(Nested.class, MethodHandles.lookup()); + } } /** @@ -112,47 +170,28 @@ public String toString() { * @param elements the elements */ record Array(List elements) implements Value { + + @Unmarshal + public Array(final Marshaller marshaller, final Stream> elements) { + this(elements.map(marshaller::unmarshal).toList()); + } + + @Marshal + public void destructor(final Marshaller marshaller, final Out>> elements) { + elements.set(this.elements.stream().map(marshaller::marshal)); + } + @Override public String toString() { return elements.toString(); } - } - } - - // --- Marshalling conversion helpers --- - /** - * Converts a raw marshalled {@link Object} back to a typed {@link Value}. - * Handles the legacy format (plain Object) and the structured format (AnnotationTypeUsage, List). - */ - private static Value toValue(final Object raw) { - return switch (raw) { - case Value v -> v; - case AnnotationTypeUsage a -> new Value.Nested(a); - case List list -> new Value.Array(list.stream() - .map(AnnotationValue::toValue) - .toList()); - default -> new Value.Literal(raw); - }; - } - - /** - * Converts a {@link Value} back to the raw object used in the marshal format. - */ - private static Object fromValue(final Value value) { - return switch (value) { - case Value.Literal(var v) -> v; - case Value.Nested(var a) -> a; - case Value.Array(var elements) -> elements.stream() - .map(AnnotationValue::fromValue) - .toList(); - case Value.ClassRef(var typeName) -> typeName; - case Value.EnumConstant(var typeName, var constantName) -> typeName.canonicalName() + "." + constantName; - }; + static { + Marshalling.register(Array.class, MethodHandles.lookup()); + } + } } - // --- - private final IrreducibleName name; private final Value value; @@ -169,20 +208,20 @@ public AnnotationValue(@Bound final CodeModel codeModel, final Marshaller marshaller, final Stream> traits, final IrreducibleName name, - final Object value) { + final Marshalled value) { super(codeModel, marshaller, traits); this.name = name; - this.value = toValue(value); + this.value = marshaller.unmarshal(value); } @Marshal public void destructor(final Marshaller marshaller, final Out>> traits, final Out name, - final Out value) { + final Out> value) { super.destructor(marshaller, traits); name.set(this.name); - value.set(fromValue(this.value)); + value.set(marshaller.marshal(this.value)); } public IrreducibleName name() { diff --git a/codemodel-foundation/src/test/java/build/codemodel/foundation/usage/MarshallingTests.java b/codemodel-foundation/src/test/java/build/codemodel/foundation/usage/MarshallingTests.java index 5781897..4b1711f 100644 --- a/codemodel-foundation/src/test/java/build/codemodel/foundation/usage/MarshallingTests.java +++ b/codemodel-foundation/src/test/java/build/codemodel/foundation/usage/MarshallingTests.java @@ -22,6 +22,7 @@ import java.io.IOException; import java.io.StringReader; import java.io.StringWriter; +import java.util.List; import java.util.Optional; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -87,6 +88,91 @@ void shouldMarshallAndTransportAndUnmarshallAnnotationTypeUsage() AnnotationValue.of(codeModel, "Annotation2", "Value2"))); } + /** + * Ensures that an {@link AnnotationValue.Value.ClassRef} survives a marshal → transport → + * unmarshal round-trip. Regression test: the raw-{@link Object} marshalling this replaced + * serialized a {@link AnnotationValue.Value.ClassRef} to a bare {@link TypeName} with no + * matching deserialize case, so it silently came back as a {@link AnnotationValue.Value.Literal} + * instead. + * + * @throws IOException if an error occurs during marshalling, transport or unmarshalling + */ + @Test + void shouldMarshallAndTransportAndUnmarshallAnnotationValueClassRef() + throws IOException { + + final var typeName = TypeName.of( + ModuleName.of("some.module", this.nameProvider), + Optional.empty(), + Optional.empty(), + IrreducibleName.of("MyType")); + + marshallAndTransportAndUnMarshalAndAssert( + AnnotationValue.of(codeModel, "classValue", new AnnotationValue.Value.ClassRef(typeName))); + } + + /** + * Ensures that an {@link AnnotationValue.Value.EnumConstant} survives a marshal → transport → + * unmarshal round-trip. Regression test: the raw-{@link Object} marshalling this replaced + * serialized an {@link AnnotationValue.Value.EnumConstant} to a formatted {@code "Type.CONSTANT"} + * string with no matching deserialize case, so it silently came back as a + * {@link AnnotationValue.Value.Literal} instead, losing the type name and constant name as + * separate fields. + * + * @throws IOException if an error occurs during marshalling, transport or unmarshalling + */ + @Test + void shouldMarshallAndTransportAndUnmarshallAnnotationValueEnumConstant() + throws IOException { + + final var typeName = TypeName.of( + ModuleName.of("some.module", this.nameProvider), + Optional.empty(), + Optional.empty(), + IrreducibleName.of("MyEnum")); + + marshallAndTransportAndUnMarshalAndAssert( + AnnotationValue.of(codeModel, "enumValue", new AnnotationValue.Value.EnumConstant(typeName, "CONSTANT"))); + } + + /** + * Ensures that an {@link AnnotationValue.Value.Nested} annotation value survives a marshal → + * transport → unmarshal round-trip. + * + * @throws IOException if an error occurs during marshalling, transport or unmarshalling + */ + @Test + void shouldMarshallAndTransportAndUnmarshallAnnotationValueNested() + throws IOException { + + final var typeName = TypeName.of( + ModuleName.of("some.module", this.nameProvider), + Optional.empty(), + Optional.empty(), + IrreducibleName.of("NestedAnnotation")); + final var nested = AnnotationTypeUsage.of(codeModel, typeName, + AnnotationValue.of(codeModel, "inner", "innerValue")); + + marshallAndTransportAndUnMarshalAndAssert( + AnnotationValue.of(codeModel, "nestedValue", new AnnotationValue.Value.Nested(nested))); + } + + /** + * Ensures that an {@link AnnotationValue.Value.Array} survives a marshal → transport → + * unmarshal round-trip. + * + * @throws IOException if an error occurs during marshalling, transport or unmarshalling + */ + @Test + void shouldMarshallAndTransportAndUnmarshallAnnotationValueArray() + throws IOException { + + marshallAndTransportAndUnMarshalAndAssert( + AnnotationValue.of(codeModel, "arrayValue", new AnnotationValue.Value.Array(List.of( + new AnnotationValue.Value.Literal("a"), + new AnnotationValue.Value.Literal("b"))))); + } + /** * Ensures that {@link ArrayTypeUsage} can be marshalled, transported and unmarshalled using a {@link JsonTransport}. *