heyo-sdk 0.1.0

Rust SDK for the Heyo cloud sandbox API.
Documentation
//! Public types. Field names mirror the cloud HTTP API (snake_case both on
//! the wire and in Rust), so the SDK stays a thin wrapper rather than a
//! parallel domain model.

use std::collections::HashMap;

use serde::{Deserialize, Serialize};

/// `US` or `EU`. The wire field is uppercase; we preserve that.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum SandboxRegion {
    US,
    EU,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum SandboxDriver {
    Libvirt,
    Firecracker,
    Kvm,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum SandboxSize {
    Micro,
    Mini,
    Small,
    Medium,
    Large,
}

impl SandboxSize {
    pub fn as_str(&self) -> &'static str {
        match self {
            SandboxSize::Micro => "micro",
            SandboxSize::Mini => "mini",
            SandboxSize::Small => "small",
            SandboxSize::Medium => "medium",
            SandboxSize::Large => "large",
        }
    }
}

/// Lifecycle states the cloud reports.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum SandboxStatus {
    Provisioning,
    Running,
    Stopped,
    Paused,
    Failed,
    #[serde(rename = "cold-stored")]
    ColdStored,
    #[serde(other)]
    Unknown,
}

/// Options accepted by `Sandbox::create`. All fields except `image`/`region`
/// may be left unset; the server applies defaults.
#[derive(Debug, Clone, Default, Serialize)]
pub struct SandboxCreateOptions {
    #[serde(skip_serializing_if = "Option::is_none")]
    pub name: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none", rename = "archive_id")]
    pub archive_id: Option<String>,
    /// Defaults to `US` if unset.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub region: Option<SandboxRegion>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub driver: Option<SandboxDriver>,
    /// Image identifier (e.g. `ubuntu:24.04`, `bun`, `pi-…`). Defaults to
    /// `ubuntu:24.04` if unset.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub image: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none", rename = "start_command")]
    pub start_command: Option<String>,
    #[serde(default, rename = "open_ports", skip_serializing_if = "Vec::is_empty")]
    pub open_ports: Vec<u16>,
    #[serde(skip_serializing_if = "Option::is_none", rename = "ttl_seconds")]
    pub ttl_seconds: Option<u64>,
    #[serde(skip_serializing_if = "Option::is_none", rename = "disk_size_gb")]
    pub disk_size_gb: Option<u32>,
    #[serde(skip_serializing_if = "Option::is_none", rename = "working_directory")]
    pub working_directory: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none", rename = "env_vars")]
    pub env_vars: Option<HashMap<String, String>>,
    #[serde(skip_serializing_if = "Option::is_none", rename = "setup_hooks")]
    pub setup_hooks: Option<Vec<String>>,
    #[serde(skip_serializing_if = "Option::is_none", rename = "size_class")]
    pub size_class: Option<SandboxSize>,
    /// Maximum time `create()` will wait for the sandbox to leave the
    /// `provisioning` state. `None` ⇒ default 5 minutes. `Some(Duration::ZERO)`
    /// ⇒ return immediately while it's still provisioning.
    #[serde(skip_serializing)]
    pub wait_for_ready: Option<std::time::Duration>,
}

#[derive(Debug, Clone, Deserialize)]
pub struct SandboxInfo {
    pub id: String,
    #[serde(default)]
    pub name: String,
    pub status: SandboxStatus,
    #[serde(default)]
    pub image: String,
    #[serde(default)]
    pub region: Option<String>,
    #[serde(default)]
    pub start_command: Option<String>,
    #[serde(default)]
    pub working_directory: Option<String>,
    #[serde(default)]
    pub size_class: Option<String>,
    #[serde(default)]
    pub env_vars: Option<HashMap<String, String>>,
    #[serde(default)]
    pub setup_hooks: Option<Vec<String>>,
    #[serde(default)]
    pub uptime_secs: u64,
    #[serde(default)]
    pub ttl_seconds: Option<u64>,
    #[serde(default)]
    pub is_deployed: bool,
    #[serde(default)]
    pub error_message: Option<String>,
    pub status_changed_at: String,
    #[serde(default)]
    pub urls: Vec<BoundUrl>,
    /// Free-form JSON tags set by the server (e.g. `{"project_id": "..."}`).
    /// Mirrors the `metadata` column on `deployed_sandbox`.
    #[serde(default)]
    pub metadata: Option<serde_json::Value>,
}

#[derive(Debug, Clone, Deserialize)]
pub struct PublicImage {
    pub id: String,
    #[serde(default)]
    pub name: Option<String>,
    #[serde(default)]
    pub description: Option<String>,
    #[serde(default)]
    pub backend_type: Option<String>,
    #[serde(default)]
    pub format: String,
    #[serde(default)]
    pub size_bytes: u64,
    #[serde(default)]
    pub created_at: String,
}

#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct BoundUrl {
    pub subdomain: String,
    #[serde(default)]
    pub hostname: String,
    #[serde(default)]
    pub url: String,
    pub port: u16,
    #[serde(default, rename = "is_public")]
    pub is_public: bool,
}

#[derive(Debug, Clone, Default)]
pub struct CommandRunOptions {
    pub cwd: Option<String>,
    pub env: Option<HashMap<String, String>>,
    pub timeout: Option<std::time::Duration>,
}

#[derive(Debug, Clone)]
pub struct CommandResult {
    pub stdout: String,
    pub stderr: String,
    pub exit_code: i32,
    /// Combined stdout+stderr as the backend reports it.
    pub output: String,
}