-
Notifications
You must be signed in to change notification settings - Fork 8
[Router] Thrift friendly router for vertx #112
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 5 commits
ca818de
b5a4fdb
b3230f6
286d0fa
61e26bb
8bba81e
6a3e713
b62d877
e28ec33
2e7c6ed
5b5c145
e27b9c8
39b9259
12683c9
65846d7
feed133
e2acf6d
c593729
91d6d9e
28d638e
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,178 @@ | ||
| package ai.chronon.router; | ||
|
|
||
| import io.vertx.core.Handler; | ||
| import io.vertx.ext.web.RoutingContext; | ||
| import io.vertx.core.json.JsonObject; | ||
|
|
||
| import java.lang.reflect.Method; | ||
| import java.lang.reflect.Type; | ||
| import java.lang.reflect.ParameterizedType; | ||
| import java.io.StringWriter; | ||
| import java.io.PrintWriter; | ||
| import java.util.stream.Collectors; | ||
| import java.util.Arrays; | ||
| import java.util.HashMap; | ||
| import java.util.List; | ||
| import java.util.Map; | ||
| import java.util.function.Function; | ||
|
|
||
| public class RouteHandlerWrapper { | ||
|
|
||
| /** | ||
| * Creates a RoutingContext handler that maps parameters to an Input object and transforms it to Output | ||
| * | ||
| * @param transformer Function to convert from Input to Output | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. one thing to maybe call out explicitly is the transformer is responsible for wrapping the work in a vert.x future if its time consuming. Else we'll quickly exhaust event threads |
||
| * @param inputClass Class object for the Input type | ||
| * @param <I> Input type with setter methods | ||
| * @param <O> Output type | ||
| * @return Handler for RoutingContext that produces Output | ||
| */ | ||
| public static <I, O> Handler<RoutingContext> createHandler( | ||
| Function<I, O> transformer, | ||
| Class<I> inputClass) { | ||
|
|
||
| return ctx -> { | ||
| try { | ||
| // Create parameter map | ||
| Map<String, String> params = new HashMap<>(); | ||
|
|
||
| // Add path parameters | ||
| params.putAll(ctx.pathParams()); | ||
|
|
||
| // Add query parameters | ||
| for (Map.Entry<String, String> entry : ctx.queryParams().entries()) { | ||
| params.put(entry.getKey(), entry.getValue()); | ||
| } | ||
|
||
|
|
||
| I input = createInputFromParams(params, inputClass); | ||
| O output = transformer.apply(input); | ||
|
|
||
| // Send response | ||
| ctx.response() | ||
| .putHeader("content-type", "application/json") | ||
| .end(JsonObject.mapFrom(output).encode()); | ||
|
|
||
| } catch (Exception e) { | ||
| e.printStackTrace(); | ||
|
||
|
|
||
| StringWriter sw = new StringWriter(); | ||
| PrintWriter pw = new PrintWriter(sw); | ||
| e.printStackTrace(pw); | ||
| String stackTrace = sw.toString(); | ||
|
|
||
| ctx.response() | ||
| .setStatusCode(400) | ||
| .putHeader("content-type", "application/json") | ||
| .end(new JsonObject() | ||
| .put("error", stackTrace) | ||
| .encode()); | ||
| } | ||
| }; | ||
| } | ||
|
|
||
| private static <I> I createInputFromParams(Map<String, String> params, Class<I> inputClass) | ||
| throws Exception { | ||
| // Create new instance using no-args constructor | ||
| I input = inputClass.getDeclaredConstructor().newInstance(); | ||
|
|
||
| // Get all methods | ||
| Method[] methods = inputClass.getMethods(); | ||
|
|
||
| // Find and invoke setters for matching parameters | ||
| for (Method method : methods) { | ||
| if (isSetter(method)) { | ||
| String fieldName = getFieldNameFromSetter(method); | ||
| String paramValue = params.get(fieldName); | ||
|
|
||
| if (paramValue != null) { | ||
| Type paramType = method.getGenericParameterTypes()[0]; | ||
| Object convertedValue = convertValue(paramValue, paramType); | ||
| method.invoke(input, convertedValue); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| return input; | ||
| } | ||
|
|
||
| private static boolean isSetter(Method method) { | ||
| return method.getName().startsWith("set") && | ||
| !method.getName().endsWith("IsSet") && | ||
| method.getParameterCount() == 1 && | ||
| (method.getReturnType() == void.class || method.getReturnType() == method.getDeclaringClass()); | ||
| } | ||
|
|
||
| private static String getFieldNameFromSetter(Method method) { | ||
| String methodName = method.getName(); | ||
| String fieldName = methodName.substring(3); // Remove "set" | ||
| return fieldName.substring(0, 1).toLowerCase() + fieldName.substring(1); | ||
| } | ||
|
|
||
| private static Object convertValue(String value, Type targetType) { // Changed parameter to Type | ||
| // Handle Class types | ||
| if (targetType instanceof Class<?>) { | ||
| Class<?> targetClass = (Class<?>) targetType; | ||
|
|
||
| if (targetClass == String.class) { | ||
| return value; | ||
| } else if (targetClass == Integer.class || targetClass == int.class) { | ||
| return Integer.parseInt(value); | ||
| } else if (targetClass == Long.class || targetClass == long.class) { | ||
| return Long.parseLong(value); | ||
| } else if (targetClass == Double.class || targetClass == double.class) { | ||
| return Double.parseDouble(value); | ||
| } else if (targetClass == Boolean.class || targetClass == boolean.class) { | ||
| return Boolean.parseBoolean(value); | ||
| } else if (targetClass == Float.class || targetClass == float.class) { | ||
| return Float.parseFloat(value); | ||
| } else if (targetClass.isEnum()) { | ||
| try { | ||
| // Try custom fromString method first | ||
| Method fromString = targetClass.getMethod("fromString", String.class); | ||
| Object result = fromString.invoke(null, value); | ||
| if (value != null && result == null) { | ||
| throw new IllegalArgumentException(String.format( | ||
| "Invalid enum value %s for type %s", value, targetClass.getSimpleName())); | ||
| } | ||
| return result; | ||
| } catch (NoSuchMethodException e) { | ||
nikhil-zlai marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| // Fall back to standard enum valueOf | ||
| return Enum.valueOf(targetClass.asSubclass(Enum.class), value.toUpperCase()); | ||
| } catch (Exception e) { | ||
| throw new IllegalArgumentException(String.format( | ||
| "Error converting %s to enum type %s : %s", | ||
| value, targetClass.getSimpleName(), e.getMessage())); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| // Handle parameterized types (List, Map) | ||
| if (targetType instanceof ParameterizedType) { | ||
| ParameterizedType parameterizedType = (ParameterizedType) targetType; | ||
| Class<?> rawType = (Class<?>) parameterizedType.getRawType(); | ||
|
|
||
| // Handle List types | ||
| if (List.class.isAssignableFrom(rawType)) { | ||
| Type elementType = parameterizedType.getActualTypeArguments()[0]; | ||
| return Arrays.stream(value.split(",")) | ||
| .map(v -> convertValue(v.trim(), elementType)) | ||
| .collect(Collectors.toList()); | ||
| } | ||
|
|
||
| // Handle Map types | ||
| if (Map.class.isAssignableFrom(rawType)) { | ||
| Type keyType = parameterizedType.getActualTypeArguments()[0]; | ||
| Type valueType = parameterizedType.getActualTypeArguments()[1]; | ||
| return Arrays.stream(value.split(",")) | ||
| .map(entry -> entry.split(":")) | ||
| .collect(Collectors.toMap( | ||
| kv -> convertValue(kv[0].trim(), keyType), | ||
| kv -> convertValue(kv[1].trim(), valueType) | ||
| )); | ||
| } | ||
| } | ||
nikhil-zlai marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| throw new IllegalArgumentException("Unsupported type: " + targetType.getTypeName()); | ||
| } | ||
| } | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,16 @@ | ||
| package ai.chronon.router.test; | ||
|
|
||
| public enum OrderStatus { | ||
| PENDING, | ||
| PROCESSING, | ||
| COMPLETED, | ||
| CANCELLED; | ||
|
|
||
| public static OrderStatus fromString(String value) { | ||
| try { | ||
| return valueOf(value.toUpperCase()); | ||
| } catch (Exception e) { | ||
| return null; | ||
| } | ||
| } | ||
nikhil-zlai marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I pulled out the vertx deps into a collection in my PR to migrate play to vertx, so post rebase you could pull that collection in
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
done