Skip to main content

pipedash_plugin_api/
schema.rs

1use std::collections::{
2    BTreeMap,
3    HashMap,
4};
5
6use serde::{
7    Deserialize,
8    Serialize,
9};
10
11#[derive(Debug, Clone, Serialize, Deserialize)]
12pub enum ConfigFieldType {
13    Text,
14    Password,
15    TextArea,
16    Boolean,
17    Select,
18    MultiSelect,
19    Number,
20}
21
22#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
23#[serde(rename_all = "snake_case")]
24pub enum ConfigSection {
25    Authentication,
26    Connection,
27    Settings,
28}
29
30#[derive(Debug, Clone, Serialize, Deserialize)]
31pub struct ConfigField {
32    pub key: String,
33    pub label: String,
34    pub description: Option<String>,
35    pub field_type: ConfigFieldType,
36    pub required: bool,
37    pub default_value: Option<serde_json::Value>,
38    pub options: Option<Vec<String>>,
39    pub validation_regex: Option<String>,
40    pub validation_message: Option<String>,
41    pub section: ConfigSection,
42}
43
44#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
45#[serde(rename_all = "snake_case")]
46pub enum AuthMode {
47    None,
48    Token,
49    SignIn,
50}
51
52pub const AUTH_MODE_KEY: &str = "auth_mode";
53
54pub fn is_no_auth(config: &HashMap<String, String>) -> bool {
55    config
56        .get(AUTH_MODE_KEY)
57        .is_some_and(|mode| mode.trim() == "none")
58}
59
60#[derive(Debug, Clone, Serialize, Deserialize)]
61pub struct ConfigSchema {
62    pub fields: Vec<ConfigField>,
63    #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
64    pub auth_modes: BTreeMap<String, Vec<AuthMode>>,
65    #[serde(default, skip_serializing_if = "std::ops::Not::not")]
66    pub no_auth: bool,
67}
68
69impl ConfigSchema {
70    pub fn new() -> Self {
71        Self {
72            fields: Vec::new(),
73            auth_modes: BTreeMap::new(),
74            no_auth: false,
75        }
76    }
77
78    pub fn add_field(mut self, field: ConfigField) -> Self {
79        self.fields.push(field);
80        self
81    }
82
83    pub fn add_auth_field(mut self, modes: &[AuthMode], field: ConfigField) -> Self {
84        self.auth_modes.insert(field.key.clone(), modes.to_vec());
85        self.fields.push(field);
86        self
87    }
88
89    pub fn allow_no_auth(mut self) -> Self {
90        self.no_auth = true;
91        self
92    }
93}
94
95impl Default for ConfigSchema {
96    fn default() -> Self {
97        Self::new()
98    }
99}
100
101#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
102pub enum ColumnDataType {
103    String,
104    Number,
105    DateTime,
106    Duration,
107    Status,
108    Badge,
109    Url,
110    Json,
111    Boolean,
112    Custom(String),
113}
114
115#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
116pub enum CellRenderer {
117    Text,
118    Badge,
119    DateTime,
120    Duration,
121    StatusBadge,
122    Commit,
123    Avatar,
124    TruncatedText,
125    Link,
126    JsonViewer,
127    Custom(String),
128}
129
130#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
131pub enum ColumnVisibility {
132    Always,
133    WhenPresent,
134    WhenCapability(String),
135    Conditional {
136        field: String,
137        equals: serde_json::Value,
138    },
139}
140
141#[derive(Debug, Clone, Serialize, Deserialize)]
142pub struct ColumnDefinition {
143    pub id: String,
144    pub label: String,
145    pub description: Option<String>,
146    pub field_path: String,
147    pub data_type: ColumnDataType,
148    pub renderer: CellRenderer,
149    pub visibility: ColumnVisibility,
150    #[serde(default = "default_visible_true")]
151    pub default_visible: bool,
152    pub width: Option<u32>,
153    pub sortable: bool,
154    pub filterable: bool,
155}
156
157fn default_visible_true() -> bool {
158    true
159}
160
161#[derive(Debug, Clone, Serialize, Deserialize)]
162pub struct TableDefinition {
163    pub id: String,
164    pub name: String,
165    pub description: Option<String>,
166    pub columns: Vec<ColumnDefinition>,
167    pub default_sort_column: Option<String>,
168    pub default_sort_direction: Option<String>,
169}
170
171#[derive(Debug, Clone, Serialize, Deserialize)]
172pub struct TableSchema {
173    pub tables: Vec<TableDefinition>,
174}
175
176impl TableSchema {
177    pub fn new() -> Self {
178        Self { tables: Vec::new() }
179    }
180
181    pub fn add_table(mut self, table: TableDefinition) -> Self {
182        self.tables.push(table);
183        self
184    }
185
186    pub fn get_table(&self, table_id: &str) -> Option<&TableDefinition> {
187        self.tables.iter().find(|t| t.id == table_id)
188    }
189}
190
191impl Default for TableSchema {
192    fn default() -> Self {
193        Self::new()
194    }
195}
196
197#[cfg(test)]
198mod tests {
199    use serde_json::json;
200
201    use super::*;
202
203    fn field(key: &str) -> ConfigField {
204        ConfigField {
205            key: key.to_string(),
206            label: key.to_string(),
207            description: None,
208            field_type: ConfigFieldType::Text,
209            required: false,
210            default_value: None,
211            options: None,
212            validation_regex: None,
213            validation_message: None,
214            section: ConfigSection::Connection,
215        }
216    }
217
218    #[test]
219    fn auth_fields_serialize_their_modes_by_key() {
220        let schema = ConfigSchema::new()
221            .allow_no_auth()
222            .add_field(field("deck_url"))
223            .add_auth_field(&[AuthMode::SignIn], field("sso_issuer"))
224            .add_auth_field(&[AuthMode::Token, AuthMode::SignIn], field("kubeconfig"));
225        let value = serde_json::to_value(&schema).unwrap();
226        assert_eq!(
227            value["auth_modes"],
228            json!({"sso_issuer": ["sign_in"], "kubeconfig": ["token", "sign_in"]})
229        );
230        assert_eq!(value["no_auth"], json!(true));
231        assert_eq!(value["fields"].as_array().unwrap().len(), 3);
232
233        let plain = serde_json::to_value(ConfigSchema::new()).unwrap();
234        assert!(plain.get("auth_modes").is_none());
235        assert!(plain.get("no_auth").is_none());
236    }
237
238    #[test]
239    fn no_auth_is_read_from_the_auth_mode_key() {
240        let config = |mode: &str| HashMap::from([(AUTH_MODE_KEY.to_string(), mode.to_string())]);
241        assert!(is_no_auth(&config("none")));
242        assert!(!is_no_auth(&config("token")));
243        assert!(!is_no_auth(&HashMap::new()));
244    }
245
246    #[test]
247    fn fields_serialize_their_section_in_snake_case() {
248        let mut auth = field("token");
249        auth.section = ConfigSection::Authentication;
250        assert_eq!(
251            serde_json::to_value(&auth).unwrap()["section"],
252            json!("authentication")
253        );
254    }
255}