aboutsummaryrefslogtreecommitdiffhomepage
path: root/crates/mozart-core/src/package_info.rs
blob: d67729ace003eb9884a90c9650a101ecf7667378 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
//! Mirrors `Composer\Util\PackageInfo`.
//!
//! The PHP class exposes two small static helpers that the `licenses`,
//! `show`, `outdated`, and `funding` commands lean on to produce a
//! "view source" link for a package — preferring the explicit
//! `support.source` URL, falling back to the package's `source` URL,
//! and finally the homepage. Empty strings are normalised to `None`.

/// Minimal contract for the package fields [`view_source_url`] and
/// [`view_source_or_homepage_url`] consult.
pub trait PackageUrls {
    /// `support.source` from `composer.json` (mirrors
    /// `CompletePackageInterface::getSupport()['source']`). Returns
    /// `None` when the support block is absent or the `source` key is
    /// missing.
    fn support_source(&self) -> Option<&str>;
    /// `source.url` (mirrors `PackageInterface::getSourceUrl()`).
    fn source_url(&self) -> Option<&str>;
    /// `homepage` (mirrors `CompletePackageInterface::getHomepage()`).
    fn homepage(&self) -> Option<&str>;
}

/// Mirror of `PackageInfo::getViewSourceUrl`.
///
/// PHP returns the support-source URL when it is set and non-empty,
/// otherwise `getSourceUrl()`. Empty strings are treated as absent.
pub fn view_source_url<P: PackageUrls + ?Sized>(package: &P) -> Option<String> {
    if let Some(s) = package.support_source().filter(|s| !s.is_empty()) {
        return Some(s.to_string());
    }
    package
        .source_url()
        .filter(|s| !s.is_empty())
        .map(String::from)
}

/// Mirror of `PackageInfo::getViewSourceOrHomepageUrl`.
///
/// Falls back to the package homepage when no source URL is available.
/// An empty homepage string is normalised to `None`, matching PHP's
/// `if ($url === '') { return null; }` guard.
pub fn view_source_or_homepage_url<P: PackageUrls + ?Sized>(package: &P) -> Option<String> {
    view_source_url(package)
        .or_else(|| package.homepage().map(String::from))
        .filter(|s| !s.is_empty())
}

#[cfg(test)]
mod tests {
    use super::*;

    #[derive(Default)]
    struct P {
        support_source: Option<String>,
        source_url: Option<String>,
        homepage: Option<String>,
    }

    impl PackageUrls for P {
        fn support_source(&self) -> Option<&str> {
            self.support_source.as_deref()
        }
        fn source_url(&self) -> Option<&str> {
            self.source_url.as_deref()
        }
        fn homepage(&self) -> Option<&str> {
            self.homepage.as_deref()
        }
    }

    #[test]
    fn prefers_support_source() {
        let p = P {
            support_source: Some("https://github.com/foo/bar".to_string()),
            source_url: Some("https://example.com/repo".to_string()),
            ..Default::default()
        };
        assert_eq!(
            view_source_url(&p).as_deref(),
            Some("https://github.com/foo/bar")
        );
    }

    #[test]
    fn empty_support_source_falls_through_to_source_url() {
        let p = P {
            support_source: Some(String::new()),
            source_url: Some("https://example.com/repo".to_string()),
            ..Default::default()
        };
        assert_eq!(
            view_source_url(&p).as_deref(),
            Some("https://example.com/repo")
        );
    }

    #[test]
    fn falls_back_to_homepage() {
        let p = P {
            homepage: Some("https://example.com/".to_string()),
            ..Default::default()
        };
        assert_eq!(
            view_source_or_homepage_url(&p).as_deref(),
            Some("https://example.com/")
        );
    }

    #[test]
    fn empty_homepage_is_none() {
        let p = P {
            homepage: Some(String::new()),
            ..Default::default()
        };
        assert!(view_source_or_homepage_url(&p).is_none());
    }

    #[test]
    fn no_urls_at_all_returns_none() {
        let p = P::default();
        assert!(view_source_url(&p).is_none());
        assert!(view_source_or_homepage_url(&p).is_none());
    }
}