-
Notifications
You must be signed in to change notification settings - Fork 43
Expand file tree
/
Copy pathservice.rs
More file actions
157 lines (142 loc) · 5.08 KB
/
service.rs
File metadata and controls
157 lines (142 loc) · 5.08 KB
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
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
use crate::{Error, common::LicenseRefMapping, source_document::model::SourceDocument};
use sea_orm::{ConnectionTrait, DbBackend, FromQueryResult, PaginatorTrait, Statement};
use spdx_expression;
use std::collections::BTreeMap;
use tracing::instrument;
use trustify_module_storage::service::{StorageBackend, StorageKey, dispatch::DispatchBackend};
#[derive(Copy, Clone, Eq, PartialEq, Debug)]
pub enum DocumentType {
Advisory,
Sbom,
}
/// Fetch all unique key/value labels matching the `filter_text` for all the `r#type` entities, i.e. `DocumentType::Advisory` or `DocumentType::Sbom`
///
/// If limit=0 then all data will be fetched
#[instrument(skip(connection), err(level=tracing::Level::INFO))]
pub async fn fetch_labels<C: ConnectionTrait>(
r#type: DocumentType,
filter_text: String,
limit: u64,
connection: &C,
) -> Result<Vec<serde_json::Value>, Error> {
let sql = format!(
r#"
SELECT DISTINCT ON (kv.key, kv.value)
kv.key,
CASE
WHEN kv.value IS NULL OR kv.value = '' THEN NULL
ELSE kv.value
END AS value
FROM {table},
LATERAL jsonb_each_text(labels) AS kv
WHERE
CASE
WHEN kv.value IS NULL THEN kv.key
ELSE kv.key || '=' || kv.value
END ILIKE $1 ESCAPE '\'
ORDER BY
kv.key, kv.value
"#,
table = match r#type {
DocumentType::Advisory => "advisory",
DocumentType::Sbom => "sbom",
}
);
let statement = Statement::from_sql_and_values(
DbBackend::Postgres,
sql,
[format!("%{}%", escape(filter_text)).into()],
);
let selector = serde_json::Value::find_by_statement(statement);
let labels: Vec<serde_json::Value> = if limit == 0 {
selector.all(connection).await?
} else {
selector.paginate(connection, limit).fetch().await?
};
Ok(labels)
}
fn escape(text: String) -> String {
text.replace('%', "\\").replace('\\', "\\\\")
}
/// Delete the original raw json doc from storage. An appropriate
/// message is returned in the event of an error, but it's up to the
/// caller to either log the message or return failure to its caller.
pub async fn delete_doc(doc: &SourceDocument, storage: impl DocumentDelete) -> Result<(), Error> {
let key = doc.try_into()?;
storage.delete(key).await
}
pub trait DocumentDelete {
fn delete(&self, key: StorageKey) -> impl Future<Output = Result<(), Error>>;
}
impl DocumentDelete for &DispatchBackend {
async fn delete(&self, key: StorageKey) -> Result<(), Error> {
(*self).delete(key).await.map_err(Error::Storage)
}
}
/// Extract LicenseRef mappings from SPDX license expressions
///
/// This function parses SPDX license expressions and extracts LicenseRef mappings,
/// which are then added to the provided `licenses_ref_mapping` vector.
///
/// # Arguments
/// * `license_name` - The SPDX license expression to parse
/// * `licensing_infos` - A BTreeMap containing license ID to license name mappings
/// * `licenses_ref_mapping` - A mutable vector where LicenseRef mappings will be added
pub fn extract_license_ref_mappings(
license_name: &str,
licensing_infos: &BTreeMap<String, String>,
licenses_ref_mapping: &mut Vec<LicenseRefMapping>,
) {
if let Ok(parsed) = spdx_expression::SpdxExpression::parse(license_name) {
parsed
.licenses()
.into_iter()
.filter(|license| license.license_ref)
.for_each(|license| {
let license_id = license.to_string();
let license_name = licensing_infos
.get(&license_id)
.cloned()
.unwrap_or_default();
licenses_ref_mapping.push(LicenseRefMapping {
license_id,
license_name,
});
});
}
}
#[cfg(test)]
mod test {
use super::*;
use anyhow::anyhow;
use test_log::test;
use trustify_module_storage::service::StorageKey;
#[test(tokio::test)]
async fn delete_failure() -> Result<(), anyhow::Error> {
// Setup mock that simulates a delete error
struct FailingDelete {}
impl DocumentDelete for FailingDelete {
async fn delete(&self, _key: StorageKey) -> Result<(), Error> {
Err(Error::Storage(anyhow!("Delete failed")))
}
}
// Failing to delete an invalid doc from storage should log an error
let doc = SourceDocument::default();
match delete_doc(&doc, FailingDelete {}).await {
Ok(_) => panic!("expected error"),
Err(e) => assert!(e.to_string().contains("Missing prefix")),
};
// Failing to delete a valid doc from storage should log a different error
let doc = SourceDocument {
sha256: String::from(
"sha256:488c5d97daed3613746f0c246f4a3d1b26ea52ce43d6bdd33f4219f881a00c07",
),
..Default::default()
};
match delete_doc(&doc, FailingDelete {}).await {
Ok(_) => panic!("expected error"),
Err(e) => assert!(e.to_string().contains("Delete failed")),
};
Ok(())
}
}