1use indexmap::IndexMap;
8use serde::{Deserialize, Serialize};
9
10#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
12#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
13#[serde(rename_all = "camelCase")]
14pub struct OperationsConfig {
15 #[serde(default, skip_serializing_if = "IndexMap::is_empty")]
17 pub plugins: IndexMap<String, PluginOperationsConfig>,
18 #[serde(default, skip_serializing_if = "Vec::is_empty")]
20 pub custom: Vec<CustomPluginOperationsConfig>,
21}
22
23impl OperationsConfig {
24 pub fn is_empty(&self) -> bool {
26 self.plugins.is_empty() && self.custom.is_empty()
27 }
28
29 pub fn without_settings(&self) -> Self {
31 let strip = |config: &PluginOperationsConfig| PluginOperationsConfig {
32 settings: IndexMap::new(),
33 approval: config.approval.clone(),
34 };
35 Self {
36 plugins: self
37 .plugins
38 .iter()
39 .map(|(name, config)| (name.clone(), strip(config)))
40 .collect(),
41 custom: self
42 .custom
43 .iter()
44 .map(|plugin| CustomPluginOperationsConfig {
45 name: plugin.name.clone(),
46 version: plugin.version.clone(),
47 config: strip(&plugin.config),
48 })
49 .collect(),
50 }
51 }
52}
53
54#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
56#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
57#[serde(rename_all = "camelCase")]
58pub struct PluginOperationsConfig {
59 #[serde(default, skip_serializing_if = "IndexMap::is_empty")]
61 pub settings: IndexMap<String, OperationSettingValue>,
62 #[serde(default, skip_serializing_if = "IndexMap::is_empty")]
65 pub approval: IndexMap<String, OperationApproval>,
66}
67
68#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
70#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
71#[serde(rename_all = "camelCase")]
72pub struct CustomPluginOperationsConfig {
73 pub name: String,
75 pub version: String,
77 #[serde(flatten)]
79 pub config: PluginOperationsConfig,
80}
81
82#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
84#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
85#[serde(untagged)]
86pub enum OperationSettingValue {
87 Literal(String),
89 Input {
91 input: String,
93 },
94 Resources {
96 resources: Vec<String>,
98 },
99 Env {
102 env: String,
104 },
105}
106
107#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
109#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
110#[serde(untagged)]
111pub enum OperationApproval {
112 Decision(OperationApprovalDecision),
114 Detailed {
116 decision: OperationApprovalDecision,
118 #[serde(rename = "maxRisk", default, skip_serializing_if = "Option::is_none")]
121 max_risk: Option<String>,
122 },
123}
124
125impl OperationApproval {
126 pub fn decision(&self) -> OperationApprovalDecision {
128 match self {
129 Self::Decision(decision) | Self::Detailed { decision, .. } => *decision,
130 }
131 }
132}
133
134#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
136#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
137#[serde(rename_all = "lowercase")]
138pub enum OperationApprovalDecision {
139 Auto,
141 Manual,
143}
144
145#[cfg(test)]
146mod tests {
147 use super::*;
148
149 #[test]
150 fn round_trips_every_setting_and_approval_form() {
151 let json = serde_json::json!({
152 "plugins": {
153 "db": {
154 "settings": {
155 "url": { "input": "dbUrl" },
156 "database": "app",
157 "password": { "env": "DB_PASSWORD" },
158 "buckets": { "resources": ["data"] }
159 },
160 "approval": { "*": "manual", "health": { "decision": "auto", "maxRisk": "read-only" } }
161 }
162 },
163 "custom": [{ "name": "mine", "version": "1.2.0", "approval": { "*": "auto" } }]
164 });
165 let config: OperationsConfig = serde_json::from_value(json.clone()).unwrap();
166 let db = &config.plugins["db"];
167 assert_eq!(
168 db.settings["database"],
169 OperationSettingValue::Literal("app".into())
170 );
171 assert_eq!(
172 db.settings["url"],
173 OperationSettingValue::Input {
174 input: "dbUrl".into()
175 }
176 );
177 assert_eq!(
178 db.approval["*"].decision(),
179 OperationApprovalDecision::Manual
180 );
181 assert_eq!(
182 db.approval["health"].decision(),
183 OperationApprovalDecision::Auto
184 );
185 assert_eq!(
186 config.custom[0].config.approval["*"].decision(),
187 OperationApprovalDecision::Auto
188 );
189 assert_eq!(serde_json::to_value(&config).unwrap(), json);
190 }
191}