Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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.
Expand Down
Original file line number Diff line number Diff line change
@@ -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.
* <p>
* 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}.
* </p>
* <p>
* This class is designed to be extended by encoders that need additional type handling,
* such as {@link PageableSpringQueryMapEncoder}.
* </p>
*
* @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.
* <p>
* Uses {@link FieldQueryMapEncoder} for Records and {@link BeanQueryMapEncoder} for
* POJOs.
* </p>
*/
public RecordQueryMapEncoder() {
this(new FieldQueryMapEncoder(), new BeanQueryMapEncoder());
}

/**
* Creates a new instance with custom encoders.
* <p>
* This constructor is primarily intended for testing and advanced use cases where
* custom encoding behavior is required.
* </p>
* @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<String, Object> encode(Object object) {
if (object == null) {
return Collections.emptyMap();
}

if (object.getClass().isRecord()) {
return this.recordDelegate.encode(object);
}

return this.beanDelegate.encode(object);
}

}
Original file line number Diff line number Diff line change
@@ -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<String, Object> 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<String, Object> result = this.encoder.encode(record);

// then
assertThat(result).isNotNull();
assertThat(result).isEmpty();
}

@Test
void shouldReturnEmptyMapForNullInput() {
// when
Map<String, Object> 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<String, Object> 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;
}

}

}