Skip to main content

verbs/
prove_plan.rs

1// SPDX-License-Identifier: Apache-2.0
2//! Pure `heddle prove` planning helpers (no hosted network / FS I/O).
3//!
4//! Status labels and host/repo validation are pure so the CLI can keep
5//! protobuf transport, file writes, and recovery advice locally.
6
7/// Identity-proof status kinds aligned with hosted `ProofStatus` wire values
8/// (0 unspecified, 1 pending, 2 verified, 3 failed) without generated API types.
9#[derive(Debug, Clone, Copy, PartialEq, Eq)]
10pub enum ProofStatusKind {
11    Unspecified,
12    Pending,
13    Verified,
14    Failed,
15}
16
17impl ProofStatusKind {
18    /// Map a raw proto `i32` status to a kind (unknown values → unspecified).
19    pub fn from_i32(status: i32) -> Self {
20        match status {
21            1 => Self::Pending,
22            2 => Self::Verified,
23            3 => Self::Failed,
24            _ => Self::Unspecified,
25        }
26    }
27
28    /// Stable human/machine status token.
29    pub fn label(self) -> &'static str {
30        match self {
31            Self::Verified => "verified",
32            Self::Pending => "pending",
33            Self::Failed => "failed",
34            Self::Unspecified => "unspecified",
35        }
36    }
37}
38
39/// Status label string for a proof status kind.
40pub fn proof_status_label(kind: ProofStatusKind) -> &'static str {
41    kind.label()
42}
43
44/// Failure when start-form host/repo positionals are incomplete.
45#[derive(Debug, Clone, Copy, PartialEq, Eq)]
46pub enum HostRepoPlanError {
47    /// Host and/or repo missing when no subcommand is present.
48    MissingHostOrRepo,
49}
50
51impl HostRepoPlanError {
52    /// Stable CLI error message (matches historical prove start-form copy).
53    pub fn message(self) -> &'static str {
54        match self {
55            Self::MissingHostOrRepo => {
56                "a host and repo are required (e.g. `heddle prove github.com owner/repo`); \
57                 for other actions use `heddle prove submit <challenge_id>` or `heddle prove list`"
58            }
59        }
60    }
61}
62
63impl std::fmt::Display for HostRepoPlanError {
64    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
65        f.write_str(self.message())
66    }
67}
68
69impl std::error::Error for HostRepoPlanError {}
70
71/// Validate start-form positionals: both `host` and `repo` are required when
72/// no subcommand is given.
73pub fn require_host_repo<'a>(
74    host: Option<&'a str>,
75    repo: Option<&'a str>,
76) -> Result<(&'a str, &'a str), HostRepoPlanError> {
77    match (host, repo) {
78        (Some(h), Some(r)) => Ok((h, r)),
79        _ => Err(HostRepoPlanError::MissingHostOrRepo),
80    }
81}
82
83/// Non-negative seconds from an optional protobuf-style `seconds` field.
84pub fn timestamp_secs_u64(seconds: Option<i64>) -> u64 {
85    seconds.map(|s| s.max(0) as u64).unwrap_or(0)
86}
87
88/// RFC3339 (or raw seconds) label for verified_at display; empty when zero.
89pub fn format_unix_secs_label(secs: u64) -> String {
90    if secs == 0 {
91        return String::new();
92    }
93    chrono::DateTime::from_timestamp(secs as i64, 0)
94        .map(|d| d.to_rfc3339())
95        .unwrap_or_else(|| secs.to_string())
96}
97
98/// Optional follow-up line after `prove submit` based on status.
99pub fn proof_submit_followup(kind: ProofStatusKind, challenge_id: &str) -> Option<String> {
100    match kind {
101        ProofStatusKind::Verified => Some("Your control of the repo is verified.".to_string()),
102        ProofStatusKind::Pending => Some(format!(
103            "The marker was not found yet. Push the file, then retry: heddle prove submit {challenge_id}"
104        )),
105        ProofStatusKind::Failed => Some(format!(
106            "Verification failed. Check the marker line + path, then retry: heddle prove submit {challenge_id}"
107        )),
108        ProofStatusKind::Unspecified => None,
109    }
110}
111
112#[cfg(test)]
113mod tests {
114    use super::*;
115
116    #[test]
117    fn status_label_covers_every_variant() {
118        assert_eq!(proof_status_label(ProofStatusKind::Verified), "verified");
119        assert_eq!(proof_status_label(ProofStatusKind::Pending), "pending");
120        assert_eq!(proof_status_label(ProofStatusKind::Failed), "failed");
121        assert_eq!(
122            proof_status_label(ProofStatusKind::Unspecified),
123            "unspecified"
124        );
125        assert_eq!(ProofStatusKind::from_i32(0), ProofStatusKind::Unspecified);
126        assert_eq!(ProofStatusKind::from_i32(1), ProofStatusKind::Pending);
127        assert_eq!(ProofStatusKind::from_i32(2), ProofStatusKind::Verified);
128        assert_eq!(ProofStatusKind::from_i32(3), ProofStatusKind::Failed);
129        assert_eq!(ProofStatusKind::from_i32(99), ProofStatusKind::Unspecified);
130    }
131
132    #[test]
133    fn host_repo_guard() {
134        assert_eq!(
135            require_host_repo(Some("github.com"), Some("owner/repo")).unwrap(),
136            ("github.com", "owner/repo")
137        );
138        assert_eq!(
139            require_host_repo(None, Some("owner/repo")),
140            Err(HostRepoPlanError::MissingHostOrRepo)
141        );
142        assert_eq!(
143            require_host_repo(Some("github.com"), None),
144            Err(HostRepoPlanError::MissingHostOrRepo)
145        );
146        assert!(
147            HostRepoPlanError::MissingHostOrRepo
148                .message()
149                .contains("host and repo")
150        );
151    }
152
153    #[test]
154    fn timestamp_and_submit_followup() {
155        assert_eq!(timestamp_secs_u64(None), 0);
156        assert_eq!(timestamp_secs_u64(Some(-1)), 0);
157        assert_eq!(format_unix_secs_label(0), "");
158        assert!(!format_unix_secs_label(1_700_000_000).is_empty());
159
160        assert_eq!(
161            proof_submit_followup(ProofStatusKind::Verified, "c1").as_deref(),
162            Some("Your control of the repo is verified.")
163        );
164        assert!(
165            proof_submit_followup(ProofStatusKind::Pending, "c1")
166                .unwrap()
167                .contains("heddle prove submit c1")
168        );
169        assert!(
170            proof_submit_followup(ProofStatusKind::Failed, "c2")
171                .unwrap()
172                .contains("heddle prove submit c2")
173        );
174        assert_eq!(
175            proof_submit_followup(ProofStatusKind::Unspecified, "c"),
176            None
177        );
178    }
179}