hyperlane-macros
2025年9月12日大约 9 分钟hyperlane-macros
一个用于构建具有增强功能的 HTTP 服务器的综合性过程宏集合。该 crate 提供了属性宏,可简化 HTTP 请求处理、协议验证、响应管理和请求数据提取。
安装
要使用此 crate,您可以运行 cmd:
cargo add hyperlane-macros
可用宏(Available Macros)
Hyperlane 核心宏
#[hyperlane(server: Server)]
—— 创建一个指定变量名和类型的Server
实例,并自动注册当前 crate 中定义的其他钩子(hooks)和路由(routes)。#[hyperlane(config: ServerConfig)]
—— 创建一个指定变量名和类型的ServerConfig
实例。
HTTP 方法宏(Method Macros)
#[methods(method1, method2, ...)]
—— 支持多个 HTTP 方法作为请求处理器。#[get]
—— 处理 GET 请求。#[post]
—— 处理 POST 请求。#[put]
—— 处理 PUT 请求。#[delete]
—— 处理 DELETE 请求。#[patch]
—— 处理 PATCH 请求。#[head]
—— 处理 HEAD 请求。#[options]
—— 处理 OPTIONS 请求。#[connect]
—— 处理 CONNECT 请求。#[trace]
—— 处理 TRACE 请求。
协议检查宏(Protocol Check Macros)
这些宏用于限制函数仅在特定协议或连接类型下执行:
#[ws]
—— WebSocket 检查,确保函数仅在 WebSocket 升级请求时执行。#[http]
—— HTTP 检查,确保函数仅处理标准 HTTP 请求。#[h2c]
—— HTTP/2 明文(Cleartext)检查,仅适用于 HTTP/2 未加密连接。#[http0_9]
—— HTTP/0.9 协议检查。#[http1_0]
—— HTTP/1.0 协议检查。#[http1_1]
—— HTTP/1.1 协议检查。#[http1_1_or_higher]
—— HTTP/1.1 或更高版本检查。#[http2]
—— HTTP/2 协议检查。#[http3]
—— HTTP/3 协议检查。#[tls]
—— TLS 检查,确保函数仅在 TLS 加密连接上执行。
响应设置宏(Response Setting Macros)
用于设置 HTTP 响应的相关字段:
#[response_status_code(code)]
—— 设置响应状态码(支持字面量和全局常量)。#[response_reason_phrase("phrase")]
—— 设置响应原因短语(如 "OK"、"Not Found" 等)。#[response_header("key", "value")]
—— 添加响应头(若已存在同名头,则追加)。#[response_header("key" => "value")]
—— 设置响应头(若已存在则覆盖)。#[response_body("data")]
—— 设置响应体内容(支持字符串字面量或常量)。#[response_version(version)]
—— 设置响应使用的 HTTP 版本(如HttpVersion::HTTP_11
)。
发送操作宏(Send Operation Macros)
控制响应数据的发送方式:
#[send]
—— 函数执行后,发送完整的响应(包括头和体)。#[send_body]
—— 函数执行后,仅发送响应体。#[send_once]
—— 函数执行后,恰好一次发送完整响应(防止重复发送)。#[send_once_body]
—— 函数执行后,恰好一次发送响应体。
刷新宏(Flush Macros)
#[flush]
—— 函数执行后立即刷新响应流,确保数据即时传输到客户端。
中断请求处理宏(Aborted Macros)
#[aborted]
—— 处理被提前终止的请求,提供连接中断时的清理逻辑。
连接关闭处理宏(Closed Operation Macros)
#[closed]
—— 处理已关闭的连接流,用于执行连接完成后的资源清理。
条件判断宏(Conditional Macros)
#[filter(condition)]
—— 仅当condition
(返回布尔值的代码块)为true
时才继续执行。#[reject(condition)]
—— 仅当condition
为false
时才继续执行。
请求体提取宏(Request Body Macros)
#[request_body(variable_name)]
—— 将原始请求体提取到指定变量中,类型为RequestBody
。#[request_body_json(variable_name: type)]
—— 将请求体解析为 JSON,并赋值给指定变量(需指定类型,如user: User
)。
属性提取宏(Attribute Macros)
#[attribute(key => variable_name: type)]
—— 从上下文中提取指定键的属性,并转换为指定类型。
所有属性提取宏(Attributes Macros)
#[attributes(variable_name)]
—— 获取所有属性,以HashMap
形式存入指定变量,用于全面访问上下文属性。
路由参数提取宏(Route Param Macros)
#[route_param(key => variable_name)]
—— 从路由中提取指定键的参数(如/user/{id}
中的id
),赋值给变量。#[route_params(variable_name)]
—— 获取所有路由参数,以集合形式(如HashMap
)存入变量。
请求查询参数宏(Request Query Macros)
#[request_query(key => variable_name)]
—— 从 URL 查询字符串中提取指定键的查询参数(如?name=alice
)。#[request_querys(variable_name)]
—— 获取所有查询参数,以集合形式存入变量。
请求头提取宏(Request Header Macros)
#[request_header(key => variable_name)]
—— 从请求头中提取指定名称的头字段。#[request_headers(variable_name)]
—— 获取所有请求头,以集合形式(如HeaderMap
)存入变量。
请求 Cookie 提取宏(Request Cookie Macros)
#[request_cookie(key => variable_name)]
—— 从Cookie
请求头中提取指定键的 cookie 值。#[request_cookies(variable_name)]
—— 获取原始Cookie
头字符串,可用于手动解析所有 cookie。
请求版本提取宏(Request Version Macros)
#[request_version(variable_name)]
—— 将 HTTP 请求版本(如 HTTP/1.1)提取到指定变量中。
请求路径提取宏(Request Path Macros)
#[request_path(variable_name)]
—— 将 HTTP 请求路径(如/api/users
)提取到指定变量中。
主机检查宏(Host Macros)
#[host("hostname")]
—— 仅当请求的Host
头匹配指定值时才执行函数。#[reject_host("hostname")]
—— 若Host
头匹配指定值,则拒绝该请求。
Referer 检查宏(Referer Macros)
#[referer("url")]
—— 仅当请求的Referer
头匹配指定 URL 时才执行。#[reject_referer("url")]
—— 若Referer
头匹配指定 URL,则拒绝该请求。
钩子宏(Hook Macros)
用于注册执行时机不同的钩子函数:
#[prologue_hook(function_name)]
—— 在主处理函数之前执行指定函数。#[epilogue_hook(function_name)]
—— 在主处理函数之后执行指定函数。#[connected_hook]
—— 当新客户端连接建立时执行。#[panic_hook]
—— 当服务器发生 panic 时执行,用于错误恢复或日志记录。#[prologue_upgrade_hook]
—— 在任何协议升级(如 HTTP → WebSocket)前执行。#[prologue_hooks(macro1, macro2, ...)]
—— 在被装饰函数之前注入一系列宏调用。#[epilogue_hooks(macro1, macro2, ...)]
—— 在被装饰函数之后注入一系列宏调用。
禁用钩子宏(Disable Hook Macros)
#[disable_http_hook]
—— 禁用该路由的 HTTP 处理逻辑。#[disable_ws_hook]
—— 禁用该路由的 WebSocket 处理逻辑。
中间件宏(Middleware Macros)
#[request_middleware]
—— 将函数注册为请求中间件(在请求处理前执行)。#[response_middleware]
—— 将函数注册为响应中间件(在响应发送前执行)。
注意:支持
order
参数控制执行顺序。若未指定order
,则优先级高于指定顺序的钩子。
响应头宏(Response Header Macros)
#[response_header("key", "value")]
—— 添加响应头(允许重复,多值头)。#[response_header("key" => "value")]
—— 设置响应头(覆盖已有值,单值头)。
响应体宏(Response Body Macros)
#[response_body(value)]
—— 设置响应体内容(支持字符串、字节、JSON 等)。
路由宏(Route Macros)
#[route("path")]
—— 将函数注册为指定路径的路由处理器(需前提:已使用#[hyperlane(server: Server)]
定义服务器实例)。
使用提示(Helper Tips)
- 请求相关宏(如提取数据)属于“获取(get)”操作 —— 用于从请求中提取信息。
- 响应相关宏(如设置头、体)属于“设置(set)”操作 —— 用于配置响应内容。
- 对于支持
order
参数的钩子宏(如中间件、panic_hook
等),未指定order
的钩子具有更高优先级。
最佳实践警告(Best Practice Warning)
- 请求相关宏大多是“查询”操作,响应相关宏大多是“赋值”操作,请勿混淆用途。
- 不推荐在同一个函数上同时使用
#[prologue_hook]
或#[epilogue_hook]
与其他功能宏(如#[get]
、#[post]
、#[http]
等)。正确做法是:将钩子逻辑放在独立的函数中,并在该函数内部使用相应宏。如果不清楚宏展开机制,混合使用可能导致不可预期的行为或编译错误。
示例用法
use hyperlane::*;
use hyperlane_macros::*;
use serde::{Deserialize, Serialize};
const STEP: &str = "step";
const TEST_ATTRIBUTE_KEY: &str = "test_attribute_key";
const CUSTOM_STATUS_CODE: i32 = 200;
const CUSTOM_REASON: &str = "Accepted";
const CUSTOM_HEADER_NAME: &str = "X-Custom-Header";
const CUSTOM_HEADER_VALUE: &str = "custom-value";
const RESPONSE_DATA: &str = "{\"status\": \"success\"}";
#[derive(Debug, Serialize, Deserialize, Clone)]
struct TestData {
name: String,
age: u32,
}
#[panic_hook]
#[panic_hook(1)]
#[panic_hook("2")]
#[epilogue_hooks(response_body("panic_hook"), send)]
async fn panic_hook(ctx: Context) {
if true {
return;
}
}
#[route("/disable_http_hook")]
#[response_body("disable_http_hook")]
#[disable_http_hook("/disable_http_hook")]
async fn disable_http_hook(ctx: Context) {}
#[route("/disable_ws_hook")]
#[response_body("disable_ws_hook")]
#[disable_ws_hook("/disable_ws_hook")]
async fn disable_ws_hook(ctx: Context) {}
#[connected_hook]
#[connected_hook(1)]
#[connected_hook("2")]
#[response_header(STEP => "connected_hook")]
async fn connected_hook(ctx: Context) {}
#[prologue_upgrade_hook]
#[prologue_upgrade_hook(1)]
#[prologue_upgrade_hook("2")]
#[response_header(STEP => "prologue_upgrade_hook")]
async fn prologue_upgrade_hook(ctx: Context) {}
#[request_middleware]
#[response_header(SERVER => HYPERLANE)]
#[response_version(HttpVersion::HTTP1_1)]
#[response_header(STEP => "request_middleware_1")]
async fn request_middleware_1(ctx: Context) {}
#[request_middleware(2)]
#[response_header(STEP => "request_middleware_2")]
async fn request_middleware_2(ctx: Context) {}
#[request_middleware("3")]
#[response_header(STEP => "request_middleware_3")]
async fn request_middleware_3(ctx: Context) {}
#[response_middleware]
#[response_header(STEP => "response_middleware_1")]
async fn response_middleware_1(ctx: Context) {}
#[response_middleware(2)]
#[prologue_hooks(
reject(ctx.get_request().await.is_ws()),
response_header(STEP => "response_middleware_2")
)]
#[epilogue_hooks(send, flush)]
async fn response_middleware_2(ctx: Context) {
if true {
return;
}
}
#[response_middleware("3")]
#[prologue_hooks(
ws,
response_header(STEP => "response_middleware_3")
)]
#[epilogue_hooks(send_body, flush)]
async fn response_middleware_3(ctx: Context) {}
#[get]
#[http]
async fn prologue_hook(ctx: Context) {}
#[response_status_code(200)]
async fn epilogue_hook(ctx: Context) {}
#[route("/response")]
#[response_body(RESPONSE_DATA)]
#[response_reason_phrase(CUSTOM_REASON)]
#[response_status_code(CUSTOM_STATUS_CODE)]
#[response_header(CUSTOM_HEADER_NAME => CUSTOM_HEADER_VALUE)]
async fn response(ctx: Context) {}
#[route("/connect")]
#[prologue_hooks(connect, response_body("connect"))]
async fn connect(ctx: Context) {}
#[route("/delete")]
#[prologue_hooks(delete, response_body("delete"))]
async fn delete(ctx: Context) {}
#[route("/head")]
#[prologue_hooks(head, response_body("head"))]
async fn head(ctx: Context) {}
#[route("/options")]
#[prologue_hooks(options, response_body("options"))]
async fn options(ctx: Context) {}
#[route("/patch")]
#[prologue_hooks(patch, response_body("patch"))]
async fn patch(ctx: Context) {}
#[route("/put")]
#[prologue_hooks(put, response_body("put"))]
async fn put(ctx: Context) {}
#[route("/trace")]
#[prologue_hooks(trace, response_body("trace"))]
async fn trace(ctx: Context) {}
#[route("/h2c")]
#[prologue_hooks(h2c, response_body("h2c"))]
async fn h2c(ctx: Context) {}
#[route("/http")]
#[prologue_hooks(http, response_body("http"))]
async fn http_only(ctx: Context) {}
#[route("/http0_9")]
#[prologue_hooks(http0_9, response_body("http0_9"))]
async fn http0_9(ctx: Context) {}
#[route("/http1_0")]
#[prologue_hooks(http1_0, response_body("http1_0"))]
async fn http1_0(ctx: Context) {}
#[route("/http1_1")]
#[prologue_hooks(http1_1, response_body("http1_1"))]
async fn http1_1(ctx: Context) {}
#[route("/http2")]
#[prologue_hooks(http2, response_body("http2"))]
async fn http2(ctx: Context) {}
#[route("/http3")]
#[prologue_hooks(http3, response_body("http3"))]
async fn http3(ctx: Context) {}
#[route("/tls")]
#[prologue_hooks(tls, response_body("tls"))]
async fn tls(ctx: Context) {}
#[route("/http1_1_or_higher")]
#[prologue_hooks(http1_1_or_higher, response_body("http1_1_or_higher"))]
async fn http1_1_or_higher(ctx: Context) {}
#[route("/unknown_method")]
#[prologue_hooks(
filter(ctx.get_request().await.is_unknown_method()),
response_body("unknown_method")
)]
async fn unknown_method(ctx: Context) {}
#[route("/get")]
#[send_once_body]
#[prologue_hooks(ws, get, response_body("get"))]
async fn get(ctx: Context) {}
#[send_once]
#[route("/post")]
#[prologue_hooks(post, response_body("post"))]
async fn post(ctx: Context) {}
#[route("/ws")]
#[prologue_hooks(ws, response_body("ws"))]
async fn websocket(ctx: Context) {}
#[route("/hook")]
#[prologue_hook(prologue_hook)]
#[epilogue_hook(epilogue_hook)]
#[response_body("Testing hook macro")]
async fn hook(ctx: Context) {}
#[closed]
#[route("/get_post")]
#[prologue_hooks(
http,
methods(get, post),
response_body("get_post"),
response_status_code(200),
response_reason_phrase("OK")
)]
async fn get_post(ctx: Context) {}
#[route("/attributes")]
#[response_body(format!("request attributes: {request_attributes:?}"))]
#[attributes(request_attributes)]
async fn attributes(ctx: Context) {}
#[route("/route_params/:test")]
#[response_body(format!("request route params: {request_route_params:?}"))]
#[route_params(request_route_params)]
async fn route_params(ctx: Context) {}
#[route("/request_querys")]
#[response_body(format!("request querys: {request_querys:?}"))]
#[request_querys(request_querys)]
async fn request_querys(ctx: Context) {}
#[route("/request_headers")]
#[response_body(format!("request headers: {request_headers:?}"))]
#[request_headers(request_headers)]
async fn request_headers(ctx: Context) {}
#[route("/route_param/:test")]
#[response_body(format!("route param: {request_route_param:?}"))]
#[route_param("test" => request_route_param)]
async fn route_param(ctx: Context) {}
#[route("/request_query")]
#[response_body(format!("request query: {request_query_option:?}"))]
#[request_query("test" => request_query_option)]
async fn request_query(ctx: Context) {}
#[route("/request_header")]
#[response_body(format!("request header: {request_header_option:?}"))]
#[request_header(HOST => request_header_option)]
async fn request_header(ctx: Context) {}
#[response_body(format!("raw body: {raw_body:?}"))]
#[request_body(raw_body)]
#[route("/request_body")]
async fn request_body(ctx: Context) {}
#[route("/host")]
#[host("localhost")]
#[response_body("host string literal: localhost")]
async fn host(ctx: Context) {}
#[route("/reject_host")]
#[prologue_hooks(
reject_host("filter.localhost"),
response_body("host filter string literal")
)]
async fn reject_host(ctx: Context) {}
#[route("/attribute")]
#[response_body(format!("request attribute: {request_attribute_option:?}"))]
#[attribute(TEST_ATTRIBUTE_KEY => request_attribute_option: TestData)]
async fn attribute(ctx: Context) {}
#[route("/request_body_json")]
#[response_body(format!("request data: {request_data_result:?}"))]
#[request_body_json(request_data_result: TestData)]
async fn request_body_json(ctx: Context) {}
#[route("/referer")]
#[prologue_hooks(
referer("http://localhost"),
response_body("referer string literal: http://localhost")
)]
async fn referer(ctx: Context) {}
#[route("/reject_referer")]
#[prologue_hooks(
reject_referer("http://localhost"),
response_body("referer filter string literal")
)]
async fn reject_referer(ctx: Context) {}
#[route("/cookies")]
#[response_body(format!("All cookies: {cookie_value:?}"))]
#[request_cookies(cookie_value)]
async fn cookies(ctx: Context) {}
#[route("/cookie")]
#[response_body(format!("Session cookie: {session_cookie_opt:?}"))]
#[request_cookie("test" => session_cookie_opt)]
async fn cookie(ctx: Context) {}
#[route("/request_version")]
#[response_body(format!("HTTP Version: {http_version}"))]
#[request_version(http_version)]
async fn request_version_test(ctx: Context) {}
#[route("/request_path")]
#[response_body(format!("Request Path: {request_path}"))]
#[request_path(request_path)]
async fn request_path_test(ctx: Context) {}
#[route("/response_header")]
#[response_body("Testing header set and replace operations")]
#[response_header("X-Add-Header", "add-value")]
#[response_header("X-Set-Header" => "set-value")]
async fn response_header_test(ctx: Context) {}
#[route("/literals")]
#[response_status_code(201)]
#[response_header(CONTENT_TYPE => APPLICATION_JSON)]
#[response_body("{\"message\": \"Resource created\"}")]
#[response_reason_phrase(HttpStatus::Created.to_string())]
async fn literals(ctx: Context) {}
#[hyperlane(server: Server)]
#[hyperlane(config: ServerConfig)]
#[tokio::main]
async fn main() {
config.disable_nodelay().await;
server.config(config).await;
let server_hook: ServerHook = server.run().await.unwrap_or_default();
server_hook.wait().await;
}
许可证
本项目使用 MIT 协议,详情请参见 LICENSE 文件。
贡献
欢迎贡献代码!请提交 issue 或 pull request。
联系方式
如有任何问题,请联系作者 [email protected]。