Rust实践项目:电影信息维护程序

主要内容

  • 从txt文件导入电影信息(一次性)
  • 基于命令行的电影管理小程序(CRUD)
    • 登录验证
    • CRUD

txt文件示例:

DVD.txt

1
2
3
4
5
6
7
8
9
10
11
12
13
14
5.
1987 华尔街 Wall Street.mkv
1998 细细的红线 The Thin Red Line.mkv
1999 星球大战1魅影危机 Star Wars Episode I The Phantom Menace.mkv
2000 猎杀U-571 U-571.mkv
2002 星球大战2克隆人的进攻 Star Wars Episode II Attack Of The Clones.mkv
2005 豺狼帝国 Empire of the Wolves.mkv
2005 翻译风波 The Interpreter.mkv
2005 星球大战3西斯的反击 Star Wars Episode III Revenge Of The Sith.mkv
2005 查理和巧克力工厂 Charlie and the Chocolate Factory.mkv(儿童)
2007 灵魂战车 Ghost Rider Extended Cut.mkv
2008 好好先生 Yes Man.mkv
2009 真人游戏 Gamer.mkv
2009 巫山历险记 Race To Witch Mountain.mkv

从txt文件导入电影信息

第一步:文件选择

添加原生文件打开/保存对话框:cargo add rfd

使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
use movie_importer::read_txt_to_json;
use std::{error::Error, process};

use rfd::FileDialog; // 引入文件选择框

fn main() -> Result<(), Box<dyn Error>> {
if let Some(file_path) = FileDialog::new()
.add_filter("Text Files", &["txt"])
.set_title("⚙Select the DVD text file")
.set_directory("E:/Study/RustStudy/Projects/movie_importer")
.pick_file()
{
let save_path = read_txt_to_json(&file_path)?;
println!("Data saved to: {save_path:#?}");
Ok(())
} else {
eprintln!("File not selected.");
process::exit(-1);
}
}

第二步:txt转json

实现函数read_txt_to_json:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 读取txt文件 并根据规则转为json
pub fn read_txt_to_json(file_path: &PathBuf) -> Result<PathBuf, Box<dyn Error>> {
// 取出文本内容
let txt = fs::read_to_string(file_path)?;

// 定义光盘号
let mut disc_no = 0u32;
let disc_regex = Regex::new(r"^(\d+)\.$")?;
let movie_regex = Regex::new(r"^(\d{4})(.*?)((儿童))?$")?;
let mut movies = Vec::new();
// 处理文件内容 使用迭代器(iter)的map方法去除两边的空白 再用filter过滤掉空行
for line in txt.lines().map(str::trim).filter(|l| !l.is_empty()) {
if let Some(no) = disc_number(line, &disc_regex) {
disc_no = no;
} else if let Some(movie) = parse_movie(disc_no, line, &movie_regex) {
movies.push(movie);
}
}
save_to_json(movies)
}

实现帮助函数disc_number,parse_movie。这里需要添加正则表达式crate cargo add regex

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 将str转换成Movie对象
fn parse_movie(disc_no: u32, line: &str, re: &Regex) -> Option<Movie> {
re.captures(line).map(|cap| {
Movie::new(
disc_no,
cap.get(1).unwrap().as_str().trim().to_string(),
cap.get(2).unwrap().as_str().trim().to_string(),
cap.get(3).map(|mat| mat.as_str().trim().to_string()),
)
})
}

// 判断是否为光盘号
fn disc_number(line: &str, regex: &Regex) -> Option<u32> {
regex
.captures(line)
.map(|cap| cap.get(1).unwrap().as_str().parse().unwrap())
}

补上Movie的定义:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 构建结构体 存储文件信息
#[derive(Debug, Serialize, Deserialize)]
struct Movie {
disc: u32, // 光盘号
year: String, // 年份
title: String, // 电影名称
remark: Option<String>, // 备注
}

impl Movie {
fn new(disc: u32, year: String, title: String, remark: Option<String>) -> Movie {
Movie {
disc,
year,
title,
remark,
}
}
}

最后实现保存为json文件,添加cratecargo add serde -F derivecargo add serde_json实现json序列化:

1
2
3
4
5
6
7
8
9
10
11
fn save_to_json(movies: Vec<Movie>) -> Result<PathBuf, Box<dyn Error>> {
let json_str = serde_json::to_string_pretty(&movies)?;
let path = FileDialog::new()
.add_filter("Json", &["json"])
.set_title("Save Data To Json File")
.set_directory("E:/Study/RustStudy/Projects/movie_importer")
.save_file()
.ok_or_else(|| "No Save location selected".to_string())?;
fs::write(&path, json_str)?;
Ok(path)
}

基于命令行的电影管理小程序

命令行程序设计:

  • movie
    • movie login -username 然后输入 password
    • movie logout
    • moive list
    • movie add -disc -year -title -remark
    • movie delete -disc -index
    • movie edit -disc -index 然后输入四个字段

第一步:添加clap crate

运行命令:cargo add clap -F dervie

关于clap:clap - crates.io: Rust 包注册中心 — clap - crates.io: Rust Package Registry

clap使用示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
use clap::Parser;

/// Simple program to greet a person
#[derive(Parser, Debug)]
#[command(version, about, long_about = None)]
struct Args {
/// Name of the person to greet
#[arg(short, long)]
name: String,

/// Number of times to greet
#[arg(short, long, default_value_t = 1)]
count: u8,
}

fn main() {
let args = Args::parse();

for _ in 0..args.count {
println!("Hello {}!", args.name);
}
}

试试它:

1
2
3
4
5
6
7
8
9
10
11
12
13
$ demo --help
A simple to use, efficient, and full-featured Command Line Argument Parser

Usage: demo[EXE] [OPTIONS] --name <NAME>

Options:
-n, --name <NAME> Name of the person to greet
-c, --count <COUNT> Number of times to greet [default: 1]
-h, --help Print help
-V, --version Print version

$ demo --name Me
Hello Me!

第二步:实现命令

我们要有movie login -username,这里的login是一个子命令,所以我们要这么设计:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#[derive(Parser)] // 用于将结构体字段转换为命令行参数
#[command(version, about = "Movie app", long_about = "Movie information app")] // 一种宏属性 用于设置Cli的原信息
struct Cli {
#[command(subcommand)] // 表示这是个子命令
command: Option<Commands>, // Option 表示可有可无
}

#[derive(Subcommand)] // 子命令
enum Commands {
/// User log into system
Login {
/// The username of the user
#[arg(short, long)]
username: String,
},
}

加上其他命令:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
#[derive(Subcommand)]
enum Commands {
/// User log into system
Login {
/// The username of the user
#[arg(short, long)]
username: String,
},

/// Log out
Logout,

/// List all the movies
List,

/// Add a movie
Add {
// The disc no. of the movie
#[arg(short, long)]
disc: usize,

// The year when the movie was released
#[arg(short, long)]
year: String,

// The title / file name of the movie
#[arg(short, long)]
title: String,

// Optional remark of the movie
#[arg(short, long)]
remark: Option<String>,
},

/// Delete a movie
Delete {
/// The disc no. of the movie
#[arg(short, long)]
disc: usize,
/// The index of the movie in the disc
#[arg(short, long)]
index: usize,
},

/// Modify a movie
Edit {
/// The disc no. of the movie
#[arg(short, long)]
disc: usize,
/// The index of the movie in the disc
#[arg(short, long)]
index: usize,
},
}

对命令进行处理:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
fn main() -> Result<(), Box<dyn Error>> {
let cli = Cli::parse();
match &cli.command {
Some(Commands::Login { username }) => handle_login(username)?,
Some(Commands::Logout) => handle_logout()?,
Some(Commands::List) => handle_list()?,
Some(Commands::Add {
disc,
year,
title,
remark,
}) => handle_add(disc, year, title, remark)?,
Some(Commands::Delete { disc, index }) => handle_delete(disc, index)?,
Some(Commands::Edit {disc, index}) => handle_edit(disc, index)?,
_ => {
println!("No command provided or command not recoginized.")
}
}

Ok(())
}

实现handle.rs处理命令行(类似Web MVC 中的controller):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
use crate::models::{Movie, Role};
use crate::services::{
get_logged_in_role, get_users, list_movies, login_success, logout, read_from_json,
write_to_json,
};
use std::cmp::PartialEq;
use std::error::Error;
use std::io;
use std::io::Write;

pub fn handle_login(username: &str) -> Result<(), Box<dyn Error>> {
println!("Username: {username}");

if let Some(user) = get_users()
.iter()
.find(|user| user.username.eq_ignore_ascii_case(username))
{
println!("Please enter the password:");
// let mut pwd = String::new();
// if io::stdin().read_line(&mut pwd).is_ok() {
// println!("Log in successfully.");
// } else {
// println!("Failed to read password.")
// }

match rpassword::read_password() {
Ok(password) => {
if user.password.eq(&password) {
login_success(&user.role)?;
println!("Log in successfully.");
} else {
println!("Incorrect password.")
}
}
Err(_) => {
println!("Failed to read password.")
}
}
} else {
println!("User not found.")
}
Ok(())
}

pub fn handle_logout() -> Result<(), Box<dyn Error>> {
match logout() {
Ok(_) => {
println!("Log out succsessfully.");
}
Err(_) => {
println!("Please Log in before this action.");
}
}
Ok(())
}

pub fn handle_list() -> Result<(), Box<dyn Error>> {
match get_logged_in_role()? {
None => {
println!("You need to log in to view the movies.")
}
Some(_) => {
let movies = read_from_json()?;
list_movies(&movies);
}
}
Ok(())
}

pub fn handle_add(
disc: &usize,
year: &String,
title: &String,
remark: &Option<String>,
) -> Result<(), Box<dyn Error>> {
match get_logged_in_role()? {
Some(Role::Admin) => {
let mut movies = read_from_json()?;
let new_movie = Movie {
disc: *disc,
year: year.to_string(),
title: title.to_string(),
remark: remark.clone(),
};

movies.push(new_movie);
write_to_json(&movies)?;
println!("Movie added.")
}
_ => {
println!("You need to log in as Admin to add a movie.")
}
}

Ok(())
}

pub fn handle_delete(disc: &usize, index: &usize) -> Result<(), Box<dyn Error>> {
match get_logged_in_role()? {
Some(Role::Admin) => {
let movies = read_from_json()?;

if let Some(movie) = movies
.iter()
.filter(|m| m.disc == *disc)
.enumerate()
.find(|(i, _)| *i == *index)
.map(|(_, m)| m.clone())
{
let movies = movies
.into_iter()
.filter(|m| *m != movie)
.collect::<Vec<Movie>>();
write_to_json(&movies)?;
println!("Movie deleted.")
}
}
_ => {
println!("You need to log in as Admin to add a movie.")
}
}

Ok(())
}

pub fn handle_edit(disc: &usize, index: &usize) -> Result<(), Box<dyn Error>> {
match get_logged_in_role()? {
Some(Role::Admin) => {
let mut movies = read_from_json()?;

if let Some(movie) = movies
.iter_mut()
.filter(|m| m.disc == *disc)
.enumerate()
.find(|(i, _)| *i == *index)
.map(|(_, m)| m)
{
println!("The movie disc no.: {}", movie.disc);
print!("Enter the new disc no.: ");
io::stdout().flush()?;

let mut disc = String::new();
io::stdin().read_line(&mut disc)?;
let disc = disc.trim();
if let Ok(disc) = disc.parse::<usize>() {
movie.disc = disc;
} else {
println!("Invalid disc number.");
return Ok(());
}

println!("The movie year: {}", movie.year);
print!("Enter the new year: ");
io::stdout().flush()?;

let mut year = String::new();
io::stdin().read_line(&mut year)?;
let year = year.trim();
if !year.is_empty() {
movie.year = year.to_string();
} else {
println!("Invalid year");
return Ok(());
}

println!("The movie title: {}", movie.title);
print!("Enter the new title: ");
io::stdout().flush()?;
let mut title = String::new();
io::stdin().read_line(&mut title)?;
let title = title.trim();
if !title.is_empty() {
movie.title = title.to_string();
} else {
println!("The title cannot be empty.");
return Ok(());
}

println!("The movie remark: {}", movie.title);
print!("Enter the new remark: ");
io::stdout().flush()?;
let mut remark = String::new();
io::stdin().read_line(&mut remark)?;
let remark = remark.trim();
if !remark.is_empty() {
movie.remark = Some(remark.to_string());
} else {
movie.remark = None;
}

write_to_json(&movies)?;
println!("Movie modified.")
}
}
_ => {
println!("You need to log in as Admin to add a movie.")
}
}

Ok(())
}

实现services.rs(类似Web MVC 中的service):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
use crate::models::{Movie, Role, User};
use std::error::Error;
use std::{fs, io};

// 模仿从数据库读取用户
pub fn get_users() -> Vec<User> {
vec![
User {
username: "Admin".to_string(),
password: "admin".to_string(),
role: Role::Admin,
},
User {
username: "Dave".to_string(),
password: "Mustaine".to_string(),
role: Role::User,
},
User {
username: "Nick".to_string(),
password: "Carter".to_string(),
role: Role::User,
},
]
}

pub fn login_success(role: &Role) -> Result<(), Box<dyn Error>> {
fs::write(".session", role.to_string())?;
Ok(())
}

pub fn get_logged_in_role() -> Result<Option<Role>, Box<dyn Error>> {
let role = fs::read_to_string(".session");
if let Ok(role) = role {
match role.as_str() {
"Administrator" => Ok(Some(Role::Admin)),
"User" => Ok(Some(Role::User)),
_ => Ok(None),
}
} else {
// println!("Please Log in before this action.");
Ok(None)
}
}

pub fn logout() -> Result<(), Box<dyn Error>> {
fs::remove_file(".session")?;
Ok(())
}

pub fn read_from_json() -> Result<Vec<Movie>, Box<dyn Error>> {
let file = fs::File::open("movie.json")?;
let reader = io::BufReader::new(file);
let movies = serde_json::from_reader(reader)?;
Ok(movies)
}

pub fn list_movies(movies: &[Movie]) {
println!("{:<5}{:<10}{:<80}{:<15}", "Disc", "Year", "Title", "Remark");
println!("{:-<110}", "");

movies.iter().for_each(|m| {
let remark = m.remark.as_deref().unwrap_or("");
let title = pad_display_width(&m.title, 80);
let remark = pad_display_width(remark, 15);
println!("{:<5}{:<7}{}{}", m.disc, m.year, title, remark);
})
}

pub fn pad_display_width(text: &str, target_width: usize) -> String {
let width = unicode_width::UnicodeWidthStr::width(text);
format!("{}{}", text, " ".repeat(target_width.saturating_sub(width)))
}

pub fn write_to_json(movies: &[Movie]) -> Result<(), Box<dyn Error>> {
let file = fs::File::create("movie.json")?;
let writer = io::BufWriter::new(file);

serde_json::to_writer_pretty(writer, movies)?;
Ok(())
}

实现models.rs:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
use serde::{Deserialize, Serialize};
use std::fmt::{Display, Formatter};

pub struct User {
pub username: String,
pub password: String,
pub role: Role,
}

pub enum Role {
Admin,
User,
}

impl Display for Role {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
Role::Admin => {
write!(f, "Administrator")
}
Role::User => {
write!(f, "User")
}
}
}
}

#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct Movie {
pub disc: usize,
pub year: String,
pub title: String,
pub remark: Option<String>,
}

impl PartialEq for Movie {
fn eq(&self, other: &Self) -> bool {
self.disc == other.disc
&& self.year == other.year
&& self.title == other.title
&& self.remark == other.remark
}
}

这些都需要声明在lib.rs:

1
2
3
pub mod handler;
pub mod models;
pub mod services;

cargo.toml:

1
2
3
4
5
6
7
8
9
10
11
[package]
name = "movie"
version = "0.1.0"
edition = "2024"

[dependencies]
clap = { version = "4.5.40", features = ["derive"] }
rpassword = "7.4.0" # 用于输入命令行输入密码不显示
serde = { version = "1.0.219", features = ["derive"] }
serde_json = "1.0.140"
unicode-width = "0.2.1" # 用于格式化命令行输入