shep_core/config/app/
behavior.rs1use super::schema::AppConfig;
2use core::fmt;
3use std::collections::BTreeMap;
4use crate::values::UpDuration;
6
7impl fmt::Debug for AppConfig {
9 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
10 f.debug_struct("AppConfig")
11 .field("name", &self.name)
12 .field("script", &self.script)
13 .field("env", &format_args!("<{} vars>", self.env.len()))
14 .finish_non_exhaustive()
15 }
16}
17
18impl Default for AppConfig {
19 fn default() -> Self {
20 Self {
21 name: String::new(),
22 script: String::new(),
23 args: Vec::new(),
24 cwd: None,
25 interpreter: None,
26 env: BTreeMap::new(),
27 environment: None,
28 instances: 1,
29 autorestart: true,
30 autostart: true,
31 stop_exit_codes: Vec::new(),
32 min_uptime: UpDuration::from_millis(1000),
33 max_restarts: 16,
34 restart_delay: None,
35 exp_backoff_restart_delay: Some(UpDuration::from_millis(100)),
38 kill_signal: None,
39 kill_timeout: UpDuration::from_millis(1600),
40 shutdown_with_message: false,
41 listen_timeout: UpDuration::from_millis(3000),
42 graceful_timeout: UpDuration::from_millis(8000),
43 action_timeout: UpDuration::from_millis(3000),
44 max_memory: None,
45 watch: false,
46 ignore_watch: Vec::new(),
47 watch_delay: None,
48 cron_restart: None,
49 fold: None,
50 depends_on: Vec::new(),
51 user: None,
52 group: None,
53 out_file: None,
54 err_file: None,
55 merge_logs: false,
56 level_rules: Vec::new(),
57 channel: false,
58 stdin: false,
59 wait_ready: false,
60 reuse_port: false,
61 readiness_probe: None,
62 liveness_probe: None,
63 watch_options: Vec::new(),
64 cron_timezone: None,
65 }
66 }
67}
68
69impl AppConfig {
70 #[must_use]
72 pub fn minimal(name: &str, script: &str) -> Self {
73 Self {
74 name: name.to_string(),
75 script: script.to_string(),
76 ..Self::default()
77 }
78 }
79
80 #[must_use]
106 pub fn drifted_fields(&self, other: &Self) -> Vec<String> {
107 if self == other {
108 return Vec::new();
109 }
110 let (Ok(serde_json::Value::Object(mine)), Ok(serde_json::Value::Object(theirs))) =
115 (serde_json::to_value(self), serde_json::to_value(other))
116 else {
117 return Vec::new();
118 };
119 let mut fields: Vec<String> = mine
120 .iter()
121 .filter(|(key, value)| theirs.get(key.as_str()) != Some(value))
122 .map(|(key, _)| key.clone())
123 .collect();
124 fields.sort_unstable();
125 fields
126 }
127}
128
129#[cfg(test)]
130mod tests {
131 use super::super::schema::AppConfig;
132
133 #[test]
136 fn an_unedited_config_has_drifted_in_no_field() {
137 let app = AppConfig::minimal("web", "./srv");
138
139 assert!(app.drifted_fields(&app.clone()).is_empty());
140 }
141
142 #[test]
143 fn drift_names_every_edited_field_and_no_other() {
144 let stored = AppConfig::minimal("proto-api", "./proto-enum-api");
147 let mut edited = stored.clone();
148 edited.cwd = Some("/srv/pogo-proto-api".to_string());
149 edited.args = vec!["-config".to_string(), "config.toml".to_string()];
150
151 assert_eq!(
152 stored.drifted_fields(&edited),
153 vec!["args".to_string(), "cwd".to_string()]
154 );
155 }
156
157 #[test]
158 fn drift_reports_env_by_name_and_never_by_value() {
159 let stored = AppConfig::minimal("web", "./srv");
160 let mut edited = stored.clone();
161 edited
162 .env
163 .insert("DATABASE_URL".to_string(), "postgres://hunter2".to_string());
164
165 let fields = edited.drifted_fields(&stored);
166
167 assert_eq!(fields, vec!["env".to_string()]);
168 assert!(!fields.concat().contains("hunter2"));
170 }
171
172 #[test]
173 fn drift_is_symmetric() {
174 let stored = AppConfig::minimal("web", "./srv");
175 let mut edited = stored.clone();
176 edited.instances = 4;
177
178 assert_eq!(
179 stored.drifted_fields(&edited),
180 edited.drifted_fields(&stored)
181 );
182 assert_eq!(
183 stored.drifted_fields(&edited),
184 vec!["instances".to_string()]
185 );
186 }
187}