claude-code-client-sdk 0.1.46

Rust SDK for integrating Claude Code as a subprocess with typed APIs
Documentation
use std::path::PathBuf;

use claude_code::{
    ClaudeAgentOptions, ContentBlock, InputPrompt, Message, PermissionMode, UserContent, query,
};
use serde_json::{Value, json};

fn fixture_cli_path() -> PathBuf {
    PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures/mock_claude_cli.py")
}

fn base_options() -> ClaudeAgentOptions {
    ClaudeAgentOptions {
        cli_path: Some(fixture_cli_path()),
        permission_mode: Some(PermissionMode::AcceptEdits),
        max_turns: Some(1),
        ..Default::default()
    }
}

fn extract_result_structured_output(messages: &[Message]) -> &Value {
    messages
        .iter()
        .find_map(|message| match message {
            Message::Result(result) => result.structured_output.as_ref(),
            _ => None,
        })
        .expect("missing result.structured_output")
}

#[tokio::test]
async fn test_e2e_simple_structured_output() {
    let schema = json!({
        "type": "object",
        "properties": {
            "file_count": {"type": "number"},
            "has_tests": {"type": "boolean"},
            "test_file_count": {"type": "number"}
        },
        "required": ["file_count", "has_tests"]
    });

    let mut options = base_options();
    options.output_format = Some(json!({
        "type": "json_schema",
        "schema": schema
    }));

    let messages = query(
        InputPrompt::Text("Count files and detect tests.".to_string()),
        Some(options),
        None,
    )
    .await
    .expect("query");

    let output = extract_result_structured_output(&messages);
    assert!(output.get("file_count").is_some_and(Value::is_number));
    assert!(output.get("has_tests").is_some_and(Value::is_boolean));
}

#[tokio::test]
async fn test_e2e_nested_structured_output() {
    let schema = json!({
        "type": "object",
        "properties": {
            "analysis": {
                "type": "object",
                "properties": {
                    "word_count": {"type": "number"},
                    "character_count": {"type": "number"}
                },
                "required": ["word_count", "character_count"]
            },
            "words": {"type": "array", "items": {"type": "string"}}
        },
        "required": ["analysis", "words"]
    });

    let mut options = base_options();
    options.output_format = Some(json!({
        "type": "json_schema",
        "schema": schema
    }));

    let messages = query(
        InputPrompt::Text("Analyze 'Hello world'.".to_string()),
        Some(options),
        None,
    )
    .await
    .expect("query");

    let output = extract_result_structured_output(&messages);
    assert_eq!(output["analysis"]["word_count"], 2);
    assert_eq!(output["analysis"]["character_count"], 11);
    assert_eq!(output["words"].as_array().map_or(0, std::vec::Vec::len), 2);
}

#[tokio::test]
async fn test_e2e_structured_output_with_enum() {
    let schema = json!({
        "type": "object",
        "properties": {
            "has_tests": {"type": "boolean"},
            "test_framework": {
                "type": "string",
                "enum": ["pytest", "unittest", "nose", "unknown"]
            },
            "test_count": {"type": "number"}
        },
        "required": ["has_tests", "test_framework"]
    });

    let mut options = base_options();
    options.output_format = Some(json!({
        "type": "json_schema",
        "schema": schema
    }));

    let messages = query(
        InputPrompt::Text("Detect test framework.".to_string()),
        Some(options),
        None,
    )
    .await
    .expect("query");

    let output = extract_result_structured_output(&messages);
    let framework = output
        .get("test_framework")
        .and_then(Value::as_str)
        .unwrap_or_default();
    assert!(matches!(
        framework,
        "pytest" | "unittest" | "nose" | "unknown"
    ));
    assert_eq!(framework, "pytest");
    assert_eq!(output["has_tests"], true);
}

#[tokio::test]
async fn test_e2e_structured_output_with_tools() {
    let schema = json!({
        "type": "object",
        "properties": {
            "file_count": {"type": "number"},
            "has_readme": {"type": "boolean"}
        },
        "required": ["file_count", "has_readme"]
    });

    let mut options = base_options();
    options.output_format = Some(json!({
        "type": "json_schema",
        "schema": schema
    }));
    options.env.insert(
        "MOCK_CLAUDE_STRUCTURED_WITH_TOOLS".to_string(),
        "1".to_string(),
    );

    let messages = query(
        InputPrompt::Text("Count files with tool use.".to_string()),
        Some(options),
        None,
    )
    .await
    .expect("query");

    let saw_tool_use = messages.iter().any(|message| match message {
        Message::Assistant(assistant) => assistant
            .content
            .iter()
            .any(|block| matches!(block, ContentBlock::ToolUse(_))),
        _ => false,
    });
    assert!(saw_tool_use, "expected assistant tool_use block");

    let saw_tool_result = messages.iter().any(|message| match message {
        Message::User(user) => match &user.content {
            UserContent::Blocks(blocks) => blocks
                .iter()
                .any(|block| matches!(block, ContentBlock::ToolResult(_))),
            UserContent::Text(_) => false,
        },
        _ => false,
    });
    assert!(saw_tool_result, "expected user tool_result block");

    let output = extract_result_structured_output(&messages);
    assert!(output.get("file_count").is_some_and(Value::is_number));
    assert!(output.get("has_readme").is_some_and(Value::is_boolean));
}