use crate::context::{self, Context};
use crate::error::{Error, Result};
use crate::resource::{self, ObjectSummary};
use crate::storage::EnergymlStorage;
#[derive(Debug, Clone, Default, PartialEq)]
#[non_exhaustive]
pub struct Query {
pub dataspace: Option<String>,
pub types: Vec<String>,
pub type_contains: Option<String>,
pub title: Option<String>,
}
impl Query {
pub fn new() -> Self {
Self::default()
}
pub fn in_dataspace(mut self, uri: impl Into<String>) -> Self {
self.dataspace = Some(uri.into());
self
}
pub fn with_types<I: IntoIterator<Item = S>, S: Into<String>>(mut self, types: I) -> Self {
self.types = types.into_iter().map(Into::into).collect();
self
}
pub fn type_containing(mut self, s: impl Into<String>) -> Self {
self.type_contains = Some(s.into());
self
}
pub fn titled(mut self, s: impl Into<String>) -> Self {
self.title = Some(s.into());
self
}
}
#[derive(Debug)]
#[non_exhaustive]
pub struct Search {
pub objects: Vec<ObjectSummary>,
pub listed: usize,
pub headers_read: usize,
pub unreadable: Vec<(String, Error)>,
}
pub async fn find<S: EnergymlStorage + Sync + ?Sized>(
store: &S,
query: &Query,
) -> Result<Vec<ObjectSummary>> {
let mut search = find_report(store, query).await?;
if search.unreadable.is_empty() {
Ok(search.objects)
} else {
Err(search.unreadable.swap_remove(0).1)
}
}
pub async fn find_report<S: EnergymlStorage + Sync + ?Sized>(store: &S, query: &Query) -> Result<Search> {
check_wildcards(&query.types)?;
let listed = match &query.dataspace {
Some(dataspace) => {
let context = Context::dataspace(dataspace.clone()).with_types(query.types.iter().cloned());
store.resources_in(&context).await?.resources
}
None => {
let all = store.resources().await?;
if query.types.is_empty() {
all
} else {
all.into_iter()
.filter(|summary| {
query
.types
.iter()
.any(|pattern| context::type_matches(pattern, &summary.qualified_type))
})
.collect()
}
}
};
let listed_count = listed.len();
let candidates: Vec<ObjectSummary> = match &query.type_contains {
Some(filter) => listed
.into_iter()
.filter(|summary| resource::type_matches(&summary.qualified_type, filter))
.collect(),
None => listed,
};
let Some(filter) = &query.title else {
return Ok(Search {
objects: candidates,
listed: listed_count,
headers_read: 0,
unreadable: Vec::new(),
});
};
let uris: Vec<String> = candidates.iter().map(|summary| summary.uri.clone()).collect();
let headers = store.headers(&uris).await;
if headers.len() != candidates.len() {
return Err(Error::Remote {
store: store.store_kind(),
reason: format!(
"asked for {} header(s), the store answered {}",
candidates.len(),
headers.len()
),
});
}
let mut objects = Vec::new();
let mut unreadable = Vec::new();
for (summary, header) in candidates.into_iter().zip(headers) {
match header {
Ok(header) => {
if header
.title
.as_deref()
.is_some_and(|title| resource::title_matches(title, filter))
{
objects.push(summary);
}
}
Err(error) => unreadable.push((summary.uri, error)),
}
}
Ok(Search {
objects,
listed: listed_count,
headers_read: uris.len(),
unreadable,
})
}
pub async fn resolve_uuid<S: EnergymlStorage + Sync + ?Sized>(
store: &S,
uuid: &str,
qualified_type: Option<&str>,
) -> Result<String> {
if uuid.is_empty() {
return Err(Error::BadUri {
uri: String::new(),
reason: "an empty uuid names no object".into(),
});
}
let given = match qualified_type {
Some(qualified_type) => format!("{qualified_type}({uuid})"),
None => uuid.to_string(),
};
let mut matching: Vec<String> = store
.resources()
.await?
.into_iter()
.filter(|summary| summary.uuid.eq_ignore_ascii_case(uuid))
.filter(|summary| qualified_type.is_none_or(|wanted| summary.qualified_type == wanted))
.map(|summary| summary.uri)
.collect();
match matching.len() {
0 => Err(Error::NotFound(given)),
1 => Ok(matching.swap_remove(0)),
_ => Err(Error::Ambiguous {
uri: given,
candidates: matching,
}),
}
}
fn check_wildcards(types: &[String]) -> Result<()> {
match types
.iter()
.find(|t| t.contains('*') && (!t.ends_with(".*") || t[..t.len() - 1].contains('*')))
{
Some(odd) => Err(Error::BadUri {
uri: odd.clone(),
reason: "a type wildcard is a schema version followed by `.*` (ETP 1.2 ยง23.34.15)".into(),
}),
None => Ok(()),
}
}