fusabi-stdlib-ext 0.1.6

Extended standard library modules and domain packs for Fusabi
Documentation
//! Enhanced HTTP client module.
//!
//! Provides advanced HTTP client functionality with retries, streaming,
//! and fine-grained timeout control. Extends the basic `net` module.
//!
//! ## Features
//!
//! - HTTP requests with custom headers and options
//! - Automatic retries with configurable backoff
//! - Streaming downloads and uploads
//! - Connection pooling
//! - Timeout controls per request
//!
//! ## Example
//!
//! ```rust,ignore
//! use fusabi_stdlib_ext::net_http;
//!
//! // Make a request with custom options
//! let response = net_http::request(&[
//!     Value::String("GET".into()),
//!     Value::String("https://api.example.com/data".into()),
//!     Value::Map(headers),
//!     Value::Map(options),
//! ], &ctx)?;
//!
//! // Stream a large download
//! let stream = net_http::download_stream(&[
//!     Value::String("https://cdn.example.com/file.bin".into()),
//! ], &ctx)?;
//! ```

use fusabi_host::{ExecutionContext, Result, Value, Error};
use std::collections::HashMap;
use std::sync::Arc;
use crate::safety::SafetyConfig;

/// Make an HTTP request with full control over options.
///
/// # Arguments
///
/// * `args[0]` - HTTP method (GET, POST, PUT, DELETE, etc.)
/// * `args[1]` - URL
/// * `args[2]` - Headers (map of header name -> value)
/// * `args[3]` - Options (map with timeout, retries, body, etc.)
///
/// Options map can contain:
/// - `timeout`: Timeout in milliseconds (optional)
/// - `retries`: Number of retry attempts (optional, default 0)
/// - `retry_delay`: Delay between retries in ms (optional, default 1000)
/// - `body`: Request body (optional)
/// - `follow_redirects`: Boolean (optional, default true)
///
/// # Returns
///
/// Map with `status`, `headers`, and `body`
pub fn request(args: &[Value], _ctx: &ExecutionContext) -> Result<Value> {
    let method = args
        .first()
        .and_then(|v| v.as_str())
        .ok_or_else(|| Error::host_function("net_http.request: missing method argument"))?;

    let url = args
        .get(1)
        .and_then(|v| v.as_str())
        .ok_or_else(|| Error::host_function("net_http.request: missing url argument"))?;

    let empty_map = HashMap::new();
    let headers = args
        .get(2)
        .and_then(|v| v.as_map())
        .unwrap_or(&empty_map);

    let empty_options = HashMap::new();
    let options = args
        .get(3)
        .and_then(|v| v.as_map())
        .unwrap_or(&empty_options);

    // Extract options
    let timeout = options
        .get("timeout")
        .and_then(|v| v.as_int())
        .unwrap_or(30000);

    let retries = options
        .get("retries")
        .and_then(|v| v.as_int())
        .unwrap_or(0);

    let _retry_delay = options
        .get("retry_delay")
        .and_then(|v| v.as_int())
        .unwrap_or(1000);

    let _body = options
        .get("body")
        .and_then(|v| v.as_str());

    // TODO: Validate URL and check safety allowlist
    // TODO: Implement actual HTTP request with reqwest

    tracing::info!(
        "net_http.request: {} {} (timeout={}ms, retries={}, headers={})",
        method, url, timeout, retries, headers.len()
    );

    // Mock response
    let mut response = HashMap::new();
    response.insert("status".to_string(), Value::Int(200));
    response.insert("body".to_string(), Value::String(format!("Response from {}", url)));

    let mut response_headers = HashMap::new();
    response_headers.insert("content-type".to_string(), Value::String("application/json".to_string()));
    response.insert("headers".to_string(), Value::Map(response_headers));

    Ok(Value::Map(response))
}

/// Download a file as a stream.
///
/// Returns a stream handle that can be used to read chunks.
///
/// # Arguments
///
/// * `args[0]` - URL to download
/// * `args[1]` - Chunk size in bytes (optional, default 8192)
///
/// # Returns
///
/// Stream handle (integer)
pub fn download_stream(args: &[Value], _ctx: &ExecutionContext) -> Result<Value> {
    let url = args
        .first()
        .and_then(|v| v.as_str())
        .ok_or_else(|| Error::host_function("net_http.download_stream: missing url argument"))?;

    let _chunk_size = args
        .get(1)
        .and_then(|v| v.as_int())
        .unwrap_or(8192);

    // TODO: Implement streaming download
    // For now, return a mock handle
    tracing::debug!("net_http.download_stream: starting download from {}", url);

    // Mock handle
    Ok(Value::Int(1001))
}

/// Upload data from a stream.
///
/// # Arguments
///
/// * `args[0]` - URL to upload to
/// * `args[1]` - Stream handle to upload from
/// * `args[2]` - Headers (map, optional)
///
/// # Returns
///
/// Map with `status` and response details
pub fn upload_stream(args: &[Value], _ctx: &ExecutionContext) -> Result<Value> {
    let url = args
        .first()
        .and_then(|v| v.as_str())
        .ok_or_else(|| Error::host_function("net_http.upload_stream: missing url argument"))?;

    let _stream_handle = args
        .get(1)
        .and_then(|v| v.as_int())
        .ok_or_else(|| Error::host_function("net_http.upload_stream: missing stream handle"))?;

    // TODO: Implement streaming upload
    tracing::debug!("net_http.upload_stream: uploading to {}", url);

    let mut response = HashMap::new();
    response.insert("status".to_string(), Value::Int(201));
    response.insert("body".to_string(), Value::String("Upload complete".to_string()));

    Ok(Value::Map(response))
}

/// Read next chunk from a download stream.
///
/// Returns `null` when download is complete.
///
/// # Arguments
///
/// * `args[0]` - Stream handle
///
/// # Returns
///
/// String containing the chunk data, or null when complete
pub fn read_stream_chunk(args: &[Value], _ctx: &ExecutionContext) -> Result<Value> {
    let _handle = args
        .first()
        .and_then(|v| v.as_int())
        .ok_or_else(|| Error::host_function("net_http.read_stream_chunk: missing handle argument"))?;

    // TODO: Actually read from stream
    // For now, return mock data
    Ok(Value::String("Mock chunk data".to_string()))
}

/// Close a stream and release resources.
///
/// # Arguments
///
/// * `args[0]` - Stream handle
pub fn close_stream(args: &[Value], _ctx: &ExecutionContext) -> Result<Value> {
    let handle = args
        .first()
        .and_then(|v| v.as_int())
        .ok_or_else(|| Error::host_function("net_http.close_stream: missing handle argument"))?;

    tracing::debug!("net_http.close_stream: closing handle {}", handle);
    Ok(Value::Null)
}

/// Helper function to validate safety config for HTTP requests.
pub fn check_request_safety(
    _safety: &Arc<SafetyConfig>,
    _url: &str,
) -> Result<()> {
    // TODO: Extract host and check allowlist
    // TODO: Validate timeout against max_timeout
    Ok(())
}