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 log_timestamps: true,
57 level_rules: Vec::new(),
58 dogs: BTreeMap::new(),
59 channel: false,
60 stdin: false,
61 wait_ready: false,
62 reuse_port: false,
63 readiness_probe: None,
64 liveness_probe: None,
65 watch_options: Vec::new(),
66 cron_timezone: None,
67 }
68 }
69}
70
71impl AppConfig {
72 #[must_use]
74 pub fn minimal(name: &str, script: &str) -> Self {
75 Self {
76 name: name.to_string(),
77 script: script.to_string(),
78 ..Self::default()
79 }
80 }
81
82 #[must_use]
108 pub fn drifted_fields(&self, other: &Self) -> Vec<String> {
109 if self == other {
110 return Vec::new();
111 }
112 let (Ok(serde_json::Value::Object(mine)), Ok(serde_json::Value::Object(theirs))) =
117 (serde_json::to_value(self), serde_json::to_value(other))
118 else {
119 return Vec::new();
120 };
121 let mut fields: Vec<String> = mine
122 .iter()
123 .filter(|(key, value)| theirs.get(key.as_str()) != Some(value))
124 .map(|(key, _)| key.clone())
125 .collect();
126 fields.sort_unstable();
127 fields
128 }
129}
130
131#[cfg(test)]
132mod tests {
133 use super::super::schema::AppConfig;
134 use crate::config::{FlockFormat, Flockfile};
135
136 #[test]
139 fn log_lines_are_stamped_unless_a_flockfile_says_false() {
140 let parse = |extra: &str| {
141 let src = format!("[[app]]\nname = \"web\"\nscript = \"./srv\"\n{extra}");
142 Flockfile::parse(&src, FlockFormat::Toml)
143 .expect("parses")
144 .apps[0]
145 .log_timestamps
146 };
147
148 assert!(AppConfig::minimal("web", "./srv").log_timestamps);
149 assert!(parse(""));
150 assert!(parse("log_timestamps = true"));
151 assert!(!parse("log_timestamps = false"));
152 }
153
154 #[test]
155 fn an_unedited_config_has_drifted_in_no_field() {
156 let app = AppConfig::minimal("web", "./srv");
157
158 assert!(app.drifted_fields(&app.clone()).is_empty());
159 }
160
161 #[test]
162 fn drift_names_every_edited_field_and_no_other() {
163 let stored = AppConfig::minimal("proto-api", "./proto-enum-api");
166 let mut edited = stored.clone();
167 edited.cwd = Some("/srv/pogo-proto-api".to_string());
168 edited.args = vec!["-config".to_string(), "config.toml".to_string()];
169
170 assert_eq!(
171 stored.drifted_fields(&edited),
172 vec!["args".to_string(), "cwd".to_string()]
173 );
174 }
175
176 #[test]
177 fn drift_reports_env_by_name_and_never_by_value() {
178 let stored = AppConfig::minimal("web", "./srv");
179 let mut edited = stored.clone();
180 edited
181 .env
182 .insert("DATABASE_URL".to_string(), "postgres://hunter2".to_string());
183
184 let fields = edited.drifted_fields(&stored);
185
186 assert_eq!(fields, vec!["env".to_string()]);
187 assert!(!fields.concat().contains("hunter2"));
189 }
190
191 #[test]
192 fn drift_is_symmetric() {
193 let stored = AppConfig::minimal("web", "./srv");
194 let mut edited = stored.clone();
195 edited.instances = 4;
196
197 assert_eq!(
198 stored.drifted_fields(&edited),
199 edited.drifted_fields(&stored)
200 );
201 assert_eq!(
202 stored.drifted_fields(&edited),
203 vec!["instances".to_string()]
204 );
205 }
206}