use indexmap::IndexMap;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
#[serde(rename_all = "camelCase")]
pub struct OperationsConfig {
#[serde(default, skip_serializing_if = "IndexMap::is_empty")]
pub plugins: IndexMap<String, PluginOperationsConfig>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub custom: Vec<CustomPluginOperationsConfig>,
}
impl OperationsConfig {
pub fn is_empty(&self) -> bool {
self.plugins.is_empty() && self.custom.is_empty()
}
pub fn without_settings(&self) -> Self {
let strip = |config: &PluginOperationsConfig| PluginOperationsConfig {
settings: IndexMap::new(),
approval: config.approval.clone(),
};
Self {
plugins: self
.plugins
.iter()
.map(|(name, config)| (name.clone(), strip(config)))
.collect(),
custom: self
.custom
.iter()
.map(|plugin| CustomPluginOperationsConfig {
name: plugin.name.clone(),
version: plugin.version.clone(),
config: strip(&plugin.config),
})
.collect(),
}
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
#[serde(rename_all = "camelCase")]
pub struct PluginOperationsConfig {
#[serde(default, skip_serializing_if = "IndexMap::is_empty")]
pub settings: IndexMap<String, OperationSettingValue>,
#[serde(default, skip_serializing_if = "IndexMap::is_empty")]
pub approval: IndexMap<String, OperationApproval>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
#[serde(rename_all = "camelCase")]
pub struct CustomPluginOperationsConfig {
pub name: String,
pub version: String,
#[serde(flatten)]
pub config: PluginOperationsConfig,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
#[serde(untagged)]
pub enum OperationSettingValue {
Literal(String),
Input {
input: String,
},
Resources {
resources: Vec<String>,
},
Env {
env: String,
},
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
#[serde(untagged)]
pub enum OperationApproval {
Decision(OperationApprovalDecision),
Detailed {
decision: OperationApprovalDecision,
#[serde(rename = "maxRisk", default, skip_serializing_if = "Option::is_none")]
max_risk: Option<String>,
},
}
impl OperationApproval {
pub fn decision(&self) -> OperationApprovalDecision {
match self {
Self::Decision(decision) | Self::Detailed { decision, .. } => *decision,
}
}
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
#[serde(rename_all = "lowercase")]
pub enum OperationApprovalDecision {
Auto,
Manual,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn round_trips_every_setting_and_approval_form() {
let json = serde_json::json!({
"plugins": {
"db": {
"settings": {
"url": { "input": "dbUrl" },
"database": "app",
"password": { "env": "DB_PASSWORD" },
"buckets": { "resources": ["data"] }
},
"approval": { "*": "manual", "health": { "decision": "auto", "maxRisk": "read-only" } }
}
},
"custom": [{ "name": "mine", "version": "1.2.0", "approval": { "*": "auto" } }]
});
let config: OperationsConfig = serde_json::from_value(json.clone()).unwrap();
let db = &config.plugins["db"];
assert_eq!(
db.settings["database"],
OperationSettingValue::Literal("app".into())
);
assert_eq!(
db.settings["url"],
OperationSettingValue::Input {
input: "dbUrl".into()
}
);
assert_eq!(
db.approval["*"].decision(),
OperationApprovalDecision::Manual
);
assert_eq!(
db.approval["health"].decision(),
OperationApprovalDecision::Auto
);
assert_eq!(
config.custom[0].config.approval["*"].decision(),
OperationApprovalDecision::Auto
);
assert_eq!(serde_json::to_value(&config).unwrap(), json);
}
}