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(msg:"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(visitor: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}
323