zilliz 1.0.0

TUI and CLI tool for managing Zilliz Cloud clusters and Milvus operations
Documentation
use std::collections::HashMap;

use serde_json::Value;

use crate::api::client::ApiClient;
use crate::api::error::ApiError;
use crate::model::types::Operation;

/// Shared operation executor used by both CLI and TUI modes.
/// Takes an Operation definition + user-provided param values, builds the API call.
pub struct OperationExecutor<'a> {
    client: &'a ApiClient,
}

impl<'a> OperationExecutor<'a> {
    pub fn new(client: &'a ApiClient) -> Self {
        Self { client }
    }

    /// Execute an API operation with the given parameter values.
    pub async fn execute(
        &self,
        operation: &Operation,
        param_values: &HashMap<String, Value>,
    ) -> Result<Value, ApiError> {
        let mut path_params = HashMap::new();
        let mut body = serde_json::Map::new();

        // Apply body defaults first
        for (key, value) in &operation.body_defaults {
            body.insert(key.clone(), value.clone());
        }

        // Classify params into path params vs body/query params
        for param in &operation.params {
            let value = param_values.get(&param.name);

            if param.is_path_param() {
                if let Some(val) = value {
                    let str_val = match val {
                        Value::String(s) => s.clone(),
                        other => other.to_string(),
                    };
                    path_params.insert(param.name.clone(), str_val);
                }
            } else if let Some(val) = value {
                // Skip null values
                if !val.is_null() {
                    body.insert(param.name.clone(), val.clone());
                }
            }
        }

        // Apply body transforms (compose fields, infer values)
        if let Some(ref transform) = operation.body_transform {
            apply_body_transform(&mut body, transform);
        }

        let body_value = if body.is_empty() && operation.method() == "GET" {
            None
        } else {
            Some(Value::Object(body))
        };

        self.client
            .call(
                operation.method(),
                operation.path(),
                Some(&path_params),
                body_value.as_ref(),
            )
            .await
    }

    /// Execute a paginated list operation, fetching all pages.
    pub async fn execute_all_pages(
        &self,
        operation: &Operation,
        param_values: &HashMap<String, Value>,
    ) -> Result<Vec<Value>, ApiError> {
        let pagination = match &operation.pagination {
            Some(p) => p,
            None => return self.execute(operation, param_values).await.map(|v| vec![v]),
        };

        let mut all_items = Vec::new();
        let mut current_page = 1u64;
        let page_size = pagination.default_page_size as u64;

        loop {
            let mut page_params = param_values.clone();
            page_params.insert(
                pagination.page_size_param.clone(),
                Value::Number(page_size.into()),
            );
            page_params.insert(
                pagination.page_param.clone(),
                Value::Number(current_page.into()),
            );

            let result = self.execute(operation, &page_params).await?;

            // Extract items from the data field
            let items = result
                .get(&pagination.data_field)
                .and_then(|v| v.as_array())
                .cloned()
                .unwrap_or_default();

            let count = items.len();
            all_items.extend(items);

            // Check if we've fetched all items
            if count < page_size as usize {
                break;
            }

            current_page += 1;
        }

        Ok(all_items)
    }
}

/// Apply body transform rules: `infer` sets values conditionally, `compose` builds
/// structured fields from flat params and removes consumed params from the body.
fn apply_body_transform(body: &mut serde_json::Map<String, Value>, transform: &Value) {
    // Phase 1: infer -- conditionally set fields based on presence of other params
    if let Some(infer) = transform.get("infer").and_then(|v| v.as_object()) {
        for (field_name, rule) in infer {
            let when_param = rule.get("when").and_then(|v| v.as_str()).unwrap_or("");
            if !when_param.is_empty() && body.contains_key(when_param) {
                // Only set if not already explicitly provided by user
                if !body.contains_key(field_name) {
                    if let Some(value) = rule.get("value") {
                        body.insert(field_name.clone(), value.clone());
                    }
                }
            }
        }
    }

    // Phase 2: compose -- build structured fields from flat params
    if let Some(compose) = transform.get("compose").and_then(|v| v.as_object()) {
        for (field_name, rule) in compose {
            let when_param = rule.get("when").and_then(|v| v.as_str()).unwrap_or("");
            if !when_param.is_empty() && !body.contains_key(when_param) {
                continue;
            }

            if let Some(template) = rule.get("template") {
                let resolved = resolve_template(template, body);
                body.insert(field_name.clone(), resolved);
            }

            if let Some(consume) = rule.get("consume").and_then(|v| v.as_array()) {
                for param in consume {
                    if let Some(name) = param.as_str() {
                        body.remove(name);
                    }
                }
            }
        }
    }
}

/// Recursively resolve `{paramName}` placeholders in a JSON template using body values.
fn resolve_template(template: &Value, body: &serde_json::Map<String, Value>) -> Value {
    match template {
        Value::String(s) => {
            // Exact placeholder match: "{paramName}" -> use the raw value
            if s.starts_with('{') && s.ends_with('}') && s.len() > 2 {
                let param_name = &s[1..s.len() - 1];
                if let Some(val) = body.get(param_name) {
                    return val.clone();
                }
            }
            // Inline replacement for partial placeholders
            let mut result = s.clone();
            for (key, val) in body {
                let str_val = match val {
                    Value::String(v) => v.clone(),
                    Value::Number(n) => n.to_string(),
                    Value::Bool(b) => b.to_string(),
                    _ => continue,
                };
                result = result.replace(&format!("{{{}}}", key), &str_val);
            }
            Value::String(result)
        }
        Value::Array(arr) => {
            Value::Array(arr.iter().map(|item| resolve_template(item, body)).collect())
        }
        Value::Object(obj) => {
            let mut new_obj = serde_json::Map::new();
            for (k, v) in obj {
                new_obj.insert(k.clone(), resolve_template(v, body));
            }
            Value::Object(new_obj)
        }
        other => other.clone(),
    }
}