diff --git a/lib/src/main/java/io/cloudquery/helper/ReflectionPathResolver.java b/lib/src/main/java/io/cloudquery/helper/ReflectionPathResolver.java new file mode 100644 index 0000000..6ad605b --- /dev/null +++ b/lib/src/main/java/io/cloudquery/helper/ReflectionPathResolver.java @@ -0,0 +1,57 @@ +package io.cloudquery.helper; + +import java.lang.reflect.Field; + +/** + * A simple reflection-based path resolver. + */ +public class ReflectionPathResolver { + + public static class PathResolverException extends Exception { + public PathResolverException(String message, Throwable ex) { + super(message, ex); + } + } + + /** + * Resolve a path of an object using reflection. + *

+ * e.g. if we have a class: + *

+     *      class TestClass {
+     *          private String name;
+     *          private TestClass child;
+     *      }
+     * 
+ *

+ * Then the following are valid paths to retrieve the associated values: + *

+ * `name` + * `child.name` + *

+ * NOTE: this implementation is currently very simplistic and only supports simple field and nested field resolution. + * It does not support collection resolution - unlike the Go SDK which uses go-funk. + * + * @param object The object to resolve the path on + * @param path The path to resolve + * @return The value of the property at the resolved path + * @throws PathResolverException If the path cannot be resolved + */ + public static Object resolve(Object object, String path) throws PathResolverException { + Object current = object; + + for (String currentPath : path.split("\\.")) { + try { + Field currentField = object.getClass().getDeclaredField(currentPath); + if (!currentField.canAccess(current)) { + currentField.setAccessible(true); + } + object = currentField.get(object); + } catch (NoSuchFieldException | IllegalAccessException e) { + throw new PathResolverException("Unable to resolve path " + currentPath, e); + } + } + + return object; + } +} diff --git a/lib/src/test/java/io/cloudquery/helper/ReflectionPathResolverTest.java b/lib/src/test/java/io/cloudquery/helper/ReflectionPathResolverTest.java new file mode 100644 index 0000000..9c41208 --- /dev/null +++ b/lib/src/test/java/io/cloudquery/helper/ReflectionPathResolverTest.java @@ -0,0 +1,49 @@ +package io.cloudquery.helper; + +import lombok.Builder; +import org.junit.jupiter.api.Test; + +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +class ReflectionPathResolverTest { + @Builder + private static class TestClass { + private String name; + + @Builder.Default + private List numbers = List.of(1, 2, 3); + + private TestClass singleChild; + + private List multipleChildren; + } + + private static final TestClass TEST_DATA = TestClass.builder().name("root"). + singleChild(TestClass.builder().name("single-child1").build()). + multipleChildren( + List.of( + TestClass.builder().name("multi-child1").build(), + TestClass.builder().name("multi-child2").build() + ) + ). + build(); + + @Test + public void shouldResolveSimpleFields() throws ReflectionPathResolver.PathResolverException { + assertEquals("root", ReflectionPathResolver.resolve(TEST_DATA, "name")); + assertEquals(List.of(1, 2, 3), ReflectionPathResolver.resolve(TEST_DATA, "numbers")); + } + + @Test + public void shouldResolveNestedField() throws ReflectionPathResolver.PathResolverException { + assertEquals("single-child1", ReflectionPathResolver.resolve(TEST_DATA, "singleChild.name")); + } + + @Test + public void shouldThrowAnErrorIfWeEncounterACollection() { + assertThrows(ReflectionPathResolver.PathResolverException.class, () -> ReflectionPathResolver.resolve(TEST_DATA, "multiplChildren.name")); + } +}