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}