From d2c3956f174f2b2dea98756c9551cc11e2be65f0 Mon Sep 17 00:00:00 2001 From: seonghoJoo Date: Sun, 30 Nov 2025 19:39:43 +0900 Subject: [PATCH] Add RecordQueryMapEncoder for @SpringQueryMap Record support - Create RecordQueryMapEncoder for Record type detection - Modify PageableSpringQueryMapEncoder to extend RecordQueryMapEncoder - Delegate Record encoding to FieldQueryMapEncoder - Maintain backward compatibility for Pageable/Sort/Bean - Add comprehensive test coverage Fixes gh-1266 Signed-off-by: brightJoo --- .../PageableSpringQueryMapEncoder.java | 4 +- .../support/RecordQueryMapEncoder.java | 88 ++++++++++ .../support/RecordQueryMapEncoderTests.java | 150 ++++++++++++++++++ 3 files changed, 239 insertions(+), 3 deletions(-) create mode 100644 spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/support/RecordQueryMapEncoder.java create mode 100644 spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/support/RecordQueryMapEncoderTests.java diff --git a/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/support/PageableSpringQueryMapEncoder.java b/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/support/PageableSpringQueryMapEncoder.java index 6ec743b44..be6a3b24c 100644 --- a/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/support/PageableSpringQueryMapEncoder.java +++ b/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/support/PageableSpringQueryMapEncoder.java @@ -21,8 +21,6 @@ import java.util.List; import java.util.Map; -import feign.querymap.BeanQueryMapEncoder; - import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Sort; @@ -35,7 +33,7 @@ * @author Gokalp Kuscu * @since 2.2.8 */ -public class PageableSpringQueryMapEncoder extends BeanQueryMapEncoder { +public class PageableSpringQueryMapEncoder extends RecordQueryMapEncoder { /** * Page index parameter name. diff --git a/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/support/RecordQueryMapEncoder.java b/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/support/RecordQueryMapEncoder.java new file mode 100644 index 000000000..84c055ae9 --- /dev/null +++ b/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/support/RecordQueryMapEncoder.java @@ -0,0 +1,88 @@ +/* + * Copyright 2013-present the original author or authors. + * + * Licensed 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 + * + * https://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.springframework.cloud.openfeign.support; + +import java.util.Collections; +import java.util.Map; + +import feign.QueryMapEncoder; +import feign.querymap.BeanQueryMapEncoder; +import feign.querymap.FieldQueryMapEncoder; + +/** + * A {@link QueryMapEncoder} that supports Java Record types. + *

+ * This encoder detects Record types using {@link Class#isRecord()} and delegates to + * {@link FieldQueryMapEncoder} for field-based encoding. For non-Record types (standard + * JavaBeans/POJOs), it falls back to {@link BeanQueryMapEncoder}. + *

+ *

+ * This class is designed to be extended by encoders that need additional type handling, + * such as {@link PageableSpringQueryMapEncoder}. + *

+ * + * @author Joo + * @since 4.2.0 + * @see FieldQueryMapEncoder + * @see BeanQueryMapEncoder + * @see PageableSpringQueryMapEncoder + */ +public class RecordQueryMapEncoder implements QueryMapEncoder { + + private final QueryMapEncoder recordDelegate; + + private final QueryMapEncoder beanDelegate; + + /** + * Creates a new instance with default encoders. + *

+ * Uses {@link FieldQueryMapEncoder} for Records and {@link BeanQueryMapEncoder} for + * POJOs. + *

+ */ + public RecordQueryMapEncoder() { + this(new FieldQueryMapEncoder(), new BeanQueryMapEncoder()); + } + + /** + * Creates a new instance with custom encoders. + *

+ * This constructor is primarily intended for testing and advanced use cases where + * custom encoding behavior is required. + *

+ * @param recordDelegate encoder for Record types + * @param beanDelegate encoder for non-Record types (POJOs) + */ + public RecordQueryMapEncoder(QueryMapEncoder recordDelegate, QueryMapEncoder beanDelegate) { + this.recordDelegate = recordDelegate; + this.beanDelegate = beanDelegate; + } + + @Override + public Map encode(Object object) { + if (object == null) { + return Collections.emptyMap(); + } + + if (object.getClass().isRecord()) { + return this.recordDelegate.encode(object); + } + + return this.beanDelegate.encode(object); + } + +} diff --git a/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/support/RecordQueryMapEncoderTests.java b/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/support/RecordQueryMapEncoderTests.java new file mode 100644 index 000000000..2f3ddd40b --- /dev/null +++ b/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/support/RecordQueryMapEncoderTests.java @@ -0,0 +1,150 @@ +/* + * Copyright 2013-present the original author or authors. + * + * Licensed 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 + * + * https://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.springframework.cloud.openfeign.support; + +import java.util.Collections; +import java.util.Map; + +import feign.QueryMapEncoder; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +/** + * Tests for {@link RecordQueryMapEncoder}. + * + * @author Joo + */ +class RecordQueryMapEncoderTests { + + private final RecordQueryMapEncoder encoder = new RecordQueryMapEncoder(); + + @Test + void shouldEncodeSimpleRecord() { + // given + SimpleRecord record = new SimpleRecord("hello", 1); + + // when + Map result = this.encoder.encode(record); + + // then + assertThat(result).isNotNull(); + assertThat(result).hasSize(2); + assertThat(result.get("keyword")).isEqualTo("hello"); + assertThat(result.get("page")).isEqualTo(1); + } + + @Test + void shouldEncodeEmptyRecord() { + // given + EmptyRecord record = new EmptyRecord(); + + // when + Map result = this.encoder.encode(record); + + // then + assertThat(result).isNotNull(); + assertThat(result).isEmpty(); + } + + @Test + void shouldReturnEmptyMapForNullInput() { + // when + Map result = this.encoder.encode(null); + + // then + assertThat(result).isNotNull(); + assertThat(result).isEmpty(); + } + + @Test + void shouldDelegateToBeanEncoderForPojo() { + // given + SimplePojo pojo = new SimplePojo(); + pojo.setName("test"); + + // when + Map result = this.encoder.encode(pojo); + + // then + assertThat(result).isNotNull(); + assertThat(result).containsEntry("name", "test"); + } + + @Test + void shouldUseProvidedDelegates() { + // given + QueryMapEncoder mockRecordEncoder = mock(QueryMapEncoder.class); + QueryMapEncoder mockBeanEncoder = mock(QueryMapEncoder.class); + RecordQueryMapEncoder customEncoder = new RecordQueryMapEncoder(mockRecordEncoder, mockBeanEncoder); + + SimpleRecord record = new SimpleRecord("test", 1); + when(mockRecordEncoder.encode(record)).thenReturn(Collections.emptyMap()); + + // when + customEncoder.encode(record); + + // then + verify(mockRecordEncoder).encode(record); + } + + @Test + void shouldUseProvidedBeanDelegate() { + // given + QueryMapEncoder mockRecordEncoder = mock(QueryMapEncoder.class); + QueryMapEncoder mockBeanEncoder = mock(QueryMapEncoder.class); + RecordQueryMapEncoder customEncoder = new RecordQueryMapEncoder(mockRecordEncoder, mockBeanEncoder); + + SimplePojo pojo = new SimplePojo(); + when(mockBeanEncoder.encode(pojo)).thenReturn(Collections.emptyMap()); + + // when + customEncoder.encode(pojo); + + // then + verify(mockBeanEncoder).encode(pojo); + } + + // Test Records (local definitions) + record SimpleRecord(String keyword, int page) { + } + + record EmptyRecord() { + } + + record RecordWithNullField(String value) { + } + + // Test POJO for fallback testing + public static class SimplePojo { + + private String name; + + public String getName() { + return this.name; + } + + public void setName(String name) { + this.name = name; + } + + } + +}