Skip to main content

riff_core/
selector.rs

1//! Selectors: the address of a post (R51).
2//!
3//! # Rules
4//!
5//! - A post has a `to` list of selectors. Text in the body never wakes
6//!   a session.
7//! - A selector names one or more fields. A field that it leaves out
8//!   matches each session.
9//! - A session matches a selector when each named field matches. A post
10//!   wakes each session that matches one or more selectors.
11//!
12//! | Field | Matches |
13//! |---|---|
14//! | `user` | The user. |
15//! | `session` | The session ID. |
16//! | `host` | The host. |
17//! | `repo` | `OWNER/REPO`, or `-` outside git. |
18//! | `worktree` | The worktree. The main worktree has none. |
19//! | `claim` | One of the claims that the session holds. |
20//! | `lead` | `true`: the lead of its user in its repository. `false`: each other session. |
21//! | `person` | `true`: the terminals of a person (`riff chat`, `riff tail`, the phone), which have no session ID. `false`: each agent session. |
22//!
23//! On the command line, a selector is `FIELD=VALUE` pairs with commas
24//! between them.
25//!
26//! # A selector of a later build (01M3XSF90E9JYYTC13D9THY4WE)
27//!
28//! The set of fields can grow, and so can the form of a selector. A
29//! selector that this build does not know is a selector `other`: an
30//! object with a field or a value that the build does not know, and
31//! each other form of JSON (a text, a list, a number, `null`). It keeps
32//! its JSON as it came ([`Selector::other`]), and writes it again. So a
33//! reader of a later build gets the selector as it was, and the check
34//! of a signed message covers it. A selector `other` matches no
35//! session, because a field that the build cannot check never makes a
36//! selector wider. A `post` call with such a selector is refused.
37//!
38//! ```
39//! use riff_core::name::SessionUri;
40//! use riff_core::selector::Selector;
41//!
42//! let uri: SessionUri = "riff://mike@pangolin/como-technologies/riff?session=a6cf".parse()?;
43//! for json in [r#"{"user":"mike","wave":"17"}"#, r#"{"user":null,"wave":"17"}"#, r#""all""#, "null"] {
44//!     let later: Selector = serde_json::from_str(json).unwrap();
45//!     assert!(later.is_other() && !later.matches(&uri), "{json}");
46//!     assert_eq!(later.to_string(), json);
47//!     assert_eq!(serde_json::to_string(&later).unwrap(), json);
48//! }
49//! // The same selector with only the field that the build knows.
50//! assert!("user=mike".parse::<Selector>()?.matches(&uri));
51//! # Ok::<(), riff_core::name::NameError>(())
52//! ```
53//!
54//! # Example
55//!
56//! ```
57//! use riff_core::name::SessionUri;
58//! use riff_core::selector::Selector;
59//!
60//! let uri: SessionUri =
61//!     "riff://mike@pangolin/como-technologies/riff?session=a6cf&claim=issue-6#api".parse()?;
62//!
63//! let mikes_on_pangolin: Selector = "user=mike,host=pangolin".parse()?;
64//! assert!(mikes_on_pangolin.matches(&uri));
65//!
66//! let holder: Selector = "claim=issue-6".parse()?;
67//! assert!(holder.matches(&uri));
68//!
69//! let brett: Selector = "user=brett".parse()?;
70//! assert!(!brett.matches(&uri));
71//!
72//! let mikes_lead: Selector = "user=mike,repo=como-technologies/riff,lead=true".parse()?;
73//! assert!(!mikes_lead.matches(&uri));
74//! assert!(mikes_lead.matches(&uri.with_lead(true)));
75//! # Ok::<(), riff_core::name::NameError>(())
76//! ```
77
78use std::fmt;
79use std::str::FromStr;
80
81use schemars::JsonSchema;
82use serde::{Deserialize, Deserializer, Serialize, Serializer};
83
84use crate::name::{NameError, SessionUri};
85
86/// Picks sessions by who they are, where they work, and what they hold.
87/// Each field that is set must match. Set one or more fields.
88///
89/// The derive of serde is `remote = "Self"`: it gives the read and the
90/// write of the fields of this build as functions, and the impls below
91/// add the selector `other`. So the tolerant reader holds this struct
92/// itself, not a second copy of its fields (01M49W17GGV1K5FZEKJRPV0GNA).
93#[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    /// The user, for example `mike`.
98    #[serde(default, skip_serializing_if = "Option::is_none")]
99    pub user: Option<String>,
100    /// The session ID from the `session` part of a session URI.
101    #[serde(default, skip_serializing_if = "Option::is_none")]
102    pub session: Option<String>,
103    /// The host, for example `pangolin`.
104    #[serde(default, skip_serializing_if = "Option::is_none")]
105    pub host: Option<String>,
106    /// The repository as `OWNER/REPO`, for example `como-technologies/riff`.
107    #[serde(default, skip_serializing_if = "Option::is_none")]
108    pub repo: Option<String>,
109    /// The worktree, for example `issue-6`.
110    #[serde(default, skip_serializing_if = "Option::is_none")]
111    pub worktree: Option<String>,
112    /// A claimed work item, for example `issue-6`.
113    #[serde(default, skip_serializing_if = "Option::is_none")]
114    pub claim: Option<String>,
115    /// True picks the lead of its user in its repository. Use it with
116    /// `user` and `repo`.
117    #[serde(default, skip_serializing_if = "Option::is_none")]
118    pub lead: Option<bool>,
119    /// True picks the terminals of a person: they have no session ID.
120    /// False picks the agent sessions. Use `user=USER,person=true` to
121    /// reach a person and no agent session of that person.
122    #[serde(default, skip_serializing_if = "Option::is_none")]
123    pub person: Option<bool>,
124    /// The JSON of a selector that this build does not know, as it
125    /// came. Each field above is then empty, and the selector matches
126    /// no session.
127    #[serde(skip)]
128    #[schemars(skip)]
129    pub other: Option<serde_json::Value>,
130}
131
132/// A selector `other` writes its JSON as it came.
133impl 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
142/// A build reads each selector that it does not know as a selector
143/// `other`: an object with a field or a value that it does not know, and
144/// each other form of JSON (01M3XSF90E9JYYTC13D9THY4WE).
145impl<'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        // Only an object is a selector of this build: a list of values
149        // in the order of the fields is not.
150        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    /// True for a selector `other`: a selector that this build does not
163    /// know.
164    pub fn is_other(&self) -> bool {
165        self.other.is_some()
166    }
167
168    /// A selector for one session.
169    pub fn session(id: &str) -> Self {
170        Self {
171            session: Some(id.to_owned()),
172            ..Self::default()
173        }
174    }
175
176    /// A selector for the lead of `user` in the repository `repo`.
177    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    /// True when the selector names no field.
187    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    /// True when each named field matches the session. A selector
195    /// `other` matches no session.
196    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    /// The `FIELD=VALUE` pairs. A selector `other` shows its JSON.
228    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    /// Parses `FIELD=VALUE` pairs with commas between them.
247    ///
248    /// ```
249    /// use riff_core::selector::Selector;
250    ///
251    /// let s: Selector = "user=mike,claim=issue-6".parse()?;
252    /// assert_eq!(s.to_string(), "user=mike,claim=issue-6");
253    /// let lead: Selector = "user=mike,lead=true".parse()?;
254    /// assert_eq!(lead.lead, Some(true));
255    /// let person: Selector = "user=mike,person=true".parse()?;
256    /// assert_eq!(person.person, Some(true));
257    /// assert!("lead=yes".parse::<Selector>().is_err());
258    /// assert!("person=yes".parse::<Selector>().is_err());
259    /// assert!("".parse::<Selector>().is_err());
260    /// assert!("colour=blue".parse::<Selector>().is_err());
261    /// # Ok::<(), riff_core::name::NameError>(())
262    /// ```
263    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        // The selector of a later build, and the same selector with
404        // only the fields that this build knows.
405        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            // A value of a field that the build does not know.
416            (r#"{"lead":"maybe","user":"mike"}"#, r#"{"user":"mike"}"#),
417            (r#"{"user":["mike","brett"]}"#, "{}"),
418            // Each other form of JSON.
419            (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            // The selector is written again as it came.
429            assert_eq!(serde_json::to_string(&later).unwrap(), json);
430            assert_eq!(later.to_string(), json);
431            // Two selectors of a later build are the same only when
432            // their JSON is the same.
433            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            // The selector with only the known fields is wider.
437            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        // The command line still takes only the fields of this build.
442        assert!("user=mike,wave=17".parse::<Selector>().is_err());
443    }
444
445    /// A field that the build knows can be `null`: it is not set. An
446    /// agent tool sends such a selector.
447    #[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}