#routes #yaml #middleware #hyper #http-framework #hyper-server

rustgram

基于yaml文件构建应用程序的快速hyper服务器框架

7个版本

0.2.1 2024年6月26日
0.2.0 2024年6月10日
0.1.8 2023年4月15日
0.1.7 2022年8月31日
0.1.5 2022年7月25日

#87 in HTTP服务器

每月下载量:35

MIT许可证

49KB
921

Rustgram

Rustgram是一个基于hyper构建的轻量级、快速且易于使用的http路由和中间件框架

特性

  • 像Tower服务一样构建路由和中间件
  • 使用yaml文件定义路由

Cargo.toml中安装

[dependencies]
# hyper when using Statuscode
hyper = { version = "0.14", features = ["full"] }

# tokio for the async main fn
tokio = { version = "1", features = ["full"] }

# rustgram
rustgram = "0.1"

文档

入门

  1. 使用一个未找到处理程序服务(例如一个函数)创建路由器
  2. 添加路由,对于每个HTTP方法都有一个路由函数:get、post、put、delete、head等
    1. 使用r函数将处理程序传递给路由器
    2. 使用方法函数定义此路由应匹配给定路径的哪个方法
    3. 设置URL路径
  3. 输入监听连接的套接字地址
use hyper::StatusCode;
use rustgram::{r, Router, Request, Response};
use std::net::SocketAddr;

async fn not_found_handler(_req: Request) -> Response
{
	return hyper::Response::builder()
		.status(StatusCode::NOT_FOUND)
		.body("Not found".into())
		.unwrap();
}

pub async fn test_handler(_req: Request) -> String
{
	format!("test called")
}

#[tokio::main]
async fn main()
{
	let mut router = Router::new(crate::not_found_handler);
	router.get("/", r(test_handler));
	router.get("/api", r(test_handler));

	let addr = SocketAddr::from(([127, 0, 0, 1], 3000));

	//start the app
	rustgram::start(router, addr).await;
}

中间件

  • 中间件是一个服务。
  • 中间件调用下一个服务(通过中间件转换获得)。
  • 转换用于返回一个具有下一个服务的新中间件
  • 每次将中间件应用于路由时都会调用转换函数

中间件栈的构建类似于服务调用栈。

中间件栈的顺序与应用的顺序相反。

use hyper::StatusCode;
use rustgram::service::{Service, ServiceTransform};
use rustgram::{Request, Response};

//define a middleware service
pub struct Middleware<S>
{
	inner: S,   //space the inner service to call it later
}

impl<S> Service<Request> for Middleware<S>
	where
		S: Service<Request, Output=Response>, //define the return types from the next service
{
	type Output = S::Output;

	fn call(&self, req: Request) -> impl Future<Output=Self::Output> + Send + 'static
	{
		// before the request handler from the router is called
		self.inner.call(req)  //call the next handler 
		// after the request handler is called with the response 
	}
}

//define a middleware transform
pub struct MiddlewareTransform;

impl<S> ServiceTransform<S> for MiddlewareTransform
	where
		S: Service<Request, Output=Response>, //define the return types from the next service
{
	type Service = Middleware<S>;

	fn transform(&self, inner: S) -> Self::Service
	{
		Middleware {
			inner,
		}
	}
}

//or define a middleware transform with a function
pub fn middleware_transform<S>(inner: S) -> Middleware<S>
{
	Middleware {
		inner,
	}
}

async fn not_found_handler(_req: Request) -> Response
{
	return hyper::Response::builder()
		.status(StatusCode::NOT_FOUND)
		.body("Not found".into())
		.unwrap();
}

pub async fn test_handler(_req: Request) -> String
{
	format!("test called")
}

//Apply a middleware to a route after the r function

#[tokio::main]
async fn main()
{
	let mut router = Router::new(crate::not_found_handler);
	router.get("/", r(test_handler)
		.add(middleware_transform)
	);

	router.get("/api", r(test_handler)
		.add(MiddlewareTransform)
	);

	//apply multiple middleware to a route
	router.get("/multiple", r(test_handler)
		.add(MiddlewareTransform)   //then this at last
		.add(middleware_transform)  //then this ^
		.add(middleware_transform)  //then this ^
		.add(middleware_transform)  //then this ^
		.add(middleware_transform)  //then this ^
		.add(middleware_transform)  //this is called first ^
	);

	let addr = SocketAddr::from(([127, 0, 0, 1], 3000));

	//start the app
	rustgram::start(router, addr).await;
}

异步处理的中间件

如果请求处理之前或之后必须执行工作,则需要Box Future

在响应之前调用异步操作时,需要Arc以避免生命周期问题

  • 使用Pin Box功能
  • 将内部服务作为Arc指针使用
  • 在调用异步块之前克隆Arc指针
  • 在异步块中执行异步操作
use std::future::Future;
use std::sync::Arc;

use rustgram::service::{Service, ServiceTransform};
use rustgram::{Request, Response};

pub struct Middleware<S>
{
	inner: Arc<S>,   //use Arc here to avoid lifetime issues
}

impl<S> Service<Request> for Middleware<S>
	where
		S: Service<Request, Output=Response>,
{
	type Output = S::Output;

	fn call(&self, req: Request) -> impl Future<Output=Self::Output> + Send + 'static
	{
		//must clone the service to call it in the async move block. (we are cloning the arc ref, to avoid lifetime issues)
		let next = self.inner.clone();

		async move {
			//do async fn before req

			next.call(req).await
			//do async fn after req 
		}
	}
}

pub fn mw_transform<S>(inner: S) -> Middleware<S>
{
	Middleware {
		inner: Arc::new(inner), //use Arc here!
	}
}

只有在响应之后才执行异步操作

use std::future::Future;

use rustgram::service::{Service, ServiceTransform};
use rustgram::{Request, Response};

pub struct Middleware<S>
{
	inner: S,
}

impl<S> Service<Request> for Middleware<S>
	where
		S: Service<Request, Output=Response>,
{
	type Output = S::Output;

	fn call(&self, req: Request) -> impl Future<Output=Self::Output> + Send + 'static
	{
		//no Arc cloning needed because we move the feature into the async block
		//but change the req is not possible here
		let res = self.inner.call(req);

		async move {
			let res = res.await;
			//do async fn after req 

			res
		}
	}
}

pub fn mw_transform<S>(inner: S) -> Middleware<S>
{
	Middleware {
		inner, //no Arc here!
	}
}

处理程序返回和错误处理

路由器只使用Service traits。对于正常函数和闭包,这已经实现。

函数不需要返回Hyper Response,但它们的返回值会被转换为hyper响应。

支持的返回值包括

  • Hyper Response
  • String
  • &'static str
  • Result<String, GramStdHttpErr>
  • 结果
  • 结果

GramStdHttpErr被转换为超响应。

IntoResponse可以为每种类型实现。在这种情况下,它为错误实现。

如果返回HttpErr,它将在从Error创建的Response中创建。

use hyper::StatusCode;
use rustgram::{Response, Request};
use rustgram::service::IntoResponse;

pub struct HttpErr
{
	http_status_code: u16,
	api_error_code: u32,
	msg: &'static str,
}

impl HttpErr
{
	pub fn new(http_status_code: u16, api_error_code: u32, msg: &'static str) -> Self
	{
		Self {
			http_status_code,
			api_error_code,
			msg
		}
	}
}

impl IntoResponse<Response> for HttpErr
{
	fn into_response(self) -> Response
	{
		let status = match StatusCode::from_u16(self.http_status_code) {
			Ok(s) => s,
			Err(_e) => StatusCode::BAD_REQUEST,
		};

		//the msg for the end user
		let msg = format!(
			"{{\"status\": {}, \"error_message\": \"{}\"}}",
			self.api_error_code, self.msg
		);

		hyper::Response::builder()
			.status(status)
			.header("Content-Type", "application/json")
			.body(hyper::Body::from(msg))
			.unwrap()
	}
}

//example usage:

pub async fn test_handler_err(_req: Request) -> Result<String, HttpErr>
{
	Err(HttpErr::new(400, 1, "Input not valid"))
}

结果

R可以是任何实现了IntoResponse的类型。

返回json字符串的示例

use rustgram::service::IntoResponse;
use rustgram::Response;
use serde::Serialize;
use serde_json::to_string;

pub struct JsonResult<T: Serialize>(pub T);

impl<T: Serialize> IntoResponse<Response> for JsonResult<T>
{
	fn into_response(self) -> Response
	{
		//to string from serde_json
		let string = match to_string(&self.0) {
			Ok(s) => s,
			//the Http err from the previous example
			Err(_e) => return HttpErr::new(422, 1, "Json to string error", None).get_res(),
		};

		hyper::Response::builder()
			.header("Content-Type", "application/json")
			.body(string.into())
			.unwrap()
	}
}

//in another file
use serde::{Deserialize, Serialize};

#[derive(Serialize, Deserialize)]
pub struct ResultMsg(pub String);

pub async fn test_handler_json_result(_req: Request) -> Result<JsonResult<ResultMsg>, HttpErr>
{
	Ok(JsonResult(ResultMsg(String::from("Hello world"))))
}

路由构建器和组

  • 组只能由路由构建器构建
  • 构建器解析yml文件并创建一个新的路由文件。此文件包含一个返回路由器(稍后使用)的函数。
  • 路由中的所有路由共享相同的中间件和相同的前缀
  • 嵌套组也是可能的
  1. 为路由构建器创建第二个bin crate。
  2. 此crate调用构建器函数
  3. 设置输入和输出路径(都相对于当前工作目录)
  4. 使用带有route_builder功能的rustgram安装
  5. 每次更改路由时,构建并执行路由构建器
  6. 使用新文件中的route函数来获取路由器
# in a workspace just create a new crate
cargo new route_builder

在Cargo-toml文件中

rustgram = { version = "0.1", features = ["route_builder"] }

src/main.rs中打开main函数

use rustgram::route_parser;

fn main()
{
//input path: from the root of the current working directory 
// output path: into the app crate (created via cargo new app)
	route_parser::start(
		"routes.yml".to_string(),
		"app/src/routes.rs".to_string()
	);
}

创建路由文件。

# define the namespace where the route handlers live
# or leave it empty and use the full path to the handler
base_handler: test_handler
# define the namespace for the middleware
base_mw: test_mw
# prefix for all routes
prefix: "/"

# the routes and groups. use the method followed by the path (p) and the handler (s)
routes:
  - get:
      p: ""
      # must match the base_handler namespace
      s: test_handler::test_handler
  # a put route with middleware 
  - put:
      p: ""
      s: test_handler::test_handler
      mw:
        - mw1_transform
        - mw_transform
  # a group of routes.
  # a prefix (p) for all routes and middleware (mw) like routes
  - group:
      p: admin
      mw:
        - mw1_transform
        - mw_transform
      # the routes to this group
      gr:
        - get:
            p: ""
            s: test_handler_db::test_handler_db_to_json
        - get:
            p: "/user/:id"
            s: test_handler::test_handler
        # for this route, mw is called first, then mw1, mw2 and mw3
        - put:
            p: "/many_mw"
            s: test_handler::test_handler
            mw:
              - mw3_transform
              - mw2_transform
  - group:
      p: nested
      mw:
        - mw1_transform
        - mw_transform
      gr:
        # define a new group inside the group routes
        - group:
            p: "/management"
            mw:
              - mw1_transform
              - mw_transform
            gr:
              - put:
                  p: "/put"
                  s: test_handler::test_handler
                  mw:
                    - mw5_transform
                    - mw4_transform

该文件被解析为

/**
# Generated route files by rustgram route builder.

Please do not modify this file. Any changes will be overridden by the next route build.
Use the returned router instead
 */
use rustgram::{r, Router};

use crate::test_handler::*;
use crate::test_mw::*;

pub(crate) fn routes(router: &mut Router)
{
	router.get("/", r(test_handler::test_handler));

	router.put(
		"/",
		r(test_handler::test_handler)
			.add(mw1_transform)
			.add(mw_transform),
	);

	router.get(
		"/admin",
		r(test_handler_db::test_handler_db_to_json)
			.add(mw1_transform)
			.add(mw_transform),
	);

	router.get(
		"/admin/user/:id",
		r(test_handler::test_handler)
			.add(mw1_transform)
			.add(mw_transform),
	);

	router.put(
		"/admin/many_mw",
		r(test_handler::test_handler)
			.add(mw3_transform)
			.add(mw2_transform)
			.add(mw1_transform)
			.add(mw_transform),
	);

	router.put(
		"/nested/management/put",
		r(test_handler::test_handler)
			.add(mw5_transform)
			.add(mw4_transform)
			.add(mw3_transform)
			.add(mw2_transform)
			.add(mw1_transform)
			.add(mw_transform),
	);
}

//Now the route file can be used like this:

#[tokio::main]
async fn main()
{
	let mut router = Router::new(crate::not_found_handler);

	routes(&mut router);

	let addr = SocketAddr::from(([127, 0, 0, 1], 3000));

	//start the app
	rustgram::start(router, addr).await;
}

依赖项

~4–6MB
~105K SLoC