Skip to content

Commit

Permalink
Java implementation
Browse files Browse the repository at this point in the history
  • Loading branch information
nsimonides committed Jan 3, 2025
1 parent 05b5dce commit 67c3611
Show file tree
Hide file tree
Showing 8 changed files with 358 additions and 40 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -6,32 +6,166 @@
import community.flock.wirespec.java.Wirespec;
import org.springframework.stereotype.Component;

import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.StreamSupport;

@Component
public class WirespecSerializer implements Wirespec.Serialization<String> {

private final ObjectMapper objectMapper;
private final Map<Class<?>, Function<String, Object>> primitiveTypesConversion;

public WirespecSerializer(ObjectMapper objectMapper) {
this.objectMapper = objectMapper;
this.primitiveTypesConversion = initPrimitiveTypesConversion();
}

private Map<Class<?>, Function<String, Object>> initPrimitiveTypesConversion() {
Map<Class<?>, Function<String, Object>> conversion = new HashMap<>();
conversion.put(String.class, s -> s);
conversion.put(Integer.class, Integer::valueOf);
conversion.put(Long.class, Long::valueOf);
conversion.put(Double.class, Double::valueOf);
conversion.put(Float.class, Float::valueOf);
conversion.put(Boolean.class, Boolean::valueOf);
conversion.put(Character.class, s -> s.charAt(0));
conversion.put(Byte.class, Byte::valueOf);
conversion.put(Short.class, Short::valueOf);
return conversion;
}

@Override
public <T> String serialize(T t, Type type) {
public <T> String serialize(T body, Type type) {
try {
return objectMapper.writeValueAsString(t);
if (body instanceof String) {
return (String) body;
}
return objectMapper.writeValueAsString(body);
} catch (JsonProcessingException e) {
throw new SerializationException(e);
}
}

@Override
public <T> T deserialize(String s, Type type) {
public <T> List<String> serializeQuery(T value, Type type) {
if (value == null) {
return null;
}
if (isIterable(type)) {
return StreamSupport.stream(((Iterable<?>) value).spliterator(), false)
.map(Object::toString)
.collect(Collectors.toList());
}
return List.of(value.toString());
}

@Override
@SuppressWarnings("unchecked")
public <T> T deserialize(String raw, Type valueType) {
if (raw == null) {
return null;
}
try {
return objectMapper.readValue(s, objectMapper.constructType(type));
if (valueType == String.class) {
return (T) raw;
}
return objectMapper.readValue(raw, objectMapper.constructType(valueType));
} catch (JsonProcessingException e) {
throw new SerializationException(e);
}
}
}

@Override
@SuppressWarnings("unchecked")
public <T> T deserializeQuery(List<String> values, Type type) {
if (values == null || values.isEmpty()) {
return null;
}
if (isIterable(type)) {
return (T) deserializeList(values, getIterableElementType(type));
}
if (isWirespecEnum(type)) {
return (T) deserializeEnum(values, (Class<?>) type);
}
return (T) deserializePrimitive(values, (Class<?>) type);
}

private List<Object> deserializeList(List<String> values, Type type) {
if (isWirespecEnum(type)) {
return values.stream()
.map(value -> findEnumByLabel((Class<?>) type, value))
.collect(Collectors.toList());
}
return deserializePrimitiveList(values, (Class<?>) type);
}

private Object deserializePrimitive(List<String> values, Class<?> clazz) {
String value = values.stream().findFirst()
.orElseThrow(() -> new IllegalArgumentException("No value provided for type: " + clazz.getSimpleName()));

Function<String, Object> converter = primitiveTypesConversion.get(clazz);
if (converter == null) {
throw new IllegalArgumentException("Unsupported primitive type: " + clazz.getSimpleName());
}
return converter.apply(value);
}

private List<Object> deserializePrimitiveList(List<String> values, Class<?> clazz) {
Function<String, Object> converter = primitiveTypesConversion.get(clazz);
if (converter == null) {
throw new IllegalArgumentException("Unsupported list element type: " + clazz.getSimpleName());
}
return values.stream()
.map(converter)
.collect(Collectors.toList());
}

private Object deserializeEnum(List<String> values, Class<?> enumClass) {
String value = values.stream().findFirst()
.orElseThrow(() -> new IllegalArgumentException("No enum value provided for type: " + enumClass.getSimpleName()));
return findEnumByLabel(enumClass, value);
}

private Object findEnumByLabel(Class<?> enumClass, String label) {
for (Object enumConstant : enumClass.getEnumConstants()) {
if (enumConstant instanceof Wirespec.Enum &&
((Wirespec.Enum) enumConstant).getLabel().equals(label)) {
return enumConstant;
}
}
throw new IllegalArgumentException("Invalid enum value '" + label + "' for type: " + enumClass.getSimpleName());
}

private boolean isIterable(Type type) {
return type instanceof ParameterizedType &&
Iterable.class.isAssignableFrom((Class<?>) ((ParameterizedType) type).getRawType());
}

private boolean isWirespecEnum(Type type) {
if (type instanceof Class<?>) {
Class<?> clazz = (Class<?>) type;
for (Class<?> iface : clazz.getInterfaces()) {
if (iface == Wirespec.Enum.class) {
return true;
}
}
}
return false;
}

private Type getIterableElementType(Type type) {
if (type instanceof ParameterizedType) {
Type[] typeArguments = ((ParameterizedType) type).getActualTypeArguments();
if (typeArguments.length > 0) {
return typeArguments[0];
}
}
return null;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -261,7 +261,7 @@ open class JavaEmitter(
|${Spacer(3)}return new Wirespec.RawRequest(
|${Spacer(4)}request.method.name(),
|${Spacer(4)}java.util.List.of(${endpoint.path.joinToString { when (it) {is Endpoint.Segment.Literal -> """"${it.value}""""; is Endpoint.Segment.Param -> it.emitIdentifier() } }}),
|${Spacer(4)}${if (endpoint.queries.isNotEmpty()) "java.util.Map.ofEntries(${endpoint.queries.joinToString { it.emitSerialized("queries") }})" else "java.util.Collections.emptyMap()"},
|${Spacer(4)}${if (endpoint.queries.isNotEmpty()) "java.util.Map.ofEntries(" + endpoint.queries.joinToString(", ") { it.emitSerializedQuery() } + ")" else "java.util.Collections.emptyMap()"},
|${Spacer(4)}${if (endpoint.headers.isNotEmpty()) "java.util.Map.ofEntries(${endpoint.headers.joinToString { it.emitSerialized("headers") }})" else "java.util.Collections.emptyMap()"},
|${Spacer(4)}serialization.serialize(request.getBody(), Wirespec.getType(${content.emit()}.class, ${content?.reference?.isIterable ?: false}))
|${Spacer(3)});
Expand Down Expand Up @@ -307,7 +307,7 @@ open class JavaEmitter(

private fun Endpoint.Request.emitDeserializedParams(endpoint: Endpoint) = listOfNotNull(
endpoint.indexedPathParams.joinToString { it.emitDeserialized() }.orNull(),
endpoint.queries.joinToString { it.emitDeserialized("queries") }.orNull(),
endpoint.queries.joinToString { it.emitDeserializedQuery() }.orNull(),
endpoint.headers.joinToString { it.emitDeserialized("headers") }.orNull(),
content?.let { """${Spacer(4)}serialization.deserialize(request.body(), Wirespec.getType(${it.emit()}.class, ${it.reference.isIterable}))""" }
).joinToString(",\n").let { if (it.isBlank()) "" else "\n$it\n${Spacer(3)}" }
Expand All @@ -326,12 +326,18 @@ open class JavaEmitter(
private fun Field.emitSerialized(fields: String) =
"""java.util.Map.entry("${identifier.value}", serialization.serialize(request.$fields.${emit(identifier)}, Wirespec.getType(${reference.emitType()}.class, ${reference.isIterable})))"""

private fun Field.emitSerializedQuery() =
"""java.util.Map.entry("${emit(identifier)}", (request.queries.${emit(identifier)}${if(!isNullable) " != null" else ""}) ? serialization.serializeQuery(request.queries.${emit(identifier)}, Wirespec.getType(${reference.emitType()}.class, ${reference.isIterable})) : java.util.Collections.emptyList())"""

private fun IndexedValue<Endpoint.Segment.Param>.emitDeserialized() =
"""${Spacer(4)}serialization.<${value.reference.emit()}>deserialize(request.path().get(${index}), Wirespec.getType(${value.reference.emitType()}.class, ${value.reference.isIterable}))"""

private fun Field.emitDeserialized(fields: String) =
"""${Spacer(4)}java.util.Optional.ofNullable(request.$fields().get("${identifier.value}")).map(it -> serialization.<${reference.emit()}>deserialize(it, Wirespec.getType(${reference.emitType()}.class, ${reference.isIterable})))${if(!isNullable) ".get()" else ""}"""

private fun Field.emitDeserializedQuery() =
"""${Spacer(4)}java.util.Optional.ofNullable(request.queries().get("${identifier.value}")).map(it -> serialization.deserializeQuery(it, Wirespec.getType(${reference.emitType()}.class, ${reference.isIterable})))${if(!isNullable) ".get()" else ""}"""

private fun Field.emitSerializedHeader() =
"""java.util.Map.entry("${identifier.value}", serialization.serialize(r.getHeaders().${emit(identifier)}(), Wirespec.getType(${reference.emitType()}.class, ${reference.isIterable})))"""

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ data object JavaShared : Shared {
|
|import java.lang.reflect.Type;
|import java.lang.reflect.ParameterizedType;
|import java.util.List;
|import java.util.Map;
|
|public interface Wirespec {
|${Spacer}interface Enum { String getLabel(); }
Expand Down Expand Up @@ -42,10 +44,12 @@ data object JavaShared : Shared {
|${Spacer}interface Request<T> { Path getPath(); Method getMethod(); Queries getQueries(); Headers getHeaders(); T getBody(); interface Headers extends Wirespec.Headers {} }
|${Spacer}interface Response<T> { int getStatus(); Headers getHeaders(); T getBody(); interface Headers extends Wirespec.Headers {} }
|${Spacer}interface Serialization<RAW> extends Serializer<RAW>, Deserializer<RAW> {}
|${Spacer}interface Serializer<RAW> { <T> RAW serialize(T t, Type type); }
|${Spacer}interface Deserializer<RAW> { <T> T deserialize(RAW raw, Type type); }
|${Spacer}record RawRequest(String method, java.util.List<String> path, java.util.Map<String, String> queries, java.util.Map<String, String> headers, String body) {}
|${Spacer}record RawResponse(int statusCode, java.util.Map<String, String> headers, String body) {}
|${Spacer}interface QueryParamSerializer { <T> List<String> serializeQuery(T value, Type type); }
|${Spacer}interface Serializer<RAW> extends QueryParamSerializer { <T> RAW serialize(T t, Type type); }
|${Spacer}interface QueryParamDeserializer { <T> T deserializeQuery(List<String> values, Type type); }
|${Spacer}interface Deserializer<RAW> extends QueryParamDeserializer { <T> T deserialize(RAW raw, Type type); }
|${Spacer}record RawRequest(String method, List<String> path, Map<String, List<String>> queries, Map<String, String> headers, String body) {}
|${Spacer}record RawResponse(int statusCode, Map<String, String> headers, String body) {}
|${Spacer}static Type getType(final Class<?> type, final boolean isIterable) {
|${Spacer(2)}if(isIterable) {
|${Spacer(3)}return new ParameterizedType() {
Expand All @@ -59,4 +63,4 @@ data object JavaShared : Shared {
|}
|
""".trimMargin()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -237,7 +237,7 @@ class CompileFullEndpointTest {
| return new Wirespec.RawRequest(
| request.method.name(),
| java.util.List.of("todos", serialization.serialize(request.path.id, Wirespec.getType(String.class, false))),
| java.util.Map.ofEntries(java.util.Map.entry("done", serialization.serialize(request.queries.done, Wirespec.getType(Boolean.class, false)))),
| java.util.Map.ofEntries(java.util.Map.entry("done", (request.queries.done != null) ? serialization.serializeQuery(request.queries.done, Wirespec.getType(Boolean.class, false)) : java.util.Collections.emptyList())),
| java.util.Map.ofEntries(java.util.Map.entry("token", serialization.serialize(request.headers.token, Wirespec.getType(Token.class, false)))),
| serialization.serialize(request.getBody(), Wirespec.getType(PotentialTodoDto.class, false))
| );
Expand All @@ -246,7 +246,7 @@ class CompileFullEndpointTest {
| static Request fromRequest(Wirespec.Deserializer<String> serialization, Wirespec.RawRequest request) {
| return new Request(
| serialization.<String>deserialize(request.path().get(1), Wirespec.getType(String.class, false)),
| java.util.Optional.ofNullable(request.queries().get("done")).map(it -> serialization.<Boolean>deserialize(it, Wirespec.getType(Boolean.class, false))).get(),
| java.util.Optional.ofNullable(request.queries().get("done")).map(it -> serialization.deserializeQuery(it, Wirespec.getType(Boolean.class, false))).get(),
| java.util.Optional.ofNullable(request.headers().get("token")).map(it -> serialization.<Token>deserialize(it, Wirespec.getType(Token.class, false))).get(),
| serialization.deserialize(request.body(), Wirespec.getType(PotentialTodoDto.class, false))
| );
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,43 +14,92 @@ import org.springframework.context.annotation.Import
@Import(WirespecResponseBodyAdvice::class, WirespecWebMvcConfiguration::class)
open class WirespecSerializationConfiguration {

private val primitiveTypesConversion = mapOf<Class<*>, (String) -> Any>(
String::class.java to { this },
Int::class.javaObjectType to String::toInt,
Long::class.javaObjectType to String::toLong,
Double::class.javaObjectType to String::toDouble,
Float::class.javaObjectType to String::toFloat,
Boolean::class.javaObjectType to String::toBoolean,
Char::class.javaObjectType to String::single,
Byte::class.javaObjectType to String::toByte,
Short::class.javaObjectType to String::toShort
)

@Bean
open fun wirespecSerialization(objectMapper: ObjectMapper) = object : Wirespec.Serialization<String> {

private val wirespecObjectMapper = objectMapper.copy().registerModule(WirespecModuleJava())
private val stringListDelimiter = ","

override fun <T> serialize(body: T, type: Type?): String = when {
body is String -> body
isStringIterable(type) && body is Iterable<*> -> body.joinToString(stringListDelimiter)
else -> wirespecObjectMapper.writeValueAsString(body)
}

override fun <T> serializeQuery(value: T?, type: Type?): List<String>? = when {
value == null -> null
isIterable(type) -> (value as Iterable<*>).map { it.toString() }
else -> listOf(value.toString())
}

override fun <T : Any> deserialize(raw: String?, valueType: Type?): T? = raw?.let {
when {
valueType == String::class.java -> {
@Suppress("UNCHECKED_CAST")
raw as T
}
isStringIterable(valueType) -> {
@Suppress("UNCHECKED_CAST")
raw.split(stringListDelimiter) as T
}
else -> {
val type = wirespecObjectMapper.constructType(valueType)
wirespecObjectMapper.readValue(it, type)
}
}
}

private fun isStringIterable(type: Type?): Boolean {
if (type !is ParameterizedType) return false
@Suppress("UNCHECKED_CAST")
override fun <T> deserializeQuery(values: List<String>?, type: Type?): T? = when {
values.isNullOrEmpty() -> null
isIterable(type) -> deserializeList(values, getIterableElementType(type))
isWirespecEnum(type) -> deserializeEnum(values, type as Class<*>)
else -> deserializePrimitive(values, type as Class<*>)
} as T?

private fun deserializeList(values: List<String>, type: Type?): List<Any> = when {
isWirespecEnum(type) -> values.map { findEnumByLabel(type as Class<*>, it) }
else -> deserializePrimitiveList(values, type as Class<*>)
}

private fun deserializePrimitive(values: List<String>, clazz: Class<*>): Any {
val value = values.firstOrNull()
?: throw IllegalArgumentException("No value provided for type: ${clazz.simpleName}")
return primitiveTypesConversion[clazz]?.invoke(value)
?: throw IllegalArgumentException("Unsupported primitive type: ${clazz.simpleName}")
}

private fun deserializePrimitiveList(values: List<String>, clazz: Class<*>): List<Any> {
val converter = primitiveTypesConversion[clazz]
?: throw IllegalArgumentException("Unsupported list element type: ${clazz.simpleName}")
return values.map(converter)
}

private fun deserializeEnum(values: List<String>, enumClass: Class<*>): Any {
val value = values.firstOrNull()
?: throw IllegalArgumentException("No enum value provided for type: ${enumClass.simpleName}")
return findEnumByLabel(enumClass, value)
}

private fun findEnumByLabel(enumClass: Class<*>, label: String): Any =
enumClass.enumConstants.firstOrNull {
(it as Wirespec.Enum).label == label
} ?: throw IllegalArgumentException("Invalid enum value '$label' for type: ${enumClass.simpleName}")

val rawType = type.rawType as Class<*>
if (!Iterable::class.java.isAssignableFrom(rawType)) return false
private fun isIterable(type: Type?): Boolean =
type is ParameterizedType && Iterable::class.java.isAssignableFrom(type.rawType as Class<*>)

val typeArgument = type.actualTypeArguments.firstOrNull()
return typeArgument == String::class.java
private fun isWirespecEnum(type: Type?): Boolean = when (type) {
is Class<*> -> type.interfaces.contains(Wirespec.Enum::class.java)
else -> false
}

private fun getIterableElementType(type: Type?): Type? =
(type as? ParameterizedType)?.actualTypeArguments?.firstOrNull()
}
}
Loading

0 comments on commit 67c3611

Please sign in to comment.