rodarte_minigrep/lib.rs
1//! # Minigrep
2//!
3//! A library to perform simple text searching against a string content target.
4
5use std::{env, error, fs};
6
7/// Main structure that holds the data required for the process to work.
8#[derive(Debug, PartialEq)]
9pub struct Config {
10 /// What are we searching for in the string content target?
11 pub query: String,
12 /// Where does the content target live in my file system?
13 pub file_path: String,
14 /// Should the searching process be case sensitive (`false`) or insensitive (`true`)?
15 pub ignore_case: bool,
16}
17
18impl Config {
19 /// Tries to build an instance of `Config` by reading the first three arguments passed to the command.
20 ///
21 /// 1. The first argument is **ignored** as the project name is NOT used in this application.
22 /// 2. The second argument will be bound to the `query` field
23 /// 3. The third argument will be bound to the `file_path` field.
24 ///
25 /// An optional `IGNORE_CASE` environment value is accepted if case-insensitive searching is desired.
26 /// You may set `IGNORE_CASE` to any valid value in order to trigger this behavior, which sets the `ignore_case` field to `true`
27 ///
28 /// # Examples
29 /// ```
30 /// use minigrep::{run, Config};
31 ///
32 /// let args = vec![
33 /// String::from("target/debug/minigrep"),
34 /// String::from("foo"),
35 /// String::from("/path/to/file.txt")
36 /// ].into_iter();
37 ///
38 /// let result = Config::build(args);
39 ///
40 /// if let Ok(config) = result {
41 /// assert!(!config.ignore_case);
42 /// run(config);
43 /// }
44 /// ```
45 ///
46 /// # Errors
47 /// The function returns an `Err` if it fails to find at least two arguments inside the iterator
48 ///
49 /// ```
50 /// use minigrep::Config;
51 ///
52 /// let args = vec![
53 /// String::from("target/debug/minigrep"),
54 /// String::from("foo")
55 /// ].into_iter();
56 ///
57 /// let result = Config::build(args);
58 ///
59 /// assert_eq!(result, Err("Need to provide a second 'file_path' argument"));
60 /// ```
61 pub fn build(
62 mut args: impl Iterator<Item = String>,
63 ) -> Result<Self, &'static str> {
64 args.next();
65
66 let query: String = match args.next() {
67 Some(q) => q,
68 None => return Err("Need to provide a first 'query' argument"),
69 };
70
71 let file_path: String = match args.next() {
72 Some(f) => f,
73 None => return Err("Need to provide a second 'file_path' argument"),
74 };
75
76 let ignore_case: bool = env::var("IGNORE_CASE").is_ok();
77
78 Ok(Config {
79 query,
80 file_path,
81 ignore_case,
82 })
83 }
84}
85
86/// Entrypoint function that bootstraps the searching application using a `Config` instance.
87///
88/// It reads the file defined in `config.file_path` and runs either the `search` or `search_case_insensitive`
89/// functions, depending on what `config.ignore_case` is set to.
90///
91/// If `config.ignore_case` is `true`, `search_case_insensitive` is called for a case-insensitive search is done.
92/// If `config.ignore_case` is `false`, `search` is called and a case-sensitive search is done.
93///
94/// Matching lines are printed to standard output
95///
96/// # Example
97///
98/// ```
99/// use minigrep::{run, Config};
100///
101/// let args = vec![
102/// String::from("target/debug/minigrep"),
103/// String::from("foo"),
104/// String::from("/path/to/file.txt")
105/// ].into_iter();
106///
107/// let result = Config::build(args);
108///
109/// if let Ok(config) = result {
110/// run(config);
111/// }
112/// ```
113///
114/// # Errors
115/// An `Err` variant is returned if `fs::read_to_string` fails to read the file.
116pub fn run(config: Config) -> Result<(), Box<dyn error::Error>> {
117 let contents: String = fs::read_to_string(config.file_path)?;
118
119 let results: Vec<&str> = if !config.ignore_case {
120 search(&config.query, &contents)
121 } else {
122 search_case_insensitive(&config.query, &contents)
123 };
124
125 for line in results {
126 println!("{line}");
127 }
128
129 Ok(())
130}
131
132/// Performs a case-sensitive search of a `query` string against a target `contents` string,
133/// returning a vector with the matching lines.
134///
135/// # Example
136///
137/// ```
138/// use minigrep::search;
139///
140/// let query = "rust";
141///
142/// let contents = "\
143/// RUST is cool. This should NOT match.
144/// rUsT is awesome. This should NOT match.
145/// rust is nice. This should match.
146/// ";
147///
148/// let results = search(query, contents);
149///
150/// assert_eq!(results, vec!["rust is nice. This should match."]);
151/// ```
152pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
153 contents
154 .lines()
155 .filter(|&line| line.contains(query))
156 .collect()
157}
158
159/// Performs a case-insensitive search of a `query` string against a target `contents` string,
160/// returning a vector with the matching lines.
161///
162/// # Example
163///
164/// ```
165/// use minigrep::search_case_insensitive;
166///
167/// let query = "rust";
168///
169/// let contents = "\
170/// I am quite RUsTy. This should match.
171/// I am a rUStacean. This should match.
172/// Python is nice. This should NOT match.
173/// ";
174///
175/// let results = search_case_insensitive(query, contents);
176///
177/// assert_eq!(
178/// results,
179/// vec![
180/// "I am quite RUsTy. This should match.",
181/// "I am a rUStacean. This should match."
182/// ]
183/// );
184/// ```
185pub fn search_case_insensitive<'a>(
186 query: &str,
187 contents: &'a str,
188) -> Vec<&'a str> {
189 let query: String = query.to_lowercase();
190 let mut results: Vec<&str> = Vec::new();
191 for line in contents.lines() {
192 if line.to_lowercase().contains(&query) {
193 results.push(line)
194 }
195 }
196 results
197}
198
199#[cfg(test)]
200mod tests {
201 use super::*;
202
203 #[test]
204 fn case_sensitive() {
205 let query: &str = "duct";
206 let contents: &str = "\
207Rust:
208safe, fast, productive.
209Pick three.
210Duct tape.";
211
212 assert_eq!(vec!["safe, fast, productive."], search(query, contents));
213 }
214
215 #[test]
216 fn case_insensitive() {
217 let query: &str = "rUsT";
218 let contents: &str = "\
219Rust:
220safe, fast, productive.
221Pick three.
222Trust me.";
223
224 assert_eq!(
225 vec!["Rust:", "Trust me."],
226 search_case_insensitive(query, contents)
227 );
228 }
229}