pywatt_macros 0.2.9

Procedural macros for the PyWatt SDK
Documentation
# PyWatt Macros

This crate provides procedural macros to simplify the development of Rust-based PyWatt modules.

## `#[pywatt_sdk::module]`

The primary macro offered is `#[pywatt_sdk::module]`. This attribute macro is designed to be applied to an `async` function that returns an `axum::Router` and takes an `pywatt_sdk::AppState<T>` as its argument (where `T` is your custom module state).

It automates the following boilerplate tasks:

1.  **Initialization**: Sets up module logging.
2.  **Orchestrator Handshake**: Reads initial configuration from the orchestrator.
3.  **Secret Management**:
    *   Creates a `SecretClient` for interacting with the orchestrator's secret service.
    *   Prefetches specified secrets.
    *   Optionally subscribes to secret rotation events for the prefetched secrets.
4.  **State Creation**:
    *   Builds the user-defined module state. It calls a user-provided state builder function (or `Default::default()` if none is specified), passing the orchestrator's initialization data and any prefetched secrets.
    *   Constructs the main `pywatt_sdk::AppState` which wraps the user state and other SDK components.
5.  **Router Setup**:
    *   Calls the annotated function to get the base Axum router.
    *   Layers the `pywatt_sdk::AppState` into the router so it's accessible to handlers.
6.  **Standard Endpoints**:
    *   Adds a health check endpoint (defaults to `/health`, path is configurable).
    *   Optionally adds a Prometheus metrics endpoint (defaults to `/metrics`, currently a placeholder).
7.  **Module Announcement**: Announces the module's listen address and its HTTP endpoints (including health and metrics if enabled) to the orchestrator.
8.  **Server Startup**: Starts an Axum HTTP server listening on the TCP address provided by the orchestrator.

### Arguments

The `#[pywatt_sdk::module]` macro accepts the following arguments:

*   `secrets: [&str]`: An array of secret keys (string literals) to be prefetched at startup. Example: `secrets = ["api_key", "db_password"]`.
*   `rotate: bool`: If `true`, subscribes to rotation events for the secrets specified in the `secrets` argument. Defaults to `false`. Example: `rotate = true`.
*   `endpoints: [AnnouncedEndpoint]`: An array of `pywatt_sdk::AnnouncedEndpoint` structs describing the custom endpoints your module provides. Example: `endpoints = [pywatt_sdk::AnnouncedEndpoint { path: "/my/data".to_string(), methods: vec!["GET".to_string(), "POST".to_string()], auth: None }]`.
*   `health: &str` (or `health_path: &str`): Optional. Custom path for the health check endpoint. Defaults to `"/health"`. Example: `health = "/status"`.
*   `metrics: bool`: If `true`, enables a `/metrics` endpoint. Defaults to `false`. Example: `metrics = true`.
*   `version: &str`: Optional. A version string to prefix all announced custom endpoint paths (health and metrics paths are not prefixed). Example: `version = "v1"` would change an endpoint `/data` to `/v1/data` in the announcement.
*   `state: fn_path`: Optional. A path to a function that builds your custom module state. The function must have a signature compatible with `fn(&pywatt_sdk::OrchestratorInit, Vec<secrecy::SecretString>) -> UserStateType`. If not provided, `Default::default()` is used to create the user state. Example: `state = my_custom_state_builder`.
*   `channels`, `security_level`, `auth_required`: These arguments are currently parsed but ignored. They are included for potential future compatibility.

### Example Usage

```rust
use pywatt_sdk::{self, AppState, AnnouncedEndpoint, Result};
use axum::{routing::get, Router};
use secrecy::ExposeSecret;

// Define your custom module state (if any)
#[derive(Default, Clone)]
struct MyModuleState {
    api_key: Option<String>,
}

// Optional: Define a custom state builder function
fn build_my_state(init_data: &pywatt_sdk::OrchestratorInit, secrets: Vec<secrecy::SecretString>) -> MyModuleState {
    println!("Building MyModuleState. Orchestrator API: {}", init_data.orchestrator_api);
    let mut api_key = None;
    if let Some(secret_value) = secrets.get(0) { // Assuming "MY_API_KEY" is the first secret
        api_key = Some(secret_value.expose_secret().clone());
        println!("API Key prefetched!");
    }
    MyModuleState { api_key }
}

async fn get_hello(state: AppState<MyModuleState>) -> String {
    if let Some(key) = &state.user_state.api_key {
        format!("Hello from PyWatt module with API key: {}...", &key[..5])
    } else {
        "Hello from PyWatt module!".to_string()
    }
}

#[pywatt_sdk::module(
    secrets = ["MY_API_KEY"],
    rotate = true,
    endpoints = [
        AnnouncedEndpoint {
            path: "/hello".to_string(),
            methods: vec!["GET".to_string()],
            auth: None,
        }
    ],
    state = build_my_state,
    version = "v1"
)]
async fn my_module_router(state: AppState<MyModuleState>) -> Router {
    Router::new().route("/hello", get(get_hello).with_state(state))
}

// The macro will generate the main function and all boilerplate.
// To run this (assuming it's in src/main.rs and Cargo.toml is set up):
// cargo run
```

This macro significantly reduces the amount of code needed to get a PyWatt module up and running, allowing developers to focus on the module's core logic.