Rust 代码挑战系列 2
这是一个学习 Rust 的小系列文章,通过完成来自 shuttle 平台所举办的 2023 Christmas Code Hunt 里的每个小挑战,来学习 rust web 框架的使用,此为第二篇文章。
本篇内涉及了 5、6 这两部分的挑战内容。
前言
在上文中忘记介绍 shuttle 提供的 validator cli,使用该工具可本地验证自己的实现是否正确(若之前已经安装过,可再运行一遍确保安装了最新版):
# (全局)安装
cargo install cch23-validator
# 验证用例
cch23-validator 4
Part 5
圣诞老人的礼物名单太长了,我们需要帮他过滤下。
Task 1
# 输入输出示例
curl -X POST "http://localhost:8000/5?offset=3&limit=5" \
-H 'Content-Type: application/json' \
-d '[
"Ava", "Caleb", "Mia", "Owen", "Lily", "Ethan", "Zoe",
"Nolan", "Harper", "Lucas", "Stella", "Mason", "Olivia"
]'
["Owen", "Lily", "Ethan", "Zoe", "Nolan"]
新增一个路由:
Router::new().route("/5", post(d5));
实现路由函数:
pub async fn d5(
query: Query<HashMap<String, usize>>,
body: Json<Vec<String>>,
) -> Json<Vec<String>> {
let offset = query.get("offset").unwrap();
let limit = query.get("limit").unwrap();
let result = body
.iter()
.skip(*offset)
.take(*limit)
.map(|x| x.to_string())
.collect::<Vec<_>>();
Json::from(result)
}
我们使用了 Query
提取器来提取 url 参数信息,因为本例中的参数都是数值,因此直接使用 HashMap<String, usize>
来表示,而对于不同数据类型的参数,axum 自然也是支持的,将 HashMap
改成自定义的 struct 便可实现,此处暂不演示。
我们使用了 iterator 的 skip()
和 take()
来分别跳过指定个数和获取指定个数的元素,然后调用 map
将引用类型的元素转换为 owned 类型返回。
Task 2
该任务在上个任务的基础上新增了 split
参数,并且 offset
和 limit
不一定会包含,且返回的内容需要基于 split
进行拆分:
# 输入输出示例 1
curl -X POST http://localhost:8000/5?split=4 \
-H 'Content-Type: application/json' \
-d '[
"Ava", "Caleb", "Mia", "Owen", "Lily", "Ethan", "Zoe",
"Nolan", "Harper", "Lucas", "Stella", "Mason", "Olivia"
]'
[
["Ava", "Caleb", "Mia", "Owen"],
["Lily", "Ethan", "Zoe", "Nolan"],
["Harper", "Lucas", "Stella", "Mason"],
["Olivia"]
]
# 输入输出示例 2
curl -X POST "http://localhost:8000/5?offset=5&split=2" \
-H 'Content-Type: application/json' \
-d '[
"Ava", "Caleb", "Mia", "Owen", "Lily", "Ethan", "Zoe",
"Nolan", "Harper", "Lucas", "Stella", "Mason", "Olivia"
]'
[
["Ethan", "Zoe"],
["Nolan", "Harper"],
["Lucas", "Stella"],
["Mason", "Olivia"]
]
该任务仍使用的是 /5
路由,故我们直接在 d5
函数内做修改:
pub async fn d5(query: Query<HashMap<String, usize>>, body: Json<Vec<String>>) -> Json<Value> {
let offset = *query.get("offset").unwrap_or(&0);
let limit = query.get("limit");
let mut arr = body.iter().skip(offset).cloned().collect::<Vec<_>>();
match limit {
Some(limit) => {
if *limit == 0 {
return Json::from(json!([]));
}
if *limit > 0 {
arr = arr.iter().take(*limit).cloned().collect::<Vec<_>>();
}
}
None => {}
}
match query.get("split") {
Some(split) => {
let result = arr.chunks(*split).map(|x| x.to_owned()).collect::<Vec<_>>();
Json::from(json!(result))
}
None => Json::from(json!(arr)),
}
}
该任务中需要注意的几个点:
limit
参数的不同场景处理:为 0 时表示返回的是 0 个元素,大于 0 时表示返回指定个数的元素,不存在时表示返回所有元素- 在使用
split
参数的值拆分数组时,我们使用了Vec
提供的chunks()
函数,很方便得完成任务要求。
一些补充:
-
Rust 是类型严格的语言,下述写法是不行的:
let iter = body.iter().skip(offset); if limit > 0 { iter = iter.take(limit); // compiler error }
虽然
skip()
和take()
返回的都是 iterator,但 Iterator 是一个 trait,有很多类型都实现了它,rust 仅允许前后赋值是相同的类型。 -
上述我们使用了很多 iterator 的函数,其中在处理 offset 和 skip 时,我们也可以使用
Vec
的get
函数获取 slice 内容:// 传入 slice 来获取子数组内容 let mut arr = body.get(offset..(offset + limit));
Part 6
那些藏在货架(shelf)里的小精灵(elf)。
Task 1
精灵梦太会藏了,从这些字符里找到他们!
# 输入输出示例
curl -X POST http://localhost:8000/6 \
-H 'Content-Type: text/plain' \
-d 'The mischievous elf peeked out from behind the toy workshop,
and another elf joined in the festive dance.
Look, there is also an elf on that shelf!'
{"elf":4}
新增一个路由:
Router::new().route("/6", post(d6_1));
#[derive(Serialize)]
pub struct D6Response {
elf: u32,
}
pub async fn d6_1(str: String) -> Json<D6Response> {
let words = str
.split_whitespace()
.map(|item| item.trim_matches(|item: char| !item.is_alphabetic()))
.filter(|item| item.contains("elf"))
.collect::<Vec<_>>()
.len();
Json::from(D6Response { elf: words as u32 })
}
从字符串里找到 elf
,思路还是较为清晰的,按空格拆分得到单词,然后判断单词是否含有这些字母。
Task 2
# 输入输出示例
curl -X POST http://localhost:8000/6 \
-H 'Content-Type: text/plain' \
-d 'there is an elf on a shelf on an elf.
there is also another shelf in Belfast.'
{"elf":5,"elf on a shelf":1,"shelf with no elf on it":1}
仍使用 /6/
路由:
Router::new().route("/6", post(d6_2));
由于和任务 1 的返回值不同,我们新建一个路由处理函数:
use once_cell::sync::Lazy;
use regex::Regex;
#[derive(Debug, Serialize)]
pub struct D62Response {
elf: usize,
#[serde(rename(serialize = "elf on a shelf"))]
shelf_elf: usize,
#[serde(rename(serialize = "shelf with no elf on it"))]
shelf_no_elf: usize,
}
pub async fn d6_2(str: String) -> Json<D62Response> {
let elf_counts = str
.split_whitespace()
.map(|item| item.trim_matches(|item: char| !item.is_alphabetic()))
.fold(
(0, 0, 0),
|(elf_partial_words, shelf_words, elf_words), item| {
let mut ret = (elf_partial_words, shelf_words, elf_words);
if item.to_lowercase().contains("elf") {
// Match all "*elf*"
if item.contains("elf") {
ret.0 += 1;
}
// Match "shelf" word with case-insensitive
if item.to_lowercase().contains("shelf") {
ret.1 += 1;
}
// If it's not a "shelf", it should be a "elf"
else {
ret.2 += 1;
}
}
return ret;
},
);
static RE: Lazy<Regex> = Lazy::new(|| Regex::new(r"on a shelf").unwrap());
let result = RE.find_iter(&str).map(|item| item.as_str()).count();
Json::from(D62Response {
elf: elf_counts.0,
shelf_elf: result,
shelf_no_elf: elf_counts.1 - result,
})
}
这个任务的描述笔者感觉怪难理解的,读者朋友可查看原问题的描述,该问题的实现思路也并非是固定的,在此对个人实现中碰到的技术点做一些小结:
上面我们使用到了两个新包,因此需要安装一下:
cargo add regex once_cell
其中 regex
是用于实现正则表达式(rust 没有内置正则表达式的相关库),那 once_cell
是干嘛的呢?
从 regex
的文档描述 可知,反复进行同一个正则表达式的构建是一项开销很大的操作,因此需要确保其仅被构建一次,once_cell
给我们封装了这个能力,它可以实现全局数据管理和懒执行。
回到上述代码,我们使用到了 iterator 的 fold()
函数来实现累加逻辑,如果知道 javascript 的 reduce
函数那就能很快理解这个函数,当然 rust 里也有 reduce()
函数,它和 fold()
的区别是前者不支持传入初始值且返回的是一个 Option
。
小结
在挑战过程中,或许最阻碍我们更多的是对 rust 内置的 api 了解不多导致有点无从下手,笔者认为学一段时间后紧跟着做一遍梳理,有助于更好的掌握所学之物,隔一段时间自我梳理一遍自己经常用到的数据结构和它的 api,比如 String
、Vec
、Iterator
、HashMap
。
Vec
和 Iterator
在 rust 是两回事,各自有着不同的函数,对于 Javascript 玩家的我来说,老容易他俩当成一回事,这是不对的。