pipedash-plugin-api 0.10.0

Plugin API for Pipedash - CI/CD pipeline dashboard
Documentation
use std::collections::{
    BTreeMap,
    HashMap,
};

use serde::{
    Deserialize,
    Serialize,
};

#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum ConfigFieldType {
    Text,
    Password,
    TextArea,
    Boolean,
    Select,
    MultiSelect,
    Number,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ConfigSection {
    Authentication,
    Connection,
    Settings,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ConfigField {
    pub key: String,
    pub label: String,
    pub description: Option<String>,
    pub field_type: ConfigFieldType,
    pub required: bool,
    pub default_value: Option<serde_json::Value>,
    pub options: Option<Vec<String>>,
    pub validation_regex: Option<String>,
    pub validation_message: Option<String>,
    pub section: ConfigSection,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum AuthMode {
    None,
    Token,
    SignIn,
}

pub const AUTH_MODE_KEY: &str = "auth_mode";

pub fn is_no_auth(config: &HashMap<String, String>) -> bool {
    config
        .get(AUTH_MODE_KEY)
        .is_some_and(|mode| mode.trim() == "none")
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ConfigSchema {
    pub fields: Vec<ConfigField>,
    #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
    pub auth_modes: BTreeMap<String, Vec<AuthMode>>,
    #[serde(default, skip_serializing_if = "std::ops::Not::not")]
    pub no_auth: bool,
}

impl ConfigSchema {
    pub fn new() -> Self {
        Self {
            fields: Vec::new(),
            auth_modes: BTreeMap::new(),
            no_auth: false,
        }
    }

    pub fn add_field(mut self, field: ConfigField) -> Self {
        self.fields.push(field);
        self
    }

    pub fn add_auth_field(mut self, modes: &[AuthMode], field: ConfigField) -> Self {
        self.auth_modes.insert(field.key.clone(), modes.to_vec());
        self.fields.push(field);
        self
    }

    pub fn allow_no_auth(mut self) -> Self {
        self.no_auth = true;
        self
    }
}

impl Default for ConfigSchema {
    fn default() -> Self {
        Self::new()
    }
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub enum ColumnDataType {
    String,
    Number,
    DateTime,
    Duration,
    Status,
    Badge,
    Url,
    Json,
    Boolean,
    Custom(String),
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub enum CellRenderer {
    Text,
    Badge,
    DateTime,
    Duration,
    StatusBadge,
    Commit,
    Avatar,
    TruncatedText,
    Link,
    JsonViewer,
    Custom(String),
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub enum ColumnVisibility {
    Always,
    WhenPresent,
    WhenCapability(String),
    Conditional {
        field: String,
        equals: serde_json::Value,
    },
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ColumnDefinition {
    pub id: String,
    pub label: String,
    pub description: Option<String>,
    pub field_path: String,
    pub data_type: ColumnDataType,
    pub renderer: CellRenderer,
    pub visibility: ColumnVisibility,
    #[serde(default = "default_visible_true")]
    pub default_visible: bool,
    pub width: Option<u32>,
    pub sortable: bool,
    pub filterable: bool,
}

fn default_visible_true() -> bool {
    true
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TableDefinition {
    pub id: String,
    pub name: String,
    pub description: Option<String>,
    pub columns: Vec<ColumnDefinition>,
    pub default_sort_column: Option<String>,
    pub default_sort_direction: Option<String>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TableSchema {
    pub tables: Vec<TableDefinition>,
}

impl TableSchema {
    pub fn new() -> Self {
        Self { tables: Vec::new() }
    }

    pub fn add_table(mut self, table: TableDefinition) -> Self {
        self.tables.push(table);
        self
    }

    pub fn get_table(&self, table_id: &str) -> Option<&TableDefinition> {
        self.tables.iter().find(|t| t.id == table_id)
    }
}

impl Default for TableSchema {
    fn default() -> Self {
        Self::new()
    }
}

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

    use super::*;

    fn field(key: &str) -> ConfigField {
        ConfigField {
            key: key.to_string(),
            label: key.to_string(),
            description: None,
            field_type: ConfigFieldType::Text,
            required: false,
            default_value: None,
            options: None,
            validation_regex: None,
            validation_message: None,
            section: ConfigSection::Connection,
        }
    }

    #[test]
    fn auth_fields_serialize_their_modes_by_key() {
        let schema = ConfigSchema::new()
            .allow_no_auth()
            .add_field(field("deck_url"))
            .add_auth_field(&[AuthMode::SignIn], field("sso_issuer"))
            .add_auth_field(&[AuthMode::Token, AuthMode::SignIn], field("kubeconfig"));
        let value = serde_json::to_value(&schema).unwrap();
        assert_eq!(
            value["auth_modes"],
            json!({"sso_issuer": ["sign_in"], "kubeconfig": ["token", "sign_in"]})
        );
        assert_eq!(value["no_auth"], json!(true));
        assert_eq!(value["fields"].as_array().unwrap().len(), 3);

        let plain = serde_json::to_value(ConfigSchema::new()).unwrap();
        assert!(plain.get("auth_modes").is_none());
        assert!(plain.get("no_auth").is_none());
    }

    #[test]
    fn no_auth_is_read_from_the_auth_mode_key() {
        let config = |mode: &str| HashMap::from([(AUTH_MODE_KEY.to_string(), mode.to_string())]);
        assert!(is_no_auth(&config("none")));
        assert!(!is_no_auth(&config("token")));
        assert!(!is_no_auth(&HashMap::new()));
    }

    #[test]
    fn fields_serialize_their_section_in_snake_case() {
        let mut auth = field("token");
        auth.section = ConfigSection::Authentication;
        assert_eq!(
            serde_json::to_value(&auth).unwrap()["section"],
            json!("authentication")
        );
    }
}