1use std::fmt;
4use std::hash::Hash;
5use std::hash::Hasher;
6use std::str::FromStr;
7
8use serde_with::DeserializeFromStr;
9use serde_with::SerializeDisplay;
10use thiserror::Error;
11
12#[derive(Debug, Error)]
14pub enum LicenseError {
15 #[error("license expression cannot be empty")]
17 Empty,
18
19 #[error("invalid SPDX license expression: {0}")]
21 Invalid(String),
22}
23
24#[derive(Clone, SerializeDisplay, DeserializeFromStr)]
30pub struct LicenseExpression(spdx::Expression);
31
32impl LicenseExpression {
33 pub fn as_expression(&self) -> &spdx::Expression {
35 &self.0
36 }
37
38 pub fn as_str(&self) -> &str {
40 self.0.as_ref()
41 }
42}
43
44impl fmt::Debug for LicenseExpression {
45 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
46 f.debug_tuple("LicenseExpression")
47 .field(&self.as_str())
48 .finish()
49 }
50}
51
52impl fmt::Display for LicenseExpression {
53 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
54 f.write_str(self.as_str())
55 }
56}
57
58impl PartialEq for LicenseExpression {
59 fn eq(&self, other: &Self) -> bool {
60 self.as_str() == other.as_str()
61 }
62}
63
64impl Eq for LicenseExpression {}
65
66impl Hash for LicenseExpression {
67 fn hash<H: Hasher>(&self, state: &mut H) {
68 self.as_str().hash(state);
69 }
70}
71
72impl FromStr for LicenseExpression {
73 type Err = LicenseError;
74
75 fn from_str(s: &str) -> Result<Self, Self::Err> {
76 let trimmed = s.trim();
77 if trimmed.is_empty() {
78 return Err(LicenseError::Empty);
79 }
80 let expr =
81 spdx::Expression::parse(trimmed).map_err(|e| LicenseError::Invalid(format!("{e}")))?;
82 Ok(Self(expr))
83 }
84}
85
86impl From<LicenseExpression> for String {
87 fn from(expr: LicenseExpression) -> Self {
88 expr.as_str().to_string()
89 }
90}
91
92#[cfg(test)]
93mod tests {
94 use super::*;
95
96 #[test]
97 fn accepts_simple_licenses() {
98 for s in ["MIT", "Apache-2.0", "BSD-3-Clause", "GPL-3.0-only"] {
99 assert!(s.parse::<LicenseExpression>().is_ok(), "rejected `{s}`");
100 }
101 }
102
103 #[test]
104 fn accepts_compound_licenses() {
105 for s in [
106 "MIT OR Apache-2.0",
107 "MIT AND Apache-2.0",
108 "(MIT OR Apache-2.0) AND BSD-3-Clause",
109 "Apache-2.0 WITH LLVM-exception",
110 ] {
111 assert!(s.parse::<LicenseExpression>().is_ok(), "rejected `{s}`");
112 }
113 }
114
115 #[test]
116 fn rejects_unknown_id() {
117 assert!("MIT-2.0".parse::<LicenseExpression>().is_err());
118 }
119
120 #[test]
121 fn rejects_empty() {
122 assert!("".parse::<LicenseExpression>().is_err());
123 assert!(" ".parse::<LicenseExpression>().is_err());
124 }
125
126 #[test]
127 fn round_trips_via_serde() {
128 let license: LicenseExpression = "MIT OR Apache-2.0".parse().unwrap();
129 let json = serde_json::to_string(&license).unwrap();
130 let parsed: LicenseExpression = serde_json::from_str(&json).unwrap();
131 assert_eq!(parsed, license);
132 }
133}