1use anyhow::Result;
4
5use crate::jobstore::{JobDir, resolve_root};
6use crate::schema::{
7 NotificationConfig, NotifySetData, OutputMatchConfig, OutputMatchStream, OutputMatchType,
8 Response,
9};
10
11pub struct NotifySetOpts<'a> {
13 pub job_id: &'a str,
15 pub root: Option<&'a str>,
17 pub command: Option<String>,
19 pub output_pattern: Option<String>,
21 pub output_match_type: Option<String>,
23 pub output_stream: Option<String>,
25 pub output_command: Option<String>,
27 pub output_file: Option<String>,
29}
30
31pub fn set(opts: NotifySetOpts) -> Result<()> {
36 let root = resolve_root(opts.root);
37 let job_dir = JobDir::open(&root, opts.job_id)?;
38
39 let mut meta = job_dir.read_meta()?;
40
41 let existing_notify_command = meta
43 .notification
44 .as_ref()
45 .and_then(|n| n.notify_command.clone());
46 let existing_notify_file = meta
47 .notification
48 .as_ref()
49 .and_then(|n| n.notify_file.clone());
50 let existing_on_output_match = meta
51 .notification
52 .as_ref()
53 .and_then(|n| n.on_output_match.clone());
54
55 let new_notify_command = opts.command.or(existing_notify_command);
57
58 let new_on_output_match = build_output_match_config(
60 opts.output_pattern,
61 opts.output_match_type,
62 opts.output_stream,
63 opts.output_command,
64 opts.output_file,
65 existing_on_output_match,
66 );
67
68 let has_anything = new_notify_command.is_some()
70 || existing_notify_file.is_some()
71 || new_on_output_match.is_some();
72
73 meta.notification = if has_anything {
74 Some(NotificationConfig {
75 notify_command: new_notify_command,
76 notify_file: existing_notify_file,
77 on_output_match: new_on_output_match,
78 })
79 } else {
80 None
81 };
82
83 job_dir.write_meta_atomic(&meta)?;
84
85 let notification = meta.notification.unwrap_or(NotificationConfig {
86 notify_command: None,
87 notify_file: None,
88 on_output_match: None,
89 });
90 let response = Response::new(
91 "notify.set",
92 NotifySetData {
93 job_id: opts.job_id.to_string(),
94 notification,
95 },
96 );
97 response.print();
98 Ok(())
99}
100
101pub fn build_output_match_config(
109 output_pattern: Option<String>,
110 output_match_type: Option<String>,
111 output_stream: Option<String>,
112 output_command: Option<String>,
113 output_file: Option<String>,
114 existing: Option<OutputMatchConfig>,
115) -> Option<OutputMatchConfig> {
116 let has_new_input = output_pattern.is_some()
117 || output_match_type.is_some()
118 || output_stream.is_some()
119 || output_command.is_some()
120 || output_file.is_some();
121
122 if !has_new_input {
123 return existing;
124 }
125
126 let base = existing.unwrap_or_else(|| OutputMatchConfig {
128 pattern: String::new(),
129 match_type: OutputMatchType::default(),
130 stream: OutputMatchStream::default(),
131 command: None,
132 file: None,
133 });
134
135 let pattern = output_pattern.unwrap_or(base.pattern);
136
137 let match_type = match output_match_type.as_deref() {
138 Some("regex") => OutputMatchType::Regex,
139 Some("contains") => OutputMatchType::Contains,
140 _ => base.match_type,
141 };
142
143 let stream = match output_stream.as_deref() {
144 Some("stdout") => OutputMatchStream::Stdout,
145 Some("stderr") => OutputMatchStream::Stderr,
146 Some("either") => OutputMatchStream::Either,
147 _ => base.stream,
148 };
149
150 let command = output_command.or(base.command);
152 let file = output_file.or(base.file);
153
154 if pattern.is_empty() {
156 return None;
157 }
158
159 Some(OutputMatchConfig {
160 pattern,
161 match_type,
162 stream,
163 command,
164 file,
165 })
166}