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 参数,并且 offsetlimit 不一定会包含,且返回的内容需要基于 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)),
    }
}

该任务中需要注意的几个点:

一些补充:

  1. Rust 是类型严格的语言,下述写法是不行的:

    let iter = body.iter().skip(offset);
    if limit > 0 {
         iter = iter.take(limit); // compiler error
    }
    

    虽然 skip()take() 返回的都是 iterator,但 Iterator 是一个 trait,有很多类型都实现了它,rust 仅允许前后赋值是相同的类型。

  2. 上述我们使用了很多 iterator 的函数,其中在处理 offset 和 skip 时,我们也可以使用 Vecget 函数获取 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,比如 StringVecIteratorHashMap

VecIterator 在 rust 是两回事,各自有着不同的函数,对于 Javascript 玩家的我来说,老容易他俩当成一回事,这是不对的。