1use std::fmt;
79use std::str::FromStr;
80
81use schemars::JsonSchema;
82use serde::{Deserialize, Deserializer, Serialize, Serializer};
83
84use crate::name::{NameError, SessionUri};
85
86#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
94#[serde(remote = "Self", deny_unknown_fields)]
95#[schemars(deny_unknown_fields)]
96pub struct Selector {
97 #[serde(default, skip_serializing_if = "Option::is_none")]
99 pub user: Option<String>,
100 #[serde(default, skip_serializing_if = "Option::is_none")]
102 pub session: Option<String>,
103 #[serde(default, skip_serializing_if = "Option::is_none")]
105 pub host: Option<String>,
106 #[serde(default, skip_serializing_if = "Option::is_none")]
108 pub repo: Option<String>,
109 #[serde(default, skip_serializing_if = "Option::is_none")]
111 pub worktree: Option<String>,
112 #[serde(default, skip_serializing_if = "Option::is_none")]
114 pub claim: Option<String>,
115 #[serde(default, skip_serializing_if = "Option::is_none")]
118 pub lead: Option<bool>,
119 #[serde(default, skip_serializing_if = "Option::is_none")]
123 pub person: Option<bool>,
124 #[serde(skip)]
128 #[schemars(skip)]
129 pub other: Option<serde_json::Value>,
130}
131
132impl Serialize for Selector {
134 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
135 match &self.other {
136 Some(other) => other.serialize(serializer),
137 None => Selector::serialize(self, serializer),
138 }
139 }
140}
141
142impl<'de> Deserialize<'de> for Selector {
146 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
147 let value = serde_json::Value::deserialize(deserializer)?;
148 let known = value
151 .is_object()
152 .then(|| Selector::deserialize(&value).ok())
153 .flatten();
154 Ok(known.unwrap_or_else(|| Selector {
155 other: Some(value),
156 ..Selector::default()
157 }))
158 }
159}
160
161impl Selector {
162 pub fn is_other(&self) -> bool {
165 self.other.is_some()
166 }
167
168 pub fn session(id: &str) -> Self {
170 Self {
171 session: Some(id.to_owned()),
172 ..Self::default()
173 }
174 }
175
176 pub fn lead(user: &str, repo: &str) -> Self {
178 Self {
179 user: Some(user.to_owned()),
180 repo: Some(repo.to_owned()),
181 lead: Some(true),
182 ..Self::default()
183 }
184 }
185
186 pub fn is_empty(&self) -> bool {
188 self.lead.is_none()
189 && self.person.is_none()
190 && !self.is_other()
191 && self.fields().iter().all(|(_, v)| v.is_none())
192 }
193
194 pub fn matches(&self, uri: &SessionUri) -> bool {
197 let place = uri.place();
198 let who = uri.who();
199 let eq = |want: &Option<String>, have: Option<&str>| {
200 want.as_deref().is_none_or(|w| Some(w) == have)
201 };
202 !self.is_empty()
203 && !self.is_other()
204 && eq(&self.user, Some(who.user()))
205 && eq(&self.session, who.session())
206 && eq(&self.host, Some(place.host()))
207 && eq(&self.repo, Some(&place.repo_text()))
208 && eq(&self.worktree, place.worktree())
209 && self.claim.as_ref().is_none_or(|c| uri.claims().contains(c))
210 && self.lead.is_none_or(|l| l == uri.lead())
211 && self.person.is_none_or(|p| p == who.session().is_none())
212 }
213
214 fn fields(&self) -> [(&'static str, &Option<String>); 6] {
215 [
216 ("user", &self.user),
217 ("session", &self.session),
218 ("host", &self.host),
219 ("repo", &self.repo),
220 ("worktree", &self.worktree),
221 ("claim", &self.claim),
222 ]
223 }
224}
225
226impl fmt::Display for Selector {
227 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
229 if let Some(other) = &self.other {
230 return write!(f, "{other}");
231 }
232 let pairs: Vec<String> = self
233 .fields()
234 .iter()
235 .filter_map(|(k, v)| v.as_ref().map(|v| format!("{k}={v}")))
236 .chain(self.lead.map(|l| format!("lead={l}")))
237 .chain(self.person.map(|p| format!("person={p}")))
238 .collect();
239 f.write_str(&pairs.join(","))
240 }
241}
242
243impl FromStr for Selector {
244 type Err = NameError;
245
246 fn from_str(s: &str) -> Result<Self, Self::Err> {
264 let mut out = Self::default();
265 for pair in s.split(',').filter(|p| !p.is_empty()) {
266 let (key, value) = pair
267 .split_once('=')
268 .filter(|(_, v)| !v.is_empty())
269 .ok_or_else(|| NameError::new(format!("not FIELD=VALUE: {pair}")))?;
270 if key == "lead" || key == "person" {
271 let flag = value
272 .parse()
273 .map_err(|_| NameError::new(format!("{key} is true or false, not {value}")))?;
274 if key == "lead" {
275 out.lead = Some(flag);
276 } else {
277 out.person = Some(flag);
278 }
279 continue;
280 }
281 let field = match key {
282 "user" => &mut out.user,
283 "session" => &mut out.session,
284 "host" => &mut out.host,
285 "repo" => &mut out.repo,
286 "worktree" => &mut out.worktree,
287 "claim" => &mut out.claim,
288 _ => {
289 return Err(NameError::new(format!(
290 "no field named {key}. Use user, session, host, repo, worktree, claim, lead or person."
291 )));
292 }
293 };
294 *field = Some(value.to_owned());
295 }
296 if out.is_empty() {
297 return Err(NameError::new("a selector needs one or more fields"));
298 }
299 Ok(out)
300 }
301}
302
303#[cfg(test)]
304mod tests {
305 use super::*;
306
307 fn uri(text: &str) -> SessionUri {
308 text.parse().unwrap()
309 }
310
311 fn sel(text: &str) -> Selector {
312 text.parse().unwrap()
313 }
314
315 #[test]
316 fn each_field_selects_on_its_own() {
317 let u = uri("riff://mike@pangolin/como-technologies/riff?session=a6cf&claim=issue-6#api");
318 for s in [
319 "user=mike",
320 "session=a6cf",
321 "host=pangolin",
322 "repo=como-technologies/riff",
323 "worktree=api",
324 "claim=issue-6",
325 ] {
326 assert!(sel(s).matches(&u), "{s}");
327 }
328 for s in [
329 "user=brett",
330 "session=77e0",
331 "host=heron",
332 "repo=como-technologies/other",
333 "worktree=docs",
334 "claim=issue-7",
335 ] {
336 assert!(!sel(s).matches(&u), "{s}");
337 }
338 }
339
340 #[test]
341 fn lead_selects_the_lead_or_the_others() {
342 let u = uri("riff://mike@pangolin/como-technologies/riff?session=a6cf");
343 let lead = u.clone().with_lead(true);
344 assert!(sel("lead=true").matches(&lead));
345 assert!(!sel("lead=true").matches(&u));
346 assert!(sel("lead=false").matches(&u));
347 assert!(!sel("lead=false").matches(&lead));
348 let s = Selector::lead("mike", "como-technologies/riff");
349 assert_eq!(
350 s.to_string(),
351 "user=mike,repo=como-technologies/riff,lead=true"
352 );
353 assert!(s.matches(&lead));
354 assert_eq!(
355 serde_json::to_string(&s).unwrap(),
356 r#"{"user":"mike","repo":"como-technologies/riff","lead":true}"#
357 );
358 }
359
360 #[test]
361 fn person_selects_the_terminals_or_the_agent_sessions() {
362 let agent = uri("riff://mike@pangolin/como-technologies/riff?session=a6cf");
363 let person = uri("riff://mike@pangolin");
364 let to_person = sel("user=mike,host=pangolin,person=true");
365 assert!(to_person.matches(&person));
366 assert!(!to_person.matches(&agent));
367 assert!(!to_person.matches(&agent.clone().with_lead(true)));
368 assert!(sel("person=false").matches(&agent));
369 assert!(!sel("person=false").matches(&person));
370 assert_eq!(to_person.to_string(), "user=mike,host=pangolin,person=true");
371 }
372
373 #[test]
374 fn all_named_fields_must_match() {
375 let u = uri("riff://mike@pangolin/como-technologies/riff?session=a6cf");
376 assert!(sel("user=mike,host=pangolin").matches(&u));
377 assert!(!sel("user=mike,host=heron").matches(&u));
378 }
379
380 #[test]
381 fn the_main_worktree_matches_no_worktree_selector() {
382 let u = uri("riff://mike@pangolin/como-technologies/riff?session=a6cf");
383 assert!(!sel("worktree=api").matches(&u));
384 }
385
386 #[test]
387 fn an_empty_selector_matches_nothing() {
388 let u = uri("riff://mike@pangolin/como-technologies/riff?session=a6cf");
389 assert!(!Selector::default().matches(&u));
390 }
391
392 #[test]
393 fn json_leaves_out_unset_fields() {
394 let s = Selector::session("a6cf");
395 assert_eq!(serde_json::to_string(&s).unwrap(), r#"{"session":"a6cf"}"#);
396 assert!(!s.is_other());
397 }
398
399 #[test]
400 fn a_selector_of_a_later_build_matches_no_session() {
401 let u = uri("riff://mike@pangolin/como-technologies/riff?session=a6cf&claim=issue-6#api")
402 .with_lead(true);
403 for (json, known) in [
406 (r#"{"colour":"blue"}"#, "{}"),
407 (r#"{"user":"mike","wave":"17"}"#, r#"{"user":"mike"}"#),
408 (r#"{"session":"a6cf","wave":17}"#, r#"{"session":"a6cf"}"#),
409 (r#"{"lead":true,"wave":{"n":17}}"#, r#"{"lead":true}"#),
410 (
411 r#"{"claim":"issue-6","wave":null}"#,
412 r#"{"claim":"issue-6"}"#,
413 ),
414 (r#"{"user":null,"wave":"17"}"#, "{}"),
415 (r#"{"lead":"maybe","user":"mike"}"#, r#"{"user":"mike"}"#),
417 (r#"{"user":["mike","brett"]}"#, "{}"),
418 (r#""all""#, "{}"),
420 (r#"["user","mike"]"#, "{}"),
421 ("17", "{}"),
422 ("true", "{}"),
423 ("null", "{}"),
424 ] {
425 let later: Selector = serde_json::from_str(json).unwrap();
426 assert!(later.is_other() && !later.is_empty(), "{json}");
427 assert!(!later.matches(&u), "{json}");
428 assert_eq!(serde_json::to_string(&later).unwrap(), json);
430 assert_eq!(later.to_string(), json);
431 let same: Selector = serde_json::from_str(json).unwrap();
434 assert_eq!(later, same);
435 assert_ne!(later, serde_json::from_str(r#"{"wave":"18"}"#).unwrap());
436 let known: Selector = serde_json::from_str(known).unwrap();
438 assert!(!known.is_other(), "{json}");
439 assert!(known.is_empty() || known.matches(&u), "{json}");
440 }
441 assert!("user=mike,wave=17".parse::<Selector>().is_err());
443 }
444
445 #[test]
448 fn a_null_of_a_field_that_the_build_knows_is_a_field_that_is_not_set() {
449 let s: Selector = serde_json::from_str(r#"{"user":"mike","session":null}"#).unwrap();
450 assert_eq!(s, sel("user=mike"));
451 assert!(!s.is_other());
452 assert_eq!(serde_json::to_string(&s).unwrap(), r#"{"user":"mike"}"#);
453 }
454
455 #[test]
456 fn the_schema_of_a_selector_has_only_the_fields_of_this_build() {
457 let schema = serde_json::to_value(schemars::schema_for!(Selector)).unwrap();
458 assert_eq!(schema["additionalProperties"], false);
459 let mut fields: Vec<&String> = schema["properties"].as_object().unwrap().keys().collect();
460 fields.sort();
461 assert_eq!(
462 fields,
463 [
464 "claim", "host", "lead", "person", "repo", "session", "user", "worktree"
465 ]
466 );
467 }
468}