lino_rest_api/
collection.rs1use lino_objects_codec::LinoValue;
5
6use crate::query::{CollectionQuery, Filter, QueryParams, SortKey, form_encode};
7use crate::value::{array, compare, get, int, object};
8
9pub fn matches_filters(item: &LinoValue, filters: &[Filter]) -> bool {
14 filters.iter().all(|filter| {
15 let actual = get(item, &filter.field).cloned().unwrap_or(LinoValue::Null);
16 filter.values.contains(&actual)
17 })
18}
19
20pub fn sort_items(items: &[LinoValue], sort: &[SortKey]) -> Vec<LinoValue> {
25 let mut sorted = items.to_vec();
26 if sort.is_empty() {
27 return sorted;
28 }
29 sorted.sort_by(|left, right| {
30 for key in sort {
31 let left_value = get(left, &key.field).cloned().unwrap_or(LinoValue::Null);
32 let right_value = get(right, &key.field).cloned().unwrap_or(LinoValue::Null);
33 let comparison = compare(&left_value, &right_value);
34 if comparison != std::cmp::Ordering::Equal {
35 return if key.descending {
36 comparison.reverse()
37 } else {
38 comparison
39 };
40 }
41 }
42 std::cmp::Ordering::Equal
43 });
44 sorted
45}
46
47pub fn project_fields(item: &LinoValue, fields: Option<&Vec<String>>) -> LinoValue {
49 let Some(fields) = fields else {
50 return item.clone();
51 };
52 let Some(members) = crate::value::as_object(item) else {
53 return item.clone();
54 };
55 LinoValue::Object(
56 fields
57 .iter()
58 .filter_map(|field| {
59 members
60 .iter()
61 .find(|(name, _)| name == field)
62 .map(|(name, value)| (name.clone(), value.clone()))
63 })
64 .collect(),
65 )
66}
67
68pub fn collection_envelope(
80 items: Vec<LinoValue>,
81 limit: usize,
82 offset: usize,
83 total: usize,
84) -> LinoValue {
85 let count = items.len();
86 object([
87 ("items", array(items)),
88 (
89 "page",
90 object([
91 ("limit", int(limit as i64)),
92 ("offset", int(offset as i64)),
93 ("total", int(total as i64)),
94 ("count", int(count as i64)),
95 ]),
96 ),
97 ])
98}
99
100pub fn apply_collection_query(items: &[LinoValue], query: &CollectionQuery) -> LinoValue {
102 let filtered: Vec<LinoValue> = items
103 .iter()
104 .filter(|item| matches_filters(item, &query.filters))
105 .cloned()
106 .collect();
107 let ordered = sort_items(&filtered, &query.sort);
108 let page: Vec<LinoValue> = ordered
109 .into_iter()
110 .skip(query.offset)
111 .take(query.limit)
112 .map(|item| project_fields(&item, query.fields.as_ref()))
113 .collect();
114
115 collection_envelope(page, query.limit, query.offset, filtered.len())
116}
117
118pub fn pagination_link_header(
123 path: &str,
124 query: &QueryParams,
125 limit: usize,
126 offset: usize,
127 total: usize,
128) -> String {
129 if limit == 0 {
130 return String::new();
131 }
132
133 let build = |target_offset: usize| {
134 let mut parameters: Vec<String> = query
135 .iter()
136 .filter(|(name, _)| *name != "limit" && *name != "offset")
137 .map(|(name, value)| format!("{}={}", form_encode(name), form_encode(value)))
138 .collect();
139 parameters.push(format!("limit={limit}"));
140 parameters.push(format!("offset={target_offset}"));
141 format!("{path}?{}", parameters.join("&"))
142 };
143
144 let last_offset = if total == 0 {
145 0
146 } else {
147 ((total - 1) / limit) * limit
148 };
149 let mut links = vec![format!("<{}>; rel=\"first\"", build(0))];
150 if offset > 0 {
151 links.push(format!(
152 "<{}>; rel=\"prev\"",
153 build(offset.saturating_sub(limit))
154 ));
155 }
156 if offset + limit < total {
157 links.push(format!("<{}>; rel=\"next\"", build(offset + limit)));
158 }
159 links.push(format!("<{}>; rel=\"last\"", build(last_offset)));
160
161 links.join(", ")
162}