use std::collections::HashMap;
use indexmap::IndexMap;
use serde::Deserialize;
pub const DEFAULT_CONTROL_PLANE_ENDPOINT: &str = "https://api.cloud.zilliz.com";
pub const DEFAULT_CN_CONTROL_PLANE_ENDPOINT: &str = "https://api.cloud.zilliz.com.cn";
pub const DEFAULT_DEV_CONTROL_PLANE_ENDPOINT: &str = "https://api.cloud-uat3.zilliz.com";
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
#[allow(dead_code)]
pub struct CliModel {
#[serde(default)]
pub version: String,
#[serde(default)]
pub min_cli_version: String,
pub endpoint: Option<String>,
pub auth: Option<AuthConfig>,
#[serde(default)]
pub resources: IndexMap<String, Resource>,
}
impl CliModel {
pub fn resolved_endpoint(&self) -> String {
self.endpoint
.clone()
.unwrap_or_else(|| DEFAULT_CONTROL_PLANE_ENDPOINT.to_string())
}
}
#[derive(Debug, Clone, Deserialize)]
pub struct AuthConfig {
#[serde(default, alias = "auth0Domain")]
pub auth0_domain: String,
#[serde(default, alias = "clientId")]
pub client_id: String,
#[serde(default, alias = "loginApi")]
pub login_api: String,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Resource {
pub description: Option<String>,
#[serde(default)]
pub dedicated_only: bool,
#[serde(default)]
pub operations: IndexMap<String, Operation>,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
#[allow(dead_code)]
pub struct Operation {
#[serde(default)]
pub http: HttpConfig,
#[serde(default)]
pub params: Vec<Param>,
pub body_param: Option<String>,
#[serde(default)]
pub output: OutputConfig,
pub pagination: Option<Pagination>,
pub description: Option<String>,
#[serde(default)]
pub examples: Vec<String>,
#[serde(default)]
pub dedicated_only: bool,
pub body_transform: Option<serde_json::Value>,
#[serde(default)]
pub body_defaults: serde_json::Map<String, serde_json::Value>,
}
impl Operation {
pub fn method(&self) -> &str {
&self.http.method
}
pub fn path(&self) -> &str {
&self.http.path
}
}
#[derive(Debug, Clone, Default, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct HttpConfig {
#[serde(default = "default_method")]
pub method: String,
#[serde(default)]
pub path: String,
}
fn default_method() -> String {
"GET".to_string()
}
#[derive(Debug, Clone, Default, Deserialize)]
#[serde(rename_all = "camelCase")]
#[allow(dead_code)]
pub struct OutputConfig {
pub data_field: Option<String>,
#[serde(default)]
pub columns: Option<Vec<String>>,
#[serde(default)]
pub color_map: Option<HashMap<String, HashMap<String, String>>>,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
#[allow(dead_code)]
pub struct Param {
pub name: String,
#[serde(default = "default_type")]
#[serde(rename = "type")]
pub param_type: String,
#[serde(rename = "cli")]
pub cli_name: Option<String>,
#[serde(default)]
pub required: bool,
pub default: Option<serde_json::Value>,
pub position: Option<String>,
pub required_unless: Option<String>,
pub required_when: Option<serde_json::Value>,
pub description: Option<String>,
pub choices: Option<Vec<String>>,
pub transform: Option<serde_json::Value>,
}
fn default_type() -> String {
"string".to_string()
}
impl Param {
pub fn cli_flag(&self) -> String {
self.cli_name
.clone()
.unwrap_or_else(|| format!("--{}", self.name))
}
pub fn is_path_param(&self) -> bool {
self.position.as_deref() == Some("path")
}
}
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
#[allow(dead_code)]
pub struct Pagination {
#[serde(default = "default_page_size_param")]
pub page_size_param: String,
#[serde(default = "default_page_param")]
pub page_param: String,
#[serde(default = "default_count_field")]
pub count_field: String,
#[serde(default = "default_data_field")]
pub data_field: String,
#[serde(default = "default_page_size")]
pub default_page_size: u32,
}
fn default_page_size_param() -> String {
"pageSize".to_string()
}
fn default_page_param() -> String {
"currentPage".to_string()
}
fn default_count_field() -> String {
"count".to_string()
}
fn default_data_field() -> String {
"data".to_string()
}
fn default_page_size() -> u32 {
100
}
#[cfg(test)]
mod tests {
use super::*;
fn empty_model(endpoint: Option<String>) -> CliModel {
CliModel {
version: String::new(),
min_cli_version: String::new(),
endpoint,
auth: None,
resources: IndexMap::new(),
}
}
#[test]
fn resolved_endpoint_uses_configured_value_when_present() {
let m = empty_model(Some("https://staging.example.com".to_string()));
assert_eq!(m.resolved_endpoint(), "https://staging.example.com");
}
#[test]
fn resolved_endpoint_falls_back_to_default_when_none() {
let m = empty_model(None);
assert_eq!(m.resolved_endpoint(), DEFAULT_CONTROL_PLANE_ENDPOINT);
}
#[test]
fn global_and_cn_defaults_are_distinct() {
assert_ne!(
DEFAULT_CONTROL_PLANE_ENDPOINT, DEFAULT_CN_CONTROL_PLANE_ENDPOINT,
"Global and CN defaults must not collide"
);
}
#[test]
fn cn_default_targets_cn_tld() {
assert!(
DEFAULT_CN_CONTROL_PLANE_ENDPOINT.ends_with(".cn"),
"CN default endpoint must end in .cn"
);
}
#[test]
fn dev_default_is_distinct_from_global_and_cn() {
assert_ne!(
DEFAULT_DEV_CONTROL_PLANE_ENDPOINT, DEFAULT_CONTROL_PLANE_ENDPOINT,
"Dev and Global defaults must not collide"
);
assert_ne!(
DEFAULT_DEV_CONTROL_PLANE_ENDPOINT, DEFAULT_CN_CONTROL_PLANE_ENDPOINT,
"Dev and CN defaults must not collide"
);
}
#[test]
fn dev_default_targets_uat3_host() {
assert!(
DEFAULT_DEV_CONTROL_PLANE_ENDPOINT.contains("cloud-uat3.zilliz.com"),
"Dev default endpoint must target the cloud-uat3 host"
);
}
#[test]
fn resolved_endpoint_uses_dev_value_when_persisted() {
let m = empty_model(Some(DEFAULT_DEV_CONTROL_PLANE_ENDPOINT.to_string()));
assert_eq!(m.resolved_endpoint(), DEFAULT_DEV_CONTROL_PLANE_ENDPOINT);
}
}