1 | // SPDX-FileCopyrightText: 2022 HH Partners |
2 | // |
3 | // SPDX-License-Identifier: MIT |
4 | |
5 | //! The main struct of the library. |
6 | |
7 | use std::{collections::HashSet, fmt::Display, string::ToString}; |
8 | |
9 | use serde::{de::Visitor, Deserialize, Serialize}; |
10 | |
11 | use crate::{ |
12 | error::SpdxExpressionError, |
13 | expression_variant::{ExpressionVariant, SimpleExpression}, |
14 | }; |
15 | |
16 | /// Main struct for SPDX License Expressions. |
17 | #[derive (Debug, Clone, PartialEq, Eq)] |
18 | pub struct SpdxExpression { |
19 | /// The parsed expression. |
20 | inner: ExpressionVariant, |
21 | } |
22 | |
23 | impl 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 | |
161 | impl Default for SpdxExpression { |
162 | fn default() -> Self { |
163 | Self::parse("NOASSERTION" ).expect(msg:"will not fail" ) |
164 | } |
165 | } |
166 | |
167 | impl 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 | |
176 | struct SpdxExpressionVisitor; |
177 | |
178 | impl<'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 | |
208 | impl<'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 | |
217 | impl 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)] |
224 | mod 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 | |