spdx_expression/
expression.rs

1// SPDX-FileCopyrightText: 2022 HH Partners
2//
3// SPDX-License-Identifier: MIT
4
5//! The main struct of the library.
6
7use std::{collections::HashSet, fmt::Display, string::ToString};
8
9use serde::{de::Visitor, Deserialize, Serialize};
10
11use crate::{
12    error::SpdxExpressionError,
13    expression_variant::{ExpressionVariant, SimpleExpression},
14};
15
16/// Main struct for SPDX License Expressions.
17#[derive(Debug, Clone, PartialEq, Eq)]
18pub struct SpdxExpression {
19    /// The parsed expression.
20    inner: ExpressionVariant,
21}
22
23impl SpdxExpression {
24    /// Parse `Self` from a string. The input expression needs to be a syntactically valid SPDX
25    /// expression, `NONE` or `NOASSERTION`. The parser accepts license identifiers that are not
26    /// valid SPDX.
27    ///
28    /// # Examples
29    ///
30    /// ```
31    /// # use spdx_expression::SpdxExpression;
32    /// # use spdx_expression::SpdxExpressionError;
33    /// #
34    /// let expression = SpdxExpression::parse("MIT")?;
35    /// # Ok::<(), SpdxExpressionError>(())
36    /// ```
37    ///
38    /// License expressions need to be syntactically valid, but they can include license
39    /// identifiers not on the SPDX license list or not specified with `LicenseRef`.
40    ///
41    /// ```
42    /// # use spdx_expression::SpdxExpression;
43    /// # use spdx_expression::SpdxExpressionError;
44    /// #
45    /// let expression = SpdxExpression::parse("MIT OR InvalidLicenseId")?;
46    /// # Ok::<(), SpdxExpressionError>(())
47    /// ```
48    ///
49    /// # Errors
50    ///
51    /// Returns `SpdxExpressionError` if the license expression is not syntactically valid.
52    pub fn parse(expression: &str) -> Result<Self, SpdxExpressionError> {
53        Ok(Self {
54            inner: ExpressionVariant::parse(expression)
55                .map_err(|err| SpdxExpressionError::Parse(err.to_string()))?,
56        })
57    }
58
59    /// Get all license and exception identifiers from the `SpdxExpression`.
60    ///
61    /// # Examples
62    ///
63    /// ```
64    /// # use std::collections::HashSet;
65    /// # use std::iter::FromIterator;
66    /// # use spdx_expression::SpdxExpression;
67    /// # use spdx_expression::SpdxExpressionError;
68    /// #
69    /// let expression = SpdxExpression::parse("MIT OR Apache-2.0")?;
70    /// let licenses = expression.identifiers();
71    /// assert_eq!(licenses, HashSet::from_iter(["Apache-2.0".to_string(), "MIT".to_string()]));
72    /// # Ok::<(), SpdxExpressionError>(())
73    /// ```
74    ///
75    /// ```
76    /// # use std::collections::HashSet;
77    /// # use std::iter::FromIterator;
78    /// # use spdx_expression::SpdxExpression;
79    /// # use spdx_expression::SpdxExpressionError;
80    /// #
81    /// let expression = SpdxExpression::parse("MIT OR GPL-2.0-only WITH Classpath-exception-2.0")?;
82    /// let licenses = expression.identifiers();
83    /// assert_eq!(
84    ///     licenses,
85    ///     HashSet::from_iter([
86    ///         "MIT".to_string(),
87    ///         "GPL-2.0-only".to_string(),
88    ///         "Classpath-exception-2.0".to_string()
89    ///     ])
90    /// );
91    /// # Ok::<(), SpdxExpressionError>(())
92    /// ```
93    pub fn identifiers(&self) -> HashSet<String> {
94        let mut identifiers = self
95            .licenses()
96            .iter()
97            .map(ToString::to_string)
98            .collect::<HashSet<_>>();
99
100        identifiers.extend(self.exceptions().iter().map(ToString::to_string));
101
102        identifiers
103    }
104
105    /// Get all simple license expressions in `Self`. For licenses with exceptions, returns the
106    /// license without the exception
107    ///
108    /// # Examples
109    ///
110    /// ```
111    /// # use std::collections::HashSet;
112    /// # use std::iter::FromIterator;
113    /// # use spdx_expression::SpdxExpression;
114    /// # use spdx_expression::SpdxExpressionError;
115    /// #
116    /// let expression = SpdxExpression::parse(
117    ///     "(MIT OR Apache-2.0 AND (GPL-2.0-only WITH Classpath-exception-2.0 OR ISC))",
118    /// )
119    /// .unwrap();
120    ///
121    /// let licenses = expression.licenses();
122    ///
123    /// assert_eq!(
124    ///     licenses
125    ///         .iter()
126    ///         .map(|&license| license.identifier.as_str())
127    ///         .collect::<HashSet<_>>(),
128    ///     HashSet::from_iter(["Apache-2.0", "GPL-2.0-only", "ISC", "MIT"])
129    /// );
130    /// # Ok::<(), SpdxExpressionError>(())
131    /// ```
132    pub fn licenses(&self) -> HashSet<&SimpleExpression> {
133        self.inner.licenses()
134    }
135
136    /// Get all exception identifiers for `Self`.
137    ///
138    /// # Examples
139    ///
140    /// ```
141    /// # use std::collections::HashSet;
142    /// # use std::iter::FromIterator;
143    /// # use spdx_expression::SpdxExpression;
144    /// # use spdx_expression::SpdxExpressionError;
145    /// #
146    /// let expression = SpdxExpression::parse(
147    ///     "(MIT OR Apache-2.0 AND (GPL-2.0-only WITH Classpath-exception-2.0 OR ISC))",
148    /// )
149    /// .unwrap();
150    ///
151    /// let exceptions = expression.exceptions();
152    ///
153    /// assert_eq!(exceptions, HashSet::from_iter(["Classpath-exception-2.0"]));
154    /// # Ok::<(), SpdxExpressionError>(())
155    /// ```
156    pub fn exceptions(&self) -> HashSet<&str> {
157        self.inner.exceptions()
158    }
159}
160
161impl Default for SpdxExpression {
162    fn default() -> Self {
163        Self::parse("NOASSERTION").expect("will not fail")
164    }
165}
166
167impl Serialize for SpdxExpression {
168    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
169    where
170        S: serde::Serializer,
171    {
172        serializer.collect_str(self)
173    }
174}
175
176struct SpdxExpressionVisitor;
177
178impl<'de> Visitor<'de> for SpdxExpressionVisitor {
179    type Value = SpdxExpression;
180
181    fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
182        formatter.write_str("a syntactically valid SPDX expression")
183    }
184
185    fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
186    where
187        E: serde::de::Error,
188    {
189        SpdxExpression::parse(v)
190            .map_err(|err| E::custom(format!("error parsing the expression: {}", err)))
191    }
192
193    fn visit_borrowed_str<E>(self, v: &'de str) -> Result<Self::Value, E>
194    where
195        E: serde::de::Error,
196    {
197        self.visit_str(v)
198    }
199
200    fn visit_string<E>(self, v: String) -> Result<Self::Value, E>
201    where
202        E: serde::de::Error,
203    {
204        self.visit_str(&v)
205    }
206}
207
208impl<'de> Deserialize<'de> for SpdxExpression {
209    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
210    where
211        D: serde::Deserializer<'de>,
212    {
213        deserializer.deserialize_str(SpdxExpressionVisitor)
214    }
215}
216
217impl Display for SpdxExpression {
218    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
219        write!(f, "{}", self.inner)
220    }
221}
222
223#[cfg(test)]
224mod tests {
225    use std::iter::FromIterator;
226
227    use serde_json::Value;
228
229    use super::*;
230
231    #[test]
232    fn test_parsing_works() {
233        let expression = SpdxExpression::parse("MIT AND (Apache-2.0 OR ISC)").unwrap();
234        assert_eq!(expression.to_string(), "MIT AND (Apache-2.0 OR ISC)");
235    }
236
237    #[test]
238    fn test_identifiers_from_simple_expression() {
239        let expression = SpdxExpression::parse("MIT").unwrap();
240        let licenses = expression.identifiers();
241        assert_eq!(licenses, HashSet::from_iter(["MIT".to_string()]));
242    }
243
244    #[test]
245    fn test_identifiers_from_compound_or_expression() {
246        let expression = SpdxExpression::parse("MIT OR Apache-2.0").unwrap();
247        let licenses = expression.identifiers();
248        assert_eq!(
249            licenses,
250            HashSet::from_iter(["Apache-2.0".to_string(), "MIT".to_string()])
251        );
252    }
253
254    #[test]
255    fn test_identifiers_from_compound_parentheses_expression() {
256        let expression = SpdxExpression::parse(
257            "(MIT OR Apache-2.0 AND (GPL-2.0-only WITH Classpath-exception-2.0 OR ISC))",
258        )
259        .unwrap();
260        let licenses = expression.identifiers();
261        assert_eq!(
262            licenses,
263            HashSet::from_iter([
264                "Apache-2.0".to_string(),
265                "Classpath-exception-2.0".to_string(),
266                "GPL-2.0-only".to_string(),
267                "ISC".to_string(),
268                "MIT".to_string()
269            ])
270        );
271    }
272
273    #[test]
274    fn test_licenses_from_compound_parentheses_expression() {
275        let expression = SpdxExpression::parse(
276            "(MIT OR Apache-2.0 AND (GPL-2.0-only WITH Classpath-exception-2.0 OR ISC))",
277        )
278        .unwrap();
279
280        let licenses = expression.licenses();
281
282        assert_eq!(
283            licenses
284                .iter()
285                .map(|&license| license.identifier.as_str())
286                .collect::<HashSet<_>>(),
287            HashSet::from_iter(["Apache-2.0", "GPL-2.0-only", "ISC", "MIT"])
288        );
289    }
290
291    #[test]
292    fn test_exceptions_from_compound_parentheses_expression() {
293        let expression = SpdxExpression::parse(
294            "(MIT OR Apache-2.0 AND (GPL-2.0-only WITH Classpath-exception-2.0 OR ISC))",
295        )
296        .unwrap();
297
298        let exceptions = expression.exceptions();
299
300        assert_eq!(exceptions, HashSet::from_iter(["Classpath-exception-2.0"]));
301    }
302
303    #[test]
304    fn serialize_expression_correctly() {
305        let expression = SpdxExpression::parse("MIT OR ISC").unwrap();
306
307        let value = serde_json::to_value(expression).unwrap();
308
309        assert_eq!(value, Value::String("MIT OR ISC".to_string()));
310    }
311
312    #[test]
313    fn deserialize_expression_correctly() {
314        let expected = SpdxExpression::parse("MIT OR ISC").unwrap();
315
316        let value = Value::String("MIT OR ISC".to_string());
317
318        let actual: SpdxExpression = serde_json::from_value(value).unwrap();
319
320        assert_eq!(actual, expected);
321    }
322}