#2: Adding a Storage Module
Now that we have got our project up and running, let's actually implement the first piece we'll need to store data.
To do that, I made a storage.rs file in my /src directory. This will be our storage module that we'll import later to use. Just like many other programming languages, Rust comes with its own standard library that has many useful tools.
The one tool we're interested in today is HashMap
. You can import it like this:
use std::collections::HashMap;
We use the use keyword to import modules. In this case, we are HashMap from collections in standard library.
A HashMap, as the name suggests, can be used to store things like key/value pair in memory. In addition to that, it has built-in functions like .get()
or .remove()
that'll make our life easier.
Next, we want to define how our data will be stored. For the time being, let's say we want to store key/value pairs in a field called data. We can define the structure like this:
struct Database {
data: HashMap<String, String>,
}
The struct keyword tells Rust we want to define a data structure and call it Database. In that struct, we want to have a field called data which is an instance of the HashMap we imported. This HashMap will have key of type String and a value of type String. We will change this in the future.
Now let's implement the basic methods/functions to do some CRUD operations on the database. In Rust, we can define the functionalities a struct can expect, in this case, we want to let Rust know that our Database
struct will have 4 functions of new
, insert
, get
, delete
.
impl Database {
pub fn new() {}
pub fn insert() {}
pub fn get() {}
pub fn delete(){}
}
Here we used the impl
keyword to define the struct functionalities and defined the skeleton of our functions. The fn
keyword is used to define a function and the pub keyword tells Rust this function is public, meaning it can be called outside of the implementation.
Now let's define the new
function:
pub fn new() -> Self {
Database {
data: HashMap::new(),
}
}
This function is simple. The -> Self
is just defining the return type of the function. Since Self
is within the context of Database
, the return type will be of type Database
. That makes sense since within the function we are initializing an instance of the Database
struct and the data
field is also getting initialized with new function from HashMap
.
Notice that we don't have to tell Rust explicitly to return the initialized Database. In Rust, the last expression is returned by default.
Now that we can initialize a database, let's enable the "C" in "CRUD". Our insert function:
pub fn insert(&mut self, key: String, value: String) {
self.data.insert(key, value);
}
self
represents the instance of the Database
struct on which the method is called. &mut self
is a mutable reference to the instance, allowing the method to modify its state (in this case, the data
field).
If you used &self
, the method would only have read access and couldn't modify the Database
. If you used self
(without &
), the method would take ownership of the Database
, making it unusable outside the method unless explicitly returned.
self.data
accesses the data
field of the Database
instance, which is a HashMap<String, String>
. The insert
method of HashMap
is called to add or update an entry with the provided key
and value
.
For the next two functions, most of what was explained above applies. Here's how they'll look like:
pub fn get(&self, key: &str) -> Option<&String> {
self.data.get(key)
}
pub fn delete(&mut self, key: &str) -> Option<String> {
self.data.remove(key)
}
Notice that they have slightly different return types.
Option<&String>
means it returns Some(&String)
if the key exists in the HashMap
and None
if the key does not exist.
The &String
is a borrowed reference to the value stored in the HashMap
. This avoids ownership transfer. The caller can read the value but cannot modify it. The value remains owned by the HashMap
, ensuring safe access while preserving ownership.
For the delete function, Option<String>
means it returns Some(String)
if the key exists, containing the owned value that was associated with the key and it returns None
if the key does not exist.
The String
is moved out of the HashMap
and returned to the caller. This means the ownership of the value is transferred to the caller. That makes sense since after deletion, the HashMap really shouldn't keep track of/ be associated with the delete value.
Voila! We're done with implementation of the in memory storage. This will help us manage the stage of our data. In the upcoming blogs we'll see how we can use these functions.