Part 3. Implementing some methods (55 points)

At this point, you should have some code that can parse the fields of the index file and extract the author, title, and URL fields and stick them in an Entry.

In this part, you’re going to create an Index structure which will hold all of the Entrys and implement some methods on it to perform a simple search.

Example

Recall that to implement methods for a structure, we use an impl block.

#![allow(unused)]
fn main() {
#[derive(Debug, Clone)]
struct Example {
    name: String,
    url: Option<String>,
}

impl Example {
    fn new(name: String) -> Self {
        Self {
            name,
            url: None
        }
    }

    fn has_url(&self) -> bool {
        self.url.is_some()
    }

    fn url(&self) -> Option<String> {
        // Return a copy of the url member.
        self.url.clone()
    }

    fn set_url(&mut self, url: &str) {
        self.url = Some(url.to_string());
    }

    fn into_name(self) -> String {
        self.name
    }
}
}

Some things to notice here:

  1. There’s a new() function which returns a Self. Inside an impl block, Self refers to the type being implemented, here Example;
  2. The new() function does not take self, &self, or &mut self as an argument because this isn’t a method you call on an instance of an Example. Instead, you’d call it to create an instance. This is similar to a constructor in Java although it’s just convention and not inforced. To call new(), we use Example::new("Blarg".to_string()). It’s a good practice to have a new() function (which may or may not take arguments as the situation demands) and returns one of Self, Option<Self> or Result<Self, SomeErrorType>. The last two are used when it’s possible for the new() function to fail.
  3. The has_url() and url() methods take &self as the first argument. This is similar to Python’s self argument and Java’s implicit this argument in methods;
  4. The set_url() method takes &mut self rather than &self. This is necessary because modification to self requires that it be mutable. If you remove the mut from the method signature, it’ll fail to compile; and
  5. The into_name() method takes self rather than &self. This method takes ownership of the Example object (meaning it cannot be used after calling .into_name()) and returns the name field without making a copy (as the url() method does).

Your task

Define a new structure called Index which will hold all of the Entrys you’re going to parse from the index file. You’re going to implement the following methods.

#![allow(unused)]
fn main() {
type Result<T> = std::result::Result<T, Box<dyn std::error::Error>>;
struct Entry;
#[derive(Debug, Clone)]
struct Index {
    // TODO: What field(s) do you want here?
}

impl Index {
    /// Create a new `Index` by reading from `pgindex.txt`.
    fn new() -> Result<Self> {
        todo!()
    }

    /// Returns the number of entries in the index.
    fn len(&self) -> usize {
        todo!()
    }

    /// Returns a vector containing references to entries whose titles
    /// contain `search_string`.
    fn title_search(&self, search_string: &str) -> Vec<&Entry> {
        todo!()
    }

    /// Returns a vector containing references to entries whose authors
    /// contain `search_string`.
    fn author_search(&self, search_string: &str) -> Vec<&Entry> {
        todo!()
    }

    /// Returns a vector containing references to entries whose titles
    /// or authors contain `search_string`.
    fn search(&self, search_string: &str) -> Vec<&Entry> {
        todo!()
    }
}
}

To implement new(), take your code from the run() in the previous part and move it into new(). Rather than print out the entries, you’re going to need to collect them all into some data structure and store them in your Index structure and return that.

The len() method should return the number of entries in the index.

The three search methods should behave similarly: Each converts the search_string to lowercase. Then, for each entry in the index, convert the relevant fields (title, author, or both) to lowercase. If the lowercased search_string is contained in the field, add a reference to the corresponding entry to a results vector and return that.

You’ll probably want something like this.

    fn title_search(&self, search_string: &str) -> Vec<&Entry> {
        let mut results = Vec::new();
        for entry in ??? {
            if ??? {
                results.push(entry);
            }
        }

        results
    }

The other two are similar; however, remember that some entries don’t have authors. You can use

if let Some(ref author) = entry.author {
    // This entry did have an author and `author` is currently a reference
    // to the `String` stored inside the `Option<String>` that is the
    // `entry.author` field.
}

If you omit the ref in if let Some(ref author), you’ll get an error about trying to move out of entry which isn’t mutable so you cannot. ref tells Rust that you want reference instead.

Once you have implemented one of these functions, it’s worth testing your code. If you change your run() function to

fn run() -> Result<()> {
    let index = Index::new()?;

    println!("There are {} entries in the index", index.len());
    let results = index.title_search("the inferno");
    println!("{results:#?}");
    Ok(())
}

you should see three results, The Inferno by Barbusse and O’Brien, The Divine Comedy of Dante Alighieri: The Inferno by Dante, and The Inferno by Strindberg.

In the remaining parts of the lab, you’re going to handle parsing the resources, implementing the Display trait to make printing better, and finally the simple user interface shown in the lab’s overview.