rattler_build/recipe/parser/
about.rs

1use std::{
2    fmt::{Display, Formatter},
3    str::FromStr,
4};
5
6use serde::{Deserialize, Serialize};
7use serde_with::{DeserializeFromStr, SerializeDisplay};
8use spdx::Expression;
9use url::Url;
10
11use crate::{
12    _partialerror,
13    recipe::{
14        custom_yaml::{
15            HasSpan, RenderedMappingNode, RenderedNode, RenderedScalarNode, TryConvertNode,
16        },
17        error::{ErrorKind, PartialParsingError},
18    },
19    validate_keys,
20};
21
22use super::{FlattenErrors, GlobVec};
23
24/// About information.
25#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)]
26pub struct About {
27    /// The homepage of the package.
28    #[serde(skip_serializing_if = "Option::is_none")]
29    pub homepage: Option<Url>,
30    /// The repository of the package.
31    #[serde(skip_serializing_if = "Option::is_none")]
32    pub repository: Option<Url>,
33    /// The documentation of the package.
34    #[serde(skip_serializing_if = "Option::is_none")]
35    pub documentation: Option<Url>,
36    /// The license of the package.
37    #[serde(skip_serializing_if = "Option::is_none")]
38    pub license: Option<License>,
39    /// The license family of the package.
40    #[serde(skip_serializing_if = "Option::is_none")]
41    pub license_family: Option<String>,
42    /// The license file(s) of the package.
43    #[serde(default, skip_serializing_if = "GlobVec::is_empty")]
44    pub license_file: GlobVec,
45    /// The license URL of the package.
46    #[serde(skip_serializing_if = "Option::is_none")]
47    pub license_url: Option<Url>,
48    /// The summary of the package.
49    #[serde(skip_serializing_if = "Option::is_none")]
50    pub summary: Option<String>,
51    /// The description of the package.
52    #[serde(skip_serializing_if = "Option::is_none")]
53    pub description: Option<String>,
54    /// The prelink message of the package.
55    #[serde(skip_serializing_if = "Option::is_none")]
56    pub prelink_message: Option<String>,
57}
58
59impl About {
60    /// Returns true if the about has its default configuration.
61    pub fn is_default(&self) -> bool {
62        self == &Self::default()
63    }
64}
65
66impl TryConvertNode<About> for RenderedNode {
67    fn try_convert(&self, name: &str) -> Result<About, Vec<PartialParsingError>> {
68        self.as_mapping()
69            .ok_or_else(|| vec![_partialerror!(*self.span(), ErrorKind::ExpectedMapping,)])
70            .and_then(|m| m.try_convert(name))
71    }
72}
73
74impl TryConvertNode<About> for RenderedMappingNode {
75    fn try_convert(&self, _name: &str) -> Result<About, Vec<PartialParsingError>> {
76        let mut about = About::default();
77
78        validate_keys!(
79            about,
80            self.iter(),
81            homepage,
82            repository,
83            documentation,
84            license,
85            license_family,
86            license_file,
87            license_url,
88            summary,
89            description,
90            prelink_message
91        );
92
93        Ok(about)
94    }
95}
96
97/// A parsed SPDX license
98#[derive(Debug, Clone, SerializeDisplay, DeserializeFromStr)]
99pub struct License {
100    pub original: String,
101    pub expr: spdx::Expression,
102}
103
104impl PartialEq for License {
105    fn eq(&self, other: &Self) -> bool {
106        self.expr == other.expr
107    }
108}
109
110impl Display for License {
111    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
112        write!(f, "{}", &self.original)
113    }
114}
115
116impl FromStr for License {
117    type Err = spdx::ParseError;
118
119    fn from_str(s: &str) -> Result<Self, Self::Err> {
120        Ok(License {
121            original: s.to_owned(),
122            expr: Expression::parse(s)?,
123        })
124    }
125}
126
127impl TryConvertNode<License> for RenderedNode {
128    fn try_convert(&self, name: &str) -> Result<License, Vec<PartialParsingError>> {
129        self.as_scalar()
130            .ok_or_else(|| vec![_partialerror!(*self.span(), ErrorKind::ExpectedScalar,)])
131            .and_then(|m| m.try_convert(name))
132    }
133}
134
135impl TryConvertNode<License> for RenderedScalarNode {
136    fn try_convert(&self, name: &str) -> Result<License, Vec<PartialParsingError>> {
137        let original: String = self.try_convert(name)?;
138        let expr = Expression::parse(original.as_str())
139            .map_err(|err| vec![_partialerror!(*self.span(), ErrorKind::from(err),)])?;
140
141        Ok(License { original, expr })
142    }
143}
144
145#[cfg(test)]
146mod test {
147    use crate::{
148        assert_miette_snapshot,
149        recipe::{jinja::SelectorConfig, Recipe},
150        variant_config::ParseErrors,
151    };
152
153    #[test]
154    fn invalid_url() {
155        let recipe = r#"
156        package:
157            name: test
158            version: 0.0.1
159
160        about:
161            homepage: license_urla.asda:://sdskd
162        "#;
163
164        let err: ParseErrors = Recipe::from_yaml(recipe, SelectorConfig::default())
165            .unwrap_err()
166            .into();
167
168        assert_miette_snapshot!(err);
169    }
170
171    #[test]
172    fn invalid_license() {
173        let recipe = r#"
174        package:
175            name: test
176            version: 0.0.1
177
178        about:
179            license: MIT/X derivate
180        "#;
181
182        let err: ParseErrors = Recipe::from_yaml(recipe, SelectorConfig::default())
183            .unwrap_err()
184            .into();
185
186        assert_miette_snapshot!(err);
187    }
188}