network-device 0.1.0

A strongly typed eDSL and IaC contracts for network device configuration
Documentation
# network-device

`network-device` is a dependency-free Rust core for two distinct ways to
express network configuration:

- **Operational eDSL:** typed imperative operations executed immediately by
  a selected device driver.
- **Desired state:** declarative resource values planned and applied by an
  infrastructure-as-code (IaC) backend.

The crate contains neutral domain values, standard operation/resource types,
and open extension contracts. It contains no protocol clients, vendor payload
or path abstraction, persistence, Terraform/Pulumi dependencies, or runtime.

## Operational eDSL

Implement `Driver` and `Execute<Operation>` for each supported operation. The
operation types are owned by the caller and consumed exactly once. The eDSL
composes these operations with statically typed `then`; it executes in order,
stops at the first error, and returns the last operation's output.

```rust
use network_device::{
    AdministrativeState, Driver, Execute, InterfaceName, Run, SetInterfaceState,
    operation,
};
use std::convert::Infallible;
use std::future::Future;

struct Device;

impl Driver for Device {
    type Error = Infallible;
}

impl Execute<SetInterfaceState> for Device {
    type Output = ();

    fn execute<'a>(
        &'a self,
        operation: SetInterfaceState,
    ) -> impl Future<Output = Result<Self::Output, Self::Error>> + Send + 'a {
        async move {
            let _interface = operation.interface;
            let _state = operation.state;
            Ok(())
        }
    }
}

async fn enable() -> Result<(), Infallible> {
    let interface = InterfaceName::try_from("Ethernet1/1").expect("valid interface name");
    operation(SetInterfaceState::new(interface, AdministrativeState::Enabled))
        .run(&Device)
        .await
}
```

Sequential composition is static and has no boxing or dynamic dispatch:

```rust,ignore
operation(SetInterfaceDescription::new(interface.clone(), "uplink"))
    .then(operation(SetInterfaceState::new(interface, AdministrativeState::Enabled)))
    .run(&driver)
    .await?;
```

Each downstream crate can define its own operation and implement
`Execute<CustomOperation>` for a driver it owns, or implement a standard
operation for its own driver. New operation kinds do not require changes to
this crate.

## Desired State and IaC

Desired-state resources are not imperative commands. For example,
`InterfaceConfiguration` describes a target configuration, while
`SetInterfaceState` asks a driver to perform an operation now. Both can use
the same neutral values such as `InterfaceName` and `AdministrativeState`.

`IacBackend<Resource>` is generic over the resource type, not a closed enum.
Each backend chooses its own plan and apply result types and implements only
the resource types it supports:

```rust
use network_device::{
    AdministrativeState, IacBackend, InterfaceConfiguration, InterfaceName,
};
use std::convert::Infallible;
use std::future::Future;

struct ExampleBackend;

impl IacBackend<InterfaceConfiguration> for ExampleBackend {
    type Error = Infallible;
    type Plan = String;
    type ApplyResult = ();

    fn plan<'a>(
        &'a self,
        desired: &'a InterfaceConfiguration,
    ) -> impl Future<Output = Result<Self::Plan, Self::Error>> + Send + 'a {
        async move { Ok(format!("configure {}", desired.name)) }
    }

    fn apply<'a>(
        &'a self,
        _desired: &'a InterfaceConfiguration,
    ) -> impl Future<Output = Result<Self::ApplyResult, Self::Error>> + Send + 'a {
        async { Ok(()) }
    }
}

async fn apply() -> Result<(), Infallible> {
    let desired = InterfaceConfiguration::new(
        InterfaceName::try_from("Ethernet1/1").expect("valid interface name"),
    )
    .administrative_state(AdministrativeState::Enabled)
    .description("uplink");
    let backend = ExampleBackend;
    let _plan = backend.plan(&desired).await?;
    backend.apply(&desired).await
}
```

Potential implementation crates include `network-device-nxapi`,
`network-device-netconf`, `network-device-terraform`,
`network-device-opentofu`, and `network-device-pulumi`. Those names describe
future integrations only; this crate implements none of them. Terraform and
Pulumi have different semantics, so the shared IaC contract deliberately
stops at generic `plan` and `apply` operations with backend-specific result
types.

## Base Types

- `InterfaceName` rejects empty names and control characters but preserves
  vendor-specific spelling.
- `IpPrefix` validates IPv4 and IPv6 prefix lengths.
- `StaticRoute` validates that its destination and next hop use the same IP
  family.
- Standard operations: `SetInterfaceState`, `SetInterfaceDescription`,
  `EnsureStaticRoute`, and `RemoveStaticRoute`.
- Standard desired resources: `InterfaceConfiguration` and
  `StaticRouteConfiguration`.

These are shared semantic types, not a claim that every device protocol maps
them to identical native paths or payloads.