Skip to main content

shep_core/
dogs.rs

1//! The probe contract between shep and a dog: the flag names, the
2//! `shep-protocol:` line's grammar, and the schema's secret marker key.
3//!
4//! Shared by `shep-cli`'s `adopt` (the asker) and `shep_client::dogs::probe`
5//! (the answerer), so the two agree by construction rather than by copying
6//! a doc snippet.
7
8/// The flag a candidate is spawned with when shep asks for its version; the
9/// contract `docs/dogs.md` publishes. Read by `shep-cli`'s `adopt`, answered
10/// by `shep_client::dogs::probe`.
11pub const VERSION_FLAG: &str = "--version";
12
13/// The flag a candidate is spawned with when shep asks for its config
14/// schema. Asked by `shep-cli`'s `adopt` on the same terms as
15/// [`VERSION_FLAG`]: a dog that answers nothing is refused nothing.
16pub const SCHEMA_FLAG: &str = "--schema";
17
18/// The one key [`parse_version_answer`] reads in a `--version` answer.
19/// Every other `shep-` key is reserved for a number this shep has not
20/// heard of, and is ignored rather than refused, so a dog written against a
21/// later contract stays adoptable by this one.
22pub const SHEP_PROTOCOL_KEY: &str = "shep-protocol";
23
24/// The schemars extension key that marks a config field as a credential.
25/// Written by the `DogConfig` derive. A typo here fails silently: the schema
26/// still validates, the field is simply not marked, and a credential can
27/// render unredacted.
28pub const SECRET_KEY: &str = "x-shep-secret";
29
30/// What a dog answered [`VERSION_FLAG`] with, parsed by
31/// [`parse_version_answer`] from the format `docs/dogs.md` publishes.
32///
33/// `protocol` decides whether the dog can handshake at all; `version` only
34/// names the build. `protocol` is optional: an absent one reads as unknown,
35/// not a fault.
36#[derive(Debug, PartialEq, Eq)]
37pub struct DogVersion {
38    /// The last whitespace-separated field of line 1, the version. The
39    /// name before it is ignored, so a crate whose name differs from the
40    /// dog's registered name answers correctly without knowing it.
41    pub version: String,
42    /// The `shep-protocol` line's value, and `None` when the answer carried
43    /// no such line or carried one that is not a decimal number. Answering
44    /// is optional, so `None` is an unknown protocol rather than a fault.
45    pub protocol: Option<u32>,
46}
47
48/// Parses the format `docs/dogs.md` publishes: `<name> <version>` on line
49/// 1, then `<key>: <value>` lines.
50///
51/// `None` when there is no line 1. Unknown keys, blank lines, key order and
52/// a non-numeric `shep-protocol` are all tolerated rather than refused;
53/// only an exact [`SHEP_PROTOCOL_KEY`] carrying a decimal is believed.
54#[must_use]
55pub fn parse_version_answer(text: &str) -> Option<DogVersion> {
56    let mut lines = text.lines();
57    let version = lines.next()?.split_whitespace().next_back()?.to_string();
58    let mut protocol = None;
59    for line in lines {
60        if let Some((key, value)) = line.split_once(':')
61            && key.trim() == SHEP_PROTOCOL_KEY
62        {
63            protocol = value.trim().parse().ok();
64        }
65    }
66    Some(DogVersion { version, protocol })
67}
68
69#[cfg(test)]
70mod tests {
71    use super::*;
72
73    #[test]
74    fn no_output_is_no_answer() {
75        assert_eq!(parse_version_answer(""), None);
76    }
77
78    #[test]
79    fn a_bad_protocol_number_reads_as_unknown_not_a_fault() {
80        assert_eq!(
81            parse_version_answer("shep-otel 0.1.3\nshep-protocol: two\n"),
82            Some(DogVersion {
83                version: "0.1.3".to_string(),
84                protocol: None,
85            })
86        );
87    }
88}