18 个版本 (6 个破坏性更新)
新 0.13.0 | 2024 年 8 月 9 日 |
---|---|
0.12.3 | 2024 年 7 月 12 日 |
0.11.1 | 2024 年 7 月 20 日 |
0.0.0 | 2023 年 12 月 3 日 |
0.0.0-alpha | 2023 年 11 月 25 日 |
#725 在 Web 编程
每月 339 次下载
用于 hypers
4MB
7K SLoC
⚡️ 快速开始
Cargo.toml
[dependencies]
hypers = { version = "0.13", features = ["full","openapi","debug"] }
tokio = { version = "=1.38.0", features = ["full"] }
serde = { version = "=1.0.205", features = ["derive"] }
Rust 代码
use hypers::{hyper::StatusCode, once_cell::sync::Lazy, prelude::*};
use serde::{Deserialize, Serialize};
use tokio::sync::Mutex;
static STORE: Lazy<Db> = Lazy::new(new_store);
pub type Db = Mutex<Vec<Todo>>;
pub fn new_store() -> Db {
Mutex::new(Vec::new())
}
#[derive(Serialize, Deserialize, Clone, Debug, ToSchema)]
pub struct Todo {
#[hypers(schema(example = 1))]
pub id: u64,
#[hypers(schema(example = "Buy coffee"))]
pub text: String,
pub completed: bool,
}
#[derive(Clone)]
struct Api;
#[openapi(name = "/api", tag = "api1 todos")]
impl Api {
/// List todos.
#[get(
"/list_todos",
parameter(
("offset", description = "Offset is an query paramter."),
("limit", description = "Offset is an query paramter."),
)
)]
async fn list_todos(offset: Query<usize>, limit: Query<usize>) -> Json<Vec<Todo>> {
let todos = STORE.lock().await;
let todos: Vec<Todo> = todos
.clone()
.into_iter()
.skip(offset.0)
.take(limit.0)
.collect();
Json(todos)
}
/// Create new todo.
#[post("/create_todo", status(201, 409))]
async fn create_todo(req: Json<Todo>) -> Result<StatusCode, StatusError> {
let mut vec = STORE.lock().await;
for todo in vec.iter() {
if todo.id == req.id {
return Err(StatusError::bad_request().detail("todo already exists"));
}
}
vec.push(req.0);
Ok(StatusCode::CREATED)
}
}
#[derive(Clone)]
struct Base;
#[openapi(tag = "api2 todos")]
impl Base {
/// Update existing todo.
#[patch("/update_todo/:id", status(200, 404))]
async fn update_todo(id: Path<u64>, updated: Json<Todo>) -> Result<StatusCode, StatusError> {
let mut vec = STORE.lock().await;
for todo in vec.iter_mut() {
if todo.id == *id {
*todo = (*updated).clone();
return Ok(StatusCode::OK);
}
}
Err(StatusError::not_found())
}
#[delete("/:id", status(200, 401, 404))]
async fn delete_todo(id: Path<u64>) -> Result<StatusCode, StatusError> {
let mut vec = STORE.lock().await;
let len = vec.len();
vec.retain(|todo| todo.id != *id);
let deleted = vec.len() != len;
if deleted {
Ok(StatusCode::NO_CONTENT)
} else {
Err(StatusError::not_found())
}
}
#[post("/upload")]
async fn upload(file: FilePart) -> Response {
let mut res = Response::default();
let dest = format!("temp/{}", file.name.clone().unwrap_or("file".to_owned()));
println!("{dest}");
let info = if let Err(e) = std::fs::copy(file.path.clone(), std::path::Path::new(&dest)) {
res.status(StatusCode::INTERNAL_SERVER_ERROR);
format!("file not found in request: {e}")
} else {
format!("File uploaded to {dest}")
};
res.render(Text::Plain(info))
}
#[post("/uploads")]
async fn uploads(files: FileParts) -> Response {
let mut msgs = Vec::with_capacity(files.len());
let mut res = Response::default();
for file in files.0 {
let dest = format!("temp/{}", file.name.clone().unwrap_or("file".to_owned()));
if let Err(e) = std::fs::copy(file.path.clone(), std::path::Path::new(&dest)) {
res.status(StatusCode::INTERNAL_SERVER_ERROR)
.body(format!("file not found in request: {e}"));
return res;
} else {
msgs.push(dest);
}
}
res.body(format!("Files uploaded:\n\n{}", msgs.join("\n")));
return res;
}
}
pub async fn upload(_: Request) -> impl Responder {
Text::Html(
r#"<!DOCTYPE html>
<html>
<head>
<title>Upload file</title>
</head>
<body>
<h1>Upload file</h1>
<form action="/upload" method="post" enctype="multipart/form-data">
<input type="file" name="file" />
<input type="submit" value="upload" />
</form>
</body>
</html>
"#,
)
}
pub async fn uploads(_: Request) -> Response {
let res = Response::default();
res.render(Text::Html(
r#"<!DOCTYPE html>
<html>
<head>
<title>Upload files</title>
</head>
<body>
<h1>Upload files</h1>
<form action="/uploads" method="post" enctype="multipart/form-data">
<input type="file" name="files" multiple/>
<input type="submit" value="upload" />
</form>
</body>
</html>
"#,
))
}
pub async fn index(_: Request) -> impl Responder {
Text::Html(
r#"<!DOCTYPE html>
<html>
<head>
<title>Oapi todos</title>
</head>
<body>
<ul>
<li><a href="swagger_ui/" target="_blank">swagger_ui</a></li>
<li><a href="scalar" target="_blank">scalar</a></li>
<li><a href="rapidoc" target="_blank">rapidoc</a></li>
<li><a href="redoc" target="_blank">redoc</a></li>
</ul>
</body>
</html>
"#,
)
}
const USER_NAME: &str = "admin";
const PASS_WORD: &str = "123456";
const LOGIN_HTML: &str = r#"<!DOCTYPE html>
<html>
<head>
<title>swagger-ui login</title>
</head>
<style>
html,body{
margin:0;
padding:0;
width:100%;
height:100%;
}
.container{
display:flex;
align-item:center;
justify-content:center;
}
.form{
display:flex;
align-item:center;
justify-content:center;
flex-direction:column;
}
.mt-20{
margin-top: 20px;
}
</style>
<body class="container">
<form class="form" action="/swaggerLogin" method="post">
<h1>swagger-ui</h1>
<input type="text" name="username" placeholder="用户名" />
<input class="mt-20" type="password" name="password" placeholder="密码" />
<button class="mt-20" type="submit" id="submit">登录</button>
</form>
</body>
</html>
"#;
#[hook]
pub async fn auth_token(req: Request, next: Next<'_>) -> impl Responder {
let res = Response::default();
if let Some(session) = req.session() {
let username = session.get::<String>("username");
let password = session.get::<String>("password");
println!("username = {:?}", username);
println!("password = {:?}", password);
return next.next(req).await;
} else {
return res.render(Text::Html(LOGIN_HTML));
}
}
pub async fn swagger_login(mut req: Request) -> impl Responder {
let username = req.form::<String>("username").await;
let password = req.form::<String>("password").await;
let mut res = Response::default();
if let (Some(name), Some(pass)) = (username, password) {
if name.eq(USER_NAME) && pass.eq(PASS_WORD) {
let mut session = Session::new();
let _ = session.insert("username", name);
let _ = session.insert("password", pass);
res.set_session(session);
res.redirect(StatusCode::SEE_OTHER, "/swagger_ui/");
return res;
}
}
res.render(Text::Html(LOGIN_HTML))
}
#[tokio::main]
async fn main() -> Result<()> {
tracing_subscriber::fmt().init();
std::fs::create_dir_all("temp").unwrap();
let mut root = Router::default();
let session_hook = SessionHook::new(
CookieStore::new(),
b"secretabsecretabsecretabsecretabsecretabsecretabsecretabsecretab",
)?;
root.hook(session_hook, None, None);
root.hook(auth_token, vec!["/swagger_ui/"], vec!["/swaggerLogin"]);
root.get("/", index); // http://127.0.0.1:7878/
root.get("/upload", upload);
root.get("/uploads", uploads);
root.post("/swaggerLogin", swagger_login);
let mut openapi = OpenApi::new("todos api", "0.0.1");
openapi.push(Api);
openapi.push(Base);
let swagger = SwaggerUi::new("/api-doc/openapi.json");
root.get("/swagger_ui/*", swagger); // http://127.0.0.1:7878/swagger_ui/
let rapidoc = RapiDoc::new("/api-doc/openapi.json");
root.get("/rapidoc", rapidoc); // http://127.0.0.1:7878/rapidoc
let redoc = ReDoc::new("/api-doc/openapi.json");
root.get("/redoc", redoc); // http://127.0.0.1:7878/redoc
let scalar: Scalar = Scalar::new("/api-doc/openapi.json");
root.get("/scalar", scalar); // http://127.0.0.1:7878/scalar
let root = openapi.openapi("/api-doc/openapi.json", root);
println!("root = {:#?}", root);
let listener = hypers::TcpListener::bind("127.0.0.1:7878").await?;
hypers::listen(root, listener).await
}
依赖项
~19–32MB
~565K SLoC