signalwire 0.2.0

The unofficial SignalWire SDK for Rust.
Documentation
# SignalWire SDK for Rust

Unofficial SignalWire SDK for Rust. Async by default, optional blocking client behind a feature flag.

## Features

- JWT auth
- Phone number management (available, owned, buy)
- SMS send + delivery status
- Subproject (account) management
- Phone number lookup & validation (basic / carrier / CNAM)
- Built on `reqwest` with `rustls-tls` (no OpenSSL)
- Real `thiserror`-based error type — preserves status code, response body, transport errors
- 30s default timeout, `429 Too Many Requests` mapped to `RateLimited(Option<Duration>)`

## Install

```toml
[dependencies]
signalwire = "0.2"
tokio = { version = "1", features = ["macros", "rt-multi-thread"] }
```

Blocking client:

```toml
[dependencies]
signalwire = { version = "0.2", features = ["blocking"] }
```

## Configuration

```bash
# .env
SIGNALWIRE_SPACE_NAME=your_space
SIGNALWIRE_PROJECT_ID=your_project_id
SIGNALWIRE_API_KEY=your_api_key
```

## Async usage

### Init

```rust
use signalwire::{SignalWireClient, SignalWireError};

#[tokio::main]
async fn main() -> Result<(), SignalWireError> {
    let client = SignalWireClient::new("space", "project_id", "api_key")?;
    let jwt = client.get_jwt().await?;
    println!("jwt: {}", jwt.jwt_token);
    Ok(())
}
```

### Phone numbers

```rust
use signalwire::{PhoneNumberAvailableQueryParams, PhoneNumberOwnedFilterParams};

let q = PhoneNumberAvailableQueryParams::new().area_code("206").build();
let available = client.get_phone_numbers_available("US", &q).await?;

let owned = client
    .get_phone_numbers_owned(&PhoneNumberOwnedFilterParams::new().build())
    .await?;

let bought = client.buy_phone_number("+12065550100").await?;
```

### SMS

```rust
use signalwire::SmsMessage;

let msg = SmsMessage {
    from: "+12065550100".into(),
    to: "+12065550111".into(),
    body: "hello from rust".into(),
};

let resp = client.send_sms(&msg).await?;
println!("sid={} status={}", resp.sid, resp.get_status());

let status = client.get_message_status(&resp.sid).await?;
println!("status: {}", status.get_status()); // MessageStatus enum
```

### Subprojects

```rust
use signalwire::SubprojectQueryParams;

let list = client.list_subprojects(&SubprojectQueryParams::new().build()).await?;

let created = client.create_subproject("my-subproject").await?;
let updated = client.update_subproject(&created.sid, "renamed", None).await?;

// signalwire wants Status=closed before delete
client.update_subproject(&created.sid, "renamed", Some("closed")).await?;
client.delete_subproject(&created.sid).await?;

let nums = client
    .get_subproject_phone_numbers(
        &created.sid,
        &PhoneNumberOwnedFilterParams::new().build(),
    )
    .await?;
```

### Lookup

```rust
use signalwire::LookupKind;

let basic    = client.lookup_phone_number("+12065550100").await?;
let carrier  = client.lookup(&"+12065550100", LookupKind::Carrier).await?;
let cnam     = client.lookup(&"+12065550100", LookupKind::CallerName).await?;

// fields are Option<T> — no implicit defaults
if let Some(true) = basic.valid_number {
    println!("e164: {}", basic.e164.as_deref().unwrap_or("?"));
}
```

## Blocking usage

Same surface, sync. Built on `reqwest::blocking` — no hand-rolled tokio runtime per call.

```rust
use signalwire::{BlockingClient, SmsMessage};

fn main() -> Result<(), signalwire::SignalWireError> {
    let client = BlockingClient::new("space", "project_id", "api_key")?;

    let resp = client.send_sms(&SmsMessage {
        from: "+12065550100".into(),
        to: "+12065550111".into(),
        body: "sync hello".into(),
    })?;
    println!("sid: {}", resp.sid);

    let st = client.get_message_status(&resp.sid)?;
    println!("status: {}", st.get_status());

    Ok(())
}
```

## Error handling

```rust
pub enum SignalWireError {
    Http(reqwest::Error),                 // transport / TLS / timeout
    Unauthorized,                         // 401
    NotFound(String),                     // 404 + body
    RateLimited(Option<Duration>),        // 429 + Retry-After header
    Api { code: u16, body: String },      // 4xx/5xx
    Decode { source: serde_json::Error, body: String },
}
```

Match on it:

```rust
match client.send_sms(&msg).await {
    Ok(r) => println!("{}", r.sid),
    Err(SignalWireError::Unauthorized) => eprintln!("bad creds"),
    Err(SignalWireError::RateLimited(retry)) => eprintln!("backoff: {retry:?}"),
    Err(SignalWireError::Api { code, body }) => eprintln!("{code}: {body}"),
    Err(e) => eprintln!("{e}"),
}
```

## Custom HTTP client

Inject your own `reqwest::Client` (custom proxies, headers, TLS config):

```rust
let http = reqwest::Client::builder()
    .timeout(std::time::Duration::from_secs(60))
    .build()?;

let client = SignalWireClient::new("space", "id", "key")?
    .with_http_client(http);
```

## Tests

Live integration tests live in `tests/live.rs`, all `#[ignore]`-gated. Run manually:

```bash
cargo test --test live -- --ignored --nocapture jwt_async
```

`send_sms_costs_money` and `subproject_lifecycle` mutate real resources — keep them gated.

## License

[0BSD](LICENSE)

## Changelog

### 0.2.0
- Full rewrite. Edition 2024.
- One internal `handle<T>` request helper kills ~600 lines of boilerplate.
- Blocking client via `reqwest::blocking` (was: hand-rolled tokio runtime per call).
- `SignalWireError` preserves transport errors, status codes, response bodies. New `RateLimited(Option<Duration>)` variant for 429s.
- `SignalWireClient::new` / `BlockingClient::new` return `Result` — no `expect`/`unwrap` in the SDK.
- Private fields on the client (was: `pub api_key` etc.).
- `Daum``OwnedPhoneNumber`. Dead `#[serde(skip_deserializing)]` compat fields on `PhoneLookupResponse` removed.
- `LookupKind` enum + single `lookup(num, kind)` method (the three named methods kept as thin wrappers).
- One generic `QueryBuilder` replaces four duplicate query-param builders.
- Drops `dotenv`, `serde_derive`, `chrono`, `tokio` (runtime), `url` direct deps. `rustls-tls` instead of native-tls.
- 30s default request timeout.
- Tests moved to `tests/live.rs`, all `#[ignore]`-gated.

### 0.1.8
- Phone number lookup and validation, carrier + CNAM info.

### 0.1.7
- Subproject (account) management.

### 0.1.6
- SMS messaging + status checking + `MessageStatus` enum + `NotFound` error variant.

### 0.1.5
- Initial release: JWT auth, phone number management, blocking support.