pywatt_sdk 0.5.3

Standardized SDK for building PyWatt modules in Rust
Documentation
# PyWatt SDK Module Macro Documentation

## Overview

The `#[pywatt_sdk::module]` macro is a procedural macro that simplifies the creation of PyWatt modules by automatically generating the boilerplate code required for module initialization, handshake, secret management, and lifecycle management.

## Macro Syntax

```rust
#[pywatt_sdk::module(
    secrets = ["SECRET_KEY1", "SECRET_KEY2"],
    endpoints = [
        AnnouncedEndpoint { path: "/path", methods: vec!["GET"], auth: None },
        // ... more endpoints
    ],
    health = "/health",      // Optional: health check endpoint
    metrics = true,          // Optional: enable Prometheus metrics
    state = state_builder    // Function to build custom state
)]
async fn module_function(state: AppState<CustomState>) -> Router {
    // Return your Axum router here
}
```

## Macro Parameters

### Required Parameters

- **`secrets`**: Array of secret keys to fetch at startup
  - Type: `&[&str]`
  - Example: `["DATABASE_URL", "API_KEY", "JWT_SECRET"]`

- **`endpoints`**: Array of endpoints to announce to the orchestrator
  - Type: `&[AnnouncedEndpoint]`
  - Each endpoint must specify `path`, `methods`, and optional `auth`

- **`state`**: Function that builds custom state from init data and secrets
  - Type: `fn(&OrchestratorInit, Vec<SecretString>) -> CustomState`
  - Must be a function that takes init data and secrets, returns custom state

### Optional Parameters

- **`health`**: Path for health check endpoint (defaults to "/health")
  - Type: `&str`
  - The macro will automatically add a health endpoint if specified

- **`metrics`**: Enable Prometheus metrics collection (defaults to false)
  - Type: `bool`
  - Adds `/metrics` endpoint when enabled

## Generated Code

The macro generates a complete `main()` function that:

1. **Initializes logging** with proper redaction
2. **Performs handshake** with the orchestrator
3. **Fetches secrets** specified in the `secrets` parameter
4. **Builds custom state** using the provided `state` function
5. **Creates AppState** with SDK context and custom state
6. **Builds router** by calling the annotated function
7. **Announces endpoints** to the orchestrator
8. **Starts IPC processing** in background
9. **Serves the module** using Axum

## Complete Example

Here's how to use the macro:

```rust
use pywatt_sdk::prelude::*;
use axum::{Router, routing::get, Extension, Json};
use secrecy::{SecretString, ExposeSecret};
use serde_json::Value;

// Define your custom state
#[derive(Clone, Debug)]
struct MyModuleState {
    database_url: String,
    api_key: String,
    feature_flags: Vec<String>,
}

// State builder function
fn build_my_state(init: &OrchestratorInit, secrets: Vec<SecretString>) -> MyModuleState {
    let database_url = secrets
        .get(0)
        .map(|s| s.expose_secret().clone())
        .unwrap_or_else(|| "sqlite::memory:".to_string());
    
    let api_key = secrets
        .get(1)
        .map(|s| s.expose_secret().clone())
        .unwrap_or_else(|| "development-key".to_string());
    
    MyModuleState {
        database_url,
        api_key,
        feature_flags: vec!["feature_a".to_string(), "feature_b".to_string()],
    }
}

// Use the macro to define your module
#[pywatt_sdk::module(
    secrets = ["DATABASE_URL", "API_KEY"],
    endpoints = [
        AnnouncedEndpoint { 
            path: "/status".to_string(), 
            methods: vec!["GET".to_string()], 
            auth: None 
        },
        AnnouncedEndpoint { 
            path: "/config".to_string(), 
            methods: vec!["GET".to_string()], 
            auth: Some("jwt".to_string()) 
        }
    ],
    health = "/health",
    metrics = true,
    state = build_my_state
)]
async fn my_module(state: AppState<MyModuleState>) -> Router {
    Router::new()
        .route("/status", get(status_handler))
        .route("/config", get(config_handler))
        .layer(Extension(state))
}

// Handler functions
async fn status_handler(Extension(state): Extension<AppState<MyModuleState>>) -> Json<Value> {
    Json(serde_json::json!({
        "status": "ok",
        "module_id": state.module_id(),
        "database_connected": !state.user_state.database_url.is_empty(),
        "feature_flags": state.user_state.feature_flags
    }))
}

async fn config_handler(Extension(state): Extension<AppState<MyModuleState>>) -> Json<Value> {
    Json(serde_json::json!({
        "database_url": "[REDACTED]", // Never expose secrets
        "api_key": "[REDACTED]",
        "feature_flags": state.user_state.feature_flags,
        "orchestrator_api": state.orchestrator_api()
    }))
}
```

## Manual Equivalent

The macro generates code equivalent to this manual implementation:

```rust
#[tokio::main]
async fn main() -> Result<()> {
    // 1. Initialize logging
    init_module();

    // 2. Perform handshake
    let init = read_init::<OrchestratorInit>().await?;

    // 3. Fetch secrets
    let secret_client = get_module_secret_client(&init.orchestrator_api, &init.module_id).await?;
    let secrets = get_secrets(&secret_client, vec!["DATABASE_URL", "API_KEY"]).await?;

    // 4. Build custom state
    let custom_state = build_my_state(&init, secrets);

    // 5. Create AppState
    let app_state = AppState::new(
        init.module_id.clone(),
        init.orchestrator_api.clone(),
        Arc::new(secret_client),
        custom_state,
    );

    // 6. Build router
    let router = my_module(app_state.clone()).await;

    // 7. Announce endpoints
    let endpoints = vec![
        AnnouncedEndpoint { path: "/status".to_string(), methods: vec!["GET".to_string()], auth: None },
        AnnouncedEndpoint { path: "/config".to_string(), methods: vec!["GET".to_string()], auth: Some("jwt".to_string()) },
        AnnouncedEndpoint { path: "/health".to_string(), methods: vec!["GET".to_string()], auth: None },
    ];
    send_announce(&AnnounceBlob {
        listen: init.listen.clone(),
        endpoints,
    })?;

    // 8. Start IPC processing
    let ipc_handle = tokio::spawn(async move {
        process_ipc_messages(secret_client).await
    });

    // 9. Serve the module
    serve_module(router, &init.listen.parse()?).await?;

    Ok(())
}
```

## Current Implementation Status

Based on the current macro implementation in `pywatt_macros/src/lib.rs`, the macro:

1. ✅ Parses the attribute arguments correctly
2. ✅ Validates required parameters (secrets, endpoints, state)
3. ✅ Generates a complete main() function
4. ✅ Includes proper error handling
5. ✅ Supports optional health and metrics endpoints
6. ✅ Uses the correct SDK functions for lifecycle management

## Error Handling

The macro includes comprehensive error handling:

- **Handshake errors**: Logged and cause process exit
- **Secret fetch errors**: Logged and cause process exit  
- **State building errors**: Compilation errors if state function is invalid
- **Server errors**: Logged and propagated through the Result type

## Best Practices

1. **Keep state builders simple**: State building functions should be deterministic and fast
2. **Handle missing secrets gracefully**: Provide sensible defaults for development
3. **Don't expose secrets in logs**: Use "[REDACTED]" placeholders in responses
4. **Use proper authentication**: Specify `auth: Some("jwt")` for protected endpoints
5. **Follow naming conventions**: Use kebab-case for endpoint paths

## Debugging

If the macro isn't working as expected:

1. Check that all required parameters are provided
2. Ensure the state builder function signature is correct
3. Verify that `AnnouncedEndpoint` structs are properly formatted
4. Use `cargo expand` to see the generated code (requires cargo-expand)
5. Check compiler errors carefully - they'll point to the specific issue

## Migration from Manual Implementation

To migrate from manual implementation to the macro:

1. Extract your state building logic into a separate function
2. List all secrets your module needs
3. Define your endpoints using `AnnouncedEndpoint` structs
4. Replace your main() function with the macro annotation
5. Test that the behavior is identical