gemini-cli-sdk 0.1.0

Rust SDK wrapping Google's Gemini CLI as a subprocess via JSON-RPC 2.0
Documentation
//! Wire-format content block as exchanged between client and Gemini CLI.
//!
//! Gemini CLI uses a flat object with a `type` discriminator field rather than
//! a proper serde-tagged enum. All optional fields coexist on the same struct;
//! which ones are populated depends on `content_type`.

use serde::{Deserialize, Serialize};
use serde_json::Value;

/// A single content block in the Gemini CLI wire format.
///
/// The `type` field (renamed from `content_type` for serde) determines which
/// of the optional fields are meaningful:
///
/// | `type`    | Primary field(s)        |
/// |-----------|-------------------------|
/// | `"text"`  | `text`                  |
/// | `"image"` | `data`, `mime_type`     |
/// | `"file"`  | `uri`, `mime_type`      |
/// | `"resource"` | `resource`, `uri`   |
///
/// Unknown types are tolerated; `extra` captures any unknown keys.
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub(crate) struct WireContentBlock {
    /// Discriminator field — `"text"`, `"image"`, `"file"`, `"resource"`, etc.
    #[serde(default, rename = "type")]
    pub content_type: String,
    /// Textual content; present when `content_type == "text"`.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub text: Option<String>,
    /// Base-64 encoded binary payload; present when `content_type == "image"`.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub data: Option<String>,
    /// MIME type for binary or file content.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub mime_type: Option<String>,
    /// URI for file or resource references.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub uri: Option<String>,
    /// Optional human-readable name (e.g. file name).
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub name: Option<String>,
    /// Structured resource blob for `"resource"` type blocks.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub resource: Option<Value>,
    /// Unknown/future keys are preserved for forward-compatibility.
    #[serde(flatten)]
    pub extra: Value,
}

// ── Tests ─────────────────────────────────────────────────────────────────────

#[cfg(test)]
mod tests {
    use super::*;
    use serde_json::json;

    #[test]
    fn test_wire_content_block_text() {
        let raw = json!({
            "type": "text",
            "text": "Hello, world!"
        });
        let block: WireContentBlock =
            serde_json::from_value(raw).expect("deserialize text block");

        assert_eq!(block.content_type, "text");
        assert_eq!(block.text.as_deref(), Some("Hello, world!"));
        assert!(block.data.is_none());
        assert!(block.mime_type.is_none());

        // Roundtrip — serialized form must contain both `type` and `text`.
        let back = serde_json::to_value(&block).expect("serialize text block");
        assert_eq!(back["type"], "text");
        assert_eq!(back["text"], "Hello, world!");
    }

    #[test]
    fn test_wire_content_block_image() {
        let raw = json!({
            "type": "image",
            "data": "iVBORw0KGgo=",
            "mimeType": "image/png"
        });
        let block: WireContentBlock =
            serde_json::from_value(raw).expect("deserialize image block");

        assert_eq!(block.content_type, "image");
        assert_eq!(block.data.as_deref(), Some("iVBORw0KGgo="));
        assert_eq!(block.mime_type.as_deref(), Some("image/png"));
        assert!(block.text.is_none());

        // Roundtrip.
        let back = serde_json::to_value(&block).expect("serialize image block");
        assert_eq!(back["type"], "image");
        assert_eq!(back["data"], "iVBORw0KGgo=");
        assert_eq!(back["mimeType"], "image/png");
    }

    #[test]
    fn test_wire_content_block_unknown_type_preserved() {
        let raw = json!({
            "type": "future_type",
            "someNewField": 42
        });
        let block: WireContentBlock =
            serde_json::from_value(raw.clone()).expect("deserialize unknown block");
        assert_eq!(block.content_type, "future_type");

        let back = serde_json::to_value(&block).expect("serialize unknown block");
        assert_eq!(back["type"], "future_type");
        assert_eq!(back["someNewField"], 42, "extra fields must be preserved");
    }

    #[test]
    fn test_wire_content_block_default_is_empty() {
        let block = WireContentBlock::default();
        assert_eq!(block.content_type, "");
        assert!(block.text.is_none());
        assert!(block.data.is_none());
    }
}