about
RustStory Structures
9/21/2022
Rust Story Code

Chapter 4. - Rust Structures

Example libraries, tests, and documentation

4.0 Prologue

Previous chapters have provided a lot of technical details about the Rust programming language. In this chapter we will bring all that together by presenting a few libraries: Logger, CmdLineParser, and DirNavigator. For the first library, Logger we will explore library structure, its implementation, testing, and documentation. The Rust tool cargo, in concert with the rustc compiler, provide an excellent environment for building libraries and applications, with detailed testing and documentation. This chapter is a good place to start exploring that.

4.1 Logger Library

Fig 1. Rust Logger Output
The Logger Library provides a light-weight logger, useful for debugging and demonstration. Library development started with specifications:
  1. Support simultaneous writes to console and a file.
  2. Provide file open and close operations and enable or disable console output.
  3. Write text messages.
  4. Optionally prepend a message with a date-time stamp, using local system time.
Implementation starts with a struct:
Logger Struct pub struct Logger { fl:Option<File>, console:bool, }
Logger has two data members: fl and console. fl is an Option enumeration that either holds a File instance as Some(f:File) or None. When you create a logger with Logger::new(), fl is None, so the logger is not attached to a file. The member console is a boolean that determines whether a write sends its output to the console. Logger::new() sets console to true, so the logger will, by default, write to the console. To do anything useful, we need some functions that operate on that member data. Note that the Logger structure is public, but its data members are not, so only Logger methods can alter their state. Logger has nine methods:
new(), init(f:File, con:bool), console(con:bool), file(f:file), opt(f:Option<File>), open(file_name:&str), ts_write(s:&str), write(s:&str), and close()
new and init are creational methods, console, file, and opt mutate the logger state, open and close manage the state of logger's file member, and write and ts_write handle logger's main business, writing to the console and/or file.
Logger Methods and Functions: Logger Methods: #[allow(dead_code)] impl Logger { /*-- create default logger --*/ pub fn new() -> Self { Self { fl:None, console:true, } } /*-- create initialized logger --*/ pub fn init(f:File, con:bool) -> Self { Self { fl:Some(f), console:con, } } /*-- enable | disable writing to console --*/ pub fn console(&mut self, con:bool) { self.console = con } /*-- attach file --*/ pub fn file(&mut self, f:File) { self.fl = Some(f); } /*-- attach Option<File> --*/ pub fn opt(&mut self, f:Option<File>) { self.fl = f; } /*-- attempt to open logger file --*/ pub fn open(&mut self, s:&str) -> bool { use std::fs::OpenOptions; self.fl = OpenOptions::new() .write(true) .create(true) .append(true) .open(s).ok(); if self.fl.is_some() { return true; } false } /*-- write log entry with date-time stamp */ /*-- can be chained */ pub fn ts_write(&mut self, s:&str) -> &mut Self { let now: DateTime<Local> = Local::now(); /* format DateTime string */ let mut now_str = format!("\n {}", now.to_rfc2822()); /* remove trailing -0400 */ now_str.truncate(now_str.len() - 6); let _ = Logger::write(self, &now_str); let rslt = Logger::write(self, s); rslt } /*-- write log entry, can be chained --*/ pub fn write(&mut self, s:&str) -> &mut Self { if self.console { print!("{}", s); } if let Some(ref mut f) = self.fl { let rslt = f.write(s.as_bytes()); match rslt { Ok(_) => {}, Err(_) => print!("\n file write failed\n"), } } self } /*-- close log file */ pub fn close(&mut self) { self.fl = None; } } #[derive(PartialEq)] #[allow(dead_code)] pub enum OpenMode { Truncate, Append } #[allow(dead_code)] /*----------------------------------------- Helper functions */ /*-- attempt to open file by name --*/ pub fn open_file(s:&str, mode:OpenMode) -> Option<File> { let fl:Option<File>; use std::fs::OpenOptions; if mode == OpenMode::Truncate { fl = OpenOptions::new() .write(true) .truncate(true) .open(s).ok(); } else { fl = OpenOptions::new() .write(true) .create(true) .append(true) .open(s).ok(); } fl } /*-- attempt to remove file by name --*/ pub fn remove_file(s:&str) -> bool { let rslt = std::fs::remove_file(s); rslt.is_ok() } /*-- does file contain a string? --*/ pub fn file_contains(fl:&str, ts:&str) -> bool { let contents = std::fs::read_to_string(fl); let mut s = "".to_string(); if contents.is_ok() { s = contents.unwrap(); } s.contains(ts) } /*-- display contents of named file --*/ pub fn file_contents(fl:&str) { let contents = std::fs::read_to_string(fl); if contents.is_ok() { let s = contents.unwrap(); print!("{}", s); } else { print!("\n no contents"); } } /*-- does file exist? --*/ pub fn file_exists(s:&str) -> bool { let path = Path::new(s); return path.exists(); } Explanations: Methods are defined in one or more impl blocks. Each block contains method definitions, as you see in the right panel. The type Self is the type of the Structure, e.g., Logger. The variable self is a reference to the instance on which the method was called. The method new() returns an instance of the type Self, e.g., the logger we are creating with this method. It only has one line: Self { fl:None, console:true, }. Since there is no semicolon following the closing brace in this line, this is an expression, and any scope has the value of its last expression, so we are returning Logger { fl:None, console:true }. Many of the methods are relatively simple, but the write method has some peculiar code, e.g.: if let Some(ref  mut  f) = self.fl { ... } The "=" operator is not an assignment. It is a matching operator. The term "Some(ref  mut  f) is the Option<T>::Some(t:T) holding a mutable reference to a File instance. So, we are matching self.fl with a pattern that requires the Some option rather than None. If the match is satisfied, e.g., the logger has a file reference, then we write to the file. The remainder of the function is a test to see if the user elected to use the console, and if so, writes to the console. So, if both conditions match, the logger writes to both the console and the log file. The ts_write method uses code from the DateTime crate to generate the local system date and time, format it, and then write using the write method. It then repeats with its string argument.
Both write and ts_write return &mut Self, so they can be chained, e.g., l.write("first).write(", second"); Logger also contains functions:
open_file(file_name, open_mode), remove_file(file_name), file_contains(file_name, search_text), file_contents(file_name), and file_exists(file_name).
These are helper files used during logger testing. They will probably be useful for testing other projects.

4.1.1 Library Unit Tests

when cargo creates a library project with cargo new --lib, it creates a src directory containing a lib.rs file. At the end of the file you will find a #[cfg(test)] directive that defines one or more tests to run. Each test starts with a #[test] directive containing a single test function. The test function is expected to have one or more assertions. The test passes if all assertions are satisfied, otherwise it fails. You run unit tests with the cargo command: cargo test The details dropdown, below, lists all of the unit tests for the Logger library.
Logger Unit Tests: Unit Test Code: #[cfg(test)] mod tests { use super::*; #[test] fn test_open_file() { let stest = "test_open.txt"; let mut l = Logger::new(); l.open(stest); open_file(stest, OpenMode::Truncate); assert_eq!(file_exists(stest),true); remove_file(stest); assert_eq!(file_exists(stest),false); } #[test] fn test_file_contains() { let stest = "test_contains.txt"; let mut l = Logger::new(); l.open(stest); assert_eq!(l.fl.is_some(), true); l.write("test contents with a short string"); l.close(); assert_eq!(file_exists(stest), true); assert_eq!(file_contains(stest, "a short"), true); remove_file(stest); assert_eq!(file_exists(stest),false); } #[test] fn test_remove_file() { let stest = "test_remove"; open_file(stest, OpenMode::Truncate); remove_file(stest); assert_eq!(file_exists(stest),false); } #[test] fn test_file_exists() { assert_eq!(file_exists("foobar.fee"),false); } #[test] fn test_new() { let stest = "test_new"; let mut l = Logger::new(); l.open(stest); assert_eq!(file_exists(stest), true); remove_file(stest); assert_eq!(file_exists(stest), false); } #[test] fn test_init() { let stest = "test_new"; let opt = open_file(stest, OpenMode::Append); let mut l = Logger::init(opt.unwrap(), false); l.open(stest); assert_eq!(file_exists(stest), true); remove_file(stest); assert_eq!(file_exists(stest), false); } #[test] fn test_console() { let mut l = Logger::new(); assert_eq!(l.console == true, true); l.console(false); assert_eq!(l.console == false, true); } #[test] fn test_file() { let mut l = Logger::new(); let stest = "test_file"; l.file(open_file(stest, OpenMode::Append).unwrap()); assert_eq!(file_exists(stest), true); remove_file(stest); assert_eq!(file_exists(stest), false); } #[test] fn test_opt() { let mut l = Logger::new(); let stest = "test_file"; let file_opt = open_file(stest, OpenMode::Append); l.opt(file_opt); assert_eq!(file_exists(stest), true); remove_file(stest); assert_eq!(file_exists(stest), false); } #[test] fn test_open() { let mut l = Logger::new(); let stest = "test_open"; l.open(stest); assert_eq!(file_exists(stest), true); remove_file(stest); assert_eq!(file_exists(stest), false); } #[test] fn test_write() { let mut l = Logger::new(); let stest = "test_write"; l.open(stest); let stxt = "abc 012 xyz 789"; let _ = l.write(stxt); assert_eq!(file_contains(stest, stxt), true); remove_file(stest); assert_eq!(file_exists(stest), false); } #[test] fn test_ts_write() { let mut l = Logger::new(); let stest = "test_ts_write"; l.open(stest); let sdt = "2020"; // change if year != 2020 let stxt = "abc 012 xyz 789"; let _ = l.ts_write(stxt); assert_eq!(file_contains(stest,sdt), true); assert_eq!(file_contains(stest, stxt), true); remove_file(stest); assert_eq!(file_exists(stest), false); } #[test] fn test_close() { let mut l = Logger::new(); let stest = "test_close.txt"; l.open(stest); assert_eq!(l.fl.is_some(), true); l.close(); assert_eq!(l.fl.is_none(), true); remove_file(stest); assert_eq!(file_exists(stest), false); } } Output: C:\github\JimFawcett\RustBasicDemos\logger> cargo test // compiler output elided running 13 tests test tests::test_console ... ok test tests::test_file_exists ... ok test tests::test_new ... ok test tests::test_file ... ok test tests::test_close ... ok test tests::test_open ... ok test tests::test_remove_file ... ok test tests::test_file_contains ... ok test tests::test_open_file ... ok test tests::test_opt ... ok test tests::test_init ... ok test tests::test_write ... ok test tests::test_ts_write ... ok test result: ok. 13 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out
Since cargo sets up the unit test configuration code for each library crate that it creates, it is quite easy to write unit tests as we write functions. When development is finished we have a robust test sequence that can be run any time maintenance changes are made to the library.

4.1.2 Library Examples

If you create an examples directory as a sibling of src, then you can put code there with a main function that exercises your library, perhaps to demonstrate its operations for new users. If you do that then cargo run --example test1 will trigger cargo to attempt to build a binary crate from test1.rs and run its executable.
Examples: Examples/test1.rs Demonstrating Logger ====================== deleted previous log file writing to "Log.txt" ---------------------- Thu, 19 Mar 2020 18:18:59 starting log first entry, second entry, third entry contents of log.txt --------------------- Thu, 19 Mar 2020 18:18:59 starting log first entry, second entry, third entry closing then reopening log ---------------------------- Thu, 19 Mar 2020 18:18:59 reopening log after reopen found "first entry" in "log.txt" That's all Folks! Output: >cargo run --example test1 // compiler output elided Demonstrating Logger ====================== deleted previous log file writing to "Log.txt" ---------------------- Wed, 18 Mar 2020 14:37:40 starting log first entry, second entry, third entry Wed, 18 Mar 2020 14:37:40 reopening log after reopen contents = Wed, 18 Mar 2020 14:37:40 starting log first entry, second entry, third entry Wed, 18 Mar 2020 14:37:40 reopening log after reopen found first entry in log.txt That's all Folks!
You can have any number of example files in the examples directory, each with a main function that demonstrates some particular aspect of your library. You can also supply examples suitable for developer's with different levels of expertise with Rust.

4.1.3 Library Documentation

Figure 1. Logger Documentation
You can endow your own libraries with the same kind of documentation provided for the std libraries. If you put comments above each method or function with the format: /// ``` /// let mut logr = Logger::new(); /// /// sets fl:None, console:true; /// ``` pub fn new() -> Self { Self { fl:None, console:true, } } That places your comments in the document below the function, as shown in Figure 1. To create documentation, simply issue the cargo command: cargo doc --document-private-items If you don't want to display private items, then leave off that option. The doc command builds documentation and deposites it in your Target folder. This creates documentation, not only for your code, but also for code it depends upon. Be aware that if you issue a cargo clean command that will delete all the documentation because clean deletes the Target folder. One more thing: if you put documentation comments in your code, you need to set doctest = false in a [lib] section of your cargo.toml file. If you don't do this, the compiler attempts to compile your comments and emits a lot of errors and warnings.
Cargo.toml The [lib] section needs to come before [dependencies]
Logger Cargo File [package] name = "logger" version = "0.1.0" authors = ["James W. Fawcett <jfawcett@twcny.rr.com>"] edition = "2018" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [lib] # prevents rustc from attempting to compile doc comments doctest = false [dependencies] chrono = "0.2.16" display = { path = "../display" }
This completes our discussion of the Logger project. You can find the code and discussion in RustLogger repository.

4.2 CmdLine Parser Library

Fig 1. CmdLineParser Output
RustCmdLine is a facility for parsing command line arguments. Here's a sample:
/P "." /p "rs,txt" /s /H /r "abc"
where:
/P "."          - path
/p "rs,txt" - file patterns
/s                 - recurse through directory tree rooted at path
/H                 - hide directories that don't contain target files
/r "abc"       - regular expression
The intent is that a program creates an instance of CmdLineParser, uses that to parse its command line, then passes it to any code that needs to know about a pattern or an option.

Design:

This library contains a single user-defined type: CmdLineParser.
CmdLineParser Struct pub struct CmdLineParse { opt_map : Options, patterns : CmdLinePatterns, help_str : String, }
The opt_map field is a HashMap<K, V> of key-value pairs, where each key is a command line option; that is, a character from the command line that is preceeded by a '/' character. If an option, o, is succeeded by a non-option string, that becomes the value. If there is no value, then the opt_map contains "true" for the value of the key o. The field, patterns, is a vector of Strings where each string is expected to be a file extension, e.g., rs. Note that using *.rs will not work as expected for applications like RustTextFinder. It is expected that applications using CmdLineParser will process only files with the specified extensions. If none are supplied, then the application processes all files it encounters. The field, help_str, is a String containing information to supply to the user in response to an option /h. In this library, the string contains the defaults, cited above, but an application can change the string with replace_help(hs&str).

4.2.1 CmdLine Parser Methods

CmdLineParser implements the following methods and functions:
  1. new() -> Self
    Create new CmdLineParser which has an options hashmap, patterns vector, and help string.
  2. parse(&self)
    Builds internal options hashmap and patterns vector.
  3. path(&self) -> String
    Return value of relative root path, held in options map.
  4. abs_path(&self) -> String
    Return value of absolute root path, from canonicalized relative path.
  5. set_path(&mut self, p:&str)
    Replaces value of root path, held in options map.
  6. set_regex(&mut self, re:&str)
    Replaces value of regex string, held in options map.
  7. get_regex(&mut self) -> &str
    Retrieves value of regex string, held in options map.
  8. default_options(&mut self)
    Sets values of some of the options in options map.
  9. contains_option(&self, opt:char) -> bool
    returns true if options map contains key opt, else returns false.
  10. add_option(&mut self, opt:char, val:&str)
    Inserts option in options hashmap, adding key if it does not exist, else overriding previous value.
  11. value(&self, opt:char) -> &str
    Inserts option in options hashmap, adding key if it does not exist, else overriding previous value.
  12. add_pattern(&mut self, patt:&str) -> &mut self
    Inserts patt into patterns vector. Method can be chained.
  13. patterns(&self) -> &CmdLinePatterns
    Returns reference to vector of patterns.
  14. options(&self) -> &Options
    Returns reference to hashmap of options.
  15. help(&self) -> &str
    Returns default help string.
  16. replace_help(&mut self, hs:&str)
    Replace internal help string.
Code for CmdLineParser library and a demonstration program are provided in the dropdown, below.
CmdLineParser Implementation CmdLineParser lib.rs ///////////////////////////////////////////////////////////// // rust_cmd_line::lib.rs // // // // Jim Fawcett, https://JimFawcett.github.io, 19 Apr 2020 // ///////////////////////////////////////////////////////////// use std::env::{args}; use std::collections::HashMap; use std::fs::*; ///////////////////////////////////////////////////////////// // sample command line with options //----------------------------------------------------------- // /P "." /p "rs,txt" /s [true] /r "abc" /h [true] /H [true] // // P - path in either absolute or relative form // p - pattern, a file extension indicating file to process // s - recurse directory tree rooted at P // r - regular expression // H - hide directories that don't contain any target files // h - help: display this message // custom option: // /x [v] - x is application specific option which may // have value, v // Note: // Any attribute that has no value on command line will // have value "true" in option map ///////////////////////////////////////////////////////////// // CmdLineParse methods //----------------------------------------------------------- // new() -> CmdLineParse // parse(&self) // contains_option(&self, opt: char) -> bool // value(&self, char opt) -> &str // options(&self) -> &HashMap<char, String> // - defaults are created with default_options(): // - /P "." - root search path is current directory // - /s "true" - recurse // - /r "." - match all text // - /H "true" - hide unused directories // - no patterns are equivalent to all files // path(&self) -> String // abs_path(&self) -> String // set_path(&self, p:&str) // patterns(&self) -> &Vec<String> // add_pattern(&mut self, p:&str) -> &mut self // set_regex(&mut self, re:&str) // get_regex(&self) -> &str // help(&self) -> String // replace_help(&mut self, &str) -> String /// display command line arguments pub fn show_cmd_line() { print!("\n {:?}\n ", args().next()); for arg in args().skip(1) { print!("{:?} ", arg) } } pub type Options = HashMap<char, String>; pub type CmdLinePatterns = Vec<String>; /// Parses command line into options and patterns #[derive(Debug, Default)] pub struct CmdLineParse { opt_map : Options, patterns : CmdLinePatterns, help_str : String, } impl CmdLineParse { /// create new instance of parser pub fn new() -> Self { let help = CmdLineParse::help_txt(); Self { opt_map: Options::default(), patterns: CmdLinePatterns::new(), help_str: help, } } /// returns string with command line arguments example fn help_txt() -> String { let mut str = "\n Help:\n Options: /P . /p \"rs,txt\"".to_string(); str.push_str(" /s /r \"abc\" /H /h"); str } /// does the command line argument start with '/' fn is_opt(&self, s:&str) -> bool { let bytes = s.as_bytes(); bytes[0] as char == '/' } /// returns path string with default value "." pub fn path(&self) -> String { if self.contains_option('P') { self.opt_map[&'P'].clone() } else { ".".to_string() } } /// replace Win path separator "\\" with Linux "/" /// - use only with absolute paths for Windows fn replace_sep(path: &str) -> String { let mut rtn = path.to_string(); if rtn.contains("\\") { rtn = rtn.replace("\\", "/"); rtn = rtn.chars().skip(4).collect(); } rtn } /// convert relative to absolute path pub fn abs_path(&self) -> String { let abs = std::path::PathBuf::from(self.path()); let rslt = canonicalize(&abs); match rslt { Ok(path_buf) => { // print!("\n--path_buf = {:?}",path_buf); let ap = path_buf.to_string_lossy().to_string(); let ap = CmdLineParse::replace_sep(&ap); // print!("\n--abs_path = {:?}",ap); ap } Err(error) => error.to_string() } } /// set new root path pub fn set_path(&mut self, p:&str) { self.opt_map.insert('P', p.to_string()); } /// set new regex string for matching pub fn set_regex(&mut self, re:&str) { self.opt_map.insert('r', re.to_string()); } /// return current regex string pub fn get_regex(&self) -> &str { let re_opt = self.opt_map.get(&'r'); match re_opt { Some(value) => value, None => ".", } } /// commonly used default options pub fn default_options(&mut self) { self.opt_map.insert('P', ".".to_string()); // root is curr dir self.opt_map.insert('s', "true".to_string()); // recurse self.opt_map.insert('r', ".".to_string()); // regex always matches self.opt_map.insert('H', "true".to_string()); // hide unused dirs } /// does options contain opt char? pub fn contains_option(&self, opt:char) -> bool { self.opt_map.contains_key(&opt) } /// insert {o,v} if o key doesn't exist, else overwrite v pub fn add_option(&mut self, o:char, v:&str) { self.opt_map.insert(o, v.to_string()); } /// return option value pub fn value(&self, opt:char) -> &str { &self.opt_map[&opt] } /// add file ext (with no "*.") pub fn add_pattern(&mut self, p:&str) -> &mut Self { let s = String::from(p); if !self.patterns.contains(&s) { self.patterns.push(s); } self } /// returns non-mutable reference to patterns pub fn patterns(&self) -> &CmdLinePatterns { &self.patterns } /// returns non-mutable reference to options pub fn options(& self) -> &Options { &self.opt_map } /// return help string pub fn help(&self) -> &str { &self.help_str } /// replace help string pub fn replace_help(&mut self, s:&str) { self.help_str = s.to_string(); } /// parse command line arguments, provided by env() pub fn parse(&mut self) { let cl_args:Vec<String> = args().collect(); let end = cl_args.len(); for i in 1..end { if self.is_opt(&cl_args[i]) { let bytes = cl_args[i].as_bytes(); let key = bytes[1] as char; if i < end - 1 { if !self.is_opt(&cl_args[i+1]) { self.opt_map.insert(key,cl_args[i+1].to_string()); } else { self.opt_map.insert(key, "true".to_string()); } } else { self.opt_map.insert(key, "true".to_string()); } } } /*-- build patterns --*/ if self.contains_option('p') { let pat_str = self.value('p').to_string(); let split_iter = pat_str.split(','); for patt in split_iter { self.add_pattern(patt); } } } } #[cfg(test)] mod tests { use super::*; #[test] fn cl_args() { let _mock_args = vec!["/P", ".", "/p", "rs,txt", "/s"]; print!("\n cl args: "); for arg in args() { print!("{:?} ", arg); } let mut parser = CmdLineParse::new(); parser.parse(); for arg in args() { let bytes = arg.as_bytes(); if '/' == (bytes[0] as char) { assert!(parser.opt_map.contains_key(&(bytes[1] as char))); } } } } Example program ///////////////////////////////////////////////////////////// // rust_cmd_line::test1.rs // // // // Jim Fawcett, https://JimFawcett.github.io, 19 Apr 2020 // ///////////////////////////////////////////////////////////// use rust_cmd_line::*; fn main() { print!("\n Command line arguments:"); show_cmd_line(); let mut parser = CmdLineParse::new(); // let mut parser = CmdLineParse::default(); parser.default_options(); print!("\n {}\n",parser.help()); parser.parse(); print!("\n path = {:?}", parser.path()); print!("\n abspath = {:?}", parser.abs_path()); let new_path = "C:/github/foo"; parser.set_path(new_path); print!("\n setting path to {:?}", new_path); print!("\n path = {:?}", parser.path()); parser.add_pattern("rs"); parser.add_pattern("rs"); // is not repeated parser.add_pattern("exe"); let patts = parser.patterns(); print!("\n patts = {:?}", patts); print!("\n regex = {:?}", parser.get_regex()); let opts = parser.options(); print!("\n opts = {:#?}", opts); print!("\n\n adding option {{x,false}}"); parser.add_option('x', "false"); let opts = parser.options(); print!("\n opts = {:#?}", opts); print!("\n\n adding option {{x,true}}"); parser.add_option('x', "true"); let opts = parser.options(); print!("\n opts = {:#?}", opts); print!("\n\n That's all Folks!\n\n") } Output: Command line arguments: Help: Options: /P . /p "rs,txt" /s /r "abc" /H /h path = "." abspath = "C:/github/JimFawcett/RustCmdLine" setting path to "C:/github/foo" path = "C:/github/foo" patts = ["rs", "exe"] regex = "." opts = { 'r': ".", 'H': "true", 's': "true", 'P': "C:/github/foo", } adding option {x,false} opts = { 'x': "false", 'r': ".", 'H': "true", 's': "true", 'P': "C:/github/foo", } adding option {x,true} opts = { 'x': "true", 'r': ".", 'H': "true", 's': "true", 'P': "C:/github/foo", } That's all Folks!
The CmdLineParser library is used in the RustTextFinder application. I expect to use it for many more tools.

4.3 Directory Navigator Library

Fig 1. Directory Navigator Types
Fig 1. Directory Navigator Output
RustDirNav is a facility for Depth-First-Search (DFS) of a specified directory tree. It uses generic parameter App to provide application specific do_dir and do_file operations.

Design:

There is one struct, DirNav<App>, with methods and functions implementing this design: Methods:
  1. new() -> Self
    Create new DirNav which has visit method for recursive DFS.
  2. add_pat(&mut self, s&str) -> Self
    Add pattern to match file extension. Can be chained.
  3. visit(&mut self, p:&Path)
    Walks directory tree rooted at path p, looking for files matching pattern(s).
  4. recurse(&mut self, p:bool)
    Sets or resets option to recurse directory tree.
  5. hide(&mut self, p:bool)
    Sets or resets option to hide directories with no target contents.
  6. get_app(&mut self) -> &mut app
    Retrieves reference to embedded application, set with generic parameter.
  7. get_dirs(&self) -> usize
    Retrieves the number of directories entered
  8. get_files(&self) -> usize
    Retrieves the number of files processed.
  9. get_patts(&self) -> &SearchPatterns
    Retrieves vector of patterns.
  10. clear(&self)
    Returns DirNav<app> to its initial state.
The DirEvent trait constrains the App generic parameter to supply do_dir and do_file methods that define what the application will do when the navigator encounters a new directory or file.
pub trait DirEvent { fn do_dir(&mut self, d: &str); fn do_file(&mut self, f: &str); }
The DirNav package provides a demonstration of the library in its /examples/test1.rs file. For the demo, test1 simply displays each directory once, and all of the files in that directory. The App type is defined by implementing the DirEvent trait, which may contain data members.
Directory Navigator Implementation lib.rs ///////////////////////////////////////////////////////////// // rust_dir_nav::lib1.rs // // // // Jim Fawcett, https://JimFawcett.github.io, 12 Apr 2020 // ///////////////////////////////////////////////////////////// /* DirNav<App> is a directory navigator that uses the generic parameter App to define how files and directories are handled. - displays only paths that have file targets by default - hide(false) will show all directories traversed - recurses directory tree at specified root by default - recurse(false) examines only specified path. */ use std::fs::{self, DirEntry}; use std::io; use std::io::{Error, ErrorKind}; #[allow(unused_imports)] use std::path::{Path, PathBuf}; /// trait required of the App generic parameter type pub trait DirEvent { fn do_dir(&mut self, d: &str); fn do_file(&mut self, f: &str); } //--------------------------------------- // Sample implementation of DirNav param // -------------------------------------- // #[derive(Debug, Default)] // pub struct Appl; // impl DirEvent for Appl { // fn do_dir(&mut self, d:&str) { // print!("\n {:?}", d); // } // fn do_file(&mut self, f:&str) { // print!("\n {:?}", f); // } // } ///////////////////////////////////////////////// // Patterns are a collection of extension strings // used to identify files as search targets type SearchPatterns = Vec<std::ffi::OsString>; /// Directory Navigator Structure #[allow(dead_code)] #[derive(Debug, Default)] pub struct DirNav<App: DirEvent> { /// file extensions to look for pats: SearchPatterns, /// instance of App : DirEvent, /// requires do_file and do_dir methods app: App, /// number of files processed num_file: usize, /// number of dirs processed num_dir: usize, /// recurse ? recurse : bool, /// hide dirs with no targets ? hide: bool, } impl<App: DirEvent + Default> DirNav<App> { pub fn new() -> Self where App: DirEvent + Default, { Self { pats: SearchPatterns::new(), app: App::default(), num_file: 0, num_dir: 0, recurse: true, hide: true, } } /// do recursive visit? pub fn recurse(&mut self, p:bool) { self.recurse = p; } /// hide dirs with no targets? pub fn hide(&mut self, p:bool) { self.hide = p; } /// return reference to App to get results, if any pub fn get_app(&mut self) -> &mut App { &mut self.app } /// return number of dirs processed pub fn get_dirs(&self) -> usize { self.num_dir } /// return number of files processed pub fn get_files(&self) -> usize { self.num_file } /// return patterns, e.g., file extensions to look for pub fn get_patts(&self) -> &SearchPatterns { &self.pats } /// add extention to search for /// - takes either String or &str pub fn add_pat<S: Into<String>>(&mut self, p: S) -> &mut DirNav<App> { let mut t = std::ffi::OsString::new(); t.push(p.into()); self.pats.push(t); self } /// reset to default state pub fn clear(&mut self) { self.pats.clear(); self.num_dir = 0; self.num_file = 0; self.app = App::default(); } /// Depth First Search for file extentions starting at /// path dir<br /> /// Displays only directories with files matching pattern pub fn visit(&mut self, dir: &Path) -> io::Result<()> where App: DirEvent { self.num_dir += 1; let dir_name: String = self.replace_sep(dir).to_string_lossy().to_string(); let mut files = Vec::<std::ffi::OsString>::new(); let mut sub_dirs = Vec::<std::ffi::OsString>::new(); if dir.is_dir() { /* search local directory */ for entry in fs::read_dir(dir)? { let entry = entry?; let path = entry.path(); if path.is_dir() { let cd = self.replace_sep(&path); sub_dirs.push(cd); } else { self.num_file += 1; if self.in_patterns(&entry) | self.pats.is_empty() { files.push(entry.file_name()); } } } /*-- display only dirs with found files --*/ if !files.is_empty() || !self.hide { self.app.do_dir(&dir_name); } for fl in files { let flnm = fl.to_string_lossy().to_string(); self.app.do_file(&flnm); } /*-- recurse into subdirectories --*/ for sub in sub_dirs { let mut pb = std::path::PathBuf::new(); pb.push(sub); if self.recurse { self.visit(&pb)?; } } return Ok(()); // normal return } Err(Error::new(ErrorKind::Other, "not a directory")) } /// replace Win directory separator with Linux separator pub fn replace_sep(&self, path: &Path) -> std::ffi::OsString { let rtn = path.to_string_lossy(); let mod_path = rtn.replace("\\", "/"); let mut os_str: std::ffi::OsString = std::ffi::OsString::new(); os_str.push(mod_path); os_str } /// does store contain d.path().extension() ? pub fn in_patterns(&self, d: &DirEntry) -> bool { let p = d.path(); let ext = p.extension(); match ext { Some(extn) => self.pats.contains( &(extn.to_os_string()) ), None => false, } } } #[cfg(test)] mod tests { // test_setup() should run first. To ensure that: // use cargo -- --test-threads=1 // to see console output: // use cargo test -- --show-output --test-threads=1 use super::*; #[derive(Debug)] struct ApplTest { rslt_store: Vec<String>, } impl DirEvent for ApplTest { fn do_dir(&mut self, _d: &str) { //print!("\n {:?}", d); } fn do_file(&mut self, f: &str) { //print!("\n {:?}", f); self.rslt_store.push((*f).to_string()); } } impl Default for ApplTest { fn default() -> Self { ApplTest { rslt_store: Vec::<String>::new(), } } } #[test] fn test_setup() { let _ = std::fs::create_dir("./test_dir"); let _ = std::fs::create_dir("./test_dir/test_sub1_dir"); let _ = std::fs::create_dir("./test_dir/test_sub2_dir"); let _ = std::fs::File::create("./test_dir/test_file.rs"); let _ = std::fs::File::create( "./test_dir/test_sub1_dir/test_file1.rs" ); let _ = std::fs::File::create( "./test_dir/test_sub1_dir/test_file2.exe" ); let _ = std::fs::File::create( "./test_dir/test_sub2_dir/test_file3.txt" ); } #[test] fn test_walk() { let mut dn = DirNav::<ApplTest>::new(); dn.add_pat("rs").add_pat("exe") .add_pat("txt"); let mut pb = PathBuf::new(); pb.push("./test_dir".to_string()); let _ = dn.visit(&pb); let rl = &dn.get_app().rslt_store; /* run exe in target/debug with --nocapture option to see output of print statement below. */ print!("\n {:?}", rl); // test for found files let l = |s: &str| -> String { s.to_string() }; assert!(rl.contains(&l("test_file.rs"))); assert!(rl.contains(&l("test_file1.rs"))); assert!(rl.contains(&l("test_file2.exe"))); assert!(rl.contains(&l("test_file3.txt"))); /* uncomment line below to make test fail */ //assert!(rl.contains(&l("foobar"))); } #[test] fn test_patts() { let mut dn = DirNav::<ApplTest>::new(); dn.add_pat("foo").add_pat("bar"); assert_eq!(dn.get_patts().len(), 2); let pats = dn.get_patts(); let mut foo_str = std::ffi::OsString::new(); foo_str.push("foo"); assert!(pats.contains(&foo_str)); let mut bar_str = std::ffi::OsString::new(); bar_str.push("bar"); assert!(pats.contains(&bar_str)); dn.clear(); assert_eq!(dn.get_patts().len(), 0); } } test1.rs ///////////////////////////////////////////////////////////// // rust_dir_nav::test1.rs // // // // Jim Fawcett, https://JimFawcett.github.io, 12 Apr 2020 // ///////////////////////////////////////////////////////////// use rust_dir_nav::*; #[allow(unused_imports)] use std::env::current_dir; use std::io; struct Appl; impl DirEvent for Appl { fn do_dir(&mut self, d: &str) { print!("\n {}", d); } fn do_file(&mut self, f: &str) { print!("\n {}", f); } } impl Default for Appl { fn default() -> Self { Appl } } fn main() -> io::Result<()> { let mut dn = DirNav::<Appl>::new(); /*-- takes a variety of formats --*/ let _pat1: String = "rs".to_string(); let _pat4: String = "rlib".to_string(); dn.add_pat(&_pat1); dn.add_pat("toml".to_string()); dn.add_pat("txt"); dn.add_pat(_pat4); dn.add_pat(&"exe".to_string()); //dn.hide(false); let path = current_dir()?; print!("\n Searching path {:?}\n", &path); let _rslt = dn.visit(&path)?; print!( "\n\n processed {} files and {} dirs", dn.get_files(), dn.get_dirs() ); print!("\n"); dn.clear(); dn.add_pat("rs").add_pat("toml") .add_pat("exe").add_pat("txt"); let mut path = std::path::PathBuf::new(); path.push("./test_dir"); print!("\n Searching path {:?}\n", &path); let _rslt = dn.visit(&path)?; print!( "\n\n processed {} files in {} dirs", dn.get_files(), dn.get_dirs() ); /////////////////////////////////////////////// // uncomment lines below to see error return //--------------------------------------------- // print!("\n"); // path.pop(); // path.push("foobar"); // dn.visit(&path)?; print!("\n\n"); Ok(()) }
The lib.rs file defines all of the DirNav processing and, in its tests section, defines an ApplTest type that implements DirEvent and contains a vector that will hold each of the files found during test, so we can verify its operation.

4.4 Epilogue

The three libraries discussed here: Logger, CmdLineParser, and DirNav are good examples to study while learning the Rust programming language. They illustrate common Rust coding idioms and the great support for testing and documentation provided by cargo, rustc, and rustdoc. I've used the clippy tool, e.g., cargo clippy, to find non-idiomatic or inefficient code, before including here.

4.5 References

Reference Link Description
A half-hour to learn Rust Code fragments with commentary that cover most of the Rust ideas.
intorust.com A few short screen casts - Nicholas Matsakis, ...
readrust.net/getting-started Nice set of "first-look" posts.
rust-learning repository Extensive list of resources: books, videos, podcasts, ...
A Gentle Introduction To Rust A good walkthrough for most of the Rust ideas.
The Rust Programming Language Long but gentle walkthrough of the Rust Language
An alternative introduction to Rust Steve Klabnik
Rust pain-points Steve Donovan
Rust by Example Rust docs - walkthrough of syntax
Getting Started with Rust on Windows and Visual Studio Code Install Rust, Verify, Configure Visual Studio Code, Create Hello World, Create Build Task, Configuring Unit Tests, Configure Debugging,
rust-lang.org home page Links to download and documentation
rust cheat sheet Quite extensive list of cheats and helpers.
rust cargo book Very clear, simple, description of the cargo facilities and commands.
More References Basic, intermediate, and advanced references
  Next Prev Pages Sections About Keys