Skip to main content

npmgen_core/project/
identity.rs

1use super::ProjectError;
2
3/// Package identity derived from the repository URL: the npm scope (`@owner`),
4/// the npm/package name (repository basename), and the npm git URL. Assumes the
5/// npm scope matches the repository owner unless overridden.
6#[derive(Debug, Clone)]
7pub struct Identity {
8    pub scope: String,
9    pub name: String,
10    pub git_url: String,
11}
12
13impl Identity {
14    /// Derive identity from a repository URL. `scope_override` (e.g. `@acme`) is
15    /// used verbatim when present; otherwise the scope is `@<owner>`.
16    pub fn from_repository(
17        repository: &str,
18        scope_override: Option<&str>,
19    ) -> Result<Self, ProjectError> {
20        // Strip at most one trailing `/` then one `.git`, so a repository whose
21        // name legitimately ends in `.git` is not over-trimmed.
22        let trimmed = repository.trim();
23        let trimmed = trimmed.strip_suffix('/').unwrap_or(trimmed);
24        let path = trimmed.strip_suffix(".git").unwrap_or(trimmed);
25        let mut segments = path.rsplit('/');
26        let name = segments.next().unwrap_or_default();
27        let owner = segments.next().unwrap_or_default();
28        if owner.is_empty() || name.is_empty() {
29            return Err(ProjectError::MissingRepository);
30        }
31        Ok(Self {
32            scope: scope_override
33                .map(str::to_owned)
34                .unwrap_or_else(|| format!("@{owner}")),
35            name: name.to_owned(),
36            git_url: format!("git+{path}.git"),
37        })
38    }
39}
40
41#[cfg(test)]
42mod tests {
43    use super::*;
44
45    #[test]
46    fn derives_scope_name_and_git_url() {
47        let identity = Identity::from_repository("https://github.com/gglinnk/nocmd", None).unwrap();
48        assert_eq!(identity.scope, "@gglinnk");
49        assert_eq!(identity.name, "nocmd");
50        assert_eq!(identity.git_url, "git+https://github.com/gglinnk/nocmd.git");
51    }
52
53    #[test]
54    fn tolerates_trailing_slash_and_dot_git() {
55        let identity =
56            Identity::from_repository("https://github.com/acme/tool.git/", None).unwrap();
57        assert_eq!(identity.scope, "@acme");
58        assert_eq!(identity.name, "tool");
59        assert_eq!(identity.git_url, "git+https://github.com/acme/tool.git");
60    }
61
62    #[test]
63    fn scope_override_is_used_verbatim() {
64        let identity =
65            Identity::from_repository("https://github.com/gglinnk/tool", Some("@other")).unwrap();
66        assert_eq!(identity.scope, "@other");
67    }
68
69    #[test]
70    fn missing_owner_is_rejected() {
71        assert!(Identity::from_repository("nocmd", None).is_err());
72        assert!(Identity::from_repository("", None).is_err());
73    }
74}