1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
// SPDX-FileCopyrightText: 2022 HH Partners
//
// SPDX-License-Identifier: MIT

//! The main struct of the library.

use std::fmt::Display;

use crate::{error::SpdxExpressionError, inner_variant::Expression};

/// Main struct for SPDX License Expressions.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SPDXExpression {
    /// The parsed expression.
    inner: Expression,
}

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<Self, SpdxExpressionError> {
        Ok(Self {
            inner: Expression::parse(expression)
                .map_err(|err| SpdxExpressionError::Parse(err.to_string()))?,
        })
    }

    /// Get all license and exception identifiers from the `SPDXExpression`. Returns the licenses
    /// alphabetically sorted and deduped.
    ///
    /// # Examples
    ///
    /// ```
    /// # use spdx_expression::SPDXExpression;
    /// # use spdx_expression::SpdxExpressionError;
    /// #
    /// let expression = SPDXExpression::parse("MIT OR Apache-2.0")?;
    /// let licenses = expression.licenses();
    /// assert_eq!(licenses, vec!["Apache-2.0".to_string(), "MIT".to_string()]);
    /// # Ok::<(), SpdxExpressionError>(())
    /// ```
    pub fn licenses(&self) -> Vec<String> {
        let expression_string = self.to_string();
        let licenses = expression_string.split_ascii_whitespace();
        let licenses = licenses.filter(|&i| i != "OR" && i != "AND" && i != "WITH");
        let licenses = licenses.map(|i| i.replace('(', "").replace(')', ""));
        let mut licenses = licenses.collect::<Vec<_>>();
        licenses.sort_unstable();
        licenses.dedup();
        licenses
    }
}

impl Display for SPDXExpression {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "{}", self.inner)
    }
}

#[cfg(test)]
mod tests {
    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_licenses_from_simple_expression() {
        let expression = SPDXExpression::parse("MIT").unwrap();
        let licenses = expression.licenses();
        assert_eq!(licenses, vec!["MIT".to_string()]);
    }

    #[test]
    fn test_licenses_from_compound_or_expression() {
        let expression = SPDXExpression::parse("MIT OR Apache-2.0").unwrap();
        let licenses = expression.licenses();
        assert_eq!(licenses, vec!["Apache-2.0".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,
            vec![
                "Apache-2.0".to_string(),
                "Classpath-exception-2.0".to_string(),
                "GPL-2.0-only".to_string(),
                "ISC".to_string(),
                "MIT".to_string()
            ]
        );
    }
}