use std::{
fmt::{Display, Formatter},
str::FromStr,
};
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use spdx::Expression;
use crate::Error;
#[derive(Clone, Debug, PartialEq)]
pub enum License {
Spdx(Box<spdx::Expression>),
Unknown(String),
}
impl Serialize for License {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.serialize_str(&self.to_string())
}
}
impl<'de> Deserialize<'de> for License {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let s = String::deserialize(deserializer)?;
if let Ok(expr) = spdx::Expression::from_str(&s) {
return Ok(License::Spdx(Box::new(expr)));
}
Ok(License::Unknown(s))
}
}
impl License {
pub fn new(license: String) -> Result<Self, Error> {
Self::from_valid_spdx(license.clone()).or(Ok(Self::Unknown(license)))
}
pub fn from_valid_spdx(identifier: String) -> Result<Self, Error> {
let expression = match Expression::parse(&identifier) {
Ok(expr) => expr,
Err(e) => {
if e.reason == spdx::error::Reason::DeprecatedLicenseId {
return Err(Error::DeprecatedLicense(identifier));
} else {
return Err(Error::InvalidLicense(e));
}
}
};
Ok(Self::Spdx(Box::new(expression)))
}
pub fn is_spdx(&self) -> bool {
matches!(self, License::Spdx(_))
}
}
impl FromStr for License {
type Err = Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Self::new(s.to_string())
}
}
impl Display for License {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match &self {
License::Spdx(expr) => write!(f, "{expr}"),
License::Unknown(s) => write!(f, "{s}"),
}
}
}
#[cfg(test)]
mod tests {
use rstest::rstest;
use super::*;
#[rstest]
#[case("MIT", License::Spdx(Box::new(Expression::parse("MIT").unwrap())))]
#[case("Apache-2.0", License::Spdx(Box::new(Expression::parse("Apache-2.0").unwrap())))]
#[case("Apache-2.0+", License::Spdx(Box::new(Expression::parse("Apache-2.0+").unwrap())))]
#[case(
"Apache-2.0 WITH LLVM-exception",
License::Spdx(Box::new(Expression::parse("Apache-2.0 WITH LLVM-exception").unwrap()))
)]
#[case("GPL-3.0-or-later", License::Spdx(Box::new(Expression::parse("GPL-3.0-or-later").unwrap())))]
#[case("HPND-Fenneberg-Livingston", License::Spdx(Box::new(Expression::parse("HPND-Fenneberg-Livingston").unwrap())))]
#[case(
"NonStandard-License",
License::Unknown(String::from("NonStandard-License"))
)]
fn test_parse_license(
#[case] input: &str,
#[case] expected: License,
) -> testresult::TestResult<()> {
let license = input.parse::<License>()?;
assert_eq!(license, expected);
assert_eq!(license.to_string(), input.to_string());
Ok(())
}
#[rstest]
#[case("Apache-2.0 WITH",
Err(spdx::ParseError {
original: String::from("Apache-2.0 WITH"),
span: 15..15,
reason: spdx::error::Reason::Unexpected(&["<addition>"])
}.into())
)]
#[case("Custom-License",
Err(spdx::ParseError {
original: String::from("Custom-License"),
span: 0..14,
reason: spdx::error::Reason::UnknownTerm
}.into())
)]
fn test_invalid_spdx(#[case] input: &str, #[case] expected: Result<License, Error>) {
let result = License::from_valid_spdx(input.to_string());
assert_eq!(result, expected);
}
#[rstest]
#[case("BSD-2-Clause-FreeBSD")]
#[case("BSD-2-Clause-NetBSD")]
#[case("bzip2-1.0.5")]
#[case("GPL-2.0")]
fn test_deprecated_spdx(#[case] input: &str) {
let result = License::from_valid_spdx(input.to_string());
assert_eq!(result, Err(Error::DeprecatedLicense(input.to_string())));
}
#[rstest]
#[case("MIT", true)]
#[case("Custom-License", false)]
fn test_license_kind(#[case] input: &str, #[case] is_spdx: bool) -> testresult::TestResult<()> {
let spdx_license = License::from_str(input)?;
assert_eq!(spdx_license.is_spdx(), is_spdx);
Ok(())
}
}