Setup
Install Rust and WASM Target
# Install Rust
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
source $HOME/.cargo/env
# Add WASM target
rustup target add wasm32-wasip2
# Install cargo-component
cargo install cargo-component
# Create a new Rust plugin
noorle plugin new my-rust-plugin --lang rust
cd my-rust-plugin
Project Structure
my-rust-plugin/
├── src/
│ ├── lib.rs # Entry point
│ ├── tools.rs # Tool implementations
│ └── utils.rs # Utilities
├── Cargo.toml # Dependencies
├── noorle.yaml # Plugin config
├── world.wit # Component interface
├── .env
└── README.md
Define Tools
Export functions as plugin tools.Simple Functions
// src/lib.rs
pub fn greet(name: &str) -> String {
format!("Hello, {}!", name)
}
pub fn add(a: f64, b: f64) -> f64 {
a + b
}
pub fn multiply(a: f64, b: f64) -> f64 {
a * b
}
Complex Types
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct Item {
pub id: String,
pub name: String,
pub count: i32,
}
pub fn process_items(
items: Vec<Item>,
config: std::collections::HashMap<String, String>,
) -> Vec<Item> {
let prefix = config.get("prefix").cloned().unwrap_or_default();
items
.into_iter()
.map(|mut item| {
item.name = format!("{}{}", prefix, item.name);
item
})
.collect()
}
pub fn search_items(query: &str, limit: i32) -> Vec<Item> {
let limit = std::cmp::min(limit.max(0), 100) as usize;
(0..limit)
.map(|i| Item {
id: i.to_string(),
name: format!("{}_{}", query, i),
count: (i + 1) as i32,
})
.collect()
}
Structured Data
Use serde for JSON serialization:use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize)]
pub struct User {
pub id: String,
pub name: String,
pub email: String,
pub active: bool,
}
#[derive(Serialize, Deserialize)]
pub struct Result<T> {
pub success: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub data: Option<T>,
#[serde(skip_serializing_if = "Option::is_none")]
pub error: Option<String>,
}
pub fn validate_user(user: User) -> Result<User> {
if !user.email.contains("@") {
return Result {
success: false,
data: None,
error: Some("Invalid email".to_string()),
};
}
Result {
success: true,
data: Some(user),
error: None,
}
}
File I/O
Access filesystem via standard library.use std::fs;
use std::path::Path;
pub fn read_config() -> Result<String, String> {
fs::read_to_string("/allowed/path/config.json")
.map_err(|e| e.to_string())
}
pub fn write_cache(key: &str, value: &str) -> Result<bool, String> {
let cache_dir = std::env::var("CACHE_DIR").unwrap_or_else(|_| "/cache".to_string());
let path = Path::new(&cache_dir).join(format!("{}.txt", key));
fs::write(&path, value)
.map_err(|e| e.to_string())?;
Ok(true)
}
pub fn list_files(directory: &str) -> Result<Vec<String>, String> {
let entries = fs::read_dir(directory)
.map_err(|e| e.to_string())?;
let files: Result<Vec<String>, String> = entries
.filter_map(|entry| {
entry.ok().and_then(|e| {
e.file_type().ok().and_then(|ft| {
if ft.is_file() {
e.file_name().into_string().ok()
} else {
None
}
})
})
})
.map(Ok)
.collect();
files
}
Configure Permissions
Innoorle.yaml:
permissions:
filesystem:
allow:
- uri: "fs://cache/**"
access: [read, write]
- uri: "fs://config/**"
access: [read]
environment:
allow:
- key: "CACHE_DIR"
Environment Variables
Access configuration viastd::env:
pub fn get_api_credentials() -> std::collections::HashMap<String, String> {
let mut creds = std::collections::HashMap::new();
creds.insert(
"api_key".to_string(),
std::env::var("API_KEY").unwrap_or_default(),
);
creds.insert(
"api_secret".to_string(),
std::env::var("API_SECRET").unwrap_or_default(),
);
creds
}
pub fn get_database_url() -> String {
std::env::var("DATABASE_URL")
.unwrap_or_else(|_| "sqlite:///:memory:".to_string())
}
.env:
API_KEY=sk-1234567890
API_SECRET=secret-value
DATABASE_URL=postgres://localhost/mydb
Network Access
Make HTTP requests via standard library or libraries.Using reqwest (Rust standard)
pub async fn fetch_data(url: &str) -> Result<String, Box<dyn std::error::Error>> {
let client = reqwest::Client::new();
let body = client.get(url)
.timeout(std::time::Duration::from_secs(5))
.send()
.await?
.text()
.await?;
Ok(body)
}
pub async fn post_json(
url: &str,
data: serde_json::Value,
) -> Result<String, Box<dyn std::error::Error>> {
let client = reqwest::Client::new();
let response = client.post(url)
.header("Content-Type", "application/json")
.json(&data)
.send()
.await?
.text()
.await?;
Ok(response)
}
Cargo.toml:
[dependencies]
reqwest = { version = "0.11", features = ["json"] }
serde_json = "1.0"
permissions:
network:
allow:
- host: "api.example.com"
- host: "*.github.com"
Dependencies
Cargo.toml
[package]
name = "my-rust-plugin"
version = "0.1.0"
edition = "2021"
[dependencies]
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
reqwest = { version = "0.11", features = ["json"] }
tokio = { version = "1", features = ["sync"] }
uuid = { version = "1.0", features = ["v4"] }
[lib]
crate-type = ["cdylib"]
[profile.release]
opt-level = "z"
lto = true
strip = true
Add Dependencies
cargo add serde --features derive
cargo add serde_json
cargo add reqwest --features json
noorle plugin build
Size Optimization
[profile.release]
opt-level = "z" # Optimize for size
lto = true # Link-time optimization
codegen-units = 1 # Better optimization
strip = true # Strip symbols
Error Handling
Return structured errors:use thiserror::Error;
#[derive(Error, Debug, Serialize)]
pub enum PluginError {
#[error("invalid input: {0}")]
InvalidInput(String),
#[error("network error: {0}")]
NetworkError(String),
#[error("io error: {0}")]
IoError(String),
}
pub fn safe_operation(input: &str) -> Result<String, PluginError> {
if input.is_empty() {
return Err(PluginError::InvalidInput("Empty input".to_string()));
}
Ok(process(input)?)
}
fn process(input: &str) -> Result<String, PluginError> {
if input.is_empty() {
return Err(PluginError::InvalidInput("Expected input".to_string()));
}
Ok(input.to_uppercase())
}
Testing
Test locally before uploading.#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_greet() {
assert_eq!(greet("Alice"), "Hello, Alice!");
}
#[test]
fn test_add() {
assert_eq!(add(2.0, 3.0), 5.0);
}
#[test]
fn test_process_items() {
let items = vec![Item {
id: "1".to_string(),
name: "test".to_string(),
count: 5,
}];
let mut config = std::collections::HashMap::new();
config.insert("prefix".to_string(), "new_".to_string());
let result = process_items(items, config);
assert_eq!(result[0].name, "new_test");
}
}
cargo test
Building and Deploying
Build
noorle plugin build
# Output:
# ✓ Building plugin...
# ✓ Compiling Rust to WebAssembly
# ✓ Creating my-rust-plugin.npack (780KB)
Upload
noorle plugin push --api-key YOUR_KEY
# Output:
# ✓ Uploading plugin...
# ✓ Plugin ID: 550e8400-e29b-41d4-a716-446655440000
# ✓ Active version: v1
# ✓ Tools: greet, add, processItems
Best Practices
Input Validation
pub fn validate_email(email: &str) -> Result<(), Vec<String>> {
let mut errors = Vec::new();
if email.is_empty() {
errors.push("Email cannot be empty".to_string());
}
if !email.contains('@') {
errors.push("Email must contain @".to_string());
}
if email.len() > 255 {
errors.push("Email too long".to_string());
}
if errors.is_empty() {
Ok(())
} else {
Err(errors)
}
}
Logging
pub fn log(message: &str, level: &str) {
eprintln!("[{}] {}", level, message);
}
pub fn my_tool(input: &str) -> String {
log(&format!("Processing: {}", input), "INFO");
let result = process(input);
log("Completed", "DEBUG");
result
}
Documentation
/// Transform input text.
///
/// # Arguments
///
/// * `text` - The text to transform
/// * `uppercase` - Convert to uppercase if true
/// * `trim` - Remove whitespace if true
///
/// # Example
///
/// ```
/// assert_eq!(transform_text("hello", true, true), "HELLO");
/// ```
pub fn transform_text(text: &str, uppercase: bool, trim: bool) -> String {
let mut result = text.to_string();
if trim {
result = result.trim().to_string();
}
if uppercase {
result = result.to_uppercase();
}
result
}
Example: Complete Plugin
// src/lib.rs
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize, Clone)]
pub struct WeatherData {
pub city: String,
pub temp: f64,
pub description: String,
}
pub async fn get_weather(city: &str, units: &str) -> Result<WeatherData, String> {
let api_key = std::env::var("OPENWEATHER_API_KEY")
.map_err(|_| "API key not configured".to_string())?;
let url = format!(
"https://api.openweathermap.org/data/2.5/weather?q={}&appid={}&units={}",
city, api_key, units
);
let client = reqwest::Client::new();
let response: serde_json::Value = client
.get(&url)
.send()
.await
.map_err(|e| e.to_string())?
.json()
.await
.map_err(|e| e.to_string())?;
Ok(WeatherData {
city: response["name"].as_str().unwrap_or("").to_string(),
temp: response["main"]["temp"].as_f64().unwrap_or(0.0),
description: response["weather"][0]["description"]
.as_str()
.unwrap_or("")
.to_string(),
})
}
Troubleshooting
Build fails - “Target not found”:rustup target add wasm32-wasip2
- Review dependencies
- Use
cargo-bloatto analyze - Enable release optimizations
- TinyGo/WASI have limited async support
- Use
tokio1.0+ with WASI target - Consider blocking alternatives for simple cases