1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173
use std::env; use std::error::Error; use std::fs; pub fn run(config: Config) -> Result<(), Box<dyn Error>> { let contents = fs::read_to_string(config.filename)?; let results = if config.case_sensitive { search(&config.query, &contents) } else { search_case_insensitive(&config.query, &contents) }; for line in results { println!("{}", line); } Ok(()) } pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> { contents .lines() .filter(|line| line.contains(query)) .collect() } pub fn search_case_insensitive<'a>(query: &str, contents: &'a str) -> Vec<&'a str> { contents .lines() .filter(|line| line.to_lowercase().contains(&query.to_lowercase())) .collect() } pub struct Config { pub query: String, pub filename: String, pub case_sensitive: bool, } impl Config { pub fn new<I: Iterator<Item = String>>(mut args_iterator: I) -> Result<Config, &'static str> { args_iterator.next(); let query = match args_iterator.next() { Some(arg) => arg, None => return Err("Did not get a query string"), }; let filename = match args_iterator.next() { Some(arg) => arg, None => return Err("Did not get a file name"), }; let case_sensitive = env::var("CASE_INSENSITIVE").is_err(); Ok(Config { query, filename, case_sensitive, }) } } #[cfg(test)] mod tests { use super::*; #[test] fn constructor_returns_error_when_not_enough_arguments() { let args = [String::from("arg1"), String::from("arg2")]; let args_iter = args.iter().map(|s| s.to_string()); assert!(Config::new(args_iter).is_err()); } #[test] fn constructor_does_not_return_error_when_enough_arguments() { let args = [ String::from("arg1"), String::from("arg2"), String::from("args3"), ]; let args_iter = args.iter().map(|s| s.to_string()); assert!(!Config::new(args_iter).is_err()); } #[test] fn constructor_returns_valid_config_struct() { let args = [ String::from("_"), String::from("query"), String::from("filename"), ]; let args_iter = args.iter().map(|s| s.to_string()); let config = Config::new(args_iter).unwrap(); let other = Config { query: String::from("query"), filename: String::from("filename"), case_sensitive: true, }; assert!(config.filename == other.filename && config.query == other.query) } #[test] fn run_returns_ok_with_valid_file() { let args = [ String::from("_"), String::from("_"), String::from("poem.txt"), ]; let args_iter = args.iter().map(|s| s.to_string()); let config = Config::new(args_iter).unwrap(); assert!(run(config).is_ok()); } #[test] fn run_returns_err_with_invalid_file() { let args = [ String::from("_"), String::from("_"), String::from("not_valid.txt"), ]; let args_iter = args.iter().map(|s| s.to_string()); let config = Config::new(args_iter).unwrap(); assert!(run(config).is_err()); } #[test] fn one_case_sensitive_search_result() { let query = "duct"; let contents = "\ Rust: safe, fast, productive. Pick three."; assert_eq!(vec!["safe, fast, productive."], search(query, contents)); } #[test] fn no_search_results() { let query = "no_results"; let contents = "\ Rust: safe, fast, productive. Pick three."; assert_eq!(Vec::<&str>::new(), search(query, contents)); } #[test] fn case_insensitive() { let query = "rUsT"; let contents = "\ Rust: safe, fast, productive. Pick three. Trust me."; assert_eq!( vec!["Rust:", "Trust me."], search_case_insensitive(query, contents) ); } #[test] fn case_sensitive_does_not_return_insensitive() { let query = "rUsT"; let contents = "\ Rust: safe, fast, productive. Pick three. Trust me."; assert_eq!(Vec::<&str>::new(), search(query, contents)); } }