loopengine 1.1.0

Rust client for the LoopEngine Ingest API
Documentation
# LoopEngine Rust SDK

Rust client for the [LoopEngine](https://loopengine.dev) Ingest API. Create a client with your credentials, then call `send` with your payload.

**Requirements:** Rust 1.75+ (async/await, `impl Trait`)

## Installation

Add to your `Cargo.toml`:

```toml
[dependencies]
loopengine = "0.1"
tokio = { version = "1", features = ["full"] }
serde_json = "1"
```

## Usage

```rust
use loopengine::Client;

#[tokio::main]
async fn main() {
    let client = Client::new(
        std::env::var("LOOPENGINE_PROJECT_KEY").unwrap(),
        std::env::var("LOOPENGINE_PROJECT_SECRET").unwrap(),
        std::env::var("LOOPENGINE_PROJECT_ID").unwrap(),
    )
    .unwrap();

    client
        .send(serde_json::json!({"message": "User feedback here"}))
        .await
        .unwrap();
}
```

- **`Client::new(key, secret, project_id)`** — Builds a client. Use your project key, secret, and project ID from the LoopEngine dashboard. Returns an error if any credential is empty.
- **`client.send(payload).await`** — Sends the payload to the Ingest API at `api.loopengine.dev`. `project_id` is added automatically. The payload must match the **fields and constraints** configured for your project in the LoopEngine dashboard (e.g. required fields, allowed keys, value types). You can pass any value that implements `serde::Serialize` and serializes to a JSON object (a `serde_json::json!` map, a struct with `#[derive(Serialize)]`, etc.). Use **`client.send_with_geo(payload, lat, lon).await`** to send device coordinates (see Geolocation below).

The client is safe for concurrent use — it wraps a [`reqwest::Client`](https://docs.rs/reqwest) which maintains an internal connection pool.

## Geolocation

You can send device location so feedback is associated with coordinates instead of IP-based geo. Use `send_with_geo` and pass `Some(lat)` and `Some(lon)`. When **both** are provided, the SDK adds `geo_lat` and `geo_lon` to the request body; they are included in the HMAC signature. Pass `None` for both to use IP-based geolocation (or use `send(payload)`). Valid ranges: latitude -90 to 90, longitude -180 to 180.

```rust
// Without geo (IP-based location is used)
client.send(serde_json::json!({"message": "Feedback"})).await?;

// With device coordinates
client
    .send_with_geo(
        serde_json::json!({"message": "Bug at my location"}),
        Some(34.05),
        Some(-118.25),
    )
    .await?;
```

## Custom HTTP client

Use `Client::builder` to pass a custom `reqwest::Client` (e.g. to set timeouts or a custom connector):

```rust
use std::time::Duration;

let http = reqwest::Client::builder()
    .timeout(Duration::from_secs(10))
    .build()
    .unwrap();

let client = loopengine::Client::builder(key, secret, project_id)
    .with_http_client(http)
    .build()
    .unwrap();
```

## Error handling

All errors are represented by `loopengine::Error`:

| Variant | When |
|---|---|
| `MissingCredentials` | A credential is empty after trimming |
| `Serialize` | Payload could not be serialized to JSON |
| `Http` | Network / transport error |
| `ApiError { status, body }` | Server returned a non-2xx response |

## Request signing

Every request is signed with `HMAC-SHA256` over the canonical string `"METHOD\nPATH\nTIMESTAMP\nSHA256(body)"`. The signature is base64url-encoded (no padding) and sent as the `X-Signature: v1=<sig>` header alongside `X-Project-Key` and `X-Timestamp`. All signing logic is handled transparently by the SDK.

## License

MIT