alpm_types/license.rs
1use std::{
2 fmt::{Display, Formatter},
3 str::FromStr,
4};
5
6use serde::{Deserialize, Deserializer, Serialize, Serializer};
7use spdx::Expression;
8
9use crate::Error;
10
11/// Represents a license expression that can be either a valid SPDX identifier
12/// or a non-standard one.
13///
14/// ## Examples
15/// ```
16/// use std::str::FromStr;
17///
18/// use alpm_types::License;
19///
20/// # fn main() -> Result<(), alpm_types::Error> {
21/// // Create License from a valid SPDX identifier
22/// let license = License::from_str("MIT")?;
23/// assert!(license.is_spdx());
24/// assert_eq!(license.to_string(), "MIT");
25///
26/// // Create License from an invalid/non-SPDX identifier
27/// let license = License::from_str("My-Custom-License")?;
28/// assert!(!license.is_spdx());
29/// assert_eq!(license.to_string(), "My-Custom-License");
30/// # Ok(())
31/// # }
32/// ```
33#[derive(Debug, Clone, PartialEq)]
34pub enum License {
35 /// A valid SPDX license expression
36 ///
37 /// This variant is boxed to avoid large allocations
38 Spdx(Box<spdx::Expression>),
39 /// A non-standard license identifier
40 Unknown(String),
41}
42
43impl Serialize for License {
44 /// Custom serde serialization as Spdx doesn't provide a serde [`Serialize`] implementation.
45 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
46 where
47 S: Serializer,
48 {
49 serializer.serialize_str(&self.to_string())
50 }
51}
52
53impl<'de> Deserialize<'de> for License {
54 /// Custom serde serialization as Spdx doesn't provide a serde [`Deserialize`] implementation.
55 /// This implements deserialization from a string type.
56 ///
57 /// Attempt to parse the given input as an [spdx::Expression] and to return a [License::Spdx].
58 /// If that fails, treat it as a [License::Unknown] that contains the original string.
59 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
60 where
61 D: Deserializer<'de>,
62 {
63 let s = String::deserialize(deserializer)?;
64
65 if let Ok(expr) = spdx::Expression::from_str(&s) {
66 return Ok(License::Spdx(Box::new(expr)));
67 }
68
69 Ok(License::Unknown(s))
70 }
71}
72
73impl License {
74 /// Creates a new license
75 ///
76 /// This function accepts both SPDX and non-standard identifiers
77 /// and it is the same as as calling [`License::from_str`]
78 pub fn new(license: String) -> Result<Self, Error> {
79 Self::from_valid_spdx(license.clone()).or(Ok(Self::Unknown(license)))
80 }
81
82 /// Creates a new license from a valid SPDX identifier
83 ///
84 /// ## Examples
85 ///
86 /// ```
87 /// use alpm_types::{Error, License};
88 ///
89 /// # fn main() -> Result<(), alpm_types::Error> {
90 /// let license = License::from_valid_spdx("Apache-2.0".to_string())?;
91 /// assert!(license.is_spdx());
92 /// assert_eq!(license.to_string(), "Apache-2.0");
93 ///
94 /// assert!(License::from_valid_spdx("GPL-0.0".to_string()).is_err());
95 /// assert!(License::from_valid_spdx("Custom-License".to_string()).is_err());
96 ///
97 /// assert_eq!(
98 /// License::from_valid_spdx("GPL-2.0".to_string()),
99 /// Err(Error::DeprecatedLicense("GPL-2.0".to_string()))
100 /// );
101 /// # Ok(())
102 /// # }
103 /// ```
104 ///
105 /// # Errors
106 ///
107 /// Returns an error if the given input cannot be parsed or is a deprecated license.
108 pub fn from_valid_spdx(identifier: String) -> Result<Self, Error> {
109 let expression = Expression::parse(&identifier)?;
110 if spdx::license_id(&identifier)
111 .map(|v| v.is_deprecated())
112 .unwrap_or(false)
113 {
114 return Err(Error::DeprecatedLicense(identifier));
115 }
116
117 Ok(Self::Spdx(Box::new(expression)))
118 }
119
120 /// Returns `true` if the license is a valid SPDX identifier
121 pub fn is_spdx(&self) -> bool {
122 matches!(self, License::Spdx(_))
123 }
124}
125
126impl FromStr for License {
127 type Err = Error;
128
129 /// Creates a new `License` instance from a string slice.
130 ///
131 /// If the input is a valid SPDX license expression,
132 /// it will be marked as such; otherwise, it will be treated as
133 /// a non-standard license identifier.
134 ///
135 /// ## Examples
136 ///
137 /// ```
138 /// use std::str::FromStr;
139 ///
140 /// use alpm_types::License;
141 ///
142 /// # fn main() -> Result<(), alpm_types::Error> {
143 /// let license = License::from_str("Apache-2.0")?;
144 /// assert!(license.is_spdx());
145 /// assert_eq!(license.to_string(), "Apache-2.0");
146 ///
147 /// let license = License::from_str("NonStandard-License")?;
148 /// assert!(!license.is_spdx());
149 /// assert_eq!(license.to_string(), "NonStandard-License");
150 /// # Ok(())
151 /// # }
152 /// ```
153 ///
154 /// # Errors
155 ///
156 /// Returns an error if the given input is a deprecated SPDX license.
157 fn from_str(s: &str) -> Result<Self, Self::Err> {
158 Self::new(s.to_string())
159 }
160}
161
162impl Display for License {
163 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
164 match &self {
165 License::Spdx(expr) => write!(f, "{expr}"),
166 License::Unknown(s) => write!(f, "{s}"),
167 }
168 }
169}
170
171#[cfg(test)]
172mod tests {
173 use rstest::rstest;
174
175 use super::*;
176
177 #[rstest]
178 #[case("MIT", License::Spdx(Box::new(Expression::parse("MIT").unwrap())))]
179 #[case("Apache-2.0", License::Spdx(Box::new(Expression::parse("Apache-2.0").unwrap())))]
180 #[case("Apache-2.0+", License::Spdx(Box::new(Expression::parse("Apache-2.0+").unwrap())))]
181 #[case(
182 "Apache-2.0 WITH LLVM-exception",
183 License::Spdx(Box::new(Expression::parse("Apache-2.0 WITH LLVM-exception").unwrap()))
184 )]
185 #[case("GPL-3.0-or-later", License::Spdx(Box::new(Expression::parse("GPL-3.0-or-later").unwrap())))]
186 #[case("HPND-Fenneberg-Livingston", License::Spdx(Box::new(Expression::parse("HPND-Fenneberg-Livingston").unwrap())))]
187 #[case(
188 "NonStandard-License",
189 License::Unknown(String::from("NonStandard-License"))
190 )]
191 fn test_parse_license(
192 #[case] input: &str,
193 #[case] expected: License,
194 ) -> testresult::TestResult<()> {
195 let license = input.parse::<License>()?;
196 assert_eq!(license, expected);
197 assert_eq!(license.to_string(), input.to_string());
198 Ok(())
199 }
200
201 #[rstest]
202 #[case("Apache-2.0 WITH",
203 Err(spdx::ParseError {
204 original: String::from("Apache-2.0 WITH"),
205 span: 15..15,
206 reason: spdx::error::Reason::Unexpected(&["<exception>"])
207 }.into())
208 )]
209 #[case("Custom-License",
210 Err(spdx::ParseError {
211 original: String::from("Custom-License"),
212 span: 0..14,
213 reason: spdx::error::Reason::UnknownTerm
214 }.into())
215 )]
216 fn test_invalid_spdx(#[case] input: &str, #[case] expected: Result<License, Error>) {
217 let result = License::from_valid_spdx(input.to_string());
218 assert_eq!(result, expected);
219 }
220
221 #[rstest]
222 #[case("GPL-2.0")]
223 #[case("BSD-2-Clause-FreeBSD")]
224 fn test_deprecated_spdx(#[case] input: &str) {
225 let result = License::from_valid_spdx(input.to_string());
226 assert_eq!(result, Err(Error::DeprecatedLicense(input.to_string())));
227 }
228
229 #[rstest]
230 #[case("MIT", true)]
231 #[case("Custom-License", false)]
232 fn test_license_kind(#[case] input: &str, #[case] is_spdx: bool) -> testresult::TestResult<()> {
233 let spdx_license = License::from_str(input)?;
234 assert_eq!(spdx_license.is_spdx(), is_spdx);
235
236 Ok(())
237 }
238}