BuildOn - Step #2:   DirNav Pkg

 

BuildOn Project - DirNav Package

Figure 1. TextFinder Packages
Figure 2. TextFinder Structs
DirNav is a package in the first BuildOn project, TextFinder. Figure 1 is a package diagram for Textfinder, discussed in Step #0.
TextFinder Specification

TextFinder Specification:

  1. Identify all files in a directory subtree that match a pattern and contain a specified text.
  2. Specify root path, one or more file patterns (.h, .cpp, .cs, .rs, ...), and search text on command line.
  3. Specify options /s [true|false], /v [true|false], /H [true|false] /h [true|false] for recursive directory walk, verbose output header, Hidden dirs with no match, and help message, respectively.
  4. Display file name and path, without duplication of path name, e.g., organized by directory, for files containing the search text.
  5. Interesting extensions:
    • Replace text by regular expressions for both search text and file patterns.
    • Replace sequential file searches with parallel searches to improve performance and useability.
TextSearch seeks files with specified text, using services of DirNav, and if found sends the path and file name to Display. The Display package turns the sequence of successful search completion events into useful user information.
 

Step #2 - Build DirNav Package

In this step we will create the DirNav package and integrate it with a mock test application. It:
  1. Implements a struct DirNav that is responsible for walking the directory tree.
  2. DirNav accepts a path and set of file patterns, e.g., ".rs,.h,.cpp...".
  3. Searches the directory tree rooted at the specified path, looking for files matching specified pattern(s): Pseudo Code: fn visit(&mut self, path: &Path) { app.do_dir(path); find all files matching pattern app.do_file(filename); find all subdirs dir_store.push(subdir); for each subdir in store visit(subdir); }
  4. Search is conducted with a recursive Depth First Search (DFS) policy, as shown above.
  5. Each time a directory is entered it invokes DirEvent::do_dir(...).
  6. Each time a file matching the pattern(s) is encountered it invokes DirEvent::do_file(...).

Notes:

  1. DirEvent is a Rust trait, similar to a C# constraint specifying an interface: pub trait DirEvent { fn new() -> Self // factory function fn do_dir(&mut self, dn: &Path); // dir event handler fn do_file(&mut self, fn: &Path); // file event handler } DirNav is parameterised with a generic type that is constrained to implement the DirEvent trait:
    struct DirNav<T> where T:DirEvent { ... }
    For this BuildOn project, DirNav will be parameterized on a mock of TextSearcher's Finder type which implements the DirEvent trait, as shown in Figure 2.
  2. Parameterization of DirNav<T> with T simply makes T available to DirNav, but doesn't specify what that type is. It instead specifies conditions that T must satisfy with constraints, as shown in the previous item.
  3. Any struct may implement the DirEvent trait, applying its application specific code to process directories and files. So DirNav can carry out its function without ever needing to know the application's details. It just needs to get access to an instance of the implementing struct, which it can do using the DirEvent::new() factory function.
  4. For this Step #2 you don't need to use TextSearch's Finder type for T. You can use a simple mock type that pretends to do what Finder does. Of course, you can use the Finder you built in Step #1 if you wish.
  5. This structure implements the Dependency Inversion Principle. e.g., DirNav doesn't depend on Finder. Instead, both DirNav and Finder depend on the invariant DirEvent.
  6. Using the DirEvent trait allows DirNav to be reusable. We can use DirNav<T> without change for any application T that implements DirEvent.
  7. For this Step, you need to use PathBuf and &Path which you used in Step #1 - TS. Rust defines six string types, three owning types String, OsString, and PathBuf, and three literal types, str, OsStr, and Path:
    String typeDescriptionReference
    String A collection of utf-8 characters, stored in the heap and managed with a control block on the stack. std::String
    &str A collection of utf-8 characters in a contiguous block on the stack, sometimes in static memory too. std::str
    OsString Same structure as std::String, but holding characters from the platform string type, used most often to interoperate with C code through Rust's foreign function interface (ffi). std::ffi::OsString
    OsStr A literal string with the same character encoding as OsString. std::ffi::OsStr
    PathBuf Same structure as std::String, but holding characters from the platform string type, and providing methods for working with paths, e.g., extracting names, extensions, doing joins, etc. std::path::PathBuf
    &Path A literal string with the same character encoding as PathBuf. std::path::Path
    You will need to use PathBuf and &Path to work with the Rust file system.
 

Starter Code

This section contains a code example showing how to:
  1. Define a generic struct DirNav<T> where T is constrained to implement methods of the DirEvent trait, similar to an interface, as well as other methods it needs.
  2. Use the Dependency Inversion Principle to allow DirNav to avoid depending on TextSearch's Finder implementation details:
    • Constrain generic type parameter T with trait DirEvent.
    • Create an instance, using the DirEvent factory function, of the generic parameter for use in DirNav's methods.
    For demonstrations of the Dependency Inversion Principle, see Basic DIP and Generic DIP.
  3. Build a library that contains the struct and test its methods.
  4. Link the static library to a test console application and run that.
  5. That is done for a project structure very similar to what you used for TextFinder - Step #1
 
Starter Code Example This is a test mock for Step #2's project structure. It gives you a good start for building Step #2 so you can focus on learning the Rust programming language. You will need to provide directory navigation code, using facilities of the std::fs file system facilities. This demonstration focuses on its Dependency Inversion structure.
DirNav Library Source

///////////////////////////////////////////////// // Step#2::DirNav::lib.rs // // - demos mock DirNav operations // // // // Jim Fawcett, 22 Jan 2021 // ///////////////////////////////////////////////// use std::path::{Path}; /*----------------------------------------------- Rust traits are similar to interfaces. Trait DirEvent requires implementer to be constructible and have members to handle DirNav events. Constructability allows DirNav to create an instance for a type defined by a generic parameter. */ pub trait DirEvent { /*-- constructible --*/ fn new() -> Self; /*-- event handlers --*/ fn do_dir(&mut self, dir:&Path); fn do_file(&mut self, file: &Path); } /*----------------------------------------------- DirNav searches directory tree looking for files with specified patterns (extensions). App is a DirNav member that implements application specific processing of a DirEvent sent by DirNav::visit. See test1.rs in examples directory for an example App type. This DirNav is a mock type defined as an illustration of what Step #2 needs. */ pub struct DirNav<T> where T:DirEvent { app : T } impl<T> DirNav<T> where T:DirEvent { pub fn new() -> DirNav<T> { DirNav { app: T::new() // factory function } } pub fn get_app(&self) -> &T { &self.app } pub fn visit(&mut self, path: &Path) { /* pretending to search a dir tree */ self.app.do_dir(path); self.app.do_file(&Path::new("file1")); self.app.do_file(&Path::new("file2")); self.app.do_dir(&Path::new("dir2")); self.app.do_file(&Path::new("file3")); } pub fn path_to_string( path:&Path) -> String { format!("{:?}", path) } } #[cfg(test)] mod tests { use super::*; use std::path::{Path, PathBuf}; struct App { test_file: PathBuf, test_dir: PathBuf } impl DirEvent for App { fn new() -> App { App { test_file: PathBuf::from(""), test_dir: PathBuf::from("") } } fn do_dir( &mut self, dir: &Path ) { self.test_dir = dir.to_path_buf(); } fn do_file( &mut self, file: &Path ) { self.test_file = file.to_path_buf(); } } #[test] fn test_events() { let mut dn = DirNav::<App>::new(); let test_path = Path::new("test"); dn.visit(test_path); let app = dn.get_app(); let dir_app:String = format!("{:?}", app.test_dir); let dir_rqt:String = format!("{:?}", "dir2"); assert_eq!(dir_app, dir_rqt); let file_app = format!("{:?}", app.test_file); let file_rqt = format!("{:?}", "file3"); assert_eq!(file_app, file_rqt); } }
DirNav Test Results


cargo test Finished test [unoptimized + debuginfo] target(s) in 0.01s Running target\debug\deps\... running 1 test test tests::test_events ... ok test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out
Test1 Example Source

///////////////////////////////////////////////// // test1.rs - demonstrates mock DirNav ops // // // // Jim Fawcett, 22 Jan 2021 // ///////////////////////////////////////////////// use dir_nav::*; use std::path::{Path}; /*----------------------------------------------- Type MyApp is a test type that: - is a generic parameter for DirNav<App> - responds to DirNav's events - can be retrieved with DirNav::get_app() and interrogated using its member functions. */ struct MyApp {} // mock for TextSearch impl DirEvent for MyApp { fn new() -> Self { MyApp {} } fn do_dir(&mut self, dir:&Path) { print!("\n directory: {:?}", dir); } fn do_file(&mut self, file:&Path) { print!("\n file: {:?}", file); } } impl MyApp { pub fn show_type(&self) { print!( "\n my type is: {:?}", std::any::type_name::<MyApp>() ); } } fn main() { print!("\n -- demonstrate mock DirNav --\n"); let mut dn = DirNav::<MyApp>::new(); let path = &Path::new("dir1"); dn.visit(path); println!(); dn.get_app().show_type(); print!("\n\n That's all Folks!\n\n"); }
Test1 Example Output

C:\...\BuildOn\BuildOnStructure\Step2\DirNav> cargo run --example test1 Finished dev [unoptimized + debuginfo] target(s) in 0.01s Running `target\debug\examples\test1.exe` -- demonstrate mock DirNav -- directory: "dir1" file: "file1" file: "file2" directory: "dir2" file: "file3" my type is: "test1::MyApp" That's all Folks!
Project Structure

Figure 1. Project Structure - Step #2
cargo.toml

[package] name = "dir_nav" version = "0.1.0" authors = ["James W. Fawcett "] edition = "2018" # comment elided [lib] doctest=false [dependencies]
You may wish to open this code in Visual Studio Code. To do that, clone the BuildOn Repository. Then navigate to the cloned directory in a command window, cd into BuildOn/BuildOnStructure/Step2/DirNav, and emit the command code ., e.g.: code . That will bring up VSCode with the library and test1 code loaded. You can do the same thing with the RustTextFinder code. Clone the repository, then navigate into it in a command Widnow, and emit the command: code . You then select the directory of the package you want to examine and build.
 

Step #2 References

The table below provides references relevant for Step #2 : DirNav. The first links refer to specific regions of the Rust Story, from this site. Other links go to Rust documentation. You can look at the Rust Story by selecting the Rust Story link in the menu in the left panel.
 

Table 2. - Step #2 References

Topic Description Link
File System Rust has a well engineered facility for accessing files and directories.
Some key types in std::fs are: DirEntry, File, OpenOptions, ReadDir, ...
Rust story File System
std::fs
 
Error Handling Rust error handling is based on use of the enumeration: enum Result<T,E> { Ok(T), Err(E), } where T is the type of the returned value, E is the type of the expected error. Rust enums are unique in that each of the enumertion items may be a wrapper for a specified type, like Ok and Err. RustStory Enums
RustStory Error Handling
Gentle Introduction to Rust std::Result
Generics Generics in Rust are very similar to those in C# and Java, and simpler than C++ templates. They are code generators often do little more than substitute a specific type for a generic parameter. Rust generics are often constrained with traits, as discussed above. Rust Story Generics
Rust Bites Generics and Traits
The Rust Book
DesignBites A sequence of discussions of design structure alternatives with illustrating code:
Monolitic, Factored, DataFlow, Type Erasure, Plug-In
Structure alternatives
Data Flow
Type Erasure
Start with first three refs, above. Rest will be useful later.
Ownership Rusts ownership rules: There is only one owner for any resource. Owners deallocate their resources when they go out of scope. Ownership can be transferred with a Move or borrowed with a reference. References don't own resources, they just borrow them, and so never deallocate. Rust ownership does not support simultaneously aliasing and mutation. Rust Bites Ownership
Rust Story Ownership
By Example
Rust Book
Rust Nomicon
Strings Rust std strings come in two flavors: String and str, representing string objects and literal strings. Each contains utf-8 characters. The Rust library path also provides PathBuf, similar to String, and Path, similar to &str, but uses the encoding for paths provided by the current platform, e.g., Windows, Linux, or macOS. std::path
std::path::PathBuf std::path::Path
Rust by Example
struct Rust structs serve the same role as classes do in C++ and C#. Struct methods are defined inside impl StructName {} blocks. Rust Story structs
std::Stuct
keyword impl
You don't need to use all of the references in the right-most column. Just look at each quickly and use the one(s) that work(s) best for you.
 
 toggle menu