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//! # Why this lives here and not in the crate that asks or the crate that answers
5//!
6//! `shep-cli`'s `adopt` spawns a candidate binary with [`VERSION_FLAG`] and
7//! [`SCHEMA_FLAG`] and parses what it prints; `shep-client` (the dog side)
8//! answers both from `shep_client::dogs::probe`. Before that call existed,
9//! both sides read a string a dog author hand-typed from a snippet in
10//! `docs/dogs.md`, so a typo read as "protocol unknown" and nothing said so.
11//! One definition, owned by the crate both already depend on, is what lets
12//! the asker and the answerer agree by construction instead of by copying a
13//! doc snippet correctly.
14//!
15//! The asker itself (spawning the binary, applying the timeout, deciding
16//! whether an unknown protocol refuses an adopt) stays in `shep-cli`,
17//! beside the rest of the vetting `adopt` already does. Only the shape of
18//! the question and the answer moves here.
19//!
20//! [`DogVersion`] moved with the parser rather than staying behind: its two
21//! fields (`version`, `protocol`) are plain data with no CLI-specific type
22//! in them, and it IS the grammar `parse_version_answer` returns, so
23//! splitting the struct from the function that builds it would put one
24//! definition of the answer's shape in one crate and the reader of that
25//! shape in another.
26
27/// The flag a candidate is spawned with when shep asks for its version, and
28/// the one `docs/dogs.md` publishes as the contract. Read by
29/// `shep-cli`'s `adopt`; answered, from release 2, by
30/// `shep_client::dogs::probe`.
31pub const VERSION_FLAG: &str = "--version";
32
33/// The flag a candidate is spawned with when shep asks for its config
34/// schema. Asked by `shep-cli`'s `adopt`, beside the version and on the
35/// same terms: a dog that answers nothing is refused nothing. Answered by
36/// `shep_client::dogs::probe`.
37pub const SCHEMA_FLAG: &str = "--schema";
38
39/// The one key [`parse_version_answer`] reads in a `--version` answer.
40/// Every other `shep-` key is reserved for a number this shep has not
41/// heard of, and is ignored rather than refused, so a dog written against a
42/// later contract stays adoptable by this one.
43pub const SHEP_PROTOCOL_KEY: &str = "shep-protocol";
44
45/// The schemars extension key that marks a config field as a credential.
46/// Written by the `DogConfig` derive, which exists so that no dog author
47/// ever types it; the reader in `shep lookout` that redacts a field
48/// carrying it arrives in a later task. Getting this string right matters
49/// more than the other three here, because a typo in it does not fail
50/// loudly: the schema still validates, the field is simply not marked, and
51/// a credential can end up rendered on screen.
52pub const SECRET_KEY: &str = "x-shep-secret";
53
54/// What a dog answered [`VERSION_FLAG`] with, parsed by
55/// [`parse_version_answer`] from the format `docs/dogs.md` publishes.
56///
57/// Two fields rather than one, because they answer different questions:
58/// `protocol` decides whether the dog can handshake at all, and `version`
59/// only says which build it is. A dog may give the second and not the
60/// first, which is why `protocol` is optional and an absent one reads as
61/// unknown rather than as a fault.
62#[derive(Debug, PartialEq, Eq)]
63pub struct DogVersion {
64 /// The last whitespace-separated field of line 1, the version. The
65 /// name before it is ignored, so a crate whose name differs from the
66 /// dog's registered name answers correctly without knowing it.
67 pub version: String,
68 /// The `shep-protocol` line's value, and `None` when the answer carried
69 /// no such line or carried one that is not a decimal number. Answering
70 /// is optional, so `None` is an unknown protocol rather than a fault.
71 pub protocol: Option<u32>,
72}
73
74/// Parses the format `docs/dogs.md` publishes: `<name> <version>` on line
75/// 1, then `<key>: <value>` lines.
76///
77/// `None` when there is no line 1 to read a version from. Everything past
78/// that is tolerated rather than refused: unknown keys, blank lines, key
79/// order, and a `shep-protocol` that is not a number, because a shep that
80/// refuses a dog over the shape of text the dog never promised to print is
81/// refusing on its own guess. The strictness is all in the other direction:
82/// only an exact [`SHEP_PROTOCOL_KEY`] carrying a decimal is believed, and
83/// only a believed protocol can refuse.
84#[must_use]
85pub fn parse_version_answer(text: &str) -> Option<DogVersion> {
86 let mut lines = text.lines();
87 let version = lines.next()?.split_whitespace().next_back()?.to_string();
88 let mut protocol = None;
89 for line in lines {
90 if let Some((key, value)) = line.split_once(':')
91 && key.trim() == SHEP_PROTOCOL_KEY
92 {
93 protocol = value.trim().parse().ok();
94 }
95 }
96 Some(DogVersion { version, protocol })
97}
98
99#[cfg(test)]
100mod tests {
101 use super::*;
102
103 #[test]
104 fn no_output_is_no_answer() {
105 assert_eq!(parse_version_answer(""), None);
106 }
107
108 #[test]
109 fn a_bad_protocol_number_reads_as_unknown_not_a_fault() {
110 assert_eq!(
111 parse_version_answer("shep-otel 0.1.3\nshep-protocol: two\n"),
112 Some(DogVersion {
113 version: "0.1.3".to_string(),
114 protocol: None,
115 })
116 );
117 }
118}