Plugin Development
SwissArmyHammer features a flexible plugin architecture that allows you to extend functionality through custom filters, processors, and integrations. The plugin system enables seamless integration of external tools and custom processing logic.
Overview
The plugin system provides:
- Custom Filters: Transform and process prompt content
- Processing Plugins: Add new data processing capabilities
- Template Extensions: Extend the Liquid template engine
- Integration Plugins: Connect with external tools and services
- Workflow Actions: Create custom workflow step implementations
Plugin Architecture
Core Components
Plugin Interface: All plugins implement a common interface
pub trait Plugin: Send + Sync {
fn name(&self) -> &str;
fn description(&self) -> &str;
fn process(&self, input: &str, context: &PluginContext) -> PluginResult<String>;
}
Plugin Registry: Central management of available plugins
pub struct PluginRegistry {
plugins: HashMap<String, Box<dyn Plugin>>,
}
Plugin Context: Provides contextual information to plugins
pub struct PluginContext {
pub file_path: Option<String>,
pub language: Option<String>,
pub metadata: HashMap<String, String>,
pub environment: HashMap<String, String>,
}
Plugin Types
Filter Plugins: Transform text content
- Input processing and validation
- Output formatting and styling
- Content transformation and encoding
Data Plugins: Process structured data
- File format conversions
- Data extraction and parsing
- External API integrations
Workflow Plugins: Custom workflow actions
- External tool execution
- Conditional logic and branching
- State management and persistence
Built-in Plugins
Text Processing Filters
Trim Filter: Remove whitespace
use swissarmyhammer::prompt_filter::PromptFilter;
let filter = PromptFilter::Trim;
let result = filter.apply(" hello world ")?;
// Result: "hello world"
Case Transformation:
let uppercase = PromptFilter::Uppercase;
let lowercase = PromptFilter::Lowercase;
let titlecase = PromptFilter::TitleCase;
let result = uppercase.apply("hello world")?;
// Result: "HELLO WORLD"
String Manipulation:
let replace = PromptFilter::Replace {
pattern: "old".to_string(),
replacement: "new".to_string(),
};
let result = replace.apply("old text with old words")?;
// Result: "new text with new words"
Code Processing Filters
Syntax Highlighting:
let highlight = PromptFilter::CodeHighlight {
language: "rust".to_string(),
};
let code = r#"
fn main() {
println!("Hello, world!");
}
"#;
let result = highlight.apply(code)?;
// Result: HTML with syntax highlighting
Code Formatting:
let format = PromptFilter::CodeFormat {
language: "rust".to_string(),
style: FormatStyle::Standard,
};
let result = format.apply(unformatted_code)?;
File System Filters
File Reading:
let read_file = PromptFilter::FileRead {
path: "./src/main.rs".to_string(),
};
let content = read_file.apply("")?;
// Result: File contents
Directory Listing:
let list_files = PromptFilter::ListFiles {
path: "./src".to_string(),
pattern: Some("*.rs".to_string()),
};
let files = list_files.apply("")?;
// Result: Newline-separated file paths
External Tool Integration
Shell Command Execution:
let shell = PromptFilter::Shell {
command: "git log --oneline -5".to_string(),
timeout: Some(10),
};
let output = shell.apply("")?;
// Result: Command output
HTTP Requests:
let http = PromptFilter::HttpGet {
url: "https://api.example.com/data".to_string(),
headers: HashMap::new(),
timeout: Some(30),
};
let response = http.apply("")?;
// Result: HTTP response body
Creating Custom Plugins
Basic Plugin Implementation
use swissarmyhammer::plugins::{Plugin, PluginContext, PluginResult};
#[derive(Debug)]
pub struct ReverseStringPlugin;
impl Plugin for ReverseStringPlugin {
fn name(&self) -> &str {
"reverse"
}
fn description(&self) -> &str {
"Reverses the input string"
}
fn process(&self, input: &str, _context: &PluginContext) -> PluginResult<String> {
Ok(input.chars().rev().collect())
}
}
// Usage in templates:
// {{ content | reverse }}
Advanced Plugin with Context
use swissarmyhammer::plugins::*;
use std::fs;
#[derive(Debug)]
pub struct ProjectInfoPlugin;
impl Plugin for ProjectInfoPlugin {
fn name(&self) -> &str {
"project_info"
}
fn description(&self) -> &str {
"Extracts project information from context"
}
fn process(&self, _input: &str, context: &PluginContext) -> PluginResult<String> {
let mut info = Vec::new();
// Get language from context
if let Some(lang) = &context.language {
info.push(format!("Language: {}", lang));
}
// Get file path info
if let Some(path) = &context.file_path {
if let Some(name) = std::path::Path::new(path).file_name() {
info.push(format!("File: {}", name.to_string_lossy()));
}
}
// Check for project files
if std::path::Path::new("Cargo.toml").exists() {
info.push("Project: Rust".to_string());
} else if std::path::Path::new("package.json").exists() {
info.push("Project: Node.js".to_string());
}
Ok(info.join("\n"))
}
}
Error Handling in Plugins
use swissarmyhammer::plugins::*;
#[derive(Debug)]
pub struct ValidatingPlugin;
impl Plugin for ValidatingPlugin {
fn name(&self) -> &str {
"validate_json"
}
fn description(&self) -> &str {
"Validates and formats JSON content"
}
fn process(&self, input: &str, _context: &PluginContext) -> PluginResult<String> {
match serde_json::from_str::<serde_json::Value>(input) {
Ok(value) => {
// Format with indentation
match serde_json::to_string_pretty(&value) {
Ok(formatted) => Ok(formatted),
Err(e) => Err(PluginError::ProcessingError {
message: format!("Failed to format JSON: {}", e),
source: Some(Box::new(e)),
}),
}
}
Err(e) => Err(PluginError::ValidationError {
message: format!("Invalid JSON: {}", e),
input_excerpt: input.chars().take(100).collect(),
}),
}
}
}
Plugin Registration and Usage
Registering Plugins
use swissarmyhammer::plugins::PluginRegistry;
// Create registry with built-in plugins
let mut registry = PluginRegistry::with_builtin_plugins();
// Register custom plugins
registry.register(Box::new(ReverseStringPlugin))?;
registry.register(Box::new(ProjectInfoPlugin))?;
registry.register(Box::new(ValidatingPlugin))?;
// Use in prompt library
let library = PromptLibrary::new()
.with_plugin_registry(registry);
Using Plugins in Templates
<!-- Basic usage -->
{{ content | reverse }}
<!-- Chaining filters -->
{{ code | trim | code_highlight: "rust" | reverse }}
<!-- With parameters -->
{{ json_data | validate_json }}
<!-- Conditional usage -->
{% if language == "rust" %}
{{ code | code_format: "rust" }}
{% else %}
{{ code | trim }}
{% endif %}
<!-- Complex processing -->
{{ file_path | file_read | code_highlight: language | trim }}
Dynamic Plugin Loading
use swissarmyhammer::plugins::{PluginLoader, PluginConfig};
// Load plugins from directory
let loader = PluginLoader::new();
let plugins = loader.load_from_directory("./plugins")?;
// Load with configuration
let config = PluginConfig {
allow_unsafe: false,
timeout: Some(30),
memory_limit: Some(100 * 1024 * 1024), // 100MB
..Default::default()
};
let plugins = loader.load_with_config("./plugins", config)?;
// Register loaded plugins
for plugin in plugins {
registry.register(plugin)?;
}
Advanced Plugin Patterns
Stateful Plugins
use std::sync::{Arc, Mutex};
use std::collections::HashMap;
#[derive(Debug)]
pub struct CachingPlugin {
cache: Arc<Mutex<HashMap<String, String>>>,
}
impl CachingPlugin {
pub fn new() -> Self {
Self {
cache: Arc::new(Mutex::new(HashMap::new())),
}
}
}
impl Plugin for CachingPlugin {
fn name(&self) -> &str {
"cache"
}
fn description(&self) -> &str {
"Caches expensive computations"
}
fn process(&self, input: &str, context: &PluginContext) -> PluginResult<String> {
let cache_key = format!("{}:{}", input, context.file_path.as_deref().unwrap_or(""));
// Check cache first
{
let cache = self.cache.lock().unwrap();
if let Some(cached) = cache.get(&cache_key) {
return Ok(cached.clone());
}
}
// Expensive computation
let result = expensive_computation(input)?;
// Store in cache
{
let mut cache = self.cache.lock().unwrap();
cache.insert(cache_key, result.clone());
}
Ok(result)
}
}
Async Plugin Processing
use tokio::runtime::Runtime;
#[derive(Debug)]
pub struct AsyncPlugin {
runtime: Runtime,
}
impl AsyncPlugin {
pub fn new() -> PluginResult<Self> {
let runtime = Runtime::new()
.map_err(|e| PluginError::InitializationError {
message: format!("Failed to create async runtime: {}", e),
source: Some(Box::new(e)),
})?;
Ok(Self { runtime })
}
async fn async_process(&self, input: &str) -> Result<String, Box<dyn std::error::Error>> {
// Async operations like HTTP requests, database queries, etc.
let client = reqwest::Client::new();
let response = client
.post("https://api.example.com/process")
.body(input.to_string())
.send()
.await?
.text()
.await?;
Ok(response)
}
}
impl Plugin for AsyncPlugin {
fn name(&self) -> &str {
"async_processor"
}
fn description(&self) -> &str {
"Processes input asynchronously"
}
fn process(&self, input: &str, _context: &PluginContext) -> PluginResult<String> {
self.runtime.block_on(self.async_process(input))
.map_err(|e| PluginError::ProcessingError {
message: format!("Async processing failed: {}", e),
source: Some(e),
})
}
}
Configuration-Based Plugins
use serde::{Deserialize, Serialize};
#[derive(Debug, Deserialize, Serialize)]
pub struct DatabaseConfig {
pub connection_string: String,
pub timeout: u64,
pub pool_size: u32,
}
#[derive(Debug)]
pub struct DatabasePlugin {
config: DatabaseConfig,
// connection pool, etc.
}
impl DatabasePlugin {
pub fn new(config: DatabaseConfig) -> PluginResult<Self> {
// Initialize database connection
Ok(Self { config })
}
pub fn from_config_file<P: AsRef<std::path::Path>>(path: P) -> PluginResult<Self> {
let content = std::fs::read_to_string(path)
.map_err(|e| PluginError::ConfigError {
message: format!("Failed to read config file: {}", e),
source: Some(Box::new(e)),
})?;
let config: DatabaseConfig = toml::from_str(&content)
.map_err(|e| PluginError::ConfigError {
message: format!("Failed to parse config: {}", e),
source: Some(Box::new(e)),
})?;
Self::new(config)
}
}
impl Plugin for DatabasePlugin {
fn name(&self) -> &str {
"database_query"
}
fn description(&self) -> &str {
"Executes database queries"
}
fn process(&self, input: &str, _context: &PluginContext) -> PluginResult<String> {
// Execute SQL query and return results
// Implementation depends on database driver
todo!("Implement database query execution")
}
}
Testing Plugins
Unit Testing
#[cfg(test)]
mod tests {
use super::*;
use swissarmyhammer::plugins::PluginContext;
#[test]
fn test_reverse_plugin() {
let plugin = ReverseStringPlugin;
let context = PluginContext::default();
let result = plugin.process("hello", &context).unwrap();
assert_eq!(result, "olleh");
}
#[test]
fn test_plugin_with_context() {
let plugin = ProjectInfoPlugin;
let context = PluginContext {
language: Some("rust".to_string()),
file_path: Some("src/main.rs".to_string()),
..Default::default()
};
let result = plugin.process("", &context).unwrap();
assert!(result.contains("Language: rust"));
assert!(result.contains("File: main.rs"));
}
#[test]
fn test_error_handling() {
let plugin = ValidatingPlugin;
let context = PluginContext::default();
// Valid JSON
let valid_json = r#"{"name": "test"}"#;
let result = plugin.process(valid_json, &context);
assert!(result.is_ok());
// Invalid JSON
let invalid_json = r#"{"name": "test""#;
let result = plugin.process(invalid_json, &context);
assert!(result.is_err());
}
}
Integration Testing
#[cfg(test)]
mod integration_tests {
use super::*;
use swissarmyhammer::prelude::*;
#[test]
fn test_plugin_in_template() {
let mut registry = PluginRegistry::new();
registry.register(Box::new(ReverseStringPlugin)).unwrap();
let library = PromptLibrary::new()
.with_plugin_registry(registry);
let template = "{{ content | reverse }}";
let context = HashMap::from([
("content".to_string(), "hello world".to_string()),
]);
let result = library.render_template(template, &context).unwrap();
assert_eq!(result, "dlrow olleh");
}
}
Best Practices
Plugin Development
Error Handling:
- Use descriptive error messages
- Provide context about what went wrong
- Include suggestions for fixing issues
- Handle edge cases gracefully
Performance:
- Cache expensive computations
- Use appropriate data structures
- Implement timeout mechanisms
- Monitor memory usage
Security:
- Validate all inputs
- Sanitize file paths
- Limit resource usage
- Avoid executing arbitrary code
Plugin Distribution
Documentation:
- Provide clear usage examples
- Document configuration options
- Include troubleshooting guides
- Maintain API compatibility
Packaging:
# Cargo.toml for plugin crate
[package]
name = "sah-plugin-example"
version = "0.1.0"
[dependencies]
swissarmyhammer = "0.1"
serde = { version = "1.0", features = ["derive"] }
tokio = { version = "1.0", optional = true }
[features]
default = []
async = ["tokio"]
Plugin Manifest:
# plugin.toml
[plugin]
name = "example-plugin"
version = "0.1.0"
description = "Example plugin for SwissArmyHammer"
author = "Your Name <email@example.com>"
[plugin.capabilities]
filters = ["reverse", "project_info"]
processors = ["validate_json"]
[plugin.requirements]
min_sah_version = "0.1.0"
features = ["async"]
Plugin Ecosystem
Community Plugins
Popular community-developed plugins:
Development Tools:
- Code formatters and linters
- Git integration plugins
- CI/CD workflow helpers
- Documentation generators
External Integrations:
- API clients for popular services
- Database connectors
- Cloud platform integrations
- Monitoring and logging tools
Content Processing:
- Markdown processors
- Image manipulation tools
- Data format converters
- Template engines
Plugin Registry
# Install plugins from registry
sah plugin install reverse-string
sah plugin install database-query
# List installed plugins
sah plugin list
# Update plugins
sah plugin update
# Remove plugins
sah plugin remove reverse-string
Troubleshooting
Common Issues
Plugin Not Found:
- Verify plugin is registered in registry
- Check plugin name spelling
- Ensure plugin is loaded before use
Processing Errors:
- Check plugin logs for error details
- Validate input data format
- Verify plugin configuration
- Test plugin in isolation
Performance Issues:
- Profile plugin execution time
- Check for memory leaks
- Optimize expensive operations
- Implement caching where appropriate
Debug Mode
use swissarmyhammer::plugins::{PluginRegistry, DebugConfig};
let debug_config = DebugConfig {
enable_logging: true,
log_level: LogLevel::Debug,
trace_execution: true,
dump_context: true,
};
let registry = PluginRegistry::with_debug(debug_config);
The plugin system enables unlimited extensibility of SwissArmyHammer, allowing you to integrate with any tool, service, or processing pipeline while maintaining type safety and performance.