原文链接:https://alexis-lozano.com/hexagonal-architecture-in-rust-6/

翻译:trdthg

选题:trdthg

本文由 Rustt 翻译,StudyRust 荣誉推出

2021-10-09 - Rust 六边形架构 #6 - CLI

嘿,好久不见!上次,我们实现了剩余的用例,并将它们连接到我们的 API。现在,我想添加另一种方式来使用我们的程序。我们将使用 CLI。CLI 是 Command Line Interface (命令行接口) 的意思,它只是一个缩写词,意思是:“让我们通过终端使用这个程序”。

搭建脚手架

构建 CLI 意味着我们需要在项目中添加新的依赖和一个新文件夹。让我们从添加依赖开始。我们需要一种方法,再运行之前需要提示用户是运行 CLI 还是 HTTP API。我们将使用 clap 添加命令行开关让用户选择启动方式,同时还会使用 dialoguer 去创建提示信息。

打开 Cargo.toml,并添加:

[dependencies]
...
clap = "2.33.3"
dialoguer = "0.8.0"

当你再读这篇文章时可以将依赖换为最新的。现在让我们添加一些命令行开关,打开 main.rs:

#[macro_use]
extern crate clap;

use clap::{App, Arg};
And then we can use it:

fn main() {
    let repo = Arc::new(InMemoryRepository::new());

    let matches = App::new(crate_name!())
        .version(crate_version!())
        .author(crate_authors!())
        .arg(Arg::with_name("cli").long("cli").help("Runs in CLI mode"))
        .get_matches();

    match matches.occurrences_of("cli") {
        0 => api::serve("localhost:8000", repo),
        _ => unreachable!(),
    }
}

所以,首先我们创建了存储库。然后我们创建一个处理 CLI 的 clap 应用程序。如果在运行程序时添加 --cli 标志,程序现在将 panic。如果没有添加,就会运行 API。正如我之前所说,clap 能让我们快速创建一个 CLI。您可以通过运行来尝试:

cargo run -- --help

pokedex 0.1.0
Alexis Lozano <alexis.pascal.lozano@gmail.com>

USAGE:
    pokedex [FLAGS]

FLAGS:
        --cli        Runs in CLI mode
    -h, --help       Prints help information
    -V, --version    Prints version information

非常不错是吧 :)

现在我们要把 unreachable!() 替换为 cli::run(repo)。我们现在要创建一个 cli 模块,所有和 cli 相关的代码都会在该模块里。在 main.rs 里引入模块:

mod cli;

接着让我们创建 src/cli 文件夹,并在其中添加一个 mod.rs 文件。在 cli/mod.rs 中添加以下代码:

use crate::repositories::pokemon::Repository;
use std::sync::Arc;

pub fn run(repo: Arc<dyn Repository>) {}

现在运行 cargo run -- --cli 应该不会 panic 了。

接着让我们创建一个循环:

use dialoguer::{theme::ColorfulTheme, Select};

pub fn run(repo: Arc<dyn Repository>) {
    loop {
        let choices = [
            "Fetch all Pokemons",
            "Fetch a Pokemon",
            "Create a Pokemon",
            "Delete a Pokemon",
            "Exit",
        ];
        let index = match Select::with_theme(&ColorfulTheme::default())
            .with_prompt("Make your choice")
            .items(&choices)
            .default(0)
            .interact()
        {
            Ok(index) => index,
            _ => continue,
        };

        match index {
            4 => break,
            _ => continue,
        };
    }
}

我们列出了所有用户能够运行的命令。如果用户选择 Exit,程序就会退出。否则,程序暂时什么都不会做。别担心,我们马上就会实现其他的命令。

创建一个宝可梦

让我们从创建开始。如果我们能够有方法向存储库中添加宝可梦,后面的测试就更容易做了:

match index {
    2 => create_pokemon::run(repo.clone()),
    ...
};

现在我们需要创建一个新的模块:

mod create_pokemon;

好了,在 cli/create_pokemon.rs 中填加上对应的函数签名:

use crate::repositories::pokemon::Repository;
use std::sync::Arc;

pub fn run(repo: Arc<dyn Repository>) {}

为了创建一个宝可梦,CLI 需要向用户询问宝可梦的 编号、名称和类型。为了方便这个过程,并且提高提示信息的复用性,我们会为这些信息分别实现各自的提示函数:

use crate::cli::{prompt_name, prompt_number, prompt_types};

pub fn run(repo: Arc<dyn Repository>) {
    let number = prompt_number();
    let name = prompt_name();
    let types = prompt_types();
}

接着在 cli/mod.rs 中实现:

use dialoguer::{..., Input, MultiSelect};

pub fn prompt_number() -> Result<u16, ()> {
    match Input::new().with_prompt("Pokemon number").interact_text() {
        Ok(number) => Ok(number),
        _ => Err(()),
    }
}

pub fn prompt_name() -> Result<String, ()> {
    match Input::new().with_prompt("Pokemon name").interact_text() {
        Ok(name) => Ok(name),
        _ => Err(()),
    }
}

pub fn prompt_types() -> Result<Vec<String>, ()> {
    let types = ["Electric", "Fire"];
    match MultiSelect::new()
        .with_prompt("Pokemon types")
        .items(&types)
        .interact()
    {
        Ok(indexes) => Ok(indexes
            .into_iter()
            .map(|index| String::from(types[index]))
            .collect::<Vec<String>>()),
        _ => Err(()),
    }
}

提示:多选框按空格选中

如你所见,所有的提示都可能失败,让我们回到 cli/create_pokemon.rs,有了用户的输入信息,我们可以将它封装为用例中需要的 Request 结构体:

use crate::domain::create_pokemon;

pub fn run(repo: Arc<dyn Repository>) {
    ...
    let req = match (number, name, types) {
        (Ok(number), Ok(name), Ok(types)) => create_pokemon::Request {
            number,
            name,
            types,
        },
        _ => {
            println!("An error occurred during the prompt");
            return;
        }
    };

当发生输入错误时,我们会退回到主菜单。现在有了 Request 结构体,我们就能调用创建宝可梦的用例了:

pub fn run(repo: Arc<dyn Repository>) {
    ...
    match create_pokemon::execute(repo, req) {
        Ok(res) => {},
        Err(create_pokemon::Error::BadRequest) => println!("The request is invalid"),
        Err(create_pokemon::Error::Conflict) => println!("The Pokemon already exists"),
        Err(create_pokemon::Error::Unknown) => println!("An unknown error occurred"),
    };
}

我们处理了所有的错误类型,每种错误都会反馈到用户的终端上。当用例执行成功时,我们会返回一个 Response

#[derive(Debug)]
struct Response {
    number: u16,
    name: String,
    types: Vec<String>,
}

pub fn run(repo: Arc<dyn Repository>) {
    ...
    match create_pokemon::execute(repo, req) {
        Ok(res) => println!(
            "{:?}",
            Response {
                number: res.number,
                name: res.name,
                types: res.types,
            }
        ),
        ...
    };
}

好了,让我们测试一下!打开终端,以命令行模式运行:

✔ Make your choice · Create a Pokemon
Pokemon number: 25
Pokemon name: Pikachu
Pokemon types: Electric
Response { number: 25, name: "Pikachu", types: ["Electric"] }
Great! First command implemented, let's do the next one!

获取所有宝可梦

现在我们去实现获取所有宝可梦!首先我们在对应的 index 添加相应的处理函数:

match index {
    0 => fetch_all_pokemons::run(repo.clone()),
    ...
};

创建模块

mod fetch_all_pokemons;

现在实现具体的功能:

use crate::domain::fetch_all_pokemons;
use crate::repositories::pokemon::Repository;
use std::sync::Arc;

#[derive(Debug)]
struct Response {
    number: u16,
    name: String,
    types: Vec<String>,
}

pub fn run(repo: Arc<dyn Repository>) {
    match fetch_all_pokemons::execute(repo) {
        Ok(res) => res.into_iter().for_each(|p| {
            println!(
                "{:?}",
                Response {
                    number: p.number,
                    name: p.name,
                    types: p.types,
                }
            );
        }),
        Err(fetch_all_pokemons::Error::Unknown) => println!("An unknown error occurred"),
    };
}

我已经详细的解释过第一个例子,这里就没必要一步一步说明了。这个命令不需要任何参赛,所以也不需要任何提示信息,我们只需要从存储库拿到结果,依次打印即可。

查询一个宝可梦

和之前一样,创建一个模块:

mod fetch_pokemon;

...

match index {
    ...
    1 => fetch_pokemon::run(repo.clone()),
    ...
};

创建对应的处理函数

use crate::cli::prompt_number;
use crate::domain::fetch_pokemon;
use crate::repositories::pokemon::Repository;
use std::sync::Arc;

#[derive(Debug)]
struct Response {
    number: u16,
    name: String,
    types: Vec<String>,
}

pub fn run(repo: Arc<dyn Repository>) {
    let number = prompt_number();

    let req = match number {
        Ok(number) => fetch_pokemon::Request { number },
        _ => {
            println!("An error occurred during the prompt");
            return;
        }
    };
    match fetch_pokemon::execute(repo, req) {
        Ok(res) => println!(
            "{:?}",
            Response {
                number: res.number,
                name: res.name,
                types: res.types,
            }
        ),
        Err(fetch_pokemon::Error::BadRequest) => println!("The request is invalid"),
        Err(fetch_pokemon::Error::NotFound) => println!("The Pokemon does not exist"),
        Err(fetch_pokemon::Error::Unknown) => println!("An unknown error occurred"),
    }
}

这里,我们先向用户询问了编号,如果获取失败了就输出错误信息。接着构建 Request 结构体,调用用例,成功就打印结果,否则打印错误信息。

删除一个宝可梦

最后一个命令!

mod delete_pokemon;

...

match index {
    ...
    3 => delete_pokemon::run(repo.clone()),
    ...
};

创建新模块:

use crate::cli::prompt_number;
use crate::domain::delete_pokemon;
use crate::repositories::pokemon::Repository;
use std::sync::Arc;

pub fn run(repo: Arc<dyn Repository>) {
    let number = prompt_number();

    let req = match number {
        Ok(number) => delete_pokemon::Request { number },
        _ => {
            println!("An error occurred during the prompt");
            return;
        }
    };
    match delete_pokemon::execute(repo, req) {
        Ok(()) => println!("The Pokemon has been deleted"),
        Err(delete_pokemon::Error::BadRequest) => println!("The request is invalid"),
        Err(delete_pokemon::Error::NotFound) => println!("The Pokemon does not exist"),
        Err(delete_pokemon::Error::Unknown) => println!("An unknown error occurred"),
    }
}

和查询一个宝可梦基本一样,只不过不需要返回信息。

总结

现在您无需直接发送 HTTP 请求即可在终端中享受使用程序 : D 但是你知道我们缺少什么吗?一个真正存储我们的宝可梦的地方。目前,它们只存在于内存中,因此每次我们运行程序时存储库都是空的。从 CLI 创建我们的宝可梦然后从 API 获取它们会很酷 : ) 下一篇将是本系列的最后一篇文章:实现一个长期保存的存储库。

代码可以在 Github 上查看