Skip to main content

ephemeris_release/proposal/
metadata.rs

1//! Ownership marker embedded in the maintained proposal PR body.
2
3const PREFIX: &str = "<!-- ephemeris:proposal-head=";
4const SUFFIX: &str = " -->";
5
6/// Why a PR body has no usable marker.
7#[derive(Clone, Copy, Debug, Eq, PartialEq, thiserror::Error)]
8pub(crate) enum MarkerError {
9    /// The body has no marker line.
10    #[error("the pull request body has no marker")]
11    Missing,
12    /// The body has more than one marker line.
13    #[error("the pull request body has several markers")]
14    Several,
15    /// The marker value is not a 40 digit commit hash.
16    #[error("the marker value is not a 40 digit commit hash")]
17    Invalid,
18}
19
20/// Machine-readable ownership metadata embedded in the maintained PR body.
21#[derive(Clone, Debug, Eq, PartialEq)]
22pub(crate) struct ProposalMetadata {
23    /// Proposal branch head Ephemeris last pushed.
24    pub(crate) proposal_head: String,
25}
26
27impl ProposalMetadata {
28    /// Parse the unique Ephemeris marker from a PR body.
29    pub(crate) fn parse(body: &str) -> Result<Self, MarkerError> {
30        let mut markers = body
31            .lines()
32            .filter(|line| line.starts_with(PREFIX))
33            .filter_map(|line| line.strip_suffix(SUFFIX));
34        let marker = markers.next().ok_or(MarkerError::Missing)?;
35        if markers.next().is_some() {
36            return Err(MarkerError::Several);
37        }
38        let proposal_head = marker
39            .strip_prefix(PREFIX)
40            .filter(|head| valid_commit(head))
41            .ok_or(MarkerError::Invalid)?;
42        Ok(Self {
43            proposal_head: proposal_head.to_owned(),
44        })
45    }
46
47    /// Render the marker appended to a human-readable PR body.
48    #[must_use]
49    pub(crate) fn marker(&self) -> String {
50        format!("{PREFIX}{}{SUFFIX}", self.proposal_head)
51    }
52}
53
54fn valid_commit(value: &str) -> bool {
55    value.len() == 40 && value.bytes().all(|byte| byte.is_ascii_hexdigit())
56}
57
58#[cfg(test)]
59mod tests {
60    use super::{MarkerError, ProposalMetadata};
61
62    #[test]
63    fn proposal_metadata_requires_one_exact_commit_marker() {
64        let metadata = ProposalMetadata {
65            proposal_head: "0123456789abcdef0123456789abcdef01234567".into(),
66        };
67        let body = format!("Release proposal\n\n{}\n", metadata.marker());
68        assert_eq!(ProposalMetadata::parse(&body), Ok(metadata.clone()));
69        assert_eq!(
70            ProposalMetadata::parse("Release proposal\n"),
71            Err(MarkerError::Missing)
72        );
73        assert_eq!(
74            ProposalMetadata::parse(&format!("{}\n{}", metadata.marker(), metadata.marker())),
75            Err(MarkerError::Several)
76        );
77        assert_eq!(
78            ProposalMetadata::parse("<!-- ephemeris:proposal-head=short -->"),
79            Err(MarkerError::Invalid)
80        );
81    }
82
83    #[test]
84    fn the_two_field_marker_is_invalid() {
85        let body = "<!-- ephemeris:proposal-head=0123456789abcdef0123456789abcdef01234567 \
86                    authority-head=0123456789abcdef0123456789abcdef01234567 -->";
87        assert_eq!(ProposalMetadata::parse(body), Err(MarkerError::Invalid));
88    }
89}