1use std::fmt;
4use std::str::FromStr;
5
6use crate::segment::path_segment;
7
8#[derive(Clone, Debug, Eq, PartialEq)]
13pub struct RepoRef {
14 owner: String,
15 name: String,
16}
17
18impl RepoRef {
19 #[must_use]
21 pub fn owner(&self) -> &str {
22 &self.owner
23 }
24
25 #[must_use]
27 pub fn name(&self) -> &str {
28 &self.name
29 }
30
31 #[must_use]
35 pub fn path(&self, rest: &str) -> String {
36 let base = format!(
37 "repos/{}/{}",
38 path_segment(&self.owner),
39 path_segment(&self.name)
40 );
41 if rest.is_empty() {
42 base
43 } else {
44 format!("{base}/{rest}")
45 }
46 }
47}
48
49impl fmt::Display for RepoRef {
50 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
51 write!(f, "{}/{}", self.owner, self.name)
52 }
53}
54
55#[derive(Clone, Debug, Eq, PartialEq, thiserror::Error)]
57#[error("expected <owner>/<name>, got {input:?}")]
58pub struct RepoRefParseError {
59 pub input: String,
61}
62
63impl FromStr for RepoRef {
64 type Err = RepoRefParseError;
65
66 fn from_str(raw: &str) -> Result<Self, Self::Err> {
67 let mut parts = raw.split('/');
68 let owner = parts.next().unwrap_or_default();
69 let name = parts.next().unwrap_or_default();
70 if owner.is_empty() || name.is_empty() || parts.next().is_some() {
71 return Err(RepoRefParseError {
72 input: raw.to_owned(),
73 });
74 }
75 Ok(Self {
76 owner: owner.to_owned(),
77 name: name.to_owned(),
78 })
79 }
80}
81
82#[cfg(test)]
83mod tests {
84 use std::str::FromStr as _;
85
86 use super::RepoRef;
87
88 #[test]
89 fn a_well_formed_pair_parses() {
90 let repo = RepoRef::from_str("inomotech/finmqtt").expect("parses");
91 assert_eq!(repo.owner(), "inomotech");
92 assert_eq!(repo.name(), "finmqtt");
93 assert_eq!(repo.to_string(), "inomotech/finmqtt");
94 }
95
96 #[test]
97 fn the_path_encodes_both_segments() {
98 let repo = RepoRef::from_str("a b/c#d").expect("parses");
99 assert_eq!(repo.path(""), "repos/a%20b/c%23d");
100 assert_eq!(repo.path("labels"), "repos/a%20b/c%23d/labels");
101 }
102
103 #[test]
104 fn a_malformed_pair_is_rejected() {
105 for raw in ["finmqtt", "a/b/c", "/finmqtt", "inomotech/", ""] {
106 RepoRef::from_str(raw).expect_err(raw);
107 }
108 }
109}