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
19 changes: 19 additions & 0 deletions modules/typed-ids-spring-convert/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
plugins {
id("framefork.java-public")
}

dependencies {
api(project(":typed-ids"))

compileOnly("org.springframework:spring-core:6.0.0")
compileOnly(libs.jetbrains.annotations)

compileOnly(libs.autoService.annotations)
annotationProcessor(libs.autoService.processor)

testImplementation(project(":typed-ids-testing"))
testImplementation("org.springframework:spring-core:6.0.0")
testRuntimeOnly("org.junit.platform:junit-platform-launcher")
}

project.description = "TypeIds Spring Framework converters"
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
package org.framefork.typedIds.spring.convert;

import org.framefork.typedIds.bigint.ObjectBigIntId;
import org.framefork.typedIds.bigint.ObjectBigIntIdTypeUtils;
import org.framefork.typedIds.common.ReflectionHacks;
import org.jetbrains.annotations.NotNull;
import org.jspecify.annotations.Nullable;
import org.springframework.core.convert.converter.Converter;
import org.springframework.core.convert.converter.ConverterFactory;

import java.lang.invoke.MethodHandle;

/**
* Generic converter factory that converts {@link Number} to any {@link ObjectBigIntId} subtype.
*
* <p>This factory eliminates the need for individual converter classes for each Id type.
* It works by using {@link MethodHandle} to invoke the private constructor that all
* {@link ObjectBigIntId} subtypes have.
*
* <p>To use this converter, register it with Spring's conversion service:
* <pre>{@code
* @Configuration
* public class ConversionServiceConfiguration {
* @Bean
* public ConversionService conversionService() {
* DefaultConversionService service = new DefaultConversionService();
* service.addConverterFactory(new NumberToObjectBigIntIdConverterFactory());
* return service;
* }
* }
* }</pre>
*/
public class NumberToObjectBigIntIdConverterFactory implements ConverterFactory<Number, ObjectBigIntId<?>> {

@Override
public <T extends ObjectBigIntId<?>> @NotNull Converter<Number, T> getConverter(final @NotNull Class<T> targetType) {
return new NumberToObjectBigIntIdConverter<>(targetType);
}

private static final class NumberToObjectBigIntIdConverter<T extends ObjectBigIntId<?>>
implements Converter<Number, T> {

private final Class<T> targetType;

private NumberToObjectBigIntIdConverter(final Class<T> targetType) {
this.targetType = targetType;
}

@Override
public @Nullable T convert(final @Nullable Number source) {
if (source == null) {
return null;
}

try {
final var constructor = ReflectionHacks.getConstructor(targetType, long.class);
@SuppressWarnings("unchecked")
var result = (T) ObjectBigIntIdTypeUtils.wrapBigIntToIdentifier(source.longValue(), constructor);
return result;
} catch (final IllegalArgumentException e) {
throw new IllegalArgumentException(
"Cannot convert Number to " + targetType.getName() +
". Ensure it extends ObjectBigIntId and has a private constructor taking a long parameter.",
e
);
}
}
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
package org.framefork.typedIds.spring.convert;

import org.framefork.typedIds.common.ReflectionHacks;
import org.framefork.typedIds.uuid.ObjectUuid;
import org.framefork.typedIds.uuid.ObjectUuidTypeUtils;
import org.jetbrains.annotations.NotNull;
import org.jspecify.annotations.Nullable;
import org.springframework.core.convert.converter.Converter;
import org.springframework.core.convert.converter.ConverterFactory;

import java.lang.invoke.MethodHandle;
import java.util.UUID;

/**
* Generic converter factory that converts {@link String} to any {@link ObjectUuid} subtype.
*
* <p>This factory eliminates the need for individual converter classes for each Id type.
* It works by using {@link MethodHandle} to invoke the private constructor that all
* {@link ObjectUuid} subtypes have.
*
* <p>To use this converter, register it with Spring's conversion service:
* <pre>{@code
* @Configuration
* public class ConversionServiceConfiguration {
* @Bean
* public ConversionService conversionService() {
* DefaultConversionService service = new DefaultConversionService();
* service.addConverterFactory(new StringToObjectUuidConverterFactory());
* return service;
* }
* }
* }</pre>
*/
public class StringToObjectUuidConverterFactory implements ConverterFactory<String, ObjectUuid<?>> {

@Override
public <T extends ObjectUuid<?>> @NotNull Converter<String, T> getConverter(final @NotNull Class<T> targetType) {
return new StringToObjectUuidConverter<>(targetType);
}

private static final class StringToObjectUuidConverter<T extends ObjectUuid<?>>
implements Converter<String, T> {

private final Class<T> targetType;

private StringToObjectUuidConverter(final Class<T> targetType) {
this.targetType = targetType;
}

@Override
public @Nullable T convert(final @Nullable String source) {
if (source == null || source.isEmpty()) {
return null;
}

try {
final UUID uuid = UUID.fromString(source);
final var constructor = ReflectionHacks.getConstructor(targetType, UUID.class);
@SuppressWarnings("unchecked")
var result = (T) ObjectUuidTypeUtils.wrapUuidToIdentifier(uuid, constructor);
return result;
} catch (final IllegalArgumentException e) {
throw new IllegalArgumentException(
"Cannot convert String to " + targetType.getName() +
". Ensure it extends ObjectUuid and has a private constructor taking a UUID parameter.",
e
);
}
}
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
package org.framefork.typedIds.spring.convert;

import org.framefork.typedIds.bigint.ObjectBigIntId;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.core.convert.converter.Converter;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;

class NumberToObjectBigIntIdConverterFactoryTest {

private NumberToObjectBigIntIdConverterFactory factory;

@BeforeEach
void setUp() {
factory = new NumberToObjectBigIntIdConverterFactory();
}

@Test
void shouldConvertLongToObjectBigIntId() {
// Given
Converter<Number, TestBigIntId> converter = factory.getConverter(TestBigIntId.class);
long value = 12345L;

// When
TestBigIntId result = converter.convert(value);

// Then
assertThat(result).isNotNull();
assertThat(result.toLong()).isEqualTo(value);
}

@Test
void shouldConvertIntegerToObjectBigIntId() {
// Given
Converter<Number, TestBigIntId> converter = factory.getConverter(TestBigIntId.class);
int value = 42;

// When
TestBigIntId result = converter.convert(value);

// Then
assertThat(result).isNotNull();
assertThat(result.toLong()).isEqualTo(value);
}

@Test
void shouldReturnNullForNullInput() {
// Given
Converter<Number, TestBigIntId> converter = factory.getConverter(TestBigIntId.class);

// When
TestBigIntId result = converter.convert(null);

// Then
assertThat(result).isNull();
}

@Test
void shouldThrowExceptionForInvalidType() {
// Given
Converter<Number, InvalidBigIntId> converter = factory.getConverter(InvalidBigIntId.class);

// When / Then
assertThatThrownBy(() -> converter.convert(123L))
.isInstanceOf(IllegalArgumentException.class)
.hasMessageContaining("Cannot convert Number to");
}

// Test ID class
public static final class TestBigIntId extends ObjectBigIntId<TestBigIntId> {
private TestBigIntId(long inner) {
super(inner);
}

public static TestBigIntId from(long value) {
return ObjectBigIntId.fromLong(TestBigIntId::new, value);
}
}

// Invalid test class (no proper constructor)
public static final class InvalidBigIntId extends ObjectBigIntId<InvalidBigIntId> {
// This class intentionally has no long constructor to test error handling
@SuppressWarnings({"UnusedMethod", "UnusedVariable"})
private InvalidBigIntId(String invalid) {
super(0);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
package org.framefork.typedIds.spring.convert;

import org.framefork.typedIds.uuid.ObjectUuid;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.core.convert.converter.Converter;

import java.util.UUID;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;

class StringToObjectUuidConverterFactoryTest {

private StringToObjectUuidConverterFactory factory;

@BeforeEach
void setUp() {
factory = new StringToObjectUuidConverterFactory();
}

@Test
void shouldConvertStringToObjectUuid() {
// Given
Converter<String, TestUuid> converter = factory.getConverter(TestUuid.class);
UUID uuid = UUID.fromString("550e8400-e29b-41d4-a716-446655440000");
String uuidString = uuid.toString();

// When
TestUuid result = converter.convert(uuidString);

// Then
assertThat(result).isNotNull();
assertThat(result.toNativeUuid()).isEqualTo(uuid);
}

@Test
void shouldReturnNullForNullInput() {
// Given
Converter<String, TestUuid> converter = factory.getConverter(TestUuid.class);

// When
TestUuid result = converter.convert(null);

// Then
assertThat(result).isNull();
}

@Test
void shouldReturnNullForEmptyString() {
// Given
Converter<String, TestUuid> converter = factory.getConverter(TestUuid.class);

// When
TestUuid result = converter.convert("");

// Then
assertThat(result).isNull();
}

@Test
void shouldThrowExceptionForInvalidUuidString() {
// Given
Converter<String, TestUuid> converter = factory.getConverter(TestUuid.class);

// When / Then
assertThatThrownBy(() -> converter.convert("not-a-uuid"))
.isInstanceOf(IllegalArgumentException.class);
}

@Test
void shouldThrowExceptionForInvalidType() {
// Given
Converter<String, InvalidUuid> converter = factory.getConverter(InvalidUuid.class);

// When / Then
assertThatThrownBy(() -> converter.convert("550e8400-e29b-41d4-a716-446655440000"))
.isInstanceOf(IllegalArgumentException.class)
.hasMessageContaining("Cannot convert String to");
}

// Test UUID class - using UUID v4 for testing
public static final class TestUuid extends ObjectUuid<TestUuid> {
private TestUuid(UUID inner) {
super(inner);
}

public static TestUuid from(UUID value) {
return ObjectUuid.fromUuid(TestUuid::new, value);
}

public static TestUuid from(String value) {
return ObjectUuid.fromString(TestUuid::new, value);
}
}

// Invalid test class (no proper constructor)
public static final class InvalidUuid extends ObjectUuid<InvalidUuid> {
// This class intentionally has no UUID constructor to test error handling
@SuppressWarnings({"UnusedMethod", "UnusedVariable"})
private InvalidUuid(String invalid) {
super(UUID.fromString("550e8400-e29b-41d4-a716-446655440000"));
}
}
}