heyo-sdk 0.1.0

Rust SDK for the Heyo cloud sandbox API.
Documentation
//! Per-account sandbox networks. Mirrors `sdk-ts/src/networks.ts`.
//!
//! A `Network` is a control-plane record local and deployed sandboxes can
//! register into so they're addressable across machines from the same
//! account. Each account lazily gets a `default` network on first read
//! (preserved from the unmerged `feat/sdn-basics` prototype); additional
//! named networks can be created alongside it.

use reqwest::Method;
use serde::{Deserialize, Serialize};

use crate::client::{HeyoClient, HeyoClientOptions, RequestOptions};
use crate::commands::encode_path;
use crate::errors::HeyoError;

#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct NetworkInfo {
    pub id: String,
    pub account_id: String,
    pub name: String,
    pub is_default: bool,
    #[serde(default)]
    pub description: Option<String>,
    pub created_at: String,
    pub updated_at: String,
}

#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct NetworkMember {
    pub network_id: String,
    pub sandbox_kind: String,
    pub sandbox_ref: String,
    #[serde(default)]
    pub device_name: Option<String>,
    pub registered_at: String,
    #[serde(default)]
    pub last_seen_at: Option<String>,
}

#[derive(Debug, Clone, Default, Serialize)]
pub struct NetworkCreateOptions {
    pub name: String,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub description: Option<String>,
}

#[derive(Debug, Clone, Default, Serialize)]
pub struct NetworkUpdateOptions {
    #[serde(skip_serializing_if = "Option::is_none")]
    pub name: Option<String>,
    /// `None` leaves the description untouched; `Some(None)` clears it;
    /// `Some(Some("…"))` sets it.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub description: Option<Option<String>>,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum NetworkMemberKind {
    Local,
    Deployed,
}

impl NetworkMemberKind {
    pub fn as_str(&self) -> &'static str {
        match self {
            NetworkMemberKind::Local => "local",
            NetworkMemberKind::Deployed => "deployed",
        }
    }
}

#[derive(Debug, Clone, Serialize)]
pub struct NetworkMemberRegistration {
    pub sandbox_kind: NetworkMemberKind,
    pub sandbox_ref: String,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub device_name: Option<String>,
}

#[derive(Deserialize)]
struct NetworksEnvelope {
    #[serde(default)]
    networks: Vec<NetworkInfo>,
}

#[derive(Deserialize)]
struct MembersEnvelope {
    #[serde(default)]
    members: Vec<NetworkMember>,
}

#[derive(Clone)]
pub struct Network {
    info: NetworkInfo,
    client: HeyoClient,
}

impl Network {
    fn from_raw(client: HeyoClient, info: NetworkInfo) -> Self {
        Self { info, client }
    }

    pub fn id(&self) -> &str {
        &self.info.id
    }

    pub fn name(&self) -> &str {
        &self.info.name
    }

    pub fn is_default(&self) -> bool {
        self.info.is_default
    }

    pub fn info(&self) -> &NetworkInfo {
        &self.info
    }

    pub fn client(&self) -> &HeyoClient {
        &self.client
    }

    /// `POST /networks` — create a named network.
    pub async fn create(
        options: NetworkCreateOptions,
        client_options: HeyoClientOptions,
    ) -> Result<Self, HeyoError> {
        let client = HeyoClient::new(client_options)?;
        let raw: NetworkInfo = client
            .request(
                Method::POST,
                "/networks",
                Some(&options),
                RequestOptions::default(),
            )
            .await?;
        Ok(Network::from_raw(client, raw))
    }

    /// `GET /networks` — list networks for the caller's account (lazily
    /// creates the default network so the result is never empty).
    pub async fn list(client_options: HeyoClientOptions) -> Result<Vec<NetworkInfo>, HeyoError> {
        let client = HeyoClient::new(client_options)?;
        let env: NetworksEnvelope = client
            .request(Method::GET, "/networks", None::<&()>, RequestOptions::default())
            .await?;
        Ok(env.networks)
    }

    /// `GET /networks/{id}`.
    pub async fn get(id: &str, client_options: HeyoClientOptions) -> Result<Self, HeyoError> {
        let client = HeyoClient::new(client_options)?;
        let path = format!("/networks/{}", encode_path(id));
        let raw: NetworkInfo = client
            .request(Method::GET, &path, None::<&()>, RequestOptions::default())
            .await?;
        Ok(Network::from_raw(client, raw))
    }

    /// `GET /networks/me` — the caller's default network, lazily created.
    pub async fn default_for_me(
        client_options: HeyoClientOptions,
    ) -> Result<Self, HeyoError> {
        let client = HeyoClient::new(client_options)?;
        let raw: NetworkInfo = client
            .request(Method::GET, "/networks/me", None::<&()>, RequestOptions::default())
            .await?;
        Ok(Network::from_raw(client, raw))
    }

    /// `DELETE /networks/{id}` — refuses the default network (400).
    pub async fn delete_by_id(
        id: &str,
        client_options: HeyoClientOptions,
    ) -> Result<(), HeyoError> {
        let client = HeyoClient::new(client_options)?;
        let path = format!("/networks/{}", encode_path(id));
        client
            .request::<serde_json::Value>(
                Method::DELETE,
                &path,
                None::<&()>,
                RequestOptions::default(),
            )
            .await?;
        Ok(())
    }

    pub async fn delete(self) -> Result<(), HeyoError> {
        let path = format!("/networks/{}", encode_path(&self.info.id));
        self.client
            .request::<serde_json::Value>(
                Method::DELETE,
                &path,
                None::<&()>,
                RequestOptions::default(),
            )
            .await?;
        Ok(())
    }

    /// `PATCH /networks/{id}` — rename or change description. The default
    /// network refuses rename attempts (server returns 400).
    pub async fn update(&mut self, options: NetworkUpdateOptions) -> Result<(), HeyoError> {
        let path = format!("/networks/{}", encode_path(&self.info.id));
        let raw: NetworkInfo = self
            .client
            .request(Method::PATCH, &path, Some(&options), RequestOptions::default())
            .await?;
        self.info = raw;
        Ok(())
    }

    /// `GET /networks/{id}/members`.
    pub async fn list_members(&self) -> Result<Vec<NetworkMember>, HeyoError> {
        let path = format!("/networks/{}/members", encode_path(&self.info.id));
        let env: MembersEnvelope = self
            .client
            .request(Method::GET, &path, None::<&()>, RequestOptions::default())
            .await?;
        Ok(env.members)
    }

    /// `POST /networks/{id}/members` — register a sandbox into the
    /// network. Idempotent on (network_id, sandbox_kind, sandbox_ref);
    /// re-registering updates `device_name`.
    pub async fn add_member(
        &self,
        registration: NetworkMemberRegistration,
    ) -> Result<NetworkMember, HeyoError> {
        let path = format!("/networks/{}/members", encode_path(&self.info.id));
        self.client
            .request(
                Method::POST,
                &path,
                Some(&registration),
                RequestOptions::default(),
            )
            .await
    }

    /// `DELETE /networks/{id}/members/{kind}/{ref}`.
    pub async fn remove_member(
        &self,
        sandbox_kind: NetworkMemberKind,
        sandbox_ref: &str,
    ) -> Result<(), HeyoError> {
        let path = format!(
            "/networks/{}/members/{}/{}",
            encode_path(&self.info.id),
            sandbox_kind.as_str(),
            encode_path(sandbox_ref)
        );
        self.client
            .request::<serde_json::Value>(
                Method::DELETE,
                &path,
                None::<&()>,
                RequestOptions::default(),
            )
            .await?;
        Ok(())
    }
}