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:
- Import scanning: Analyze blocks to determine required crates
- 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.STATUS → source_status
@email → email
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?;
| Mode | Throughput | Binary Size | Startup Time |
|---|
| IronBullet runtime | 1000 CPM | N/A | ~200ms (GUI load) |
| Exported Rust | 1200 CPM | 8MB | ~10ms |
Exported code is ~20% faster due to:
- No JSON deserialization overhead
- Compiler optimizations (
--release)
- No inter-process communication (sidecar)
Best Practices
Test before export
Run the pipeline in IronBullet’s debug mode first to verify logic.
Review generated code
Always inspect the output for correctness. The transpiler is heuristic-based.
Pin crate versions
Use exact versions in Cargo.toml to ensure reproducible builds.
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