1use crate::error::Error;
2
3const FORGE_SEPARATOR: char = '~';
4
5#[derive(Debug, Clone, PartialEq, Eq)]
6pub struct Namespace {
7 forge: Option<String>,
8 org: String,
9 repo: String,
10 stored_org: String,
11}
12
13impl Namespace {
14 pub fn new(org: impl Into<String>, repo: impl Into<String>) -> Result<Self, Error> {
15 let (org, repo) = (org.into(), repo.into());
16
17 (is_well_formed(&org) && is_well_formed(&repo))
18 .then(|| Self {
19 forge: None,
20 stored_org: org.clone(),
21 org,
22 repo,
23 })
24 .ok_or(Error::MalformedNamespace)
25 }
26
27 pub fn on(
28 forge: impl Into<String>,
29 org: impl Into<String>,
30 repo: impl Into<String>,
31 ) -> Result<Self, Error> {
32 let forge = forge.into();
33 if !is_forge_name(&forge) {
34 return Err(Error::MalformedNamespace);
35 }
36 let ns = Self::new(org, repo)?;
37
38 Ok(Self {
39 stored_org: format!("{forge}{FORGE_SEPARATOR}{}", ns.org),
40 forge: Some(forge),
41 ..ns
42 })
43 }
44
45 pub fn forge(&self) -> Option<&str> {
46 self.forge.as_deref()
47 }
48
49 pub fn org(&self) -> &str {
50 &self.org
51 }
52
53 pub fn repo(&self) -> &str {
54 &self.repo
55 }
56
57 pub fn stored_org(&self) -> &str {
58 &self.stored_org
59 }
60
61 pub fn upstream(&self) -> String {
62 format!("{}/{}", self.org, self.repo)
63 }
64
65 pub fn url_path(&self) -> String {
66 match &self.forge {
67 Some(forge) => format!("-/{forge}/{}/{}", self.org, self.repo),
68 None => self.upstream(),
69 }
70 }
71}
72
73impl std::fmt::Display for Namespace {
74 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
75 write!(f, "{}/{}", self.stored_org, self.repo)
76 }
77}
78
79pub fn is_forge_name(name: &str) -> bool {
80 (1..=32).contains(&name.len())
81 && name.starts_with(|c: char| c.is_ascii_lowercase() || c.is_ascii_digit())
82 && name
83 .bytes()
84 .all(|b| b.is_ascii_lowercase() || b.is_ascii_digit() || b == b'-')
85 && !matches!(name, "api" | "dashboard")
86}
87
88fn is_well_formed(segment: &str) -> bool {
89 !segment.is_empty()
90 && segment.len() <= 100
91 && !segment.starts_with('.')
92 && segment
93 .bytes()
94 .all(|b| b.is_ascii_alphanumeric() || matches!(b, b'-' | b'_' | b'.'))
95}
96
97#[cfg(test)]
98mod tests;