cli_program_doc/lib.rs
1//! # Cli Program Doc
2//!
3//! `cli_program_doc` is an utility that allows you to search a string into a text
4
5use std::env;
6
7pub struct Config {
8 pub query: String,
9 pub file_path: String,
10 pub ignore_case: bool,
11}
12
13impl Config {
14 // env::args passed from main is defined in std::env::Args as implementing the Iterator trait and returning String values
15 // the args parameter has a generic type with trait bounds Iterator<Item = String> instead of &[String]
16 // we are taking ownership of argos and we’ll be mutating it by iterating over it so we need mut
17 pub fn build(mut args: impl Iterator<Item = String>) -> Result<Config, &'static str> {
18 // the first element will be the name of the program, ignore it
19 args.next();
20
21 // thanks to the iterator we don't need to clone the values
22 let query = match args.next() {
23 Some(arg) => arg,
24 None => return Err("Didn't get a query string"),
25 };
26
27 // iterate and use match to extract the value
28 let file_path = match args.next() {
29 Some(arg) => arg,
30 None => return Err("Didn't get a file path"),
31 };
32
33 // env::var returns a Result that returns Err variant if the env variable is not set
34 // is_ok returns false if the variable is not set, so we perform a case sensitive search
35 // we use is_ok instead of unwrap/expect because we don’t need the value just know if it’s set or not
36
37 let ignore_case = env::var("IGNORE_CASE").is_ok();
38
39 // clone cannot take ownership but only borrow from main
40 // we need to clone those field to have ownership
41 Ok(Config {
42 query,
43 file_path,
44 ignore_case,
45 })
46 }
47}
48
49use std::error::Error;
50use std::fs;
51
52pub fn run(config: Config) -> Result<(), Box<dyn Error>> {
53 //// we use the ? operator in place of expect, instead of panicking ? will return the error value from the current function to the caller to handle
54 let file_contents = fs::read_to_string(config.file_path)?;
55
56 let results = if config.ignore_case {
57 search_non_sensitive(&config.query, &file_contents)
58 } else {
59 search(&config.query, &file_contents)
60 }; // this is an assignment
61
62 for line in results {
63 println!("{line}");
64 }
65
66 Ok(())
67}
68
69/// Does a case sensitive search of the query you pass in the contents and returns matching lines
70/// ```
71/// let query = "Guest";
72/// let contents = "\
73/// This is a Guest line.
74/// Rust is safe, fast, productive.";
75///
76/// assert_eq!(vec!["This is a Guest line."], cli_program_doc::search(query, contents));
77/// ```
78
79pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
80 // by using an iterator and a closure we avoid using a temporary vector that handles state
81 contents
82 .lines()
83 .filter(|line| line.contains(query))
84 .collect()
85}
86
87pub fn search_non_sensitive<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
88 let query = query.to_lowercase();
89
90 contents
91 .lines()
92 .filter(|line| line.to_lowercase().contains(&query))
93 .collect()
94}
95
96#[cfg(test)]
97mod tests {
98 // bring into scope the code in the rest of the library
99 // we use the glob operator to do so
100 use super::*;
101
102 #[test]
103 fn search_kw_in_text() {
104 let query = "safe";
105 // the backlash tells Rust not to put a newline char at the beginning of the contents of this string literal
106 let contents = "\
107 Rust:
108 safe, fast, productive.
109 Pick three.
110 Safe, fast, productive.";
111 // check that we return the line that matches our query in that content
112 assert_eq!(
113 vec![" safe, fast, productive."],
114 search(query, contents)
115 );
116 }
117
118 #[test]
119 fn search_kw_in_text_non_sensitive() {
120 let query = "RUSt";
121 let contents = "\
122 Rust:
123 safe, fast, productive.
124 Pick three.
125 Safe, fast, productive.";
126
127 // Rust: should be the line returned if there is a match
128 assert_eq!(vec!["Rust:"], search_non_sensitive(query, contents));
129 }
130}