use lino_objects_codec::LinoValue;
use crate::query::{CollectionQuery, Filter, QueryParams, SortKey, form_encode};
use crate::value::{array, compare, get, int, object};
pub fn matches_filters(item: &LinoValue, filters: &[Filter]) -> bool {
filters.iter().all(|filter| {
let actual = get(item, &filter.field).cloned().unwrap_or(LinoValue::Null);
filter.values.contains(&actual)
})
}
pub fn sort_items(items: &[LinoValue], sort: &[SortKey]) -> Vec<LinoValue> {
let mut sorted = items.to_vec();
if sort.is_empty() {
return sorted;
}
sorted.sort_by(|left, right| {
for key in sort {
let left_value = get(left, &key.field).cloned().unwrap_or(LinoValue::Null);
let right_value = get(right, &key.field).cloned().unwrap_or(LinoValue::Null);
let comparison = compare(&left_value, &right_value);
if comparison != std::cmp::Ordering::Equal {
return if key.descending {
comparison.reverse()
} else {
comparison
};
}
}
std::cmp::Ordering::Equal
});
sorted
}
pub fn project_fields(item: &LinoValue, fields: Option<&Vec<String>>) -> LinoValue {
let Some(fields) = fields else {
return item.clone();
};
let Some(members) = crate::value::as_object(item) else {
return item.clone();
};
LinoValue::Object(
fields
.iter()
.filter_map(|field| {
members
.iter()
.find(|(name, _)| name == field)
.map(|(name, value)| (name.clone(), value.clone()))
})
.collect(),
)
}
pub fn collection_envelope(
items: Vec<LinoValue>,
limit: usize,
offset: usize,
total: usize,
) -> LinoValue {
let count = items.len();
object([
("items", array(items)),
(
"page",
object([
("limit", int(limit as i64)),
("offset", int(offset as i64)),
("total", int(total as i64)),
("count", int(count as i64)),
]),
),
])
}
pub fn apply_collection_query(items: &[LinoValue], query: &CollectionQuery) -> LinoValue {
let filtered: Vec<LinoValue> = items
.iter()
.filter(|item| matches_filters(item, &query.filters))
.cloned()
.collect();
let ordered = sort_items(&filtered, &query.sort);
let page: Vec<LinoValue> = ordered
.into_iter()
.skip(query.offset)
.take(query.limit)
.map(|item| project_fields(&item, query.fields.as_ref()))
.collect();
collection_envelope(page, query.limit, query.offset, filtered.len())
}
pub fn pagination_link_header(
path: &str,
query: &QueryParams,
limit: usize,
offset: usize,
total: usize,
) -> String {
if limit == 0 {
return String::new();
}
let build = |target_offset: usize| {
let mut parameters: Vec<String> = query
.iter()
.filter(|(name, _)| *name != "limit" && *name != "offset")
.map(|(name, value)| format!("{}={}", form_encode(name), form_encode(value)))
.collect();
parameters.push(format!("limit={limit}"));
parameters.push(format!("offset={target_offset}"));
format!("{path}?{}", parameters.join("&"))
};
let last_offset = if total == 0 {
0
} else {
((total - 1) / limit) * limit
};
let mut links = vec![format!("<{}>; rel=\"first\"", build(0))];
if offset > 0 {
links.push(format!(
"<{}>; rel=\"prev\"",
build(offset.saturating_sub(limit))
));
}
if offset + limit < total {
links.push(format!("<{}>; rel=\"next\"", build(offset + limit)));
}
links.push(format!("<{}>; rel=\"last\"", build(last_offset)));
links.join(", ")
}