hyperlane-macros
2025年10月2日大约 10 分钟hyperlane-macros
一个用于构建具有增强功能的 HTTP 服务器的综合性过程宏集合。该 crate 提供了属性宏,可简化 HTTP 请求处理、协议验证、响应管理和请求数据提取。
安装
要使用此 crate,您可以运行 cmd:
cargo add hyperlane-macros
可用宏
Hyperlane 宏
#[hyperlane(server: Server)]
- 创建一个具有指定变量名和类型的新Server
实例,并自动注册 crate 中定义的其他钩子和路由。#[hyperlane(config: ServerConfig)]
- 创建一个具有指定变量名和类型的新ServerConfig
实例。
HTTP 方法宏
#[methods(method1, method2, ...)]
- 接受多个 HTTP 方法#[get]
- GET 方法处理器#[post]
- POST 方法处理器#[put]
- PUT 方法处理器#[delete]
- DELETE 方法处理器#[patch]
- PATCH 方法处理器#[head]
- HEAD 方法处理器#[options]
- OPTIONS 方法处理器#[connect]
- CONNECT 方法处理器#[trace]
- TRACE 方法处理器
协议检查宏
#[ws]
- WebSocket 检查,确保函数仅对 WebSocket 升级请求执行#[http]
- HTTP 检查,确保函数仅对标准 HTTP 请求执行#[h2c]
- HTTP/2 明文检查,确保函数仅对 HTTP/2 明文请求执行#[http0_9]
- HTTP/0.9 检查,确保函数仅对 HTTP/0.9 协议请求执行#[http1_0]
- HTTP/1.0 检查,确保函数仅对 HTTP/1.0 协议请求执行#[http1_1]
- HTTP/1.1 检查,确保函数仅对 HTTP/1.1 协议请求执行#[http1_1_or_higher]
- HTTP/1.1 或更高版本检查,确保函数仅对 HTTP/1.1 或更新的协议版本执行#[http2]
- HTTP/2 检查,确保函数仅对 HTTP/2 协议请求执行#[http3]
- HTTP/3 检查,确保函数仅对 HTTP/3 协议请求执行#[tls]
- TLS 检查,确保函数仅对 TLS 安全连接执行
响应设置宏
#[response_status_code(code)]
- 设置响应状态码(支持字面量和全局常量)#[response_reason_phrase("phrase")]
- 设置响应原因短语(支持字面量和全局常量)#[response_header("key", "value")]
- 添加响应头(支持字面量和全局常量)#[response_header("key" => "value")]
- 设置响应头(支持字面量和全局常量)#[response_body("data")]
- 设置响应体(支持字面量和全局常量)#[response_version(version)]
- 设置响应 HTTP 版本(支持字面量和全局常量)#[clear_response_headers]
- 清空所有响应头
发送操作宏
#[send]
- 函数执行后发送完整响应(头和体)#[send_body]
- 函数执行后仅发送响应体#[send_once]
- 函数执行后精确发送一次完整响应#[send_body_once]
- 函数执行后精确发送一次响应体#[send_with_data("data")]
- 函数执行后发送带有指定数据的完整响应#[send_once_with_data("data")]
- 函数执行后精确发送一次带有指定数据的完整响应#[send_body_with_data("data")]
- 函数执行后发送带有指定数据的响应体
刷新宏
#[flush]
- 函数执行后刷新响应流以确保立即传输数据
中止宏
#[aborted]
- 处理中止的请求,为提前终止的连接提供清理逻辑
关闭操作宏
#[closed]
- 处理关闭的流,为已完成的连接提供清理逻辑
条件宏
#[filter(condition)]
- 仅当condition
(返回布尔值的代码块)为true
时继续执行。#[reject(condition)]
- 仅当condition
(返回布尔值的代码块)为false
时继续执行。
请求体宏
#[request_body(variable_name)]
- 将原始请求体提取到具有 RequestBody 类型的指定变量中#[request_body_json(variable_name: type)]
- 将请求体解析为 JSON 并放入指定变量和类型中
属性宏
#[attribute(key => variable_name: type)]
- 按键提取特定属性到类型化变量中
属性集合宏
#[attributes(variable_name)]
- 获取所有属性作为 HashMap 以进行综合属性访问
路由参数宏
#[route_param(key => variable_name)]
- 按键提取特定路由参数到变量中
路由参数集合宏
#[route_params(variable_name)]
- 获取所有路由参数作为集合
请求查询宏
#[request_query(key => variable_name)]
- 从 URL 查询字符串中按键提取特定查询参数
请求查询集合宏
#[request_querys(variable_name)]
- 获取所有查询参数作为集合
请求头宏
#[request_header(key => variable_name)]
- 从请求中按名称提取特定 HTTP 头
请求头集合宏
#[request_headers(variable_name)]
- 获取所有 HTTP 头作为集合
请求 Cookie 宏
#[request_cookie(key => variable_name)]
- 从请求 cookie 头中按键提取特定 cookie 值
请求 Cookie 集合宏
#[request_cookies(variable_name)]
- 从 cookie 头中获取所有 cookie 作为原始字符串
请求版本宏
#[request_version(variable_name)]
- 将 HTTP 请求版本提取到变量中
请求路径宏
#[request_path(variable_name)]
- 将 HTTP 请求路径提取到变量中
主机宏
#[host("hostname")]
- 限制函数执行到具有特定主机头值的请求#[reject_host("hostname")]
- 拒绝匹配特定主机头值的请求
引用宏
#[referer("url")]
- 限制函数执行到具有特定引用头值的请求#[reject_referer("url")]
- 拒绝匹配特定引用头值的请求
钩子宏
#[prologue_hooks(function_name)]
- 在主处理函数之前执行指定函数#[epilogue_hooks(function_name)]
- 在主处理函数之后执行指定函数#[panic_hook]
- 在服务器内发生恐慌时执行函数#[prologue_macros(macro1, macro2, ...)]
- 在装饰函数之前注入宏列表。#[epilogue_macros(macro1, macro2, ...)]
- 在装饰函数之后注入宏列表。
中间件宏
#[request_middleware]
- 将函数注册为请求中间件#[request_middleware(order)]
- 将函数注册为具有指定顺序的请求中间件#[response_middleware]
- 将函数注册为响应中间件#[response_middleware(order)]
- 将函数注册为具有指定顺序的响应中间件#[panic_hook]
- 将函数注册为恐慌钩子#[panic_hook(order)]
- 将函数注册为具有指定顺序的恐慌钩子
流处理宏
#[http_from_stream]
- 使用默认缓冲区大小包装函数体进行 HTTP 流处理。仅当成功从 HTTP 流读取数据时,函数体才会执行。#[http_from_stream(buffer_size)]
- 使用指定缓冲区大小包装函数体进行 HTTP 流处理。#[http_from_stream(variable_name)]
- 包装函数体进行 HTTP 流处理,将数据存储在指定变量名中。#[http_from_stream(buffer_size, variable_name)]
- 使用指定缓冲区大小和变量名包装函数体进行 HTTP 流处理。#[http_from_stream(variable_name, buffer_size)]
- 使用指定变量名和缓冲区大小(反向顺序)包装函数体进行 HTTP 流处理。#[ws_from_stream]
- 使用默认缓冲区大小包装函数体进行 WebSocket 流处理。仅当成功从 WebSocket 流读取数据时,函数体才会执行。#[ws_from_stream(buffer_size)]
- 使用指定缓冲区大小包装函数体进行 WebSocket 流处理。#[ws_from_stream(variable_name)]
- 包装函数体进行 WebSocket 流处理,将数据存储在指定变量名中。#[ws_from_stream(buffer_size, variable_name)]
- 使用指定缓冲区大小和变量名包装函数体进行 WebSocket 流处理。#[ws_from_stream(variable_name, buffer_size)]
- 使用指定变量名和缓冲区大小(反向顺序)包装函数体进行 WebSocket 流处理。
响应头宏
响应体宏
路由宏
#[route("path")]
- 使用默认服务器为给定路径注册路由处理器(前提:需要 #[hyperlane(server: Server)] 宏)
帮助提示
- 请求相关宏(数据提取)使用
get
操作 - 它们从请求中检索/查询数据 - 响应相关宏(数据设置)使用
set
操作 - 它们分配/配置响应数据 - 钩子宏 对于支持
order
参数的钩子相关宏,如果未指定order
,钩子将比具有指定order
的钩子具有更高优先级(仅适用于#[request_middleware]
、#[response_middleware]
、#[panic_hook]
等宏)
最佳实践警告
- 请求相关宏主要是查询函数,而响应相关宏主要是赋值函数。
- 使用
prologue_hooks
或epilogue_hooks
宏时,不建议将它们与其他宏(如#[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_macros(response_body("panic_hook"), send)]
async fn panic_hook(ctx: Context) {}
#[request_middleware]
#[epilogue_macros(
response_status_code(200),
response_version(HttpVersion::HTTP1_1),
response_header(SERVER => HYPERLANE),
response_header(CONNECTION => KEEP_ALIVE),
response_header(CONTENT_TYPE => TEXT_PLAIN),
response_header(ACCESS_CONTROL_ALLOW_ORIGIN => WILDCARD_ANY),
response_header(STEP => "request_middleware"),
)]
async fn request_middleware(ctx: Context) {}
#[ws]
#[request_middleware(1)]
#[epilogue_macros(
response_body(&vec![]),
response_status_code(101),
response_header(UPGRADE => WEBSOCKET),
response_header(CONNECTION => UPGRADE),
response_header(SEC_WEBSOCKET_ACCEPT => &WebSocketFrame::generate_accept_key(&ctx.try_get_request_header_back(SEC_WEBSOCKET_KEY).await.unwrap())),
response_header(STEP => "upgrade_hook"),
send
)]
async fn upgrade_hook(ctx: Context) {}
#[request_middleware(2)]
#[response_status_code(200)]
#[response_header(SERVER => HYPERLANE)]
#[response_version(HttpVersion::HTTP1_1)]
#[response_header(ACCESS_CONTROL_ALLOW_ORIGIN => WILDCARD_ANY)]
#[response_header(STEP => "connected_hook")]
async fn connected_hook(ctx: Context) {}
#[response_middleware]
#[response_header(STEP => "response_middleware_1")]
async fn response_middleware_1(ctx: Context) {}
#[response_middleware(2)]
#[prologue_macros(
reject(ctx.get_request().await.is_ws()),
response_header(STEP => "response_middleware_2")
)]
#[epilogue_macros(send, flush)]
async fn response_middleware_2(ctx: Context) {}
#[response_middleware("3")]
#[prologue_macros(
ws,
response_header(STEP => "response_middleware_3")
)]
#[epilogue_macros(send_body, flush)]
async fn response_middleware_3(ctx: Context) {}
#[get]
#[http]
async fn prologue_hooks(ctx: Context) {}
#[response_status_code(200)]
async fn epilogue_hooks(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_macros(connect, response_body("connect"))]
async fn connect(ctx: Context) {}
#[route("/delete")]
#[prologue_macros(delete, response_body("delete"))]
async fn delete(ctx: Context) {}
#[route("/head")]
#[prologue_macros(head, response_body("head"))]
async fn head(ctx: Context) {}
#[route("/options")]
#[prologue_macros(options, response_body("options"))]
async fn options(ctx: Context) {}
#[route("/patch")]
#[prologue_macros(patch, response_body("patch"))]
async fn patch(ctx: Context) {}
#[route("/put")]
#[prologue_macros(put, response_body("put"))]
async fn put(ctx: Context) {}
#[route("/trace")]
#[prologue_macros(trace, response_body("trace"))]
async fn trace(ctx: Context) {}
#[route("/h2c")]
#[prologue_macros(h2c, response_body("h2c"))]
async fn h2c(ctx: Context) {}
#[route("/http")]
#[prologue_macros(http, response_body("http"))]
async fn http_only(ctx: Context) {}
#[route("/http0_9")]
#[prologue_macros(http0_9, response_body("http0_9"))]
async fn http0_9(ctx: Context) {}
#[route("/http1_0")]
#[prologue_macros(http1_0, response_body("http1_0"))]
async fn http1_0(ctx: Context) {}
#[route("/http1_1")]
#[prologue_macros(http1_1, response_body("http1_1"))]
async fn http1_1(ctx: Context) {}
#[route("/http2")]
#[prologue_macros(http2, response_body("http2"))]
async fn http2(ctx: Context) {}
#[route("/http3")]
#[prologue_macros(http3, response_body("http3"))]
async fn http3(ctx: Context) {}
#[route("/tls")]
#[prologue_macros(tls, response_body("tls"))]
async fn tls(ctx: Context) {}
#[route("/http1_1_or_higher")]
#[prologue_macros(http1_1_or_higher, response_body("http1_1_or_higher"))]
async fn http1_1_or_higher(ctx: Context) {}
#[route("/unknown_method")]
#[prologue_macros(
clear_response_headers,
filter(ctx.get_request().await.is_unknown_method()),
response_body("unknown_method")
)]
async fn unknown_method(ctx: Context) {}
#[route("/get")]
#[send_body_once]
#[prologue_macros(ws, get, response_body("get"))]
async fn get(ctx: Context) {}
#[send_once]
#[route("/post")]
#[prologue_macros(post, response_body("post"))]
async fn post(ctx: Context) {}
#[ws]
#[route("/ws1")]
#[ws_from_stream]
async fn websocket_1(ctx: Context) {
let body: RequestBody = ctx.get_request_body().await;
let body_list: Vec<ResponseBody> = WebSocketFrame::create_frame_list(&body);
ctx.send_body_list_with_data(&body_list).await.unwrap();
}
#[ws]
#[route("/ws2")]
#[ws_from_stream(1024)]
async fn websocket_2(ctx: Context) {
let body: RequestBody = ctx.get_request_body().await;
let body_list: Vec<ResponseBody> = WebSocketFrame::create_frame_list(&body);
ctx.send_body_list_with_data(&body_list).await.unwrap();
}
#[ws]
#[route("/ws3")]
#[ws_from_stream(request)]
async fn websocket_3(ctx: Context) {
let body: RequestBody = request.get_body().clone();
let body_list: Vec<ResponseBody> = WebSocketFrame::create_frame_list(&body);
ctx.send_body_list_with_data(&body_list).await.unwrap();
}
#[ws]
#[route("/ws4")]
#[ws_from_stream(1024, request)]
async fn websocket_4(ctx: Context) {
let body: RequestBody = request.get_body().clone();
let body_list: Vec<ResponseBody> = WebSocketFrame::create_frame_list(&body);
ctx.send_body_list_with_data(&body_list).await.unwrap();
}
#[ws]
#[route("/ws5")]
#[ws_from_stream(request, 1024)]
async fn websocket_5(ctx: Context) {
let body: RequestBody = request.get_body().clone();
let body_list: Vec<ResponseBody> = WebSocketFrame::create_frame_list(&body);
ctx.send_body_list_with_data(&body_list).await.unwrap();
}
#[route("/hook")]
#[prologue_hooks(prologue_hooks)]
#[epilogue_hooks(epilogue_hooks)]
#[response_body("Testing hook macro")]
async fn hook(ctx: Context) {}
#[closed]
#[route("/get_post")]
#[prologue_macros(
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("/route_param/:test")]
#[response_body(&format!("route param: {request_route_param:?}"))]
#[route_param("test" => request_route_param)]
async fn route_param(ctx: Context) {}
#[route("/host")]
#[host("localhost")]
#[epilogue_macros(
response_body("host string literal: localhost"),
send,
http_from_stream
)]
#[prologue_macros(response_body("host string literal: localhost"), send)]
async fn host(ctx: Context) {}
#[route("/request_query")]
#[epilogue_macros(
request_query("test" => request_query_option),
response_body(&format!("request query: {request_query_option:?}")),
send,
http_from_stream(1024)
)]
#[prologue_macros(
request_query("test" => request_query_option),
response_body(&format!("request query: {request_query_option:?}")),
send
)]
async fn request_query(ctx: Context) {}
#[route("/request_header")]
#[epilogue_macros(
request_header(HOST => request_header_option),
response_body(&format!("request header: {request_header_option:?}")),
send,
http_from_stream(_request)
)]
#[prologue_macros(
request_header(HOST => request_header_option),
response_body(&format!("request header: {request_header_option:?}")),
send
)]
async fn request_header(ctx: Context) {}
#[route("/request_querys")]
#[epilogue_macros(
request_querys(request_querys),
response_body(&format!("request querys: {request_querys:?}")),
send,
http_from_stream(1024, _request)
)]
#[prologue_macros(
request_querys(request_querys),
response_body(&format!("request querys: {request_querys:?}")),
send
)]
async fn request_querys(ctx: Context) {}
#[route("/request_headers")]
#[epilogue_macros(
request_headers(request_headers),
response_body(&format!("request headers: {request_headers:?}")),
send,
http_from_stream(_request, 1024)
)]
#[prologue_macros(
request_headers(request_headers),
response_body(&format!("request headers: {request_headers:?}")),
send
)]
async fn request_headers(ctx: Context) {}
#[response_body(&format!("raw body: {raw_body:?}"))]
#[request_body(raw_body)]
#[route("/request_body")]
async fn request_body(ctx: Context) {}
#[route("/reject_host")]
#[prologue_macros(
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_macros(
referer("http://localhost"),
response_body("referer string literal: http://localhost")
)]
async fn referer(ctx: Context) {}
#[route("/reject_referer")]
#[prologue_macros(
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 文件。
贡献
欢迎贡献!请提交问题或拉取请求。
联系方式
如有任何问题,请联系作者 [email protected]。