Oicana

Rust using Axum

In this chapter, you'll integrate Oicana into a Rust web service using axum. axum is a web application framework built on top of Tokio and Tower, designed for building fast, reliable HTTP services. We'll create a simple async web service that compiles your Oicana template to PDF and serves it via an HTTP endpoint.


Note

This chapter assumes that you have a working Rust setup with cargo. If that is not the case, please follow the official Rust installation guide to install Rust on your machine.


Let's start with a fresh Axum project. First, create a new binary project with cargo init --bincargo init --bin in a new directory. Then add the necessary dependencies to your Cargo.tomlCargo.toml:


                                  
[dependencies]

                                  
oicana = "0.1.0-alpha.5"

                                  
oicana_files = "0.1.0-alpha.5"

                                  
oicana_input = "0.1.0-alpha.5"

                                  
oicana_export = "0.1.0-alpha.5"

                                  


                                  
axum = { version = "0.8", features = ["macros"] }

                                  
tokio = { version = "1", features = ["full"] }

                                  
tower = "0.5"

                                  
serde_json = "1.0"

                                  
[dependencies]

                                  
oicana = "0.1.0-alpha.5"

                                  
oicana_files = "0.1.0-alpha.5"

                                  
oicana_input = "0.1.0-alpha.5"

                                  
oicana_export = "0.1.0-alpha.5"

                                  


                                  
axum = { version = "0.8", features = ["macros"] }

                                  
tokio = { version = "1", features = ["full"] }

                                  
tower = "0.5"

                                  
serde_json = "1.0"

                                  
[dependencies]

                                  
oicana = "0.1.0-alpha.5"

                                  
oicana_files = "0.1.0-alpha.5"

                                  
oicana_input = "0.1.0-alpha.5"

                                  
oicana_export = "0.1.0-alpha.5"

                                  


                                  
axum = { version = "0.8", features = ["macros"] }

                                  
tokio = { version = "1", features = ["full"] }

                                  
tower = "0.5"

                                  
serde_json = "1.0"

                                  
[dependencies]

                                  
oicana = "0.1.0-alpha.5"

                                  
oicana_files = "0.1.0-alpha.5"

                                  
oicana_input = "0.1.0-alpha.5"

                                  
oicana_export = "0.1.0-alpha.5"

                                  


                                  
axum = { version = "0.8", features = ["macros"] }

                                  
tokio = { version = "1", features = ["full"] }

                                  
tower = "0.5"

                                  
serde_json = "1.0"


Run cargo buildcargo build to download and compile the dependencies. This might take a few minutes on first run.

New service endpoint

We will define a new endpoint to compile our Oicana template to a PDF and return the PDF file to the user.


  1. Create a new directory in the Rust project called templatestemplates and copy example-0.1.0.zipexample-0.1.0.zip into that directory.
  2. Replace the contents of src/main.rssrc/main.rs with a basic Axum server that loads and compiles the template:


    
                                          
    use std::fs::File;
    
                                          
    use std::sync::{Arc, Mutex};
    
                                          
    use axum::{
    
                                          
        Router,
    
                                          
        body::Body,
    
                                          
        extract::State,
    
                                          
        http::{StatusCode, header},
    
                                          
        response::{IntoResponse, Response},
    
                                          
        routing::post,
    
                                          
    };
    
                                          
    use oicana::Template;
    
                                          
    use oicana_export::pdf::export_merged_pdf;
    
                                          
    use oicana_files::packed::PackedTemplate;
    
                                          
    use oicana_input::{CompilationConfig, TemplateInputs};
    
                                          

    
                                          
    #[tokio::main]
    
                                          
    async fn main() {
    
                                          
        let template_file = File::open("templates/example-0.1.0.zip")
    
                                          
            .expect("Failed to open template file");
    
                                          
        let template = Template::init(template_file)
    
                                          
            .expect("Failed to initialize template");
    
                                          
            
    
                                          
        let template = Arc::new(Mutex::new(template));
    
                                          
        
    
                                          
        let app = Router::new()
    
                                          
            .route("/compile", post(compile))
    
                                          
            .with_state(template);
    
                                          
            
    
                                          
        let listener = tokio::net::TcpListener::bind("127.0.0.1:3000")
    
                                          
            .await
    
                                          
            .unwrap();
    
                                          
            
    
                                          
        println!("Server running at http://127.0.0.1:3000");
    
                                          
        axum::serve(listener, app).await.unwrap();
    
                                          
    }
    
                                          

    
                                          
    async fn compile(State(template): State<Arc<Mutex<Template<PackedTemplate>>>>) -> impl IntoResponse {
    
                                          
        let mut template = template.lock().unwrap();
    
                                          
        
    
                                          
        // Compile with development mode for demonstration
    
                                          
        // (uses development fallback values for inputs)
    
                                          
        let mut inputs = TemplateInputs::new();
    
                                          
        inputs.with_config(CompilationConfig::development());
    
                                          
        
    
                                          
        let result = template.compile(inputs)
    
                                          
            .expect("Failed to compile template");
    
                                          
            
    
                                          
        let pdf = export_merged_pdf(&result.document, &*template)
    
                                          
            .expect("Failed to export PDF");
    
                                          
            
    
                                          
        Response::builder()
    
                                          
            .status(StatusCode::OK)
    
                                          
            .header(header::CONTENT_TYPE, "application/pdf")
    
                                          
            .header(
    
                                          
                header::CONTENT_DISPOSITION,
    
                                          
                "attachment; filename=\"example.pdf\"",
    
                                          
            )
    
                                          
            .body(Body::from(pdf))
    
                                          
            .unwrap()
    
                                          
    }
    
                                          
    use std::fs::File;
    
                                          
    use std::sync::{Arc, Mutex};
    
                                          
    use axum::{
    
                                          
        Router,
    
                                          
        body::Body,
    
                                          
        extract::State,
    
                                          
        http::{StatusCode, header},
    
                                          
        response::{IntoResponse, Response},
    
                                          
        routing::post,
    
                                          
    };
    
                                          
    use oicana::Template;
    
                                          
    use oicana_export::pdf::export_merged_pdf;
    
                                          
    use oicana_files::packed::PackedTemplate;
    
                                          
    use oicana_input::{CompilationConfig, TemplateInputs};
    
                                          

    
                                          
    #[tokio::main]
    
                                          
    async fn main() {
    
                                          
        let template_file = File::open("templates/example-0.1.0.zip")
    
                                          
            .expect("Failed to open template file");
    
                                          
        let template = Template::init(template_file)
    
                                          
            .expect("Failed to initialize template");
    
                                          
            
    
                                          
        let template = Arc::new(Mutex::new(template));
    
                                          
        
    
                                          
        let app = Router::new()
    
                                          
            .route("/compile", post(compile))
    
                                          
            .with_state(template);
    
                                          
            
    
                                          
        let listener = tokio::net::TcpListener::bind("127.0.0.1:3000")
    
                                          
            .await
    
                                          
            .unwrap();
    
                                          
            
    
                                          
        println!("Server running at http://127.0.0.1:3000");
    
                                          
        axum::serve(listener, app).await.unwrap();
    
                                          
    }
    
                                          

    
                                          
    async fn compile(State(template): State<Arc<Mutex<Template<PackedTemplate>>>>) -> impl IntoResponse {
    
                                          
        let mut template = template.lock().unwrap();
    
                                          
        
    
                                          
        // Compile with development mode for demonstration
    
                                          
        // (uses development fallback values for inputs)
    
                                          
        let mut inputs = TemplateInputs::new();
    
                                          
        inputs.with_config(CompilationConfig::development());
    
                                          
        
    
                                          
        let result = template.compile(inputs)
    
                                          
            .expect("Failed to compile template");
    
                                          
            
    
                                          
        let pdf = export_merged_pdf(&result.document, &*template)
    
                                          
            .expect("Failed to export PDF");
    
                                          
            
    
                                          
        Response::builder()
    
                                          
            .status(StatusCode::OK)
    
                                          
            .header(header::CONTENT_TYPE, "application/pdf")
    
                                          
            .header(
    
                                          
                header::CONTENT_DISPOSITION,
    
                                          
                "attachment; filename=\"example.pdf\"",
    
                                          
            )
    
                                          
            .body(Body::from(pdf))
    
                                          
            .unwrap()
    
                                          
    }
    
                                          
    use std::fs::File;
    
                                          
    use std::sync::{Arc, Mutex};
    
                                          
    use axum::{
    
                                          
        Router,
    
                                          
        body::Body,
    
                                          
        extract::State,
    
                                          
        http::{StatusCode, header},
    
                                          
        response::{IntoResponse, Response},
    
                                          
        routing::post,
    
                                          
    };
    
                                          
    use oicana::Template;
    
                                          
    use oicana_export::pdf::export_merged_pdf;
    
                                          
    use oicana_files::packed::PackedTemplate;
    
                                          
    use oicana_input::{CompilationConfig, TemplateInputs};
    
                                          

    
                                          
    #[tokio::main]
    
                                          
    async fn main() {
    
                                          
        let template_file = File::open("templates/example-0.1.0.zip")
    
                                          
            .expect("Failed to open template file");
    
                                          
        let template = Template::init(template_file)
    
                                          
            .expect("Failed to initialize template");
    
                                          
            
    
                                          
        let template = Arc::new(Mutex::new(template));
    
                                          
        
    
                                          
        let app = Router::new()
    
                                          
            .route("/compile", post(compile))
    
                                          
            .with_state(template);
    
                                          
            
    
                                          
        let listener = tokio::net::TcpListener::bind("127.0.0.1:3000")
    
                                          
            .await
    
                                          
            .unwrap();
    
                                          
            
    
                                          
        println!("Server running at http://127.0.0.1:3000");
    
                                          
        axum::serve(listener, app).await.unwrap();
    
                                          
    }
    
                                          

    
                                          
    async fn compile(State(template): State<Arc<Mutex<Template<PackedTemplate>>>>) -> impl IntoResponse {
    
                                          
        let mut template = template.lock().unwrap();
    
                                          
        
    
                                          
        // Compile with development mode for demonstration
    
                                          
        // (uses development fallback values for inputs)
    
                                          
        let mut inputs = TemplateInputs::new();
    
                                          
        inputs.with_config(CompilationConfig::development());
    
                                          
        
    
                                          
        let result = template.compile(inputs)
    
                                          
            .expect("Failed to compile template");
    
                                          
            
    
                                          
        let pdf = export_merged_pdf(&result.document, &*template)
    
                                          
            .expect("Failed to export PDF");
    
                                          
            
    
                                          
        Response::builder()
    
                                          
            .status(StatusCode::OK)
    
                                          
            .header(header::CONTENT_TYPE, "application/pdf")
    
                                          
            .header(
    
                                          
                header::CONTENT_DISPOSITION,
    
                                          
                "attachment; filename=\"example.pdf\"",
    
                                          
            )
    
                                          
            .body(Body::from(pdf))
    
                                          
            .unwrap()
    
                                          
    }
    
                                          
    use std::fs::File;
    
                                          
    use std::sync::{Arc, Mutex};
    
                                          
    use axum::{
    
                                          
        Router,
    
                                          
        body::Body,
    
                                          
        extract::State,
    
                                          
        http::{StatusCode, header},
    
                                          
        response::{IntoResponse, Response},
    
                                          
        routing::post,
    
                                          
    };
    
                                          
    use oicana::Template;
    
                                          
    use oicana_export::pdf::export_merged_pdf;
    
                                          
    use oicana_files::packed::PackedTemplate;
    
                                          
    use oicana_input::{CompilationConfig, TemplateInputs};
    
                                          

    
                                          
    #[tokio::main]
    
                                          
    async fn main() {
    
                                          
        let template_file = File::open("templates/example-0.1.0.zip")
    
                                          
            .expect("Failed to open template file");
    
                                          
        let template = Template::init(template_file)
    
                                          
            .expect("Failed to initialize template");
    
                                          
            
    
                                          
        let template = Arc::new(Mutex::new(template));
    
                                          
        
    
                                          
        let app = Router::new()
    
                                          
            .route("/compile", post(compile))
    
                                          
            .with_state(template);
    
                                          
            
    
                                          
        let listener = tokio::net::TcpListener::bind("127.0.0.1:3000")
    
                                          
            .await
    
                                          
            .unwrap();
    
                                          
            
    
                                          
        println!("Server running at http://127.0.0.1:3000");
    
                                          
        axum::serve(listener, app).await.unwrap();
    
                                          
    }
    
                                          

    
                                          
    async fn compile(State(template): State<Arc<Mutex<Template<PackedTemplate>>>>) -> impl IntoResponse {
    
                                          
        let mut template = template.lock().unwrap();
    
                                          
        
    
                                          
        // Compile with development mode for demonstration
    
                                          
        // (uses development fallback values for inputs)
    
                                          
        let mut inputs = TemplateInputs::new();
    
                                          
        inputs.with_config(CompilationConfig::development());
    
                                          
        
    
                                          
        let result = template.compile(inputs)
    
                                          
            .expect("Failed to compile template");
    
                                          
            
    
                                          
        let pdf = export_merged_pdf(&result.document, &*template)
    
                                          
            .expect("Failed to export PDF");
    
                                          
            
    
                                          
        Response::builder()
    
                                          
            .status(StatusCode::OK)
    
                                          
            .header(header::CONTENT_TYPE, "application/pdf")
    
                                          
            .header(
    
                                          
                header::CONTENT_DISPOSITION,
    
                                          
                "attachment; filename=\"example.pdf\"",
    
                                          
            )
    
                                          
            .body(Body::from(pdf))
    
                                          
            .unwrap()
    
                                          
    }

    This code loads the template once at startup and wraps it in Arc<Mutex<Template<PackedTemplate>>>Arc<Mutex<Template<PackedTemplate>>>. The ArcArc (Atomic Reference Counted pointer) allows sharing across threads, while MutexMutex provides the mutable access needed by compile()compile(). When parallel requests come in, they share the same template - each request locks the mutex (one at a time), compiles, then releases the lock.


    The /compile/compile endpoint compiles the template and returns a PDF. We explicitly use CompilationConfig::development()CompilationConfig::development() here to demonstrate how the template uses the development value you defined for the infoinfo input ("Chuck Norris"). We will set an input value in a later step.


Start the service with cargo runcargo run and test the endpoint. You can use curl to download the PDF:


                                
curl -X POST http://127.0.0.1:3000/compile --output example.pdf

                                
curl -X POST http://127.0.0.1:3000/compile --output example.pdf

                                
curl -X POST http://127.0.0.1:3000/compile --output example.pdf

                                
curl -X POST http://127.0.0.1:3000/compile --output example.pdf

The generated example.pdfexample.pdf file should contain your template with the development value.

About performance

PDF generation should typically take only a few milliseconds per request. Since we're loading the template once at startup and sharing it via ArcArc, there's no file I/O overhead on subsequent requests.


For managing multiple templates, the open source Axum example project on GitHub demonstrates using a DashMapDashMap for thread-safe template caching.

Passing inputs from Rust

Our compilecompile function currently does not set a value for the template input. Since we use CompilationConfig::development()CompilationConfig::development(), the development value of { "name": "Chuck Norris" }{ "name": "Chuck Norris" } is used. Now we'll provide an explicit input value and switch to production mode:


                                  
async fn compile(State(template): State<Arc<Mutex<Template<PackedTemplate>>>>) -> impl IntoResponse {

                                  
    let mut template = template.lock().unwrap();

                                  
    

                                  
    let mut inputs = TemplateInputs::new();

                                  
    inputs.with_config(CompilationConfig::production());

                                  
    

                                  
    let json_value = serde_json::json!({ "name": "Baby Yoda" });

                                  
    inputs.with_input(

                                  
        oicana_input::input::json::JsonInput::new(

                                  
            "info".to_string(),

                                  
            json_value.to_string(),

                                  
        )

                                  
    );

                                  
    

                                  
    let result = template.compile(inputs)

                                  
        .expect("Failed to compile template");

                                  
        

                                  
    // ... PDF export and response code from before

                                  
}

                                  
async fn compile(State(template): State<Arc<Mutex<Template<PackedTemplate>>>>) -> impl IntoResponse {

                                  
    let mut template = template.lock().unwrap();

                                  
    

                                  
    let mut inputs = TemplateInputs::new();

                                  
    inputs.with_config(CompilationConfig::production());

                                  
    

                                  
    let json_value = serde_json::json!({ "name": "Baby Yoda" });

                                  
    inputs.with_input(

                                  
        oicana_input::input::json::JsonInput::new(

                                  
            "info".to_string(),

                                  
            json_value.to_string(),

                                  
        )

                                  
    );

                                  
    

                                  
    let result = template.compile(inputs)

                                  
        .expect("Failed to compile template");

                                  
        

                                  
    // ... PDF export and response code from before

                                  
}

                                  
async fn compile(State(template): State<Arc<Mutex<Template<PackedTemplate>>>>) -> impl IntoResponse {

                                  
    let mut template = template.lock().unwrap();

                                  
    

                                  
    let mut inputs = TemplateInputs::new();

                                  
    inputs.with_config(CompilationConfig::production());

                                  
    

                                  
    let json_value = serde_json::json!({ "name": "Baby Yoda" });

                                  
    inputs.with_input(

                                  
        oicana_input::input::json::JsonInput::new(

                                  
            "info".to_string(),

                                  
            json_value.to_string(),

                                  
        )

                                  
    );

                                  
    

                                  
    let result = template.compile(inputs)

                                  
        .expect("Failed to compile template");

                                  
        

                                  
    // ... PDF export and response code from before

                                  
}

                                  
async fn compile(State(template): State<Arc<Mutex<Template<PackedTemplate>>>>) -> impl IntoResponse {

                                  
    let mut template = template.lock().unwrap();

                                  
    

                                  
    let mut inputs = TemplateInputs::new();

                                  
    inputs.with_config(CompilationConfig::production());

                                  
    

                                  
    let json_value = serde_json::json!({ "name": "Baby Yoda" });

                                  
    inputs.with_input(

                                  
        oicana_input::input::json::JsonInput::new(

                                  
            "info".to_string(),

                                  
            json_value.to_string(),

                                  
        )

                                  
    );

                                  
    

                                  
    let result = template.compile(inputs)

                                  
        .expect("Failed to compile template");

                                  
        

                                  
    // ... PDF export and response code from before

                                  
}


Notice that we switched to CompilationConfig::production()CompilationConfig::production() now that we're providing explicit input values. Production mode is the recommended default for all document compilation in your application - it ensures you never accidentally generate a document with test data. In production mode, the template will never fall back to development values for inputs. If an input value is missing in production mode and the input does not have a default value, the compilation will fail unless your template handles nonenone values for that input.


Calling the endpoint now will result in a PDF with "Baby Yoda" instead of "Chuck Norris". Building on this minimal service, you could set input values based on database entries or the request payload. Take a look at the open source Axum example project on GitHub for a more complete showcase of the Oicana Rust integration, including blob inputs, error handling, and OpenAPI documentation.