Hello
I'm just learning Rust and experimenting with it, so far I actually like it a lot.
I've made a small application which allows you to manage your cats. You can add cats to the database (memory-based) and view them all.
Code:
//Module holding all the structs, enumerations,...
pub mod domain {
use std::string::ToString;
pub struct Cat {
name: String,
age: u8,
color: CatColor,
race: CatRace,
}
impl Cat {
pub fn new(name: String, age: u8, color: CatColor, race: CatRace) -> Cat {
Cat {
name: name,
age: age,
color: color,
race: race,
}
}
pub fn to_string(&self) -> String {
let mut s : String = String::new();
s = format!("Name: {name}\nAge: {age}\nColor: {color}\nRace: {race}",
name = self.name,
age = self.age,
color = self.color.to_string(),
race = self.race.to_string(),
);
s
}
}
#[derive(Display, Debug)]
pub enum CatColor {
Black,
White,
Orange,
Gray,
}
impl CatColor {
pub fn from_u8(value: u8) -> CatColor {
match value {
1 => CatColor::Black,
2 => CatColor::White,
3 => CatColor::Orange,
4 => CatColor::Gray,
_ => {
eprintln!("Invalid CatColor selected!");
std::process::exit(0);
}
}
}
}
#[derive(Display, Debug)]
pub enum CatRace {
Streetcat,
AmericanShorthair,
BritishShorthair,
Bengal,
EgyptianMau,
}
impl CatRace {
pub fn from_u8(value: u8) -> CatRace {
match value {
1 => CatRace::Streetcat,
2 => CatRace::AmericanShorthair,
3 => CatRace::BritishShorthair,
4 => CatRace::Bengal,
5 => CatRace::EgyptianMau,
_ => {
eprintln!("Invalid CatRace selected!");
std::process::exit(0);
}
}
}
}
}
//Module holding functions necessary for the userinterface
pub mod user_interface {
pub mod display {
use std::io;
//Function to display the start menu
pub fn display_start_menu() {
println!("Welcome to the cat database!");
println!("1) Add cat");
println!("2) View all cats");
}
//Function to display add-cat-menu
pub fn display_add_cat_menu() {
println!("Give all the info about the cat!");
}
//Function to display view-all-cats-menu
pub fn display_view_all_cats_menu() {
println!("List of all the cats:");
}
//Function to flush std output
pub fn flush() {
io::Write::flush(&mut io::stdout());
}
}
pub mod input {
use std::io;
use std::io::stdin;
//Function to get user input
pub fn get_u8() -> u8 {
print!(">");
//Flush display
io::Write::flush(&mut io::stdout());
//Get input and put it in mutable string
let mut input = String::new();
stdin()
.read_line(&mut input)
.expect("No valid input given...");
//If parsing succesful return value, else quit program
match input.trim().parse::<u8>() {
Ok(i) => {
i
}
Err(..) => {
eprintln!("{} is not a valid number...", input);
std::process::exit(0);
}
}
}
//Function to get user input
pub fn get_str() -> String {
print!(">");
//Flush display
io::Write::flush(&mut io::stdout());
//Get input and put it in mutable string
let mut input = String::new();
stdin()
.read_line(&mut input)
.expect("No valid input given...");
input
}
}
}
//Module to access, read, and write data
pub mod data_layer {
use std::vec::Vec;
use crate::domain::*;
//To manage data in memory
pub struct MemoryData {
cats: Vec<Cat>,
}
impl MemoryData {
pub fn new() -> MemoryData {
MemoryData { cats: Vec::new() }
}
pub fn add(&mut self, cat: Cat) -> bool {
self.cats.push(cat);
true
}
pub fn read(&mut self) -> &Vec<Cat> {
&self.cats
}
}
//*TO ADD:* Struct to manage data in text file and database
//will follow
}
//Module holding logic and binding everything together
pub mod app {
use crate::user_interface;
use crate::data_layer;
use crate::domain::*;
//Run the application
pub fn run(db: &mut data_layer::MemoryData) {
user_interface::display::display_start_menu();
let option: u8 = user_interface::input::get_u8();
execute_option(option, db);
}
//Execute the selected option with corresponding function
pub fn execute_option(option: u8, db: &mut data_layer::MemoryData) {
match option {
1 => {
add_cat(db);
}
2 => {
view_all_cats(db);
}
_ => {
eprintln!("Invalid option '{}' selected...", option);
}
}
}
//Function to add a cat
pub fn add_cat(db: &mut data_layer::MemoryData) {
//Displaying menu header
user_interface::display::display_add_cat_menu();
//Getting al the info
println!("Name:");
let name: String = user_interface::input::get_str();
println!("Age:");
let age: u8 = user_interface::input::get_u8();
println!("Color:\n1) Black 2) White 3) Orange 4) Gray");
let color: CatColor = CatColor::from_u8(user_interface::input::get_u8());
println!("Race:\n1) Streetcat 2) American Shorthair 3) British Shorthair 4) Bengal 5) Egyptian Mau");
let race: CatRace = CatRace::from_u8(user_interface::input::get_u8());
//Create cat object
let cat: Cat = Cat::new(name, age, color, race);
//Save cat object
db.add(cat);
println!("Cat added...");
}
//Function to view all cats
pub fn view_all_cats(db: &mut data_layer::MemoryData) {
//Displaying menu header
user_interface::display::display_view_all_cats_menu();
//user_interface::display::flush();
std::io::Write::flush(&mut std::io::stdout());
//Reading, iterating over, and outputting all the cats
for cat in db.read() {
println!("{}", cat.to_string());
}
}
}
//Crates
extern crate strum;
#[macro_use]
extern crate strum_macros;
fn main() {
//Initialize database
let ref mut db = data_layer::MemoryData::new();
while true {
app::run(db);
}
}
It works fine. But while I was making this 'application' to learn and experiment with Rust I got a few questions about the language, and how to do things efficiently and with using best-practices.
1) Function overloading
If I'm right, Rust doesn't have an option for function overloading. Which gave me a problem for the code on line 112 in the module user_interface::input. I want a function to easily get the user-input. However, that input can be a String, short (i8), integer (i32), float, double,... now I have written two functions: get_u8() and get_str(). What bothers me is that both these functions have almost exactly the same function-body only a different return value. What would be a more efficient way to do this and reducing the use of the same code? Traits? Generic functions?
2) Macros
I don't really get macros. When and why would I use it over a function? println!() for example... Why isn't it just println() ?
3) Difference between & and &mut
What exactly is the difference between these two? From my own understanding & allows you to point to a variable in memory, but you only have read-access. With &mut you can point to a variable in memory but also change the value of that variable you're pointing to. Is that correct?
4) Int to enum
Is there an easy way to cast an integer to an enumeration?
That 0 would equal the first value in the enum and so forth? Like the casting in C++?
Now I used a match-statement in the implementation of my struct. But can this be done more efficiently?
5) Better way to use MemoryData object
On line 158 I have a module called data_layer, this module holds all the code to save, read, and write data. Now I have only a struct called MemoryData. This struct holds the properties and methods to save my cats into memory.
This object is initialized in the main-function and then passed through the rest of the code using reference parameters.
But this is the problem: In the future I also want other structs like FileData and DatabaseData which allows you to persist the data in a file or database and not only the memory.
However, if I want to change the way my program saves it's data, I have to change all the values of the reference-parameters in my code... Is there a more efficient way to do this? Like making the db-variable global or by using generics/interfaces like in C#?
I want to be able to switch between using the MemoryData, FileData and DatabaseData by only changing one line of code. And that'd be line 261. I want to change let ref mut db = data_layer::MemoryData::new();
to let ref mut db = data_layer::FileData::new();
and it should work. So my parameters need a kind of generic type?
6) External crates
If you copy/paste my code and execute it, it probably won't work. Because I used an external dependency called Strum. Is it accepted to use these dependencies as much as you want to solve basic issues? Or is this considered bad because of performance reasons or crates getting outdated,...?
What's best practice to use these dependencies and share code using these dependencies?
7) Return keyword
I can use the return keyword, but I don't need to. Is there a difference when using it or when not using it? What's preferred?
8) Output flush
The only issue I got in my code is that when outputting all the cats, there is a newline after 'Name'. How to get rid of it?
9) Modules
I have used the modules like I kind of would use namespaces in other languages. I grouped the code relevant to each other basing myself on architecture principles like n-tier,... Is the way I've done it good?
I'd actually like to put each module in a separate file. And make a folder for each module having nested modules. So that the nested modules will be come a file each and the parent-module the folder name.
Wouldn't this be better than having everything in one file? How would I do that?
10) Feedback
I'd appreciate feedback on my code and how it's structured. Is everything clear and easy to read? What could be improved? Is it also Rusty enough or is it too OOP?
Thanks a lot!