Skip to content

Commit

Permalink
feat: introduce ability to apply a Query to any random context
Browse files Browse the repository at this point in the history
Should make it easier to apply the Query syntax to any context defined
by a HashMap of strings. Cleans up the analysis endpoint logic
considerably, I think.

Signed-off-by: Jim Crossley <[email protected]>
  • Loading branch information
jcrossley3 committed Oct 19, 2024
1 parent dd221df commit 2edbb11
Show file tree
Hide file tree
Showing 3 changed files with 61 additions and 68 deletions.
45 changes: 44 additions & 1 deletion common/src/db/query.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ use sea_orm::{
Iterable, Order, QueryFilter, QueryOrder, Select, Value,
};
use sea_query::{BinOper, ColumnRef, Expr, IntoColumnRef, Keyword, SimpleExpr};
use std::collections::HashMap;
use std::fmt::{Display, Formatter};
use std::str::FromStr;
use std::sync::OnceLock;
Expand Down Expand Up @@ -73,6 +74,42 @@ impl Query {
}
}

/// Apply the query to a HashMap of Strings, returning true if any
/// values in the context match any components of the query,
/// either a filter or a full-text search
pub fn apply(&self, context: HashMap<&'static str, impl Display>) -> bool {
use Operator::*;
self.parse().iter().all(|c| match c {
Constraint {
field: Some(f),
op: Some(o),
value: vs,
} => context
.get(f.as_str())
.map(|s| s.to_string())
.is_some_and(|f| match o {
Equal => vs.iter().any(|v| f.eq(v)),
NotEqual => vs.iter().all(|v| !f.eq(v)),
Like => vs.iter().any(|v| f.contains(v)),
NotLike => vs.iter().all(|v| !f.contains(v)),
GreaterThan => todo!(),
GreaterThanOrEqual => todo!(),
LessThan => todo!(),
LessThanOrEqual => todo!(),
_ => false,
}),
Constraint {
field: None,
value: vs,
..
} => context
.values()
.map(|s| s.to_string())
.any(|s| vs.iter().any(|v| s.contains(v))),
_ => false,
})
}

fn parse(&self) -> Vec<Constraint> {
// regex for filters: {field}{op}{value}
const RE: &str = r"^(?<field>[[:word:]]+)(?<op>=|!=|~|!~|>=|>|<=|<)(?<value>.*)$";
Expand Down Expand Up @@ -170,7 +207,13 @@ impl<T: EntityTrait> Filtering<T> for Select<T> {
}

#[derive(
Default, Debug, serde::Deserialize, serde::Serialize, utoipa::ToSchema, utoipa::IntoParams,
Clone,
Default,
Debug,
serde::Deserialize,
serde::Serialize,
utoipa::ToSchema,
utoipa::IntoParams,
)]
#[serde(rename_all = "camelCase")]
pub struct Query {
Expand Down
5 changes: 1 addition & 4 deletions modules/analysis/src/endpoints.rs
Original file line number Diff line number Diff line change
Expand Up @@ -439,10 +439,7 @@ mod test {

// filter on sbom_id (which has urn:uuid: prefix)
let sbom_id = response["items"][0]["sbom_id"].as_str().unwrap();
let uri = format!(
"/api/v1/analysis/root-component?q=sbom_id=urn:uuid:{}",
sbom_id
);
let uri = format!("/api/v1/analysis/root-component?q=sbom_id={}", sbom_id);
let request: Request = TestRequest::get().uri(uri.clone().as_str()).to_request();
let response: Value = app.call_and_read_body_json(request).await;
assert_eq!(&response["total"], 7);
Expand Down
79 changes: 16 additions & 63 deletions modules/analysis/src/service.rs
Original file line number Diff line number Diff line change
Expand Up @@ -274,23 +274,6 @@ pub async fn load_graphs(
}
}

fn convert_query_to_hashmap(query: &Query) -> HashMap<String, String> {
if query.q.contains('=') {
query
.q
.split('&')
.filter_map(|pair| {
pair.split_once('=').map(|(key, value)| {
let value = value.strip_prefix("urn:uuid:").unwrap_or(value);
(key.to_owned(), value.to_owned())
})
})
.collect()
} else {
HashMap::from([("re_name".to_owned(), query.q.clone())])
}
}

impl AnalysisService {
pub fn new(db: Database) -> Self {
GraphMap::get_instance();
Expand Down Expand Up @@ -358,9 +341,8 @@ impl AnalysisService {
) -> Result<PaginatedResults<AncestorSummary>, Error> {
let connection = self.db.connection(&tx);

let graph_query_map = convert_query_to_hashmap(&query);
let search_sbom_node_name_subquery = sbom_node::Entity::find()
.filtering(query)?
.filtering(query.clone())?
.select_only()
.column(sbom_node::Column::SbomId)
.distinct()
Expand Down Expand Up @@ -389,27 +371,13 @@ impl AnalysisService {
graph
.node_indices()
.filter(|&i| {
if let Some(node) = graph.node_weight(i) {
if let Some(re_name) = graph_query_map.get("re_name") {
// if no specific url params supplied then use contains search
node.name.contains(re_name)
} else {
// if any specific url params supplied then match equals
let matches_sbom_id = graph_query_map
.get("sbom_id")
.map_or(true, |sbom_id| node.sbom_id.eq(sbom_id));
let matches_node_id = graph_query_map
.get("node_id")
.map_or(true, |node_id| node.node_id.eq(node_id));
let matches_name =
graph_query_map.get("name").map_or(true, |name| {
!name.is_empty() && node.name.eq(name)
});
matches_sbom_id && matches_node_id && matches_name
}
} else {
false // Return false if the node does not exist
}
graph.node_weight(i).is_some_and(|node| {
query.apply(HashMap::from([
("sbom_id", &node.sbom_id),
("node_id", &node.node_id),
("name", &node.name),
]))
})
})
.for_each(|node_index| {
if let Some(find_match_package_node) = graph.node_weight(node_index) {
Expand Down Expand Up @@ -599,9 +567,8 @@ impl AnalysisService {
) -> Result<PaginatedResults<DepSummary>, Error> {
let connection = self.db.connection(&tx);

let graph_query_map = convert_query_to_hashmap(&query);
let search_sbom_node_name_subquery = sbom_node::Entity::find()
.filtering(query)?
.filtering(query.clone())?
.select_only()
.column(sbom_node::Column::SbomId)
.distinct()
Expand Down Expand Up @@ -630,27 +597,13 @@ impl AnalysisService {
graph
.node_indices()
.filter(|&i| {
if let Some(node) = graph.node_weight(i) {
if let Some(re_name) = graph_query_map.get("re_name") {
// if no specific url params supplied then use contains search
node.name.contains(re_name)
} else {
// if any specific url params supplied then match equals
let matches_sbom_id = graph_query_map
.get("sbom_id")
.map_or(true, |sbom_id| node.sbom_id.eq(sbom_id));
let matches_node_id = graph_query_map
.get("node_id")
.map_or(true, |node_id| node.node_id.eq(node_id));
let matches_name =
graph_query_map.get("name").map_or(true, |name| {
!name.is_empty() && node.name.eq(name)
});
matches_sbom_id && matches_node_id && matches_name
}
} else {
false // Return false if the node does not exist
}
graph.node_weight(i).is_some_and(|node| {
query.apply(HashMap::from([
("sbom_id", &node.sbom_id),
("node_id", &node.node_id),
("name", &node.name),
]))
})
})
.for_each(|node_index| {
if let Some(find_match_package_node) = graph.node_weight(node_index) {
Expand Down

0 comments on commit 2edbb11

Please sign in to comment.