diff --git a/README.md b/README.md index 3c6a3ee..cc24a48 100644 --- a/README.md +++ b/README.md @@ -75,6 +75,35 @@ Map result = new DotPathQL().obfuscate(userObject, List.of( )); ``` +### Pipeline Usage + +The pipeline feature allows you to chain multiple operations using a fluent API. Supports combining `exclude` and `obfuscate` operations: + +```java +DotPathQL dotPathQL = new DotPathQL(); + +// Chain exclude and obfuscate operations in a single pipeline +Map result = dotPathQL.pipeline(userObject) + .exclude(List.of("additionalInfo.lastLogin")) + .obfuscate(List.of("address.zipCode", "phoneNumber")) + .execute(); + +// Using default paths for exclude and obfuscate +dotPathQL.addDefaultExcludePaths(List.of("password")); +dotPathQL.addDefaultObfuscatePaths(List.of("ssn", "creditCard.number")); + +Map result = dotPathQL.pipeline(userObject) + .exclude() // Uses default exclude paths + .obfuscate() // Uses default obfuscate paths + .execute(); +``` + +#### Benefits of Pipeline API +- **Fluent Interface**: More readable and intuitive method chaining +- **Single Execution**: Apply multiple transformations in one operation +- **Performance**: Avoids multiple object traversals +- **Consistency**: Predictable processing order for combined operations + ## Supported Data Structures - Simple Properties (primitive and object types) diff --git a/src/main/java/ca/trackerforce/DotPathQL.java b/src/main/java/ca/trackerforce/DotPathQL.java index ea74382..12db6a1 100644 --- a/src/main/java/ca/trackerforce/DotPathQL.java +++ b/src/main/java/ca/trackerforce/DotPathQL.java @@ -135,4 +135,15 @@ public String toJson(Map sourceMap, boolean prettier) { return pathPrinter.toJson(sourceMap, prettier); } + /** + * Creates a pipeline for the given source object that allows chaining multiple operations. + * + * @param the type of the source object + * @param source the source object to process + * @return a Pipeline instance for method chaining + */ + public Pipeline pipeline(T source) { + return new Pipeline<>(source, pathExclude, pathObfuscate); + } + } diff --git a/src/main/java/ca/trackerforce/Pipeline.java b/src/main/java/ca/trackerforce/Pipeline.java new file mode 100644 index 0000000..050d76b --- /dev/null +++ b/src/main/java/ca/trackerforce/Pipeline.java @@ -0,0 +1,92 @@ +package ca.trackerforce; + +import ca.trackerforce.path.api.DotPath; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +/** + * Pipeline for chaining multiple operations on an object using a fluent API. + * + * @author petruki + * @since 2025-09-24 + */ +public class Pipeline { + + private final T source; + private final DotPath pathExclude; + private final DotPath pathObfuscate; + private final List excludePaths; + private final List obfuscatePaths; + + /** + * Creates a new Pipeline instance for the given source object. + * + * @param source the source object to be processed by the pipeline operations + * @param pathExclude the DotPath instance for exclude operations from DotPathQL + * @param pathObfuscate the DotPath instance for obfuscate operations from DotPathQL + */ + public Pipeline(T source, DotPath pathExclude, DotPath pathObfuscate) { + this.source = source; + this.pathExclude = pathExclude; + this.pathObfuscate = pathObfuscate; + this.excludePaths = new ArrayList<>(); + this.obfuscatePaths = new ArrayList<>(); + } + + /** + * Adds paths to be excluded from the final result. + * + * @param paths the list of paths to exclude + * @return this Pipeline instance for method chaining + */ + public Pipeline exclude(List paths) { + this.excludePaths.addAll(paths); + return this; + } + + /** + * Triggers exclusion with no paths. Used with default exclude paths if any. + * + * @return this Pipeline instance for method chaining + */ + public Pipeline exclude() { + return exclude(List.of()); + } + + /** + * Adds paths to be obfuscated in the final result. + * + * @param paths the list of paths to obfuscate + * @return this Pipeline instance for method chaining + */ + public Pipeline obfuscate(List paths) { + this.obfuscatePaths.addAll(paths); + return this; + } + + /** + * Triggers obfuscation with no paths. Used with default obfuscate paths if any. + * + * @return this Pipeline instance for method chaining + */ + public Pipeline obfuscate() { + return obfuscate(List.of()); + } + + /** + * Executes the pipeline operations and returns the final result. + * + * @return a map containing the processed object + */ + public Map execute() { + Map result = pathExclude.run(source, excludePaths); + + if (pathObfuscate.hasDefaultPaths() || !obfuscatePaths.isEmpty()) { + return pathObfuscate.run(result, obfuscatePaths); + } + + return result; + } +} diff --git a/src/main/java/ca/trackerforce/path/PathCommon.java b/src/main/java/ca/trackerforce/path/PathCommon.java index 2753159..1b3eb1f 100644 --- a/src/main/java/ca/trackerforce/path/PathCommon.java +++ b/src/main/java/ca/trackerforce/path/PathCommon.java @@ -44,6 +44,11 @@ public void addDefaultPaths(List paths) { defaultPaths.addAll(paths); } + @Override + public boolean hasDefaultPaths() { + return !defaultPaths.isEmpty(); + } + /** * Executes the path processing logic for the given source object. * diff --git a/src/main/java/ca/trackerforce/path/api/DotPath.java b/src/main/java/ca/trackerforce/path/api/DotPath.java index c959fcf..01cf012 100644 --- a/src/main/java/ca/trackerforce/path/api/DotPath.java +++ b/src/main/java/ca/trackerforce/path/api/DotPath.java @@ -25,4 +25,11 @@ public interface DotPath { * @param paths the list of paths to add as default paths */ void addDefaultPaths(List paths); + + /** + * Checks if there are any default paths set. + * + * @return true if there are default paths, false otherwise + */ + boolean hasDefaultPaths(); } diff --git a/src/test/java/ca/trackerforce/PipelineTypeClassRecordTest.java b/src/test/java/ca/trackerforce/PipelineTypeClassRecordTest.java new file mode 100644 index 0000000..1786948 --- /dev/null +++ b/src/test/java/ca/trackerforce/PipelineTypeClassRecordTest.java @@ -0,0 +1,103 @@ +package ca.trackerforce; + +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +import java.util.List; +import java.util.stream.Stream; + +import static org.junit.jupiter.api.Assertions.*; + +class PipelineTypeClassRecordTest { + + DotPathQL dotPathQL = new DotPathQL(); + + static Stream userDetailProvider() { + return Stream.of( + Arguments.of("Record type", ca.trackerforce.fixture.record.UserDetail.of()), + Arguments.of("Class type", ca.trackerforce.fixture.clazz.UserDetail.of()) + ); + } + + @ParameterizedTest(name = "{0}") + @MethodSource("userDetailProvider") + void shouldPipeObfuscateFields(String implementation, Object userDetail) { + // Given + var result = dotPathQL.pipeline(userDetail) + .obfuscate(List.of("address.zipCode")) + .execute(); + + // Then + var address = DotUtils.mapFrom(result, "address"); + assertNotNull(address); + assertEquals("****", address.get("zipCode")); + assertTrue(address.containsKey("street")); + assertTrue(address.containsKey("city")); + } + + @ParameterizedTest(name = "{0}") + @MethodSource("userDetailProvider") + void shouldPipeExcludeFields(String implementation, Object userDetail) { + // Given + var result = dotPathQL.pipeline(userDetail) + .exclude(List.of("additionalInfo.lastLogin")) + .execute(); + + // Then + var addInfo = DotUtils.mapFrom(result, "additionalInfo"); + assertNotNull(addInfo); + assertFalse(addInfo.containsKey("lastLogin")); // Excluded + assertTrue(addInfo.containsKey("subscriptionStatus")); + assertTrue(addInfo.containsKey("preferredLanguage")); + } + + @ParameterizedTest(name = "{0}") + @MethodSource("userDetailProvider") + void shouldPipeObfuscateAndExcludeFields(String implementation, Object userDetail) { + // Given + var result = dotPathQL.pipeline(userDetail) + .exclude(List.of("additionalInfo.lastLogin")) + .obfuscate(List.of("address.zipCode")) + .execute(); + + // Then + var address = DotUtils.mapFrom(result, "address"); + assertNotNull(address); + assertEquals("****", address.get("zipCode")); + assertTrue(address.containsKey("street")); + assertTrue(address.containsKey("city")); + + var addInfo = DotUtils.mapFrom(result, "additionalInfo"); + assertNotNull(addInfo); + assertFalse(addInfo.containsKey("lastLogin")); // Excluded + assertTrue(addInfo.containsKey("subscriptionStatus")); + assertTrue(addInfo.containsKey("preferredLanguage")); + } + + @ParameterizedTest(name = "{0}") + @MethodSource("userDetailProvider") + void shouldPipeObfuscateAndExcludeFieldsUsingDefaultPaths(String implementation, Object userDetail) { + // Given + dotPathQL.addDefaultExcludePaths(List.of("additionalInfo.lastLogin")); + dotPathQL.addDefaultObfuscatePaths(List.of("address.zipCode")); + + var result = dotPathQL.pipeline(userDetail) + .exclude() + .obfuscate() + .execute(); + + // Then + var address = DotUtils.mapFrom(result, "address"); + assertNotNull(address); + assertEquals("****", address.get("zipCode")); + assertTrue(address.containsKey("street")); + assertTrue(address.containsKey("city")); + + var addInfo = DotUtils.mapFrom(result, "additionalInfo"); + assertNotNull(addInfo); + assertFalse(addInfo.containsKey("lastLogin")); // Excluded + assertTrue(addInfo.containsKey("subscriptionStatus")); + assertTrue(addInfo.containsKey("preferredLanguage")); + } +}