# Writing Wasot Plugins in Rust: Complete Tutorial

This tutorial will guide you through creating your first Wasot [plugin](./plugins.md) in Rust, from setup to deployment. For the underlying concepts, see the [plugin architecture](./plugin_architecture.md) and [socket communication](./plugin_socket_communication.md) documentation. Perfect for Rust developers with about 1 year of experience.

## Prerequisites

- Basic Rust knowledge (structs, traits, async/await, cargo)
- Familiarity with protobuf concepts (helpful but not required)
- Wasot installed and working

## What We'll Build

We'll create a **word counter plugin** that:
- Counts words in paragraph blocks
- Tracks the total word count globally  
- Adds word count information to the context
- Demonstrates basic plugin patterns

## Step 1: Project Setup

### Create the Plugin Project

```bash
# Create a new Rust binary project
cargo new wasot_word_counter_plugin
cd wasot_word_counter_plugin

# This creates:
# ├── Cargo.toml
# ├── src/
# │   └── main.rs
```

### Update Cargo.toml

```toml
[package]
name = "wasot_word_counter_plugin"
version = "0.1.0"
edition = "2021"

[dependencies]
# gRPC and async runtime
tonic = "0.11"
tokio = { version = "1.0", features = ["macros", "rt-multi-thread"] }
tokio-stream = "0.1"

# Protobuf support
prost = "0.12"
prost-types = "0.12"

# JSON handling
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"

# Error handling
anyhow = "1.0"
thiserror = "1.0"

# Logging
tracing = "0.1"
tracing-subscriber = "0.3"

[build-dependencies]
tonic-build = "0.11"
```

## Step 2: Copy Protocol Buffers

Since your plugin needs to implement the same gRPC interface as Wasot, copy the proto files:

```bash
# Create proto directory in your plugin project
mkdir proto

# Copy the proto files from Wasot project
cp /path/to/wasot/proto/tools.proto proto/
cp /path/to/wasot/proto/events.proto proto/
```

### Create build.rs

```rust
// build.rs
use std::env;
use std::path::PathBuf;

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let out_dir = PathBuf::from(env::var("OUT_DIR")?);

    // Compile the tools proto with server support (we're implementing the server)
    tonic_build::configure()
        .build_server(true)   // We implement the server
        .build_client(false)  // We don't need the client
        .out_dir(&out_dir)
        .compile(&["proto/tools.proto"], &["proto"])?;

    println!("cargo:rerun-if-changed=proto/tools.proto");
    println!("cargo:rerun-if-changed=proto/events.proto");

    Ok(())
}
```

## Step 3: Implement the Plugin Server

### Main Structure (src/main.rs)

```rust
use anyhow::Result;
use std::env;
use tonic::{transport::Server, Request, Response, Status};
use tracing::{info, error};

// Include the generated protobuf code
pub mod proto {
    tonic::include_proto!("wasot.tools");
}

use proto::{
    grpc_library_server::{GrpcLibrary, GrpcLibraryServer},
    *,
};

/// Our plugin implementation
pub struct WordCounterPlugin {
    tools: Vec<ToolDefinition>,
}

impl WordCounterPlugin {
    pub fn new() -> Self {
        // Define the tools this plugin provides
        let tools = vec![
            ToolDefinition {
                name: "word_counter".to_string(),
                library: "wasot_word_counter_plugin".to_string(),
                selectors: vec!["paragraph".to_string()], // Only handle paragraphs
                optional_context_keys: vec![],
                required_context_keys: vec![],
                maybe_produced_keys: vec![
                    "paragraph_word_count".to_string(),
                    "total_word_count".to_string(),
                ],
            }
        ];

        Self { tools }
    }

    /// Count words in a text string
    fn count_words(&self, text: &str) -> usize {
        text.split_whitespace()
            .filter(|word| !word.is_empty())
            .count()
    }

    /// Parse event JSON to extract text content
    fn extract_text_from_event(&self, event_json: &str) -> Result<Option<String>> {
        let event: serde_json::Value = serde_json::from_str(event_json)?;
        
        // Check if this is a TextFinished event with lines
        if let Some(event_type) = event.get("type").and_then(|t| t.as_str()) {
            if event_type == "TextFinished" {
                if let Some(lines) = event.get("lines").and_then(|l| l.as_array()) {
                    let text = lines
                        .iter()
                        .filter_map(|line| line.as_str())
                        .collect::<Vec<_>>()
                        .join(" ");
                    return Ok(Some(text));
                }
            }
        }
        
        Ok(None)
    }

    /// Get current total word count from context
    fn get_total_word_count(&self, context: &Context) -> usize {
        // Check global context first
        if let Some(value) = context.global.get("total_word_count") {
            if let Some(kind) = &value.kind {
                match kind {
                    prost_types::value::Kind::StringValue(s) => {
                        return s.parse().unwrap_or(0);
                    }
                    prost_types::value::Kind::NumberValue(n) => {
                        return *n as usize;
                    }
                    _ => {}
                }
            }
        }
        0
    }
}

#[tonic::async_trait]
impl GrpcLibrary for WordCounterPlugin {
    /// Return the tools this plugin provides
    async fn get_tools(
        &self,
        _request: Request<GetToolsRequest>,
    ) -> Result<Response<GetToolsResponse>, Status> {
        info!("GetTools called - returning {} tools", self.tools.len());
        
        let response = GetToolsResponse {
            tools: self.tools.clone(),
        };

        Ok(Response::new(response))
    }

    /// Handle a tool call
    async fn call_tool(
        &self,
        request: Request<CallToolRequest>,
    ) -> Result<Response<CallToolResponse>, Status> {
        let request = request.into_inner();
        
        let tool_call = request.tool_call
            .ok_or_else(|| Status::invalid_argument("Missing tool_call"))?;

        info!("CallTool called for tool: {}", tool_call.tool_name);

        // Extract context
        let context = tool_call.context
            .ok_or_else(|| Status::invalid_argument("Missing context"))?;

        match tool_call.tool_name.as_str() {
            "word_counter" => {
                // Extract text from the event
                match self.extract_text_from_event(&tool_call.event_json) {
                    Ok(Some(text)) => {
                        let word_count = self.count_words(&text);
                        let total_count = self.get_total_word_count(&context) + word_count;

                        info!("Counted {} words in paragraph, total now {}", 
                              word_count, total_count);

                        // Create context changes
                        let mut changes = Vec::new();

                        // Add paragraph word count to scoped context
                        changes.push(ContextChange {
                            target: ContextChangeTarget::Scope as i32,
                            key: "paragraph_word_count".to_string(),
                            value: Some(prost_types::Value {
                                kind: Some(prost_types::value::Kind::NumberValue(word_count as f64)),
                            }),
                        });

                        // Update total word count in global context
                        changes.push(ContextChange {
                            target: ContextChangeTarget::Global as i32,
                            key: "total_word_count".to_string(),
                            value: Some(prost_types::Value {
                                kind: Some(prost_types::value::Kind::NumberValue(total_count as f64)),
                            }),
                        });

                        Ok(Response::new(CallToolResponse {
                            context_changes: changes,
                            success: true,
                            error_message: String::new(),
                        }))
                    }
                    Ok(None) => {
                        // Not a text event, nothing to count
                        Ok(Response::new(CallToolResponse {
                            context_changes: vec![],
                            success: true,
                            error_message: String::new(),
                        }))
                    }
                    Err(e) => {
                        error!("Failed to extract text from event: {}", e);
                        Ok(Response::new(CallToolResponse {
                            context_changes: vec![],
                            success: false,
                            error_message: format!("Failed to parse event: {}", e),
                        }))
                    }
                }
            }
            unknown => {
                error!("Unknown tool requested: {}", unknown);
                Err(Status::not_found(format!("Unknown tool: {}", unknown)))
            }
        }
    }
}

#[tokio::main]
async fn main() -> Result<()> {
    // Initialize logging
    tracing_subscriber::fmt::init();

    // Get socket path from command line arguments
    let args: Vec<String> = env::args().collect();
    if args.len() != 2 {
        eprintln!("Usage: {} <socket_path>", args[0]);
        std::process::exit(1);
    }
    let socket_path = &args[1];

    info!("Starting Wasot Word Counter Plugin");
    info!("Binding to socket: {}", socket_path);

    // Create the plugin service
    let plugin = WordCounterPlugin::new();
    let service = GrpcLibraryServer::new(plugin);

    // Create Unix domain socket server
    let uds = tokio::net::UnixListener::bind(socket_path)?;
    let uds_stream = tokio_stream::wrappers::UnixListenerStream::new(uds);

    info!("Plugin server started, waiting for connections...");

    // Start the gRPC server
    Server::builder()
        .add_service(service)
        .serve_with_incoming(uds_stream)
        .await?;

    Ok(())
}
```

## Step 4: Build and Test

### Build the Plugin

```bash
# Build in release mode for better performance
cargo build --release

# The binary will be at: target/release/wasot_word_counter_plugin
```

### Test the Plugin

Create a test Wasot document (`test.wasot`):

```wasot
.document "My Test Document"

.paragraph .. This is the first paragraph with several words to count.

.paragraph .. Here's another paragraph with even more words in it for testing.

.section "Results"
  .paragraph .. Final paragraph to test the word counting functionality.
```

Run Wasot with your plugin:

```bash
# This would be the eventual command when Wasot supports plugins
wasot --plugin ./target/release/wasot_word_counter_plugin test.wasot
```

## Step 5: Advanced Features

### Adding Configuration

```rust
use serde::{Deserialize, Serialize};

#[derive(Debug, Deserialize, Serialize)]
pub struct PluginConfig {
    pub min_word_length: usize,
    pub exclude_patterns: Vec<String>,
}

impl Default for PluginConfig {
    fn default() -> Self {
        Self {
            min_word_length: 1,
            exclude_patterns: vec![],
        }
    }
}
```

### Error Handling

```rust
use thiserror::Error;

#[derive(Error, Debug)]
pub enum PluginError {
    #[error("Invalid event format: {0}")]
    InvalidEvent(String),
    
    #[error("Context parsing error: {0}")]
    ContextError(String),
    
    #[error("Tool not found: {0}")]
    ToolNotFound(String),
}
```

### Logging and Debugging

```rust
use tracing::{debug, info, warn, error, instrument};

impl WordCounterPlugin {
    #[instrument(skip(self))]
    pub async fn process_paragraph(&self, text: &str) -> Result<usize> {
        debug!("Processing paragraph with {} characters", text.len());
        
        let words: Vec<&str> = text.split_whitespace().collect();
        let word_count = words.len();
        
        debug!("Found words: {:?}", words);
        info!("Paragraph contains {} words", word_count);
        
        Ok(word_count)
    }
}
```

## Step 6: Testing and Debugging

### Unit Tests

```rust
#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_word_counting() {
        let plugin = WordCounterPlugin::new();
        
        assert_eq!(plugin.count_words("hello world"), 2);
        assert_eq!(plugin.count_words("  hello   world  "), 2);
        assert_eq!(plugin.count_words(""), 0);
        assert_eq!(plugin.count_words("single"), 1);
    }

    #[test]
    fn test_event_parsing() {
        let plugin = WordCounterPlugin::new();
        let event_json = r#"{
            "type": "TextFinished",
            "lines": ["Hello world", "Second line"]
        }"#;

        let result = plugin.extract_text_from_event(event_json).unwrap();
        assert_eq!(result, Some("Hello world Second line".to_string()));
    }
}
```

### Integration Testing

```bash
# Run tests
cargo test

# Run with logs
RUST_LOG=debug cargo test

# Test the binary directly
echo '{"type": "TextFinished", "lines": ["hello world"]}' | \
  cargo run --release /tmp/test.sock
```

## Step 7: Packaging and Distribution

### Create Installation Script

```bash
#!/bin/bash
# install.sh

set -e

echo "Installing Wasot Word Counter Plugin..."

# Build the plugin
cargo build --release

# Create plugin directory
mkdir -p ~/.wasot/plugins/

# Install the binary
cp target/release/wasot_word_counter_plugin ~/.wasot/plugins/

echo "Plugin installed successfully!"
echo "Usage: wasot --plugin ~/.wasot/plugins/wasot_word_counter_plugin document.wasot"
```

### Documentation

Create a `README.md` for your plugin:

```markdown
# Wasot Word Counter Plugin

A plugin that counts words in Wasot documents.

## Features
- Counts words per paragraph
- Tracks total document word count
- Adds word count to context for other tools

## Installation
```bash
./install.sh
```

## Usage
```bash
wasot --plugin ~/.wasot/plugins/wasot_word_counter_plugin document.wasot
```
```

## Common Patterns and Best Practices

### 1. Plugin Structure
```
wasot_my_plugin/
├── Cargo.toml
├── build.rs
├── proto/              # Copy from Wasot
│   ├── tools.proto
│   └── events.proto
├── src/
│   ├── main.rs        # Plugin server
│   ├── tools/         # Tool implementations
│   │   ├── mod.rs
│   │   └── my_tool.rs
│   └── lib.rs         # Shared code
├── tests/
│   └── integration.rs
└── README.md
```

### 2. Error Handling
Always return proper gRPC responses:
```rust
// Good: Return success with empty changes
Ok(Response::new(CallToolResponse {
    context_changes: vec![],
    success: true,
    error_message: String::new(),
}))

// Good: Return error with details
Ok(Response::new(CallToolResponse {
    context_changes: vec![],
    success: false,
    error_message: "Detailed error message".to_string(),
}))
```

### 3. Context Management

For details on scoped vs global context, see [context and scoped variables](./context_and_scoped_variables.md).

```rust
// Read from context
let current_count = context.global
    .get("word_count")
    .and_then(|v| v.kind.as_ref())
    .and_then(|k| match k {
        prost_types::value::Kind::NumberValue(n) => Some(*n as usize),
        _ => None,
    })
    .unwrap_or(0);

// Write to context
ContextChange {
    target: ContextChangeTarget::Global as i32,
    key: "word_count".to_string(),
    value: Some(prost_types::Value {
        kind: Some(prost_types::value::Kind::NumberValue(count as f64)),
    }),
}
```

### 4. Selector Patterns

For full selector documentation, see [selectors](./selectors.md).

```rust
// Simple selectors
"paragraph"           // All paragraphs
".section"            // Blocks with class "section"
"#introduction"       // Block with ID "introduction"

// Attribute selectors
"code[lang=rust]"     // Code blocks with lang=rust
"link[target]"        // Links with any target attribute

// Descendant selectors
".chapter > .title"   // Title blocks directly inside chapters
".section .paragraph" // Paragraphs anywhere inside sections
```

## Troubleshooting

### Common Issues

1. **"Permission denied" on socket**
   ```bash
   # Check socket permissions
   ls -l /tmp/plugin.sock
   # Try different socket location
   /tmp/wasot_$USER.sock
   ```

2. **"Connection refused"**
   ```bash
   # Check if plugin is running
   ps aux | grep my_plugin
   # Check socket exists
   ls -l /path/to/socket
   ```

3. **"Tool not found"**
   - Check tool name matches exactly
   - Verify GetTools returns correct tools
   - Check selectors match expected events

4. **Context not updating**
   - Verify ContextChangeTarget is correct
   - Check key names match exactly
   - Ensure value is properly formatted

## Next Steps

- Study existing Wasot plugins for advanced patterns
- Implement multiple tools in one plugin
- Add configuration file support
- Create plugins in other languages (Python, Go, etc.)
- Contribute your plugin to the community

This tutorial provides everything needed to create production-ready Wasot plugins in Rust. The plugin architecture is designed to be simple yet powerful, allowing for complex document processing workflows.