This is an automated email from the ASF dual-hosted git repository. maciej pushed a commit to branch java-improvements in repository https://gitbox.apache.org/repos/asf/iggy.git
commit 18029d95fd389f0cb4431306ad0b8b69086ef157 Author: Maciej Modzelewski <[email protected]> AuthorDate: Mon Feb 2 09:42:04 2026 +0100 feat(java): improve tests and clean up json mappings --- .../blocking/http/MessageMixin.java} | 31 +-- .../client/blocking/http/ObjectMapperFactory.java | 11 +- .../blocking/http}/UserHeadersSerializer.java | 4 +- .../java/org/apache/iggy/message/HeaderKey.java | 41 ++- .../java/org/apache/iggy/message/HeaderKind.java | 16 -- .../java/org/apache/iggy/message/HeaderValue.java | 53 ++-- .../main/java/org/apache/iggy/message/Message.java | 33 +-- .../java/org/apache/iggy/message/Partitioning.java | 8 +- .../org/apache/iggy/serde/Base64Serializer.java | 35 --- .../java/org/apache/iggy/user/Permissions.java | 7 +- .../org/apache/iggy/user/StreamPermissions.java | 5 +- .../iggy/client/blocking/IntegrationTest.java | 6 + .../blocking/http/HeaderKindSerializationTest.java | 120 +++++++++ .../client/blocking/http/ObjectMapperTest.java | 286 +++++++++++++++++++++ .../apache/iggy/serde/BytesDeserializerTest.java | 76 ------ 15 files changed, 495 insertions(+), 237 deletions(-) diff --git a/foreign/java/java-sdk/src/main/java/org/apache/iggy/serde/UserHeadersSerializer.java b/foreign/java/java-sdk/src/main/java/org/apache/iggy/client/blocking/http/MessageMixin.java similarity index 56% copy from foreign/java/java-sdk/src/main/java/org/apache/iggy/serde/UserHeadersSerializer.java copy to foreign/java/java-sdk/src/main/java/org/apache/iggy/client/blocking/http/MessageMixin.java index 878ad8670..9127f079d 100644 --- a/foreign/java/java-sdk/src/main/java/org/apache/iggy/serde/UserHeadersSerializer.java +++ b/foreign/java/java-sdk/src/main/java/org/apache/iggy/client/blocking/http/MessageMixin.java @@ -17,28 +17,29 @@ * under the License. */ -package org.apache.iggy.serde; +package org.apache.iggy.client.blocking.http; +import com.fasterxml.jackson.annotation.JsonCreator; import org.apache.iggy.message.HeaderEntry; import org.apache.iggy.message.HeaderKey; import org.apache.iggy.message.HeaderValue; -import tools.jackson.core.JacksonException; -import tools.jackson.core.JsonGenerator; -import tools.jackson.databind.SerializationContext; -import tools.jackson.databind.ValueSerializer; +import org.apache.iggy.message.Message; +import org.apache.iggy.message.MessageHeader; +import tools.jackson.databind.annotation.JsonSerialize; +import java.util.List; import java.util.Map; -public class UserHeadersSerializer extends ValueSerializer<Map<HeaderKey, HeaderValue>> { +/** + * Jackson mixin for {@link Message} to keep the domain object free of serialization annotations. + */ +abstract class MessageMixin { - @Override - public void serialize(Map<HeaderKey, HeaderValue> headers, JsonGenerator gen, SerializationContext ctxt) - throws JacksonException { - gen.writeStartArray(); - for (Map.Entry<HeaderKey, HeaderValue> entry : headers.entrySet()) { - ctxt.findValueSerializer(HeaderEntry.class) - .serialize(new HeaderEntry(entry.getKey(), entry.getValue()), gen, ctxt); - } - gen.writeEndArray(); + @JsonCreator + static Message of(MessageHeader header, byte[] payload, List<HeaderEntry> userHeaders) { + throw new UnsupportedOperationException("Mixin method should not be called directly"); } + + @JsonSerialize(using = UserHeadersSerializer.class) + abstract Map<HeaderKey, HeaderValue> userHeaders(); } diff --git a/foreign/java/java-sdk/src/main/java/org/apache/iggy/client/blocking/http/ObjectMapperFactory.java b/foreign/java/java-sdk/src/main/java/org/apache/iggy/client/blocking/http/ObjectMapperFactory.java index cd7a8e9b9..167db5d36 100644 --- a/foreign/java/java-sdk/src/main/java/org/apache/iggy/client/blocking/http/ObjectMapperFactory.java +++ b/foreign/java/java-sdk/src/main/java/org/apache/iggy/client/blocking/http/ObjectMapperFactory.java @@ -21,26 +21,33 @@ package org.apache.iggy.client.blocking.http; import com.fasterxml.jackson.annotation.JsonSetter; import com.fasterxml.jackson.annotation.Nulls; +import org.apache.iggy.message.Message; import tools.jackson.databind.DeserializationFeature; +import tools.jackson.databind.EnumNamingStrategies; import tools.jackson.databind.MapperFeature; import tools.jackson.databind.ObjectMapper; import tools.jackson.databind.PropertyNamingStrategies; import tools.jackson.databind.json.JsonMapper; +import java.util.List; import java.util.Map; -public final class ObjectMapperFactory { +final class ObjectMapperFactory { private static final ObjectMapper INSTANCE = JsonMapper.builder() .enable(MapperFeature.ACCEPT_CASE_INSENSITIVE_ENUMS) .enable(DeserializationFeature.FAIL_ON_NULL_CREATOR_PROPERTIES) .propertyNamingStrategy(PropertyNamingStrategies.SNAKE_CASE) + .enumNamingStrategy(EnumNamingStrategies.LOWER_CASE) .withConfigOverride(Map.class, map -> map.setNullHandling(JsonSetter.Value.forValueNulls(Nulls.AS_EMPTY))) + .withConfigOverride( + List.class, list -> list.setNullHandling(JsonSetter.Value.forValueNulls(Nulls.AS_EMPTY))) + .addMixIn(Message.class, MessageMixin.class) .build(); private ObjectMapperFactory() {} - public static ObjectMapper getInstance() { + static ObjectMapper getInstance() { return INSTANCE; } } diff --git a/foreign/java/java-sdk/src/main/java/org/apache/iggy/serde/UserHeadersSerializer.java b/foreign/java/java-sdk/src/main/java/org/apache/iggy/client/blocking/http/UserHeadersSerializer.java similarity index 92% rename from foreign/java/java-sdk/src/main/java/org/apache/iggy/serde/UserHeadersSerializer.java rename to foreign/java/java-sdk/src/main/java/org/apache/iggy/client/blocking/http/UserHeadersSerializer.java index 878ad8670..6f8aa16d6 100644 --- a/foreign/java/java-sdk/src/main/java/org/apache/iggy/serde/UserHeadersSerializer.java +++ b/foreign/java/java-sdk/src/main/java/org/apache/iggy/client/blocking/http/UserHeadersSerializer.java @@ -17,7 +17,7 @@ * under the License. */ -package org.apache.iggy.serde; +package org.apache.iggy.client.blocking.http; import org.apache.iggy.message.HeaderEntry; import org.apache.iggy.message.HeaderKey; @@ -29,7 +29,7 @@ import tools.jackson.databind.ValueSerializer; import java.util.Map; -public class UserHeadersSerializer extends ValueSerializer<Map<HeaderKey, HeaderValue>> { +class UserHeadersSerializer extends ValueSerializer<Map<HeaderKey, HeaderValue>> { @Override public void serialize(Map<HeaderKey, HeaderValue> headers, JsonGenerator gen, SerializationContext ctxt) diff --git a/foreign/java/java-sdk/src/main/java/org/apache/iggy/message/HeaderKey.java b/foreign/java/java-sdk/src/main/java/org/apache/iggy/message/HeaderKey.java index ad65857a9..b6acef51a 100644 --- a/foreign/java/java-sdk/src/main/java/org/apache/iggy/message/HeaderKey.java +++ b/foreign/java/java-sdk/src/main/java/org/apache/iggy/message/HeaderKey.java @@ -19,44 +19,39 @@ package org.apache.iggy.message; -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonProperty; +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.builder.EqualsBuilder; +import org.apache.commons.lang3.builder.HashCodeBuilder; import java.nio.charset.StandardCharsets; -import java.util.Arrays; -import java.util.Base64; public record HeaderKey(HeaderKind kind, byte[] value) { - @JsonCreator - public static HeaderKey fromJson(@JsonProperty("kind") HeaderKind kind, @JsonProperty("value") String base64Value) { - byte[] decodedValue = Base64.getDecoder().decode(base64Value); - return new HeaderKey(kind, decodedValue); - } - public static HeaderKey fromString(String val) { - if (val.isEmpty() || val.length() > 255) { - throw new IllegalArgumentException("Value has incorrect size, must be between 1 and 255"); + public static HeaderKey fromString(String value) { + if (StringUtils.isBlank(value)) { + throw new IllegalArgumentException("Value cannot be null or empty"); + } + var bytes = value.getBytes(StandardCharsets.UTF_8); + if (bytes.length > 255) { + throw new IllegalArgumentException("Value has incorrect size, must be between 1 and 255 bytes"); } - return new HeaderKey(HeaderKind.String, val.getBytes(StandardCharsets.UTF_8)); + return new HeaderKey(HeaderKind.String, bytes); } @Override public boolean equals(Object o) { - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { - return false; - } + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; HeaderKey headerKey = (HeaderKey) o; - return kind == headerKey.kind && Arrays.equals(value, headerKey.value); + return new EqualsBuilder() + .append(value, headerKey.value) + .append(kind, headerKey.kind) + .isEquals(); } @Override public int hashCode() { - int result = kind.hashCode(); - result = 31 * result + Arrays.hashCode(value); - return result; + return new HashCodeBuilder(17, 37).append(kind).append(value).toHashCode(); } @Override diff --git a/foreign/java/java-sdk/src/main/java/org/apache/iggy/message/HeaderKind.java b/foreign/java/java-sdk/src/main/java/org/apache/iggy/message/HeaderKind.java index 753a9ee5a..00f024c25 100644 --- a/foreign/java/java-sdk/src/main/java/org/apache/iggy/message/HeaderKind.java +++ b/foreign/java/java-sdk/src/main/java/org/apache/iggy/message/HeaderKind.java @@ -19,39 +19,23 @@ package org.apache.iggy.message; -import com.fasterxml.jackson.annotation.JsonProperty; import org.apache.iggy.exception.IggyInvalidArgumentException; public enum HeaderKind { - @JsonProperty("raw") Raw(1), - @JsonProperty("string") String(2), - @JsonProperty("bool") Bool(3), - @JsonProperty("int8") Int8(4), - @JsonProperty("int16") Int16(5), - @JsonProperty("int32") Int32(6), - @JsonProperty("int64") Int64(7), - @JsonProperty("int128") Int128(8), - @JsonProperty("uint8") Uint8(9), - @JsonProperty("uint16") Uint16(10), - @JsonProperty("uint32") Uint32(11), - @JsonProperty("uint64") Uint64(12), - @JsonProperty("uint128") Uint128(13), - @JsonProperty("float32") Float32(14), - @JsonProperty("float64") Float64(15); private final int code; diff --git a/foreign/java/java-sdk/src/main/java/org/apache/iggy/message/HeaderValue.java b/foreign/java/java-sdk/src/main/java/org/apache/iggy/message/HeaderValue.java index f4fc6a4c2..937b88fe9 100644 --- a/foreign/java/java-sdk/src/main/java/org/apache/iggy/message/HeaderValue.java +++ b/foreign/java/java-sdk/src/main/java/org/apache/iggy/message/HeaderValue.java @@ -19,28 +19,26 @@ package org.apache.iggy.message; -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonProperty; +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.builder.EqualsBuilder; +import org.apache.commons.lang3.builder.HashCodeBuilder; import java.nio.ByteBuffer; import java.nio.ByteOrder; import java.nio.charset.StandardCharsets; -import java.util.Arrays; import java.util.Base64; public record HeaderValue(HeaderKind kind, byte[] value) { - @JsonCreator - public static HeaderValue fromJson( - @JsonProperty("kind") HeaderKind kind, @JsonProperty("value") String base64Value) { - byte[] decodedValue = Base64.getDecoder().decode(base64Value); - return new HeaderValue(kind, decodedValue); - } - public static HeaderValue fromString(String val) { - if (val.isEmpty() || val.length() > 255) { - throw new IllegalArgumentException("Value has incorrect size, must be between 1 and 255"); + public static HeaderValue fromString(String value) { + if (StringUtils.isBlank(value)) { + throw new IllegalArgumentException("Value cannot be null or empty"); + } + var bytes = value.getBytes(StandardCharsets.UTF_8); + if (bytes.length > 255) { + throw new IllegalArgumentException("Value has incorrect size, must be between 1 and 255 bytes"); } - return new HeaderValue(HeaderKind.String, val.getBytes(StandardCharsets.UTF_8)); + return new HeaderValue(HeaderKind.String, bytes); } public static HeaderValue fromBool(boolean val) { @@ -108,7 +106,7 @@ public record HeaderValue(HeaderKind kind, byte[] value) { public static HeaderValue fromRaw(byte[] val) { if (val.length == 0 || val.length > 255) { - throw new IllegalArgumentException("Value has incorrect size, must be between 1 and 255"); + throw new IllegalArgumentException("Value has incorrect size, must be between 1 and 255 bytes"); } return new HeaderValue(HeaderKind.Raw, val); } @@ -195,27 +193,24 @@ public record HeaderValue(HeaderKind kind, byte[] value) { } @Override - public boolean equals(Object o) { - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { - return false; - } - HeaderValue that = (HeaderValue) o; - return kind == that.kind && Arrays.equals(value, that.value); + public String toString() { + return toStringValue(); } @Override - public int hashCode() { - int result = kind.hashCode(); - result = 31 * result + Arrays.hashCode(value); - return result; + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + HeaderValue that = (HeaderValue) o; + return new EqualsBuilder() + .append(value, that.value) + .append(kind, that.kind) + .isEquals(); } @Override - public String toString() { - return toStringValue(); + public int hashCode() { + return new HashCodeBuilder(17, 37).append(kind).append(value).toHashCode(); } private String toStringValue() { diff --git a/foreign/java/java-sdk/src/main/java/org/apache/iggy/message/Message.java b/foreign/java/java-sdk/src/main/java/org/apache/iggy/message/Message.java index 144103d10..de84fc3a2 100644 --- a/foreign/java/java-sdk/src/main/java/org/apache/iggy/message/Message.java +++ b/foreign/java/java-sdk/src/main/java/org/apache/iggy/message/Message.java @@ -19,39 +19,26 @@ package org.apache.iggy.message; -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.annotation.JsonSetter; -import com.fasterxml.jackson.annotation.Nulls; -import org.apache.iggy.serde.Base64Serializer; -import org.apache.iggy.serde.UserHeadersSerializer; -import tools.jackson.databind.annotation.JsonSerialize; - +import javax.annotation.Nullable; import java.math.BigInteger; -import java.util.Base64; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; -public record Message( - MessageHeader header, - @JsonSerialize(using = Base64Serializer.class) byte[] payload, - @JsonSerialize(using = UserHeadersSerializer.class) Map<HeaderKey, HeaderValue> userHeaders) { - @JsonCreator - public static Message fromJson( - @JsonProperty("header") MessageHeader header, - @JsonProperty("payload") String base64Payload, - @JsonProperty(value = "user_headers", required = false) @JsonSetter(nulls = Nulls.AS_EMPTY) - List<HeaderEntry> userHeadersList) { - byte[] decodedPayload = Base64.getDecoder().decode(base64Payload); +public record Message(MessageHeader header, byte[] payload, Map<HeaderKey, HeaderValue> userHeaders) { + + /** + * Creates a Message from JSON deserialization. Used by Jackson mixin. + */ + public static Message of(MessageHeader header, byte[] payload, @Nullable List<HeaderEntry> userHeaders) { Map<HeaderKey, HeaderValue> headersMap = new HashMap<>(); - if (userHeadersList != null) { - for (HeaderEntry entry : userHeadersList) { + if (userHeaders != null) { + for (HeaderEntry entry : userHeaders) { headersMap.put(entry.key(), entry.value()); } } - return new Message(header, decodedPayload, headersMap); + return new Message(header, payload, headersMap); } public static Message of(String payload) { diff --git a/foreign/java/java-sdk/src/main/java/org/apache/iggy/message/Partitioning.java b/foreign/java/java-sdk/src/main/java/org/apache/iggy/message/Partitioning.java index c451510d2..ca8de8869 100644 --- a/foreign/java/java-sdk/src/main/java/org/apache/iggy/message/Partitioning.java +++ b/foreign/java/java-sdk/src/main/java/org/apache/iggy/message/Partitioning.java @@ -21,16 +21,12 @@ package org.apache.iggy.message; import org.apache.commons.lang3.ArrayUtils; import org.apache.iggy.exception.IggyInvalidArgumentException; -import org.apache.iggy.serde.Base64Serializer; -import tools.jackson.databind.annotation.JsonSerialize; import java.nio.ByteBuffer; -public record Partitioning( - PartitioningKind kind, - @JsonSerialize(using = Base64Serializer.class) byte[] value) { +public record Partitioning(PartitioningKind kind, byte[] value) { public static Partitioning balanced() { - return new Partitioning(PartitioningKind.Balanced, new byte[] {}); + return new Partitioning(PartitioningKind.Balanced, new byte[]{}); } public static Partitioning partitionId(Long id) { diff --git a/foreign/java/java-sdk/src/main/java/org/apache/iggy/serde/Base64Serializer.java b/foreign/java/java-sdk/src/main/java/org/apache/iggy/serde/Base64Serializer.java deleted file mode 100644 index 17bda8b39..000000000 --- a/foreign/java/java-sdk/src/main/java/org/apache/iggy/serde/Base64Serializer.java +++ /dev/null @@ -1,35 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -package org.apache.iggy.serde; - -import tools.jackson.core.JacksonException; -import tools.jackson.core.JsonGenerator; -import tools.jackson.databind.SerializationContext; -import tools.jackson.databind.ValueSerializer; - -import java.util.Base64; - -public class Base64Serializer extends ValueSerializer<byte[]> { - - @Override - public void serialize(byte[] value, JsonGenerator gen, SerializationContext ctxt) throws JacksonException { - gen.writeString(Base64.getEncoder().encodeToString(value)); - } -} diff --git a/foreign/java/java-sdk/src/main/java/org/apache/iggy/user/Permissions.java b/foreign/java/java-sdk/src/main/java/org/apache/iggy/user/Permissions.java index f3480e30b..c96dbdc12 100644 --- a/foreign/java/java-sdk/src/main/java/org/apache/iggy/user/Permissions.java +++ b/foreign/java/java-sdk/src/main/java/org/apache/iggy/user/Permissions.java @@ -19,11 +19,6 @@ package org.apache.iggy.user; -import com.fasterxml.jackson.annotation.JsonSetter; -import com.fasterxml.jackson.annotation.Nulls; - import java.util.Map; -public record Permissions( - GlobalPermissions global, - @JsonSetter(nulls = Nulls.AS_EMPTY) Map<Long, StreamPermissions> streams) {} +public record Permissions(GlobalPermissions global, Map<Long, StreamPermissions> streams) {} diff --git a/foreign/java/java-sdk/src/main/java/org/apache/iggy/user/StreamPermissions.java b/foreign/java/java-sdk/src/main/java/org/apache/iggy/user/StreamPermissions.java index 1b64e9ada..54c2488d8 100644 --- a/foreign/java/java-sdk/src/main/java/org/apache/iggy/user/StreamPermissions.java +++ b/foreign/java/java-sdk/src/main/java/org/apache/iggy/user/StreamPermissions.java @@ -19,9 +19,6 @@ package org.apache.iggy.user; -import com.fasterxml.jackson.annotation.JsonSetter; -import com.fasterxml.jackson.annotation.Nulls; - import java.util.Map; public record StreamPermissions( @@ -31,4 +28,4 @@ public record StreamPermissions( boolean readTopics, boolean pollMessages, boolean sendMessages, - @JsonSetter(nulls = Nulls.AS_EMPTY) Map<Long, TopicPermissions> topics) {} + Map<Long, TopicPermissions> topics) {} diff --git a/foreign/java/java-sdk/src/test/java/org/apache/iggy/client/blocking/IntegrationTest.java b/foreign/java/java-sdk/src/test/java/org/apache/iggy/client/blocking/IntegrationTest.java index 8420872d6..46afa9a9c 100644 --- a/foreign/java/java-sdk/src/test/java/org/apache/iggy/client/blocking/IntegrationTest.java +++ b/foreign/java/java-sdk/src/test/java/org/apache/iggy/client/blocking/IntegrationTest.java @@ -19,6 +19,8 @@ package org.apache.iggy.client.blocking; +import com.github.dockerjava.api.model.Capability; +import com.github.dockerjava.api.model.Ulimit; import org.apache.iggy.stream.StreamDetails; import org.apache.iggy.topic.CompressionAlgorithm; import org.junit.jupiter.api.AfterAll; @@ -64,6 +66,10 @@ public abstract class IntegrationTest { .withEnv("IGGY_ROOT_PASSWORD", "iggy") .withEnv("IGGY_TCP_ADDRESS", "0.0.0.0:8090") .withEnv("IGGY_HTTP_ADDRESS", "0.0.0.0:3000") + .withCreateContainerCmdModifier(cmd -> cmd.getHostConfig() + .withCapAdd(Capability.SYS_NICE) + .withSecurityOpts(List.of("seccomp:unconfined")) + .withUlimits(List.of(new Ulimit("memlock", -1L, -1L)))) .withLogConsumer(frame -> System.out.print(frame.getUtf8String())); iggyServer.start(); } else { diff --git a/foreign/java/java-sdk/src/test/java/org/apache/iggy/client/blocking/http/HeaderKindSerializationTest.java b/foreign/java/java-sdk/src/test/java/org/apache/iggy/client/blocking/http/HeaderKindSerializationTest.java new file mode 100644 index 000000000..cb3ff5558 --- /dev/null +++ b/foreign/java/java-sdk/src/test/java/org/apache/iggy/client/blocking/http/HeaderKindSerializationTest.java @@ -0,0 +1,120 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.iggy.client.blocking.http; + +import org.apache.iggy.message.HeaderKind; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import tools.jackson.databind.ObjectMapper; + +import java.util.stream.Stream; + +import static org.assertj.core.api.Assertions.assertThat; + +class HeaderKindSerializationTest { + + private final ObjectMapper objectMapper = ObjectMapperFactory.getInstance(); + + @Nested + class Serialization { + + @ParameterizedTest + @MethodSource("org.apache.iggy.client.blocking.http.HeaderKindSerializationTest#headerKindMappings") + void shouldSerializeToLowercase(HeaderKind kind, String expectedJson) { + // given + // kind provided by parameter + + // when + String json = objectMapper.writeValueAsString(kind); + + // then + assertThat(json).isEqualTo("\"" + expectedJson + "\""); + } + } + + @Nested + class Deserialization { + + @ParameterizedTest + @MethodSource("org.apache.iggy.client.blocking.http.HeaderKindSerializationTest#headerKindMappings") + void shouldDeserializeFromLowercase(HeaderKind expectedKind, String jsonValue) { + // given + String json = "\"" + jsonValue + "\""; + + // when + HeaderKind result = objectMapper.readValue(json, HeaderKind.class); + + // then + assertThat(result).isEqualTo(expectedKind); + } + + @ParameterizedTest + @MethodSource("org.apache.iggy.client.blocking.http.HeaderKindSerializationTest#headerKindMappings") + void shouldDeserializeCaseInsensitive(HeaderKind expectedKind, String jsonValue) { + // given + String json = "\"" + jsonValue.toUpperCase() + "\""; + + // when + HeaderKind result = objectMapper.readValue(json, HeaderKind.class); + + // then + assertThat(result).isEqualTo(expectedKind); + } + } + + @Nested + class Roundtrip { + + @ParameterizedTest + @MethodSource("org.apache.iggy.client.blocking.http.HeaderKindSerializationTest#headerKindMappings") + void shouldRoundtripAllHeaderKinds(HeaderKind kind, String ignored) { + // given + // kind provided by parameter + + // when + String json = objectMapper.writeValueAsString(kind); + HeaderKind result = objectMapper.readValue(json, HeaderKind.class); + + // then + assertThat(result).isEqualTo(kind); + } + } + + static Stream<Arguments> headerKindMappings() { + return Stream.of( + Arguments.of(HeaderKind.Raw, "raw"), + Arguments.of(HeaderKind.String, "string"), + Arguments.of(HeaderKind.Bool, "bool"), + Arguments.of(HeaderKind.Int8, "int8"), + Arguments.of(HeaderKind.Int16, "int16"), + Arguments.of(HeaderKind.Int32, "int32"), + Arguments.of(HeaderKind.Int64, "int64"), + Arguments.of(HeaderKind.Int128, "int128"), + Arguments.of(HeaderKind.Uint8, "uint8"), + Arguments.of(HeaderKind.Uint16, "uint16"), + Arguments.of(HeaderKind.Uint32, "uint32"), + Arguments.of(HeaderKind.Uint64, "uint64"), + Arguments.of(HeaderKind.Uint128, "uint128"), + Arguments.of(HeaderKind.Float32, "float32"), + Arguments.of(HeaderKind.Float64, "float64")); + } +} diff --git a/foreign/java/java-sdk/src/test/java/org/apache/iggy/client/blocking/http/ObjectMapperTest.java b/foreign/java/java-sdk/src/test/java/org/apache/iggy/client/blocking/http/ObjectMapperTest.java new file mode 100644 index 000000000..eefda1659 --- /dev/null +++ b/foreign/java/java-sdk/src/test/java/org/apache/iggy/client/blocking/http/ObjectMapperTest.java @@ -0,0 +1,286 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.iggy.client.blocking.http; + +import org.apache.iggy.message.Message; +import org.apache.iggy.message.PolledMessages; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import tools.jackson.databind.ObjectMapper; + +import java.nio.charset.StandardCharsets; +import java.util.Base64; + +import static org.assertj.core.api.Assertions.assertThat; + +class ObjectMapperTest { + + private final ObjectMapper objectMapper = ObjectMapperFactory.getInstance(); + + @Nested + class Deserialization { + + @Nested + @DisplayName("PolledMessages") + class PolledMessagesDeserialization { + + @Test + void shouldDeserializePolledMessagesWithEmptyUserHeaders() { + // given + String json = """ + { + "partition_id": 1, + "current_offset": 10, + "count": 1, + "messages": [ + { + "header": { + "checksum": 0, + "id": 42, + "offset": 0, + "timestamp": 0, + "origin_timestamp": 1000, + "user_headers_length": 0, + "payload_length": 4 + }, + "payload": "dGVzdA==", + "user_headers": [] + } + ] + } + """; + + // when + var polledMessages = objectMapper.readValue(json, PolledMessages.class); + + // then + assertThat(polledMessages).isNotNull(); + assertThat(polledMessages.messages()).hasSize(1); + assertThat(polledMessages.messages().get(0).userHeaders()).isEmpty(); + } + + @Test + void shouldDeserializePolledMessagesWithUserHeaders() { + // given + String json = """ + { + "partition_id": 1, + "current_offset": 10, + "count": 1, + "messages": [ + { + "header": { + "checksum": 0, + "id": 42, + "offset": 0, + "timestamp": 0, + "origin_timestamp": 1000, + "user_headers_length": 62, + "payload_length": 4 + }, + "payload": "dGVzdA==", + "user_headers": [ + { + "key": {"kind": "string", "value": "Y29udGVudC10eXBl"}, + "value": {"kind": "string", "value": "dGV4dC9wbGFpbg=="} + } + ] + } + ] + } + """; + + // when + var polledMessages = objectMapper.readValue(json, PolledMessages.class); + + // then + assertThat(polledMessages).isNotNull(); + assertThat(polledMessages.messages()).hasSize(1); + var headers = polledMessages.messages().get(0).userHeaders(); + assertThat(headers).hasSize(1); + var header = headers.entrySet().iterator().next(); + assertThat(header.getKey().toString()).isEqualTo("content-type"); + assertThat(header.getValue().toString()).isEqualTo("text/plain"); + } + } + + @Nested + @DisplayName("Payload") + class PayloadDeserialization { + + @Test + void shouldDeserializeBase64EncodedPayloadToBytes() { + // given + String expectedPayload = "test"; + String base64Payload = + Base64.getEncoder().encodeToString(expectedPayload.getBytes(StandardCharsets.UTF_8)); + String json = createMessageJson(base64Payload); + + // when + var polledMessages = objectMapper.readValue(json, PolledMessages.class); + + // then + byte[] actualPayload = polledMessages.messages().get(0).payload(); + assertThat(actualPayload).isEqualTo(expectedPayload.getBytes(StandardCharsets.UTF_8)); + } + + @Test + void shouldDeserializeEmptyPayload() { + // given + String base64Payload = Base64.getEncoder().encodeToString(new byte[0]); + String json = createMessageJson(base64Payload); + + // when + var polledMessages = objectMapper.readValue(json, PolledMessages.class); + + // then + byte[] actualPayload = polledMessages.messages().get(0).payload(); + assertThat(actualPayload).isEmpty(); + } + + @Test + void shouldDeserializeBinaryPayload() { + // given + byte[] binaryData = new byte[] {0x00, 0x01, 0x02, (byte) 0xFF, (byte) 0xFE}; + String base64Payload = Base64.getEncoder().encodeToString(binaryData); + String json = createMessageJson(base64Payload); + + // when + var polledMessages = objectMapper.readValue(json, PolledMessages.class); + + // then + byte[] actualPayload = polledMessages.messages().get(0).payload(); + assertThat(actualPayload).isEqualTo(binaryData); + } + + private String createMessageJson(String base64Payload) { + return """ + { + "partition_id": 1, + "current_offset": 0, + "count": 1, + "messages": [ + { + "header": { + "checksum": 0, + "id": 1, + "offset": 0, + "timestamp": 0, + "origin_timestamp": 0, + "user_headers_length": 0, + "payload_length": 4 + }, + "payload": "%s", + "user_headers": [] + } + ] + } + """.formatted(base64Payload); + } + } + } + + @Nested + class Serialization { + + @Nested + @DisplayName("Payload") + class PayloadSerialization { + + @Test + void shouldSerializePayloadToBase64() { + // given + String payloadContent = "test"; + Message message = Message.of(payloadContent); + + // when + String json = objectMapper.writeValueAsString(message); + + // then + String expectedBase64 = + Base64.getEncoder().encodeToString(payloadContent.getBytes(StandardCharsets.UTF_8)); + assertThat(json).contains("\"payload\":\"" + expectedBase64 + "\""); + } + + @Test + void shouldSerializeEmptyPayloadToBase64() { + // given + String payloadContent = ""; + Message message = Message.of(payloadContent); + + // when + String json = objectMapper.writeValueAsString(message); + + // then + String expectedBase64 = Base64.getEncoder().encodeToString(new byte[0]); + assertThat(json).contains("\"payload\":\"" + expectedBase64 + "\""); + } + + @Test + void shouldSerializeBinaryPayloadToBase64() { + // given + byte[] binaryData = new byte[] {0x00, 0x01, 0x02, (byte) 0xFF, (byte) 0xFE}; + Message message = Message.of("placeholder"); + Message binaryMessage = new Message(message.header(), binaryData, message.userHeaders()); + + // when + String json = objectMapper.writeValueAsString(binaryMessage); + + // then + String expectedBase64 = Base64.getEncoder().encodeToString(binaryData); + assertThat(json).contains("\"payload\":\"" + expectedBase64 + "\""); + } + } + } + + @Nested + class Roundtrip { + + @Test + void shouldRoundtripTextPayload() { + // given + String payloadContent = "Hello, World!"; + Message originalMessage = Message.of(payloadContent); + + // when + String json = objectMapper.writeValueAsString(originalMessage); + Message deserializedMessage = objectMapper.readValue(json, Message.class); + + // then + assertThat(deserializedMessage.payload()).isEqualTo(originalMessage.payload()); + } + + @Test + void shouldRoundtripBinaryPayload() { + // given + byte[] binaryData = new byte[] {0x00, 0x01, 0x02, (byte) 0x80, (byte) 0xFF}; + Message originalMessage = Message.of("placeholder"); + Message binaryMessage = new Message(originalMessage.header(), binaryData, originalMessage.userHeaders()); + + // when + String json = objectMapper.writeValueAsString(binaryMessage); + Message deserializedMessage = objectMapper.readValue(json, Message.class); + + // then + assertThat(deserializedMessage.payload()).isEqualTo(binaryData); + } + } +} diff --git a/foreign/java/java-sdk/src/test/java/org/apache/iggy/serde/BytesDeserializerTest.java b/foreign/java/java-sdk/src/test/java/org/apache/iggy/serde/BytesDeserializerTest.java index 36a6d5ea3..496a428e5 100644 --- a/foreign/java/java-sdk/src/test/java/org/apache/iggy/serde/BytesDeserializerTest.java +++ b/foreign/java/java-sdk/src/test/java/org/apache/iggy/serde/BytesDeserializerTest.java @@ -752,80 +752,4 @@ class BytesDeserializerTest { assertThat(tokenInfo.expiryAt()).isEmpty(); } } - - @Nested - class JsonDeserialization { - - private static final tools.jackson.databind.ObjectMapper MAPPER = - org.apache.iggy.client.blocking.http.ObjectMapperFactory.getInstance(); - - @Test - void shouldDeserializePolledMessagesWithEmptyUserHeaders() throws Exception { - String json = """ - { - "partition_id": 1, - "current_offset": 10, - "count": 1, - "messages": [ - { - "header": { - "checksum": 0, - "id": 42, - "offset": 0, - "timestamp": 0, - "origin_timestamp": 1000, - "user_headers_length": 0, - "payload_length": 4 - }, - "payload": "dGVzdA==", - "user_headers": [] - } - ] - } - """; - - var polledMessages = MAPPER.readValue(json, org.apache.iggy.message.PolledMessages.class); - - assertThat(polledMessages).isNotNull(); - assertThat(polledMessages.messages()).hasSize(1); - assertThat(polledMessages.messages().get(0).userHeaders()).isEmpty(); - } - - @Test - void shouldDeserializePolledMessagesWithUserHeaders() throws Exception { - String json = """ - { - "partition_id": 1, - "current_offset": 10, - "count": 1, - "messages": [ - { - "header": { - "checksum": 0, - "id": 42, - "offset": 0, - "timestamp": 0, - "origin_timestamp": 1000, - "user_headers_length": 62, - "payload_length": 4 - }, - "payload": "dGVzdA==", - "user_headers": [ - { - "key": {"kind": "string", "value": "Y29udGVudC10eXBl"}, - "value": {"kind": "string", "value": "dGV4dC9wbGFpbg=="} - } - ] - } - ] - } - """; - - var polledMessages = MAPPER.readValue(json, org.apache.iggy.message.PolledMessages.class); - - assertThat(polledMessages).isNotNull(); - assertThat(polledMessages.messages()).hasSize(1); - assertThat(polledMessages.messages().get(0).userHeaders()).hasSize(1); - } - } }
