Rust插件库esi的使用:高效实现ESI(Edge Side Includes)功能的Rust库

ESI for Fastly

这个crate提供了一个流式Edge Side Includes解析器和执行器,专为Fastly Compute设计。

实现是ESI语言规范1.0的一个子集,支持以下标签:

  • <esi:include> (+ alt, onerror="continue")
  • <esi:try> | <esi:attempt> | <esi:except>
  • <esi:vars> | <esi:assign>
  • <esi:choose> | <esi:when> | <esi:otherwise>
  • <esi:comment>
  • <esi:remove>

其他标签将被忽略并按原样提供给客户端。

此实现还包括一个表达式解释器和可用函数库。当前函数包括:

  • $lower(string)
  • $html_encode(string)
  • $replace(haystack, needle, replacement [, count])

示例用法

use fastly::{http::StatusCode, mime, Error, Request, Response};

fn main() { if let Err(err) = handle_request(Request::from_client()) { println!(“returning error response”);

    Response::<span class="hljs-title function_ invoke__">from_status</span>(StatusCode::INTERNAL_SERVER_ERROR)
        .<span class="hljs-title function_ invoke__">with_body</span>(err.<span class="hljs-title function_ invoke__">to_string</span>())
        .<span class="hljs-title function_ invoke__">send_to_client</span>();
}

}

fn handle_request(req: Request) -> Result<(), Error> { // 从后端获取ESI文档。 let mut beresp = req.clone_without_body().send(“origin_0”)?;

<span class="hljs-comment">// 如果响应是HTML,我们可以解析其中的ESI标签。</span>
<span class="hljs-keyword">if</span> beresp
    .<span class="hljs-title function_ invoke__">get_content_type</span>()
    .<span class="hljs-title function_ invoke__">map</span>(|c| c.<span class="hljs-title function_ invoke__">subtype</span>() == mime::HTML)
    .<span class="hljs-title function_ invoke__">unwrap_or</span>(<span class="hljs-literal">false</span>)
{
    <span class="hljs-keyword">let</span> <span class="hljs-variable">processor</span> = esi::Processor::<span class="hljs-title function_ invoke__">new</span>(
        <span class="hljs-comment">// 原始客户端请求。</span>
        <span class="hljs-title function_ invoke__">Some</span>(req),
        <span class="hljs-comment">// 使用默认ESI配置。</span>
        esi::Configuration::<span class="hljs-title function_ invoke__">default</span>()
    );

    processor.<span class="hljs-title function_ invoke__">process_response</span>(
        <span class="hljs-comment">// ESI源文档。注意,body将被消耗。</span>
        &amp;<span class="hljs-keyword">mut</span> beresp,
        <span class="hljs-comment">// 可选地提供客户端响应的模板。</span>
        <span class="hljs-title function_ invoke__">Some</span>(Response::<span class="hljs-title function_ invoke__">from_status</span>(StatusCode::OK).<span class="hljs-title function_ invoke__">with_content_type</span>(mime::TEXT_HTML)),
        <span class="hljs-comment">// 提供发送片段请求的逻辑,否则请求URL的主机名将被用作后端名称。</span>
        <span class="hljs-title function_ invoke__">Some</span>(&amp;|req| {
            <span class="hljs-built_in">println!</span>(<span class="hljs-string">"Sending request {} {}"</span>, req.<span class="hljs-title function_ invoke__">get_method</span>(), req.<span class="hljs-title function_ invoke__">get_path</span>());
            <span class="hljs-title function_ invoke__">Ok</span>(req.<span class="hljs-title function_ invoke__">with_ttl</span>(<span class="hljs-number">120</span>).<span class="hljs-title function_ invoke__">send_async</span>(<span class="hljs-string">"mock-s3"</span>)?.<span class="hljs-title function_ invoke__">into</span>())
        }),
        <span class="hljs-comment">// 可选地提供一个方法,在片段响应流式传输到客户端之前处理它们。</span>
        <span class="hljs-title function_ invoke__">Some</span>(&amp;|req, resp| {
            <span class="hljs-built_in">println!</span>(
                <span class="hljs-string">"Received response for {} {}"</span>,
                req.<span class="hljs-title function_ invoke__">get_method</span>(),
                req.<span class="hljs-title function_ invoke__">get_path</span>()
            );
            <span class="hljs-title function_ invoke__">Ok</span>(resp)
        }),
    )?;
} <span class="hljs-keyword">else</span> {
    <span class="hljs-comment">// 否则,我们可以直接返回响应。</span>
    beresp.<span class="hljs-title function_ invoke__">send_to_client</span>();
}

<span class="hljs-title function_ invoke__">Ok</span>(())

}

由于此处理器在片段可用时立即将其流式传输到客户端,一旦我们开始将响应流式传输到客户端,就无法为后续错误返回相关状态码。因此,建议您参考esi_example_advanced_error_handling应用程序,它允许您通过保持输出流的所有权来优雅地处理错误。

测试

要运行此存储库中包的测试套件,必须在您的PATH中可用viceroy。您可以通过运行以下命令安装最新版本的viceroy

cargo install viceroy

许可证

此项目的源代码和文档根据MIT许可证发布。

完整示例代码

use fastly::{http::StatusCode, mime, Error, Request, Response};

fn main() { if let Err(err) = handle_request(Request::from_client()) { println!(“returning error response”);

    Response::from_status(StatusCode::INTERNAL_SERVER_ERROR)
        .with_body(err.to_string())
        .send_to_client();
}

}

fn handle_request(req: Request) -> Result<(), Error> { // 从后端获取ESI文档 let mut beresp = req.clone_without_body().send(“origin_0”)?;

// 检查响应是否为HTML类型
if beresp
    .get_content_type()
    .map(|c| c.subtype() == mime::HTML)
    .unwrap_or(false)
{
    // 创建ESI处理器
    let processor = esi::Processor::new(
        // 原始客户端请求
        Some(req),
        // 使用默认ESI配置
        esi::Configuration::default()
    );

    // 处理ESI响应
    processor.process_response(
        // ESI源文档
        &mut beresp,
        // 客户端响应模板
        Some(Response::from_status(StatusCode::OK).with_content_type(mime::TEXT_HTML)),
        // 片段请求发送逻辑
        Some(&|req| {
            println!("Sending request {} {}", req.get_method(), req.get_path());
            Ok(req.with_ttl(120).send_async("mock-s3")?.into())
        }),
        // 片段响应处理逻辑
        Some(&|req, resp| {
            println!(
                "Received response for {} {}",
                req.get_method(),
                req.get_path()
            );
            Ok(resp)
        }),
    )?;
} else {
    // 非HTML响应直接返回
    beresp.send_to_client();
}

Ok(())

}


1 回复

esi是一个用于高效实现ESI(Edge Side Includes)功能的Rust库。该库提供了在边缘服务器上处理ESI标签的能力,帮助开发者实现内容片段的动态组装和缓存优化。

主要功能

  • 解析和处理ESI标签
  • 支持条件包含、变量替换等标准ESI功能
  • 提供异步HTTP请求处理
  • 可自定义解析器和处理器

安装方法

在Cargo.toml中添加依赖:

[dependencies]
esi = "0.3"

基本使用方法

use esi::EsiParser;
use std::collections::HashMap;

#[tokio::main]
async fn main() {
    // 示例ESI内容
    let content = r#"
        <html>
            <esi:include src="/header.html"/>
            <body>
                <esi:include src="/widget.html?user=$(USER_ID)"/>
            </body>
        </html>
    "#;

    // 创建解析器
    let mut parser = EsiParser::new();
    
    // 设置变量
    let mut variables = HashMap::new();
    variables.insert("USER_ID".to_string(), "12345".to_string());
    
    // 处理ESI标签
    let result = parser.parse(content, &variables).await.unwrap();
    
    println!("Processed content: {}", result);
}

高级配置示例

use esi::{EsiParser, EsiConfig};
use reqwest::Client;

#[tokio::main]
async fn main() {
    let config = EsiConfig::default()
        .with_http_client(Client::new())
        .with_timeout(std::time::Duration::from_secs(5));
    
    let parser = EsiParser::with_config(config);
    
    // 自定义处理器示例
    let content = r#"<esi:vars>Hello, $(USER_NAME)!</esi:vars>"#;
    
    let mut variables = HashMap::new();
    variables.insert("USER_NAME".to_string(), "Alice".to_string());
    
    let result = parser.parse(content, &variables).await.unwrap();
    println!("{}", result); // 输出: Hello, Alice!
}

错误处理

use esi::{EsiParser, EsiError};

async fn process_content(content: &str) -> Result<String, EsiError> {
    let parser = EsiParser::new();
    let variables = HashMap::new();
    
    parser.parse(content, &variables).await
}

该库支持所有标准ESI标签,包括include、vars、choose等,并提供了灵活的扩展接口用于自定义处理逻辑。

完整示例demo

use esi::{EsiParser, EsiConfig, EsiError};
use reqwest::Client;
use std::collections::HashMap;

#[tokio::main]
async fn main() -> Result<(), EsiError> {
    // 示例1: 基本使用
    println!("=== 基本使用示例 ===");
    let basic_content = r#"
        <html>
            <head>
                <title>ESI示例</title>
            </head>
            <body>
                <esi:include src="/header.html"/>
                <main>
                    <h1>欢迎使用ESI</h1>
                    <esi:include src="/widget.html?user=$(USER_ID)&lang=$(LANGUAGE)"/>
                </main>
                <esi:include src="/footer.html"/>
            </body>
        </html>
    "#;

    let mut parser = EsiParser::new();
    let mut variables = HashMap::new();
    variables.insert("USER_ID".to_string(), "1001".to_string());
    variables.insert("LANGUAGE".to_string(), "zh-CN".to_string());

    let basic_result = parser.parse(basic_content, &variables).await?;
    println!("基本示例处理结果:\n{}", basic_result);

    // 示例2: 高级配置
    println!("\n=== 高级配置示例 ===");
    let config = EsiConfig::default()
        .with_http_client(Client::new())
        .with_timeout(std::time::Duration::from_secs(3))
        .with_max_redirects(5);

    let advanced_parser = EsiParser::with_config(config);
    
    let advanced_content = r#"
        <esi:choose>
            <esi:when test="$(LEVEL) == 'VIP'">
                <div class="vip-welcome">尊贵的VIP用户,欢迎回来!</div>
            </esi:when>
            <esi:when test="$(LEVEL) == 'NORMAL'">
                <div class="normal-welcome">欢迎回来!</div>
            </esi:when>
            <esi:otherwise>
                <div class="guest-welcome">您好,请登录</div>
            </esi:otherwise>
        </esi:choose>
        
        <esi:vars>
            <p>当前时间: $(CURRENT_TIME)</p>
            <p>用户积分: $(USER_POINTS)</p>
        </esi:vars>
    "#;

    let mut advanced_vars = HashMap::new();
    advanced_vars.insert("LEVEL".to_string(), "VIP".to_string());
    advanced_vars.insert("CURRENT_TIME".to_string(), "2024-01-15 10:30:00".to_string());
    advanced_vars.insert("USER_POINTS".to_string(), "1500".to_string());

    let advanced_result = advanced_parser.parse(advanced_content, &advanced_vars).await?;
    println!("高级配置处理结果:\n{}", advanced_result);

    // 示例3: 错误处理
    println!("\n=== 错误处理示例 ===");
    let error_content = r#"<esi:include src="/non-existent-page.html"/>"#;
    
    match parser.parse(error_content, &HashMap::new()).await {
        Ok(result) => println!("成功: {}", result),
        Err(e) => println!("错误处理示例 - 预期错误: {:?}", e),
    }

    Ok(())
}

// 自定义错误处理函数示例
async fn safe_parse_content(parser: &EsiParser, content: &str, variables: &HashMap<String, String>) -> String {
    match parser.parse(content, variables).await {
        Ok(result) => result,
        Err(e) => {
            eprintln!("解析失败: {:?}", e);
            "解析失败,使用默认内容".to_string()
        }
    }
}

// 单元测试示例
#[cfg(test)]
mod tests {
    use super::*;

    #[tokio::test]
    async fn test_basic_esi_parsing() {
        let parser = EsiParser::new();
        let mut variables = HashMap::new();
        variables.insert("NAME".to_string(), "TestUser".to_string());
        
        let content = r#"<esi:vars>Hello, $(NAME)!</esi:vars>"#;
        let result = parser.parse(content, &variables).await.unwrap();
        
        assert_eq!(result, "Hello, TestUser!");
    }
}
回到顶部