alien_core/stack_authoring/
operations.rs1use indexmap::IndexMap;
7
8use super::invalid;
9use crate::error::Result;
10use crate::{CustomPluginOperationsConfig, OperationsConfig, PluginOperationsConfig};
11
12pub fn custom_plugin(spec: &str) -> Result<CustomPluginOperationsConfig> {
14 match spec.rsplit_once('@') {
15 Some((name, version)) if !name.is_empty() && !version.is_empty() => {
16 Ok(CustomPluginOperationsConfig {
17 name: name.to_string(),
18 version: version.to_string(),
19 config: PluginOperationsConfig::default(),
20 })
21 }
22 _ => Err(invalid(
23 "operations.custom",
24 format!("custom plugin '{spec}' must be written as 'name@version'"),
25 )),
26 }
27}
28
29pub fn operations_config(
31 plugins: IndexMap<String, PluginOperationsConfig>,
32 custom: Vec<CustomPluginOperationsConfig>,
33) -> Result<OperationsConfig> {
34 let config = OperationsConfig { plugins, custom };
35 if config.is_empty() {
36 return Err(invalid("operations", "needs at least one plugin"));
37 }
38 Ok(config)
39}
40
41#[cfg(test)]
42mod tests {
43 use super::*;
44 use crate::{ErrorData, OperationApproval, OperationApprovalDecision, OperationSettingValue};
45 use alien_error::AlienError;
46
47 fn field_and_reason(error: AlienError<ErrorData>) -> (String, String) {
48 match error.error.expect("error data") {
49 ErrorData::StackDefinitionInvalid { field, reason } => (field, reason),
50 other => panic!("unexpected error {other:?}"),
51 }
52 }
53
54 #[test]
55 fn a_custom_plugin_is_pinned_to_a_version() {
56 let plugin = custom_plugin("mine@1.2.0").unwrap();
57 assert_eq!(plugin.name, "mine");
58 assert_eq!(plugin.version, "1.2.0");
59 assert_eq!(plugin.config, PluginOperationsConfig::default());
60
61 let scoped = custom_plugin("@acme/diagnostics@1.4.0").unwrap();
62 assert_eq!(scoped.name, "@acme/diagnostics");
63 assert_eq!(scoped.version, "1.4.0");
64
65 for spec in ["mine", "@1.0.0", "mine@", ""] {
66 let (field, reason) = field_and_reason(custom_plugin(spec).unwrap_err());
67 assert_eq!(field, "operations.custom");
68 assert!(reason.contains("name@version"), "{reason}");
69 }
70 }
71
72 #[test]
73 fn a_declaration_names_a_plugin_and_serializes_as_the_sdk_does() {
74 let (field, reason) =
75 field_and_reason(operations_config(IndexMap::new(), vec![]).unwrap_err());
76 assert_eq!(field, "operations");
77 assert!(reason.contains("at least one plugin"), "{reason}");
78
79 let config = operations_config(
80 IndexMap::from([(
81 "kubernetes".to_string(),
82 PluginOperationsConfig {
83 settings: IndexMap::from([(
84 "database".to_string(),
85 OperationSettingValue::Literal("app".to_string()),
86 )]),
87 approval: IndexMap::from([(
88 "*".to_string(),
89 OperationApproval::Decision(OperationApprovalDecision::Auto),
90 )]),
91 },
92 )]),
93 vec![custom_plugin("mine@1.2.0").unwrap()],
94 )
95 .unwrap();
96 assert_eq!(
97 serde_json::to_value(&config).unwrap(),
98 serde_json::json!({
99 "plugins": {
100 "kubernetes": {
101 "settings": { "database": "app" },
102 "approval": { "*": "auto" }
103 }
104 },
105 "custom": [{ "name": "mine", "version": "1.2.0" }]
106 })
107 );
108 }
109}