Examples
These examples show complete, working Rust blocks covering common patterns. Each example includes the block manifest, the handler implementation, and the directory layout at runtime.
Example 1: Raster reprojection🔗
A block that reads a raster file and a resolution parameter, reprojects the raster, and writes the result.
Manifest (blocks/reproject.yaml)🔗
id: raster-tools.reproject
version: 0.1.0
kind: standard
network: false
description: Reprojects a raster to a target resolution
inputs:
source:
type: file
format: GeoTIFF
resolution:
type: number
outputs:
raster:
type: file
format: GeoTIFFHandler (src/main.rs)🔗
use spade::{run, Args, RasterFile};
use std::process::Command;
fn handler(args: Args) -> Result<RasterFile, Box<dyn std::error::Error + Send + Sync>> {
let source: RasterFile = args.input("source")?;
let resolution: f64 = args.param("resolution")?;
let output_path = "reprojected.tif";
let status = Command::new("gdalwarp")
.args([
"-tr",
&resolution.to_string(),
&resolution.to_string(),
&source.path,
output_path,
])
.status()?;
if !status.success() {
return Err("gdalwarp failed".into());
}
Ok(RasterFile::new(output_path))
}
fn main() {
run(handler);
}Runtime directory layout🔗
inputs/
source/
original.tif
params.yaml # resolution: 10
outputs/
raster/
reprojected.tifExample 2: CSV data analysis🔗
A block that reads a CSV file, computes summary statistics for a specified column, and writes the results as JSON.
Manifest (blocks/summarize.yaml)🔗
id: data-tools.summarize
version: 0.1.0
kind: standard
network: false
description: Computes summary statistics for a CSV column
inputs:
data:
type: file
format: CSV
column:
type: string
outputs:
stats:
type: jsonHandler (src/main.rs)🔗
use spade::{run, Args, TabularFile, JsonFile};
use std::fs;
fn handler(args: Args) -> Result<JsonFile, Box<dyn std::error::Error + Send + Sync>> {
let data: TabularFile = args.input("data")?;
let column: String = args.param("column")?;
// Read CSV
let content = fs::read_to_string(&data.path)?;
let mut lines = content.lines();
let headers: Vec<&str> = lines
.next()
.ok_or("empty CSV")?
.split(',')
.map(|h| h.trim())
.collect();
let col_idx = headers
.iter()
.position(|&h| h == column)
.ok_or_else(|| format!("column '{}' not found", column))?;
let mut values: Vec<f64> = Vec::new();
for line in lines {
let fields: Vec<&str> = line.split(',').collect();
if let Some(field) = fields.get(col_idx) {
if let Ok(v) = field.trim().parse::<f64>() {
values.push(v);
}
}
}
let count = values.len();
let sum: f64 = values.iter().sum();
let min = values.iter().cloned().fold(f64::INFINITY, f64::min);
let max = values.iter().cloned().fold(f64::NEG_INFINITY, f64::max);
let stats = serde_json::json!({
"column": column,
"count": count,
"mean": sum / count as f64,
"min": min,
"max": max,
});
let output_path = "summary.json";
fs::write(output_path, serde_json::to_string_pretty(&stats)?)?;
Ok(JsonFile::new(output_path))
}
fn main() {
run(handler);
}Runtime directory layout🔗
inputs/
data/
measurements.csv
params.yaml # column: temperature
outputs/
stats/
summary.jsonExample 3: Map block -- batch raster processing🔗
A map block processes each element of a collection independently. The Spade runtime invokes the handler once per input item.
Manifest (blocks/normalize.yaml)🔗
id: raster-tools.normalize
version: 0.1.0
kind: map
network: false
description: Normalizes a raster file to 0-1 range
inputs:
raster:
type: file
format: GeoTIFF
outputs:
raster:
type: file
format: GeoTIFFHandler (src/main.rs)🔗
use spade::{run, Args, RasterFile};
use std::path::Path;
use std::process::Command;
fn handler(args: Args) -> Result<RasterFile, Box<dyn std::error::Error + Send + Sync>> {
let raster: RasterFile = args.input("raster")?;
let input_path = Path::new(&raster.path);
let stem = input_path
.file_stem()
.and_then(|s| s.to_str())
.unwrap_or("output");
let output_path = format!("{}_normalized.tif", stem);
let status = Command::new("gdal_calc.py")
.args([
"-A",
&raster.path,
&format!("--outfile={}", output_path),
"--calc=(A - A.min()) / (A.max() - A.min())",
"--type=Float32",
])
.status()?;
if !status.success() {
return Err("gdal_calc failed".into());
}
Ok(RasterFile::new(output_path))
}
fn main() {
run(handler);
}When used in a pipeline with a collection of rasters, the Spade runtime calls this handler once per raster file. Each invocation sees a single raster in inputs/raster/.
Runtime directory layout (per invocation)🔗
inputs/
raster/
tile_001.tif
outputs/
raster/
tile_001_normalized.tifExample 4: Multiple outputs🔗
A handler that produces both a processed raster and a JSON statistics file using the Outputs collection.
Handler (src/main.rs)🔗
use spade::{run, Args, Outputs, RasterFile, JsonFile};
use std::fs;
fn handler(args: Args) -> Result<Outputs, Box<dyn std::error::Error + Send + Sync>> {
let source: RasterFile = args.input("source")?;
let threshold: f64 = args.param("threshold")?;
// Processing logic...
let raster_out = "classified.tif";
// ... write classified raster ...
let stats_out = "classification_stats.json";
let stats = serde_json::json!({
"threshold": threshold,
"classified_pixels": 42000,
});
fs::write(stats_out, serde_json::to_string_pretty(&stats)?)?;
let mut outputs = Outputs::new();
outputs.add("raster", RasterFile::new(raster_out));
outputs.add("stats", JsonFile::new(stats_out));
Ok(outputs)
}
fn main() {
run(handler);
}Runtime directory layout🔗
inputs/
source/
input.tif
params.yaml # threshold: 0.5
outputs/
raster/
classified.tif
stats/
classification_stats.jsonThe names passed to outputs.add() become the subdirectory names under outputs/.