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:
-
find_by_id
- to return a reference to an item by its id number -
remove_item_by_id
- to delete an item after finding it by its id -
show_report
- to print out information about the items in the collection
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
:
- asks for an id number to search for
- checks if it can find the item
-
if so, calls the
on_loan
function to check if it is on loan -
if so, calls the
return_to_library
function of the item to return itfn 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!