# Nested Plugin Routing

## Overview

This document describes the implementation of nested plugin routing, enabling method calls like `solar.mercury.info` to traverse the plugin hierarchy automatically.

## Problem

Previously, the schema advertised methods on nested plugins (e.g., `mercury.info` as a child of `solar`), but these weren't callable. The only way to access nested data was via parent methods with path parameters:

```
solar.info --path mercury  ✓ worked
solar.mercury.info         ✗ method not found
```

## Solution

Implemented Option D from the design doc: routing through the plugin hierarchy via dot-notation.

### New Components

#### 1. `ChildRouter` Trait (`src/plexus/plexus.rs`)

```rust
#[async_trait]
pub trait ChildRouter: Send + Sync {
    fn router_namespace(&self) -> &str;
    async fn router_call(&self, method: &str, params: Value) -> Result<PlexusStream, PlexusError>;
    async fn get_child(&self, name: &str) -> Option<Box<dyn ChildRouter>>;
}
```

Separate from `Activation` to avoid associated type issues with `dyn` dispatch.

#### 2. `route_to_child` Function

```rust
pub async fn route_to_child<T: ChildRouter + ?Sized>(
    parent: &T,
    method: &str,
    params: Value,
) -> Result<PlexusStream, PlexusError>
```

Splits method on first `.`, looks up child, recursively calls `router_call`.

#### 3. Hub-Macro Changes (`hub-macro/src/codegen/activation.rs`)

For activations with `hub = true`, the generated `call()` method now falls back to `route_to_child` for unmatched methods:

```rust
async fn call(&self, method: &str, params: Value) -> Result<...> {
    match method {
        "observe" => { ... }
        "info" => { ... }
        _ => route_to_child(self, method, params).await  // NEW
    }
}
```

Leaf activations (without `hub`) return `MethodNotFound` directly.

## Call Flow

```
plexus.call("solar.mercury.info", {})
  │
  ├─ Plexus::route("solar.mercury.info")
  │    └─ parse: ("solar", "mercury.info")
  │    └─ activations["solar"].call("mercury.info")
  │
  ├─ Solar::call("mercury.info")
  │    └─ no local match
  │    └─ route_to_child(self, "mercury.info")
  │         └─ split: ("mercury", "info")
  │         └─ get_child("mercury") → CelestialBodyActivation
  │         └─ child.router_call("info")
  │
  └─ CelestialBodyActivation::router_call("info")
       └─ returns Mercury's info stream
```

## Implementation Details

### Solar System Example

The solar system demonstrates 3-level nesting:

```
plexus (hub)
└── solar (hub, implements ChildRouter)
    ├── mercury (leaf)
    ├── earth (hub)
    │   └── luna (leaf)
    └── jupiter (hub)
        ├── io (leaf)
        ├── europa (leaf)
        └── ...
```

### CelestialBodyActivation

Wraps `CelestialBody` data to make it callable:

- Implements `Activation` for the `info` method
- Implements `ChildRouter` for nested routing to moons
- Created on-demand when `get_child()` is called

### Key Files Changed

| File | Change |
|------|--------|
| `hub-macro/src/codegen/activation.rs` | Conditional `route_to_child` for hubs |
| `src/plexus/plexus.rs` | `ChildRouter` trait, `route_to_child` fn, Plexus impl |
| `src/activations/solar/celestial.rs` | `CelestialBodyActivation` |
| `src/activations/solar/activation.rs` | Solar `ChildRouter` impl |

## Testing

```rust
#[tokio::test]
async fn test_nested_routing_jupiter_io() {
    let solar = Solar::new();
    let result = Activation::call(&solar, "jupiter.io.info", json!({})).await;
    assert!(result.is_ok());
}
```

Tests verify:
- Single-level nesting: `mercury.info`
- Two-level nesting: `jupiter.io.info`, `earth.luna.info`
- Invalid paths return `ActivationNotFound`

## Shortcuts & Design Asymmetries

### 1. ChildRouter vs Activation Trait Split

**Asymmetry:** `ChildRouter` is a separate trait from `Activation`.

**Why:** `Activation` has an associated type (`type Methods: MethodEnumSchema`) which prevents `dyn Activation` without specifying the type. `ChildRouter` avoids this by duplicating the `call` method as `router_call`.

**Shortcut:** Could unify with a trait object-safe base trait, but adds complexity.

### 2. CelestialBodyActivation Created On-Demand

**Asymmetry:** Child activations are instantiated fresh on every `get_child()` call.

```rust
.map(|c| Box::new(CelestialBodyActivation::new(c.clone())) as Box<dyn ChildRouter>)
```

**Why:** Avoids storing a parallel hierarchy of activation wrappers.

**Shortcut:** No caching. For deep hierarchies with frequent access, this clones data repeatedly.

### 3. Plexus.get_child Returns None

**Asymmetry:** Plexus implements `ChildRouter` but `get_child()` always returns `None`.

```rust
async fn get_child(&self, _name: &str) -> Option<Box<dyn ChildRouter>> {
    None  // Plexus routes via route(), not get_child
}
```

**Why:** Plexus has its own routing mechanism via `route()` that handles `namespace.method`. Registered activations aren't stored as `dyn ChildRouter`.

**Shortcut:** Could wrap activations in ChildRouter adapters, but most don't implement it.

### 4. Schema Advertises Methods That Require ChildRouter

**Asymmetry:** The schema shows `mercury.info` as a callable method, but it only works if the parent implements `ChildRouter`.

**Why:** Schema generation (`to_plugin_schema`) is separate from routing implementation.

**Shortcut:** No schema validation that methods are actually reachable. Trust that hubs implement `ChildRouter`.

### 5. Static Content Type in CelestialBodyActivation

**Asymmetry:** Uses `"celestial.info"` instead of dynamic `"{namespace}.info"`.

```rust
Ok(wrap_stream(stream, "celestial.info", vec![self.namespace.clone()]))
```

**Why:** `wrap_stream` requires `&'static str` for content_type.

**Shortcut:** Could leak strings or use a different API, but static is simpler.

### 6. Hub Flag Required for Routing

**Asymmetry:** Only activations with `#[hub_macro::hub_methods(..., hub)]` get nested routing.

**Why:** Non-hub activations shouldn't pay the cost of routing checks or require `ChildRouter` impl.

**Shortcut:** Could auto-detect based on `plugin_children()` existence, but explicit is clearer.

### 7. Direct Nested Routing via register_hub

**Solution:** Hub activations registered with `register_hub()` enable direct RPC routing without `plexus.call` wrapper.

```rust
// In builder.rs
Plexus::new()
    .register(Health::new())      // Leaf: regular registration
    .register(Echo::new())        // Leaf: regular registration
    .register_hub(Solar::new())   // Hub: enables direct nested routing
```

**How it works:**

1. `register_hub<A: Activation + ChildRouter>` stores the activation in both:
   - `activations` HashMap (for schema/introspection)
   - `child_routers` HashMap (for `get_child()` lookup)

2. `Plexus::get_child()` now returns registered hub activations:
   ```rust
   async fn get_child(&self, name: &str) -> Option<Box<dyn ChildRouter>> {
       self.inner.child_routers.get(name)
           .map(|router| Box::new(ArcChildRouter(router.clone())))
   }
   ```

3. RPC calls to `plexus.solar.mercury.info` now route automatically:
   - Plexus doesn't match "solar.mercury.info" locally
   - Falls back to `route_to_child("solar.mercury.info")`
   - `get_child("solar")` returns Solar
   - Continues recursively through the hierarchy

**Result:** Both patterns now work:

```bash
# Direct (via plexus namespace) - works!
{"method": "plexus.solar.mercury.info", "params": {}}
# → returns Mercury data

# Via plexus.call - still works
{"method": "plexus.call", "params": {"method": "solar.mercury.info", "params": {}}}
# → returns Mercury data
```

**Trade-off:** Requires explicit `register_hub()` for hub activations. Leaf activations use `register()` as before.

## Future Work

- Cache `CelestialBodyActivation` instances instead of creating per-call
- Unify `ChildRouter` and `Activation` with trait object-safe design
- Add schema metadata indicating whether methods are directly callable
