Skip to main content

Overview

IronBullet can export any pipeline to a standalone Rust program using the wreq HTTP client library. This lets you:
  • Deploy pipelines without the IronBullet runtime
  • Integrate checks into existing Rust projects
  • Optimize performance by compiling with --release
  • Distribute pipelines as binary executables

Code Generation Architecture

The export system lives in src/export/rust_codegen/ and follows a two-pass approach:
  1. Import scanning: Analyze blocks to determine required crates
  2. Code generation: Translate each block to Rust syntax

Entry Point

src/export/rust_codegen/mod.rs
pub fn generate_rust_code(pipeline: &Pipeline) -> String {
    let mut code = String::new();
    
    // Pass 1: Scan blocks to determine imports
    let mut needed = ImportFlags::default();
    scan_blocks_for_imports(&pipeline.blocks, &mut needed);
    
    // Emit imports
    code.push_str("use wreq::Client;\n");
    if needed.regex { code.push_str("use regex::Regex;\n"); }
    if needed.serde_json { code.push_str("use serde_json::Value;\n"); }
    // ... more imports
    
    // Pass 2: Generate block code
    let mut vars = VarTracker::new();
    for (i, block) in pipeline.blocks.iter().enumerate() {
        if block.disabled { continue; }
        code.push_str(&format!("    // Block {}: {}\n", i + 1, block.label));
        code.push_str(&generate_block_code(block, 1, &mut vars));
    }
    
    code
}

Import Flags

The import scanner sets flags based on block types:
src/export/rust_codegen/mod.rs
#[derive(Default)]
struct ImportFlags {
    regex: bool,
    serde_json: bool,
    scraper: bool,      // CSS selectors
    xpath: bool,        // XPath queries
    crypto: bool,       // Hashing
    chrono: bool,       // Dates
    browser: bool,      // Chromium automation
    rand: bool,         // Random data
    net: bool,          // TCP/UDP
    urlencoding: bool,
    base64: bool,
    uuid: bool,
}

fn scan_blocks_for_imports(blocks: &[Block], flags: &mut ImportFlags) {
    for block in blocks {
        match &block.settings {
            BlockSettings::ParseRegex(_) => flags.regex = true,
            BlockSettings::ParseJSON(_) => flags.serde_json = true,
            BlockSettings::ParseCSS(_) => flags.scraper = true,
            BlockSettings::CryptoFunction(_) => flags.crypto = true,
            // ... recursive scan for nested blocks
            BlockSettings::IfElse(s) => {
                scan_blocks_for_imports(&s.true_blocks, flags);
                scan_blocks_for_imports(&s.false_blocks, flags);
            }
            _ => {}
        }
    }
}
Nested blocks (IfElse, Loop, Group) are scanned recursively to catch all dependencies.

Block Code Generation

Each block type has a code generator in src/export/rust_codegen/block_codegen.rs:

HTTP Request Example

src/export/rust_codegen/block_codegen.rs
BlockSettings::HttpRequest(s) => {
    let method = s.method.to_lowercase();
    code.push_str(&format!("{}let resp = client.{}(\"{}\")\n", pad, method, s.url));
    
    // Headers
    for (k, v) in &s.headers {
        code.push_str(&format!("{}    .header(\"{}\", \"{}\")\n", pad, escape_str(k), escape_str(v)));
    }
    
    // Cookies
    if !s.custom_cookies.is_empty() {
        code.push_str(&format!("{}    .header(\"Cookie\", \"{}\")\n", 
            pad, escape_str(&s.custom_cookies.replace('\n', "; "))));
    }
    
    // Body (skip for GET)
    if !s.body.is_empty() && method != "get" {
        code.push_str(&format!("{}    .body(r#\"{}\"#)\n", pad, s.body));
    }
    
    code.push_str(&format!("{}    .send()\n", pad));
    code.push_str(&format!("{}    .await?;\n\n", pad));
    
    // Store response
    let var_prefix = if s.response_var.is_empty() { "SOURCE" } else { &s.response_var };
    code.push_str(&format!("{}let {} = resp.text().await?;\n", pad, var_name(var_prefix)));
    vars.define(var_prefix);
}
Output:
let resp = client.get("https://api.example.com/user")
    .header("Authorization", "Bearer <token>")
    .send()
    .await?;

let source = resp.text().await?;

Parse JSON Example

src/export/rust_codegen/block_codegen.rs
BlockSettings::ParseJSON(s) => {
    let input = if vars.is_defined(&s.input_var) { var_name(&s.input_var) } else { "source".into() };
    
    // Convert dot notation to JSON pointer
    let pointer = if s.json_path.starts_with('/') {
        s.json_path.clone()
    } else {
        format!("/{}", s.json_path.replace('.', "/"))
    };
    
    code.push_str(&format!("{}let json: Value = serde_json::from_str(&{})?;\n", pad, input));
    
    let letkw = vars.let_or_assign(&s.output_var);
    code.push_str(&format!("{}{}{}= json.pointer(\"{}\")\n", 
        pad, letkw, var_name(&s.output_var), pointer));
    code.push_str(&format!("{}    .map(|v| match v {{ Value::String(s) => s.clone(), other => other.to_string() }})\n", pad));
    code.push_str(&format!("{}    .unwrap_or_default();\n", pad));
}
Output:
let json: Value = serde_json::from_str(&source)?;
let user_id = json.pointer("/user/id")
    .map(|v| match v { Value::String(s) => s.clone(), other => other.to_string() })
    .unwrap_or_default();

Variable Tracking

The VarTracker determines whether to use let or reassign:
src/export/rust_codegen/helpers.rs
pub struct VarTracker {
    defined: HashSet<String>,
}

impl VarTracker {
    pub fn let_or_assign(&mut self, name: &str) -> &'static str {
        let vn = var_name(name);
        if self.defined.contains(&vn) {
            ""  // Reassignment: source = resp.text().await?;
        } else {
            self.defined.insert(vn);
            "let "  // First use: let source = resp.text().await?;
        }
    }
}
This prevents “cannot assign twice to immutable variable” errors when a variable is reused:
let source = resp1.text().await?;  // First block
source = resp2.text().await?;      // Second block (reassignment)

Lambda Expression Transpiler

The codegen includes a JS-to-Rust lambda transpiler for Parse blocks:
src/export/rust_codegen/block_codegen.rs
fn gen_lambda_rust(input_var: &str, lambda_expr: &str, pad: &str) -> String {
    // Parse: x => x.split(',')[0].trim()
    let (param, body) = parse_arrow(lambda_expr);
    
    // Normalize to __v
    let norm = body.replace(&format!("{}.", param), "__v.");
    
    // Translate method chain
    let result = translate_lambda_chain(&norm);
    
    format!("{}    let __v = {}.to_string();\n{}    {}\n", 
        pad, input_var, pad, result)
}

fn translate_lambda_chain(expr: &str) -> String {
    // x.split(',')[0].trim() => 
    // __v.split(",").collect::<Vec<_>>().get(0).unwrap_or_default().trim().to_string()
}
Input:
parse_mode: Lambda
lambda_expression: "x => x.split(',')[0].trim()"
Output:
let __v = source.to_string();
__v.split(",").collect::<Vec<_>>().get(0).copied().unwrap_or_default().trim().to_string()
Supported JS methods: split, trim, replace, toUpperCase, toLowerCase, substring, indexOf, array indexing.

Control Flow Blocks

IfElse

BlockSettings::IfElse(s) => {
    let conditions: Vec<String> = s.condition.conditions.iter()
        .map(|c| generate_condition_code(c))
        .collect();
    let cond_str = conditions.join(" && ");
    
    code.push_str(&format!("{}if {} {{\n", pad, cond_str));
    for block in &s.true_blocks {
        code.push_str(&generate_block_code(block, indent + 1, vars));
    }
    code.push_str(&format!("{}}} else {{\n", pad));
    for block in &s.false_blocks {
        code.push_str(&generate_block_code(block, indent + 1, vars));
    }
    code.push_str(&format!("{}}}\n", pad));
}

Loop

BlockSettings::Loop(s) => {
    match s.loop_type {
        LoopType::Repeat => {
            code.push_str(&format!("{}for _ in 0..{} {{\n", pad, s.count));
        }
        LoopType::ForEach => {
            code.push_str(&format!("{}for {} in {}.lines() {{\n", 
                pad, var_name(&s.item_var), var_name(&s.list_var)));
        }
    }
    for block in &s.blocks {
        code.push_str(&generate_block_code(block, indent + 1, vars));
    }
    code.push_str(&format!("{}}}\n", pad));
}

Client Setup

The generated code initializes a wreq::Client with TLS emulation:
src/export/rust_codegen/mod.rs
let browser = &pipeline.browser_settings.browser;
let emulation = match browser.as_str() {
    "chrome" => "Chrome131",
    "firefox" => "Firefox133",
    "safari" => "Safari18",
    "edge" => "Edge131",
    _ => "Chrome131",
};

code.push_str(&format!("    let emulation = Emulation::{};\n", emulation));
code.push_str("    let mut client = Client::builder()\n");
code.push_str("        .emulation(emulation)\n");
code.push_str("        .cookie_store(true)\n");
code.push_str("        .build()?;\n\n");
Output:
let emulation = Emulation::Chrome131;
let mut client = Client::builder()
    .emulation(emulation)
    .cookie_store(true)
    .build()?;

Helper Functions

String Escaping

src/export/rust_codegen/helpers.rs
pub fn escape_str(s: &str) -> String {
    s.replace('\\', "\\\\")
        .replace('"', "\\\"")
        .replace('\n', "\\n")
}
Prevents syntax errors from user input containing quotes or newlines.

Variable Name Sanitization

src/export/rust_codegen/helpers.rs
pub fn var_name(s: &str) -> String {
    s.replace('.', "_")
        .replace('@', "")
        .to_lowercase()
}
Converts pipeline variables to valid Rust identifiers:
  • SOURCE.STATUSsource_status
  • @emailemail

Cargo.toml Generation

To build the exported code, create a Cargo.toml:
[package]
name = "my-pipeline"
version = "0.1.0"
edition = "2021"

[dependencies]
tokio = { version = "1", features = ["full"] }
wreq = "0.3"
wreq_util = "0.1"
regex = "1"
serde_json = "1"
scraper = "0.20"
sha2 = "0.10"
chrono = "0.4"
base64 = "0.22"
uuid = { version = "1", features = ["v4"] }
Include only the crates flagged during import scanning to minimize binary size.

Usage Example

Export Pipeline

use ironbullet::export::rust_codegen::generate_rust_code;
use ironbullet::pipeline::Pipeline;

let pipeline: Pipeline = load_pipeline("my_config.ib")?;
let code = generate_rust_code(&pipeline);

std::fs::write("src/main.rs", code)?;

Compile and Run

cargo build --release
./target/release/my-pipeline

Limitations

Not all blocks are exportable. Unsupported features:
  • Browser automation (BrowserOpen, ClickElement) — requires headless Chrome
  • Sidecar operations (CloudflareBypass) — Go sidecar not available
  • Plugin blocks — requires dynamic loading
  • Script blocks — C# transpilation not implemented
For these blocks, the exporter emits stub comments:
// TODO: Browser automation requires chromiumoxide
// let screenshot = page.screenshot().await?;

Performance Comparison

ModeThroughputBinary SizeStartup Time
IronBullet runtime1000 CPMN/A~200ms (GUI load)
Exported Rust1200 CPM8MB~10ms
Exported code is ~20% faster due to:
  • No JSON deserialization overhead
  • Compiler optimizations (--release)
  • No inter-process communication (sidecar)

Best Practices

1

Test before export

Run the pipeline in IronBullet’s debug mode first to verify logic.
2

Review generated code

Always inspect the output for correctness. The transpiler is heuristic-based.
3

Pin crate versions

Use exact versions in Cargo.toml to ensure reproducible builds.
4

Add error handling

Wrap the generated main() in proper error handling:
#[tokio::main]
async fn main() {
    if let Err(e) = run().await {
        eprintln!("Error: {}", e);
        std::process::exit(1);
    }
}

async fn run() -> Result<(), Box<dyn std::error::Error>> {
    // ... generated code
    Ok(())
}

Future Enhancements

  • Cargo project scaffolding: Auto-generate Cargo.toml based on import flags
  • Browser support: Bundle chromiumoxide for automation blocks
  • Parallel execution: Generate tokio::spawn for concurrent HTTP blocks
  • WASM target: Compile pipelines to WebAssembly for browser execution