2023-11-15: Simple Library System in Rust

Here, I look at how to represent a typical object-oriented design, such as would be created in Java, within Rust. (Indeed, this example is based on a coursework I set for one of my Java programming modules.)

Rust's structs/methods/traits bear some resemblance to Java's classes/interfaces, but there are some key differences which require us to rethink any existing OO preconceptions. The design and implementation here is one way to solve the problem: there are, of course, many others.

Download code: library-system-source.zip

Representing Individual Items

The idea of the library system is to manage a collection of items. There are four kinds of item: books, cds, dvds, magazines. All of the items can be loaned out by the library.

In my original Java implementation, I had a "LoanableItem" parent class, with each of the four kinds of item being a child class. Fields were distributed between the parent and child classes, and the parent was made an abstract class.

For my Rust implementation, I have created one enum and one struct. The struct represents the "LoanableItem" - each field in the struct holds data relevant to all kinds of item, including the item id, and its return date. One field of the struct is its "kind" - the kind uses the enum "ItemKind", which specifies which of the four kinds of item this particular item is.

    enum ItemKind {
        Book(String),
        Cd(String),
        Dvd(String),
        Magazine(String),
    }
    
    pub struct LoanableItem {
        id: usize,
        title: String,
        return_date: Option<String>,
        kind: ItemKind,
    }

Although I didn't do this here, Rust's enums are flexible enough to make the definition of, e.g., Book hold whatever information you require, independently of the other kinds of item.

We can now create functions for "LoanableItem", such as one to check if an item is out on loan:

    impl LoanableItem {
        pub fn on_loan(&self) -> bool {
            self.return_date != None
        }

    // ... other functions
    }

Values of different kinds of item are easily created:

  let book = LoanableItem { 
    id: 1001,
    title: String::from("The Hobbit"),
    return_date: None,
    ItemKind::Book(String::from("J.R.R. Tolkien")),
  };
  
  let cd = LoanableItem { 
    id: 1201,
    title: String::from("Fifth Symphony"),
    return_date: None,
    ItemKind::Cd(String::from("Ludwig van Beethoven")),
  };

Collection

The library system needs to store lots of these "LoanableItem" values in some kind of collection. The collection will own the values, and keep them in a Vec:

    pub struct Collection {
        items: Vec<LoanableItem>,
    }

To add items to the collection, some convenience functions have been created. Hence, a small library could be created by:

    let mut library = library::new();
    library.create_book(1, "The Hobbit", "J.R.R. Tolkien");
    library.create_cd(2, "6th Symphony", "G. Mahler");

The convenience functions call create_item to create a new "LoanableItem" and push it onto the collection:

    impl Collection {
        // .. other functions
        
        fn create_item(&mut self, id: usize, title: &str, kind: ItemKind) {
            self.items.push(LoanableItem {
                id, 
                title: String::from(title),
                return_date: None,
                kind,
            });
        }

        pub fn create_book(&mut self, id: usize, title: &str, author: &str) {
            self.create_item(id, title, ItemKind::Book(String::from(author)));
        }
    }

Most of the interesting functions live in the Collection implementation:

For example:

    impl Collection {
        // ... other functions
        
        pub fn remove_item_by_id(&mut self, id: usize) -> bool {
            for i in 0..self.items.len() {
                if self.items[i].id == id {
                    self.items.remove(i);
                    return true;
                }
            }
            false
        }
    }

Save/Load Collection as a File

The Collection implementation also contains methods to save and load the collection to file, using the serde crate to represent the data in JSON format.

To make serde recognise our date, we need to provide the Serialize and Deserialize traits for our ItemKind, LoanableItem and Collection enum/struct types: this is however simply a matter of adding a "derive" line before each definition, e.g.:

    #[derive(Serialize, Deserialize)]
    pub struct Collection {
        items: Vec<LoanableItem>,
    }

The save/load code is almost trivial: passing a reference to the collections Vec to serde's json::to_string function returns a string representation of the collection, which can be written to file:

    impl Collection {
        // ... other functions
        
        pub fn save_collection(&self, filename: &str) -> Result<(), std::io::Error> {
            let serialised = serde_json::to_string(&self.items)?;
            println!("{}", serialised);
            std::fs::write(filename, serialised)?;
            Ok(())
        }
    }

And serde's json::from_str reconstructs the Vec from a string retrieved from a file:

    impl Collection {
        // ... other functions
        
        pub fn load_collection(&mut self, filename: &str) -> Result<(), std::io::Error> {
            let mut f = File::open(filename)?;
            let mut contents = String::new();
            f.read_to_string(&mut contents)?;
            self.items = serde_json::from_str(&contents)?;
            Ok(())
        }
    }

Comments on Code Layout

Before adding a command-line "frontend" to the library system, we can package the "backend" code into its own module, to keep the two parts of the library system separated and self-contained. This could be done by placing the code into a separate file, but, for this simple example, I used an explicit mod statement:

mod library {
    // definitions of ItemKind, LoanableItem and Collection, as above
  
     pub fn new() -> Collection {
        Collection { items: Vec::new() }
    } 
}

Notice the function new defined in the library. This is the entry-point to the library system for our "frontend" code: the code that constructs the initial library collection calls:

fn sample_library() -> library::Collection {
    let mut library = library::new();
    // .. add items to library    
    library
}

All interaction with the library system is then through the public methods provided by the Collection or LoanableItem structs.

Command-Line Interface

The command-line interface starts from the main function. This creates an instance of the library, then runs through a loop, showing a menu, reading a line of input, and responding:

fn main() {
    let mut library = sample_library();

    loop {
        show_menu();
        let mut choice = String::new();
        io::stdin().read_line(&mut choice)
            .expect("Missing input");
        match choice.trim().as_ref() {
            "1" => borrow_item(&mut library),
            "2" => return_item(&mut library),
            "3" => library.show_report_1(),
            "4" => library.show_report_2(),
            "5" => add_new_item(&mut library),
            "6" => remove_item(&mut library),
            "7" => save_collection(&library),
            "8" => load_collection(&mut library),
            "q" | "Q" => break,
            _ => println!("Unknown choice selected - try again."),
        }
    }

    println!("Goodbye!");
}

All of the options call a function which provides a wrapper around one of the Collection functions, first asking the user for relevant information, or displaying the requested information.

For example, return_item:

  1. asks for an id number to search for
  2. checks if it can find the item
  3. if so, calls the on_loan function to check if it is on loan
  4. if so, calls the return_to_library function of the item to return it
    fn return_item(library: &mut library::Collection) {
        println!("To return item, first find it:");
        let id = enter_find_by_id();
        if let Some(item) = library.find_by_id(id) {  
            if item.on_loan() {
                item.return_to_library();
                println!(" -- item {} returned", id);
            } else {
                println!(" -- item {} is not on loan!", id);
            }
        } else {
            println!(" -- could not find item {} to return", id);
        }
    }
    

Here's a sample of using the library:

Library Management System
=========================

1 Borrow Item
2 Return Item
3 Show Report - id order
4 Show Report - on-loan first

5 Add Item to Collection
6 Remove Item from Collection
7 Save Collection to File
8 Load Collection from File

Q Quit

Enter choice:
3

------------------------------
Library Collection Report 1:

1, Book, The Hobbit, no
2, CD, 6th Symphony, no
3, Book, Silmarillion, no
4, DVD, Lord of the Rings, no
5, Magazine, The Writers and Readers Magazine, no

TOTAL: 5 items with 0 on loan
------------------------------

Library Management System
=========================

1 Borrow Item
2 Return Item
3 Show Report - id order
4 Show Report - on-loan first

5 Add Item to Collection
6 Remove Item from Collection
7 Save Collection to File
8 Load Collection from File

Q Quit

Enter choice:
1
To borrow an item, first find it:
Enter ID number of item to return:
3
Enter 'return by' date (YYMMDD):
231231

Library Management System
=========================

1 Borrow Item
2 Return Item
3 Show Report - id order
4 Show Report - on-loan first

5 Add Item to Collection
6 Remove Item from Collection
7 Save Collection to File
8 Load Collection from File

Q Quit

Enter choice:
6
To remove an item, first find it:
Enter ID number of item to return:
4

Library Management System
=========================

1 Borrow Item
2 Return Item
3 Show Report - id order
4 Show Report - on-loan first

5 Add Item to Collection
6 Remove Item from Collection
7 Save Collection to File
8 Load Collection from File

Q Quit

Enter choice:
4

------------------------------
Library Collection Report 2:

3, Book, Silmarillion, yes, 231231
1, Book, The Hobbit, no
2, CD, 6th Symphony, no
5, Magazine, The Writers and Readers Magazine, no

TOTAL: 4 items with 1 on loan
------------------------------

Library Management System
=========================

1 Borrow Item
2 Return Item
3 Show Report - id order
4 Show Report - on-loan first

5 Add Item to Collection
6 Remove Item from Collection
7 Save Collection to File
8 Load Collection from File

Q Quit

Enter choice:
Q
Goodbye!

Page from Peter's Scrapbook, output from a VimWiki on 2024-11-29.