// SPDX-FileCopyrightText: 2022 HH Partners // // SPDX-License-Identifier: MIT //! The main struct of the library. use std::{collections::HashSet, fmt::Display, string::ToString}; use serde::{de::Visitor, Deserialize, Serialize}; use crate::{ error::SpdxExpressionError, expression_variant::{ExpressionVariant, SimpleExpression}, }; /// Main struct for SPDX License Expressions. #[derive(Debug, Clone, PartialEq, Eq)] pub struct SpdxExpression { /// The parsed expression. inner: ExpressionVariant, } impl SpdxExpression { /// Parse `Self` from a string. The input expression needs to be a syntactically valid SPDX /// expression, `NONE` or `NOASSERTION`. The parser accepts license identifiers that are not /// valid SPDX. /// /// # Examples /// /// ``` /// # use spdx_expression::SpdxExpression; /// # use spdx_expression::SpdxExpressionError; /// # /// let expression = SpdxExpression::parse("MIT")?; /// # Ok::<(), SpdxExpressionError>(()) /// ``` /// /// License expressions need to be syntactically valid, but they can include license /// identifiers not on the SPDX license list or not specified with `LicenseRef`. /// /// ``` /// # use spdx_expression::SpdxExpression; /// # use spdx_expression::SpdxExpressionError; /// # /// let expression = SpdxExpression::parse("MIT OR InvalidLicenseId")?; /// # Ok::<(), SpdxExpressionError>(()) /// ``` /// /// # Errors /// /// Returns `SpdxExpressionError` if the license expression is not syntactically valid. pub fn parse(expression: &str) -> Result { Ok(Self { inner: ExpressionVariant::parse(expression) .map_err(|err| SpdxExpressionError::Parse(err.to_string()))?, }) } /// Get all license and exception identifiers from the `SpdxExpression`. /// /// # Examples /// /// ``` /// # use std::collections::HashSet; /// # use std::iter::FromIterator; /// # use spdx_expression::SpdxExpression; /// # use spdx_expression::SpdxExpressionError; /// # /// let expression = SpdxExpression::parse("MIT OR Apache-2.0")?; /// let licenses = expression.identifiers(); /// assert_eq!(licenses, HashSet::from_iter(["Apache-2.0".to_string(), "MIT".to_string()])); /// # Ok::<(), SpdxExpressionError>(()) /// ``` /// /// ``` /// # use std::collections::HashSet; /// # use std::iter::FromIterator; /// # use spdx_expression::SpdxExpression; /// # use spdx_expression::SpdxExpressionError; /// # /// let expression = SpdxExpression::parse("MIT OR GPL-2.0-only WITH Classpath-exception-2.0")?; /// let licenses = expression.identifiers(); /// assert_eq!( /// licenses, /// HashSet::from_iter([ /// "MIT".to_string(), /// "GPL-2.0-only".to_string(), /// "Classpath-exception-2.0".to_string() /// ]) /// ); /// # Ok::<(), SpdxExpressionError>(()) /// ``` pub fn identifiers(&self) -> HashSet { let mut identifiers = self .licenses() .iter() .map(ToString::to_string) .collect::>(); identifiers.extend(self.exceptions().iter().map(ToString::to_string)); identifiers } /// Get all simple license expressions in `Self`. For licenses with exceptions, returns the /// license without the exception /// /// # Examples /// /// ``` /// # use std::collections::HashSet; /// # use std::iter::FromIterator; /// # use spdx_expression::SpdxExpression; /// # use spdx_expression::SpdxExpressionError; /// # /// let expression = SpdxExpression::parse( /// "(MIT OR Apache-2.0 AND (GPL-2.0-only WITH Classpath-exception-2.0 OR ISC))", /// ) /// .unwrap(); /// /// let licenses = expression.licenses(); /// /// assert_eq!( /// licenses /// .iter() /// .map(|&license| license.identifier.as_str()) /// .collect::>(), /// HashSet::from_iter(["Apache-2.0", "GPL-2.0-only", "ISC", "MIT"]) /// ); /// # Ok::<(), SpdxExpressionError>(()) /// ``` pub fn licenses(&self) -> HashSet<&SimpleExpression> { self.inner.licenses() } /// Get all exception identifiers for `Self`. /// /// # Examples /// /// ``` /// # use std::collections::HashSet; /// # use std::iter::FromIterator; /// # use spdx_expression::SpdxExpression; /// # use spdx_expression::SpdxExpressionError; /// # /// let expression = SpdxExpression::parse( /// "(MIT OR Apache-2.0 AND (GPL-2.0-only WITH Classpath-exception-2.0 OR ISC))", /// ) /// .unwrap(); /// /// let exceptions = expression.exceptions(); /// /// assert_eq!(exceptions, HashSet::from_iter(["Classpath-exception-2.0"])); /// # Ok::<(), SpdxExpressionError>(()) /// ``` pub fn exceptions(&self) -> HashSet<&str> { self.inner.exceptions() } } impl Default for SpdxExpression { fn default() -> Self { Self::parse("NOASSERTION").expect("will not fail") } } impl Serialize for SpdxExpression { fn serialize(&self, serializer: S) -> Result where S: serde::Serializer, { serializer.collect_str(self) } } struct SpdxExpressionVisitor; impl<'de> Visitor<'de> for SpdxExpressionVisitor { type Value = SpdxExpression; fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { formatter.write_str("a syntactically valid SPDX expression") } fn visit_str(self, v: &str) -> Result where E: serde::de::Error, { SpdxExpression::parse(v) .map_err(|err| E::custom(format!("error parsing the expression: {}", err))) } fn visit_borrowed_str(self, v: &'de str) -> Result where E: serde::de::Error, { self.visit_str(v) } fn visit_string(self, v: String) -> Result where E: serde::de::Error, { self.visit_str(&v) } } impl<'de> Deserialize<'de> for SpdxExpression { fn deserialize(deserializer: D) -> Result where D: serde::Deserializer<'de>, { deserializer.deserialize_str(SpdxExpressionVisitor) } } impl Display for SpdxExpression { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "{}", self.inner) } } #[cfg(test)] mod tests { use std::iter::FromIterator; use serde_json::Value; use super::*; #[test] fn test_parsing_works() { let expression = SpdxExpression::parse("MIT AND (Apache-2.0 OR ISC)").unwrap(); assert_eq!(expression.to_string(), "MIT AND (Apache-2.0 OR ISC)"); } #[test] fn test_identifiers_from_simple_expression() { let expression = SpdxExpression::parse("MIT").unwrap(); let licenses = expression.identifiers(); assert_eq!(licenses, HashSet::from_iter(["MIT".to_string()])); } #[test] fn test_identifiers_from_compound_or_expression() { let expression = SpdxExpression::parse("MIT OR Apache-2.0").unwrap(); let licenses = expression.identifiers(); assert_eq!( licenses, HashSet::from_iter(["Apache-2.0".to_string(), "MIT".to_string()]) ); } #[test] fn test_identifiers_from_compound_parentheses_expression() { let expression = SpdxExpression::parse( "(MIT OR Apache-2.0 AND (GPL-2.0-only WITH Classpath-exception-2.0 OR ISC))", ) .unwrap(); let licenses = expression.identifiers(); assert_eq!( licenses, HashSet::from_iter([ "Apache-2.0".to_string(), "Classpath-exception-2.0".to_string(), "GPL-2.0-only".to_string(), "ISC".to_string(), "MIT".to_string() ]) ); } #[test] fn test_licenses_from_compound_parentheses_expression() { let expression = SpdxExpression::parse( "(MIT OR Apache-2.0 AND (GPL-2.0-only WITH Classpath-exception-2.0 OR ISC))", ) .unwrap(); let licenses = expression.licenses(); assert_eq!( licenses .iter() .map(|&license| license.identifier.as_str()) .collect::>(), HashSet::from_iter(["Apache-2.0", "GPL-2.0-only", "ISC", "MIT"]) ); } #[test] fn test_exceptions_from_compound_parentheses_expression() { let expression = SpdxExpression::parse( "(MIT OR Apache-2.0 AND (GPL-2.0-only WITH Classpath-exception-2.0 OR ISC))", ) .unwrap(); let exceptions = expression.exceptions(); assert_eq!(exceptions, HashSet::from_iter(["Classpath-exception-2.0"])); } #[test] fn serialize_expression_correctly() { let expression = SpdxExpression::parse("MIT OR ISC").unwrap(); let value = serde_json::to_value(expression).unwrap(); assert_eq!(value, Value::String("MIT OR ISC".to_string())); } #[test] fn deserialize_expression_correctly() { let expected = SpdxExpression::parse("MIT OR ISC").unwrap(); let value = Value::String("MIT OR ISC".to_string()); let actual: SpdxExpression = serde_json::from_value(value).unwrap(); assert_eq!(actual, expected); } }