Serialization / Deserialization
You probably don't want to hand-type your list of users and recompile every time users change! You might use a local passwords file, or even a database. In this section, we'll look at how to serialize and deserialize data to and from a file.
The code for this is in
login_lib_json
andlogin_json
.
Dependencies
Serde is the de-facto standard serialization/deserialization library. It's very flexible, and can be used to serialize to and from JSON, XML, YAML, and more. We'll use JSON here.
The first thing to do is to add some dependencies to your auth
project.
You need the serde
crate, with the feature derive
. Run:
cargo add serde -F derive
You also need serde_json
:
cargo add serde_json
These commands make your Cargo.toml
file look like this:
[package]
name = "login_lib_json"
version = "0.1.0"
edition = "2021"
[dependencies]
serde = { version = "1.0.163", features = ["derive"] }
serde_json = "1.0.96"
Making Data Serializable
Import the Serialize
and Deserialize
macros:
#![allow(unused)] fn main() { use serde::{Serialize, Deserialize}; }
Then decorate your types with #[derive(Serialize, Deserialize)]
:
#![allow(unused)] fn main() { #[derive(PartialEq, Debug, Serialize, Deserialize)] pub enum LoginAction { Granted(LoginRole), Denied, } #[derive(PartialEq, Debug, Clone, Serialize, Deserialize)] pub enum LoginRole { Admin, User, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct User { pub username: String, pub password: String, pub role: LoginRole, } }
The macros write all the hard code for you. The only requirement is that every type you are including must also support Serialize and Deserialize. You can implement traits and write the serialization by hand if you prefer - but it's very verbose.
Now let's change our "get_users" system to work with a JSON file.
Serializing to JSON
First, we add a get_default_users
function. If there isn't a users file, we'll use this to make one:
#![allow(unused)] fn main() { fn get_default_users() -> HashMap<String, User> { let mut users = HashMap::new(); users.insert("admin".to_string(), User::new("admin", "password", LoginRole::Admin)); users.insert("bob".to_string(), User::new("bob", "password", LoginRole::User)); users } }
Next, let's change the get_users
function to look for a users.json
file and see if it exists:
#![allow(unused)] fn main() { pub fn get_users() -> HashMap<String, User> { let users_path = Path::new("users.json"); if users_path.exists() { // Load the file HashMap::new() } else { // Create a file and return it let users = get_default_users(); let users_json = serde_json::to_string(&users).unwrap(); std::fs::write(users_path, users_json).unwrap(); users } } }
That's all there is to creating a JSON file! We use serde_json::to_string
to convert our users
HashMap into a JSON string, and then write it to the file. Run the program, and users.json
will appear:
{"bob":{"username":"bob","password":"password","role":"User"},"admin":{"username":"admin","password":"password","role":"Admin"}}
Deserializing from JSON
Let's extend the get_users
function to read from users.json
if it exists:
#![allow(unused)] fn main() { pub fn get_users() -> HashMap<String, User> { let users_path = Path::new("users.json"); if users_path.exists() { // Load the file let users_json = std::fs::read_to_string(users_path).unwrap(); let users: HashMap<String, User> = serde_json::from_str(&users_json).unwrap(); users } else { // Create a file and return it let users = get_default_users(); let users_json = serde_json::to_string(&users).unwrap(); std::fs::write(users_path, users_json).unwrap(); users } } }
Equally simple - you load the file, deserialize it with serde_json::from_str
, and you're done! You can now edit the JSON file, and your changes will be loaded when a user tries to login.
Let's change admin's password to password2
and test it.