Skip to main content

acorn/io/api/
spdx.rs

1//! Module for working with SPDX license data
2//!
3//! See `https://github.com/spdx/license-list-data/blob/main/accessingLicenses.md` for more information on the SPDX license list and how to access it.
4//!
5//! See `https://spdx.org/rdf/terms/` for SPDX RDF terms.
6use crate::io::api::{DatabasePersistence, RemoteResource, INCLUDED_ENDPOINTS};
7use crate::io::database::schema::{LicenseRow, Table};
8use crate::io::database::{Database, Operations};
9use crate::io::{with_progress, ApiResult, ProgressType};
10use crate::schema::validate::{is_semantic_version, is_urls};
11use crate::util::{Label, Searchable};
12use async_trait::async_trait;
13use color_eyre::eyre::{eyre, Report};
14use serde::{Deserialize, Serialize};
15use validator::Validate;
16
17/// SPDX license metadata
18#[derive(Clone, Debug, Default, Deserialize, Serialize, Validate)]
19#[serde(rename_all = "camelCase")]
20pub struct LicenseMetadata {
21    /// SPDX license identifier
22    pub license_id: String,
23    /// Name of the license
24    pub name: String,
25    /// URL to a JSON file containing the license detailed information
26    #[validate(url)]
27    pub details_url: String,
28    /// Reference to the HTML format for the license file
29    #[validate(url)]
30    pub reference: String,
31    /// Deprecated - this field is generated and is no longer in use
32    pub reference_number: u32,
33    /// Cross reference URL(s) pointing to additional copies of the license
34    #[validate(custom(function = "is_urls"))]
35    pub see_also: Vec<String>,
36    /// Specifies whether the license is deprecated
37    /// ### Note
38    /// This isn't actually the best name for this particular field
39    #[serde(rename = "isDeprecatedLicenseId")]
40    pub is_deprecated: bool,
41    /// Specifies whether the License is listed as free by the Free Software Foundation (FSF)
42    ///
43    /// See <https://www.fsf.org/licensing/compliance> for more information
44    #[serde(default, rename = "isFsfLibre")]
45    pub is_free_software: bool,
46    /// Specifies whether the license is approved by the Open Source Initiative (OSI)
47    #[serde(rename = "isOsiApproved")]
48    pub is_osi: bool,
49}
50/// Struct for SPDX license list data
51#[derive(Clone, Debug, Deserialize, Serialize, Validate)]
52#[serde(rename_all = "camelCase")]
53#[derive(Default)]
54pub struct LicensesResponse {
55    /// Version of the SPDX License List
56    #[validate(custom(function = "is_semantic_version"))]
57    pub license_list_version: Option<String>,
58    /// Licenses
59    #[validate(nested)]
60    pub licenses: Vec<LicenseMetadata>,
61}
62impl From<LicenseMetadata> for LicenseRow {
63    fn from(meta: LicenseMetadata) -> Self {
64        let LicenseMetadata {
65            license_id,
66            name,
67            is_deprecated,
68            is_free_software,
69            is_osi,
70            ..
71        } = meta;
72        LicenseRow::init()
73            .identifier(license_id)
74            .name(name)
75            .is_deprecated(is_deprecated)
76            .is_free_software(is_free_software)
77            .is_open_source(is_osi)
78            .build()
79    }
80}
81impl LicenseMetadata {
82    /// Check if the license is OSI approved
83    pub fn is_open_source(&self) -> bool {
84        self.is_osi
85    }
86}
87#[async_trait]
88impl DatabasePersistence for LicensesResponse {
89    /// Persist SPDX license data to local database
90    /// ### Example
91    /// ```ignore
92    /// use acorn::io::api::spdx;
93    /// use acorn::io::database::schema::Table;
94    /// use acorn::io::database::{Database, Operations};
95    ///
96    /// let licenses = spdx::download().await.ok().unwrap();
97    /// let database = Database::<Table>::from_path(database_path.clone());
98    /// let _ = database.persist(Some(licenses));
99    /// ```
100    async fn persist(self, database: Database<Table>) -> ApiResult<usize> {
101        let Self { licenses, .. } = self;
102        let message: fn(&LicenseMetadata) -> String = |item| format!("Saving \"{}\" license metadata", item.license_id);
103        let operation = |item| async { database.insert(LicenseRow::from(item)) };
104        let finish = |count| format!("{}Saved metadata for {count} SPDX licenses", Label::CHECKMARK);
105        with_progress(licenses, message, operation, finish, None, ProgressType::Bar)
106            .await
107            .map(|counts| counts.into_iter().sum())
108            .map_err(Report::msg)
109    }
110}
111/// Download SPDX license data from homepage
112/// ### Example
113/// ```ignore
114/// use acorn::io::api::spdx;
115///
116/// let result = spdx::download().await;
117/// ```
118pub async fn download() -> ApiResult<LicensesResponse> {
119    match INCLUDED_ENDPOINTS.find_by_name("spdx") {
120        | Some(endpoint) => {
121            let response = endpoint.invoke("licenses", None).await;
122            endpoint.handle::<LicensesResponse>(response)
123        }
124        | None => Err(eyre!("No SPDX endpoint found")),
125    }
126}