Handler Functions
A handler is a function or closure that receives an Args struct and returns a Result with a typed output. The Spade library calls your handler after loading all inputs and parameters.
Basic handler pattern🔗
Every handler follows this signature:
fn handler(args: Args) -> Result<O, Box<dyn Error + Send + Sync>>Where O implements both IntoOutput and SpadeType. Pass it to run():
use spade::{run, Args, RasterFile};
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")?;
// Process the raster...
Ok(RasterFile::new("result.tif"))
}
fn main() {
run(handler);
}The run function🔗
pub fn run<F, O>(handler: F)
where
F: FnOnce(Args) -> Result<O, Box<dyn Error + Send + Sync>>,
O: IntoOutput + SpadeType + 'static,run is the main entry point. It:
- Loads parameters from
params.yaml - Scans inputs from the
inputs/directory - Builds the
Argsstruct merging parameters and inputs - Calls your handler with the
Args - Writes outputs to the
outputs/directory - Exits with code 1 on any error, printing the message to stderr with a
spade:prefix
The handler can be a function pointer or a closure.
Using closures🔗
Closures work as handlers, which is useful for capturing values:
use spade::{run, Args, RasterFile};
fn main() {
run(|args: Args| -> Result<RasterFile, Box<dyn std::error::Error + Send + Sync>> {
let source: RasterFile = args.input("source")?;
Ok(RasterFile::new("result.tif"))
});
}Accessing inputs🔗
Use args.input::<T>(name) to retrieve typed file inputs. T must implement FromInput:
// Single file
let source: RasterFile = args.input("source")?;
// source.path is a String with the file path
// Collection
let tiles: RasterFileCollection = args.input("tiles")?;
// tiles.paths is a Vec<String>
// Directory
let data: Directory = args.input("data")?;
// data.path is a String with the directory pathThe library scans inputs/<name>/ and calls the appropriate FromInput method based on whether a single file or multiple files were found.
Accessing parameters🔗
Use args.param::<T>(name) to retrieve scalar parameters. T must implement serde::de::DeserializeOwned:
let resolution: f64 = args.param("resolution")?;
let method: String = args.param("method")?;
let normalize: bool = args.param("normalize")?;Accessing secrets🔗
Use spade::get_secret to retrieve a credential the pipeline injected under a logical name, rather than reading it from params.yaml or the process environment directly:
use spade::get_secret;
let dsn: String = get_secret("db")?;
// dsn is a connection string, e.g. "postgresql://user:pass@host:5432/db"get_secret(name: &str) -> Result<String, SpadeError> returns the secret value bound to the logical name. The logical name is part of your block's contract, documented like any other parameter: the pipeline author binds it to one of their stored secrets via a secrets: map alongside args: in the pipeline YAML.
If name was not declared in the pipeline's secrets: map, or the bound secret failed to resolve, get_secret returns SpadeError::SecretNotFound { name }. A declared-but-unresolvable secret is a real error, not a silently empty string -- propagate it with ? like any other SpadeError.
get_secret never talks to a keychain or key-management service itself. It only reads values the worker or CLI already injected into the process environment before your handler ran.
Single output🔗
When the handler returns a single typed value, the library writes it to outputs/. The output directory name is determined by:
- The block manifest, if it declares exactly one output
- Otherwise, the type's
default_output_name()(e.g.,"raster"forRasterFile)
fn handler(args: Args) -> Result<RasterFile, Box<dyn std::error::Error + Send + Sync>> {
let source: RasterFile = args.input("source")?;
Ok(RasterFile::new("result.tif"))
}
// Written to outputs/raster/result.tif (or the manifest-declared name)Multiple outputs🔗
Use the Outputs collection to return multiple named outputs:
use spade::{run, Args, Outputs, RasterFile, JsonFile};
fn handler(args: Args) -> Result<Outputs, Box<dyn std::error::Error + Send + Sync>> {
let source: RasterFile = args.input("source")?;
// Process and create output files...
let mut outputs = Outputs::new();
outputs.add("raster", RasterFile::new("processed.tif"));
outputs.add("stats", JsonFile::new("stats.json"));
Ok(outputs)
}
fn main() {
run(handler);
}This writes:
outputs/
raster/
processed.tif
stats/
stats.jsonOutputs::single🔗
For a single output using the type's default name:
let outputs = Outputs::single(RasterFile::new("result.tif"));
Ok(outputs)
// Written to outputs/raster/result.tifNo-output handlers🔗
If your block performs a side effect without producing files, return ():
fn handler(args: Args) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
let source: RasterFile = args.input("source")?;
println!("Validated: {}", source.path);
Ok(())
}
fn main() {
run(handler);
}The () type implements IntoOutput and SpadeType, so run() skips the output-writing step.
Optional inputs and parameters🔗
Use has_input and has_param to check for optional arguments:
fn handler(args: Args) -> Result<RasterFile, Box<dyn std::error::Error + Send + Sync>> {
let source: RasterFile = args.input("source")?;
// Optional mask input
let mask: Option<VectorFile> = if args.has_input("mask") {
Some(args.input("mask")?)
} else {
None
};
// Optional buffer parameter with default
let buffer: f64 = if args.has_param("buffer") {
args.param("buffer")?
} else {
0.0
};
// Process with source, mask, buffer...
Ok(RasterFile::new("result.tif"))
}Error handling🔗
The handler returns Result<O, Box<dyn Error + Send + Sync>>. Use the ? operator to propagate errors from library calls, and construct custom errors as needed:
fn handler(args: Args) -> Result<RasterFile, Box<dyn std::error::Error + Send + Sync>> {
let source: RasterFile = args.input("source")?;
if !source.path.ends_with(".tif") {
return Err(format!("expected GeoTIFF, got {}", source.path).into());
}
// Processing...
Ok(RasterFile::new("result.tif"))
}The SpadeError enum also provides structured error variants:
use spade::SpadeError;
// These are returned by args.input() and args.param() automatically:
// SpadeError::InputNotFound { name }
// SpadeError::ParamNotFound { name }
// SpadeError::EmptyInputDir { name }
// SpadeError::TypeMismatch { name, expected, found }
// SpadeError::IoError(io::Error)
// SpadeError::YamlError(serde_yaml::Error)All SpadeError variants implement std::error::Error via thiserror, so they work with ? and Box<dyn Error>.