Skip to content

Instantly share code, notes, and snippets.

@aoudiamoncef
Last active February 15, 2024 20:47
Show Gist options
  • Save aoudiamoncef/87e33e2e21592d8cebc78ffa17c87438 to your computer and use it in GitHub Desktop.
Save aoudiamoncef/87e33e2e21592d8cebc78ffa17c87438 to your computer and use it in GitHub Desktop.
Reactor "expand" and "expandDeep" in action

When exploring directory structures in a reactive programming environment, the strategies of breadth-first and depth-first traversal play a crucial role. In the context of the ReactorFileUtils class, these strategies are employed by the expand and expandDeep methods to traverse and emit paths in a reactive stream. Let's delve into these strategies and understand how they work:

Breadth-First (expand Method):

Breadth-First (expand)

       A
     /   \
    B     C
   / \   / \
  D   E F   G
  • Starting Point:

    • It begins with the root directory path.
  • Exploration:

    • Checks if the path is a directory.
    • Lists immediate child paths and emits them.
    • Continues to the next level by exploring immediate children.
    • Repeats this process level by level.
  • Example:

    • For a directory structure A -> B, C -> D, E, exploration order is A, B, C, D, E.
  • Advantages:

    • Ensures processing of the most immediate neighbors first.

Depth-First (expandDeep Method):

Depth-First (expandDeep)

       A
     /   \
    B     C
   / \   / \
  D   E F   G
  • Starting Point:

    • Starts with the root directory path.
  • Exploration:

    • Checks if the path is a directory.
    • Lists immediate child paths and emits them.
    • Recursively explores each child path, going as deep as possible.
    • Backtracks only after reaching the deepest point in a branch.
  • Example:

    • For a directory structure A -> B, C -> D, E, exploration order might be A, B, D, E, C.
  • Advantages:

    • Useful for fully exploring specific branches before moving on.

Summary:

  • Breadth-First (expand):

    • Explores level by level.
    • Processes immediate neighbors first.
  • Depth-First (expandDeep):

    • Explores as deep as possible along each branch.
    • Backtracks only after reaching the deepest point in a branch.

The choice between breadth-first and depth-first depends on the specific requirements of your application. Breadth-first ensures immediate neighbors are processed first, while depth-first is suitable for fully exploring specific branches.

References

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://maven.apache.org/POM/4.0.0"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.maoudia.lib</groupId>
<artifactId>app</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>maoudia-lib</name>
<description>MAOUDIA LIB</description>
<properties>
<java.version>21</java.version>
</properties>
<dependencies>
<dependency>
<dependency>
<groupId>io.projectreactor</groupId>
<artifactId>reactor-core</artifactId>
<version>3.6.2</version>
</dependency>
<groupId>io.projectreactor</groupId>
<artifactId>reactor-test</artifactId>
<version>3.6.2</version>
<scope>test</scope>
</dependency>
</dependencies>
</project>
package com.maoudia;
import jakarta.validation.constraints.NotNull;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import reactor.util.annotation.NonNull;
import java.io.File;
import java.nio.file.Files;
import java.nio.file.Path;
/**
* Utility class for working with reactive file operations using Reactor.
*/
public class ReactorFileUtils {
/**
* Recursively expands the given directory path and emits each encountered {@link Path} in a reactive stream.
*
* @param path The root path to explore.
* @return A reactive stream of {@link Path} elements found during exploration.
*/
@NotNull
public static Flux<Path> expand(@NonNull Path path) {
return Mono.justOrEmpty(path)
.filter(Files::isDirectory)
.expand(ReactorFileUtils::listPaths);
}
/**
* Recursively expands the given directory path and emits each encountered {@link Path} in a reactive stream.
* Performs a deep exploration of the directory.
*
* @param path The root path to explore.
* @return A reactive stream of {@link Path} elements found during deep exploration.
*/
@NotNull
public static Flux<Path> expandDeep(@NonNull Path path) {
return Mono.justOrEmpty(path)
.filter(Files::isDirectory)
.expandDeep(ReactorFileUtils::listPaths);
}
/**
* Lists the paths within the given directory and emits them in a reactive stream.
*
* @param path The directory for which to list paths.
* @return A reactive stream of {@link Path} elements representing the paths in the directory.
*/
@NotNull
public static Flux<Path> listPaths(@NonNull Path path) {
return Mono.justOrEmpty(path)
.filter(Files::isDirectory)
.mapNotNull(directory -> directory.toFile().listFiles())
.flatMapMany(Flux::fromArray)
.map(File::toPath);
}
}
package com.maoudia;
import com.maoudia.ReactorFileUtils;
import org.junit.jupiter.api.Test;
import reactor.core.publisher.Flux;
import reactor.test.StepVerifier;
import java.nio.file.Path;
import java.nio.file.Paths;
class ReactorFileUtilsTest {
private static final Path ROOT_DIRECTORY = Paths.get("src/test/resources");
@Test
void expand_ValidPath_ReturnsExpectedPaths() {
// Act
Flux<Path> pathFlux = ReactorFileUtils.expand(ROOT_DIRECTORY);
// Assert
StepVerifier.create(pathFlux)
.expectNextMatches(path -> path.endsWith("resources"))
.expectNextMatches(path -> path.endsWith("file2.txt"))
.expectNextMatches(path -> path.endsWith("file1.txt"))
.expectNextMatches(path -> path.endsWith("subdirectory"))
.expectNextMatches(path -> path.endsWith("emptydirectory"))
.expectNextMatches(path -> path.endsWith("subfile1.txt"))
.expectNextMatches(path -> path.endsWith("subfile2.txt"))
.expectNextMatches(path -> path.endsWith("subsubdirectory"))
.expectNextMatches(path -> path.endsWith("subsubfile1.txt"))
.expectNextMatches(path -> path.endsWith("subsubfile2.txt"))
.verifyComplete();
}
@Test
void expand_NullPath_ReturnsEmptyFlux() {
// Act
Flux<Path> pathFlux = ReactorFileUtils.expand(null);
// Assert
StepVerifier.create(pathFlux)
.verifyComplete();
}
@Test
void expandDeep_ValidPath_ReturnsExpectedPaths() {
// Act
Flux<Path> pathFlux = ReactorFileUtils.expandDeep(ROOT_DIRECTORY);
// Assert
StepVerifier.create(pathFlux)
.expectNextMatches(path -> path.endsWith("resources"))
.expectNextMatches(path -> path.endsWith("file2.txt"))
.expectNextMatches(path -> path.endsWith("file1.txt"))
.expectNextMatches(path -> path.endsWith("subdirectory"))
.expectNextMatches(path -> path.endsWith("subfile1.txt"))
.expectNextMatches(path -> path.endsWith("subfile2.txt"))
.expectNextMatches(path -> path.endsWith("subsubdirectory"))
.expectNextMatches(path -> path.endsWith("subsubfile1.txt"))
.expectNextMatches(path -> path.endsWith("subsubfile2.txt"))
.expectNextMatches(path -> path.endsWith("emptydirectory"))
.verifyComplete();
}
@Test
void expandDeep_NullPath_ReturnsEmptyFlux() {
// Act
Flux<Path> pathFlux = ReactorFileUtils.expand(null);
// Assert
StepVerifier.create(pathFlux)
.verifyComplete();
}
@Test
void listFiles_ValidDirectory_ReturnsExpectedPaths() {
// Act
Flux<Path> pathFlux = ReactorFileUtils.listPaths(ROOT_DIRECTORY);
// Assert
StepVerifier.create(pathFlux)
.expectNextMatches(path -> path.endsWith("file2.txt"))
.expectNextMatches(path -> path.endsWith("file1.txt"))
.expectNextMatches(path -> path.endsWith("subdirectory"))
.expectNextMatches(path -> path.endsWith("emptydirectory"))
.verifyComplete();
}
@Test
void listFiles_NullPath_ReturnsEmptyFlux() {
// Act
Flux<Path> pathFlux = ReactorFileUtils.listPaths(null);
// Assert
StepVerifier.create(pathFlux)
.verifyComplete();
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment