Skip to main content

rullst_orm/
collection.rs

1use std::collections::HashMap;
2use std::hash::Hash;
3
4/// An extension trait that brings Laravel-style Collection methods natively to Rust's Vec<T>.
5pub trait RullstCollection<T> {
6    /// Keys the collection by the given closure's return value
7    fn key_by<K, F>(self, f: F) -> HashMap<K, T>
8    where
9        F: Fn(&T) -> K,
10        K: Hash + Eq;
11
12    /// Splits the collection into chunks of the given size
13    /// Maps each item using the given closure
14    fn map<U, F>(self, f: F) -> Vec<U>
15    where
16        F: FnMut(T) -> U;
17
18    /// Filters items using the given closure
19    fn filter<F>(self, f: F) -> Vec<T>
20    where
21        F: FnMut(&T) -> bool;
22
23    fn chunk(self, size: usize) -> Vec<Vec<T>>;
24
25    /// Joins the items into a single string using the given separator and closure
26    fn implode<F>(&self, separator: &str, f: F) -> String
27    where
28        F: Fn(&T) -> String;
29
30    /// Sums up the values returned by the closure
31    fn sum_by<N, F>(&self, f: F) -> N
32    where
33        F: Fn(&T) -> N,
34        N: std::iter::Sum;
35
36    /// Finds the maximum value returned by the closure
37    fn max_by_key<K, F>(&self, f: F) -> Option<&T>
38    where
39        F: Fn(&T) -> K,
40        K: Ord;
41
42    /// Finds the minimum value returned by the closure
43    fn min_by_key<K, F>(&self, f: F) -> Option<&T>
44    where
45        F: Fn(&T) -> K,
46        K: Ord;
47
48    /// Serializes the entire collection using an ApiResource transformer
49    fn collection_resource(&self) -> serde_json::Value
50    where
51        T: crate::resource::ApiResource;
52}
53
54impl<T> RullstCollection<T> for Vec<T> {
55    fn key_by<K, F>(self, f: F) -> HashMap<K, T>
56    where
57        F: Fn(&T) -> K,
58        K: Hash + Eq,
59    {
60        let mut map = HashMap::with_capacity(self.len());
61        for item in self {
62            map.insert(f(&item), item);
63        }
64        map
65    }
66
67    fn map<U, F>(self, f: F) -> Vec<U>
68    where
69        F: FnMut(T) -> U,
70    {
71        self.into_iter().map(f).collect()
72    }
73
74    fn filter<F>(self, f: F) -> Vec<T>
75    where
76        F: FnMut(&T) -> bool,
77    {
78        self.into_iter().filter(f).collect()
79    }
80
81    fn chunk(self, size: usize) -> Vec<Vec<T>> {
82        if self.is_empty() {
83            return vec![];
84        }
85        if size == 0 {
86            return vec![self];
87        }
88
89        let mut remaining = self.len();
90        let mut chunks = Vec::with_capacity(remaining.div_ceil(size));
91        let mut iter = self.into_iter();
92
93        while let Some(first) = iter.next() {
94            let chunk_cap = std::cmp::min(size, remaining);
95            let mut chunk = Vec::with_capacity(chunk_cap);
96            chunk.push(first);
97            remaining -= 1;
98            for _ in 1..size {
99                if let Some(item) = iter.next() {
100                    chunk.push(item);
101                    remaining -= 1;
102                } else {
103                    break;
104                }
105            }
106            chunks.push(chunk);
107        }
108
109        chunks
110    }
111
112    fn implode<F>(&self, separator: &str, f: F) -> String
113    where
114        F: Fn(&T) -> String,
115    {
116        // Pre-allocate to reduce intermediate allocations
117        let mut result = String::with_capacity(self.len() * 16);
118        let mut iter = self.iter();
119        if let Some(first) = iter.next() {
120            result.push_str(&f(first));
121            for item in iter {
122                result.push_str(separator);
123                result.push_str(&f(item));
124            }
125        }
126        result
127    }
128
129    fn sum_by<N, F>(&self, f: F) -> N
130    where
131        F: Fn(&T) -> N,
132        N: std::iter::Sum,
133    {
134        self.iter().map(f).sum()
135    }
136
137    fn max_by_key<K, F>(&self, f: F) -> Option<&T>
138    where
139        F: Fn(&T) -> K,
140        K: Ord,
141    {
142        self.iter().max_by_key(|item| f(*item))
143    }
144
145    fn min_by_key<K, F>(&self, f: F) -> Option<&T>
146    where
147        F: Fn(&T) -> K,
148        K: Ord,
149    {
150        self.iter().min_by_key(|item| f(*item))
151    }
152
153    #[cfg_attr(test, mutants::skip)]
154    fn collection_resource(&self) -> serde_json::Value
155    where
156        T: crate::resource::ApiResource,
157    {
158        crate::resource::ResourceCollection::new(self).resolve()
159    }
160}
161
162#[cfg(test)]
163mod tests {
164    use super::*;
165
166    #[test]
167    fn test_key_by() {
168        let v = vec![(1u32, "a"), (2, "b"), (3, "c")];
169        let map = v.key_by(|(k, _)| *k);
170        assert_eq!(map[&1].1, "a");
171        assert_eq!(map[&3].1, "c");
172    }
173
174    #[test]
175    fn test_map() {
176        let v = vec![1, 2, 3];
177        let mapped = v.map(|x| x * 2);
178        assert_eq!(mapped, vec![2, 4, 6]);
179    }
180
181    #[test]
182    fn test_filter() {
183        let v = vec![1, 2, 3, 4];
184        let filtered = v.filter(|x| x % 2 == 0);
185        assert_eq!(filtered, vec![2, 4]);
186    }
187
188    #[test]
189    fn test_chunk_even() {
190        let v = vec![1, 2, 3, 4];
191        let chunks = v.chunk(2);
192        assert_eq!(chunks.len(), 2);
193        assert_eq!(chunks[0], vec![1, 2]);
194        assert_eq!(chunks[1], vec![3, 4]);
195    }
196
197    #[test]
198    fn test_chunk_with_remainder() {
199        let v = vec![1, 2, 3, 4, 5];
200        let chunks = v.chunk(2);
201        assert_eq!(chunks.len(), 3);
202        assert_eq!(chunks[2], vec![5]);
203        assert_eq!(chunks[0].capacity(), 2);
204        assert_eq!(chunks[1].capacity(), 2);
205        assert_eq!(chunks[2].capacity(), 1);
206    }
207
208    #[test]
209    fn test_chunk_zero_returns_all() {
210        let v = vec![1, 2, 3];
211        let chunks = v.chunk(0);
212        assert_eq!(chunks.len(), 1);
213        assert_eq!(chunks[0], vec![1, 2, 3]);
214    }
215
216    #[test]
217    fn test_implode() {
218        let v = vec![1, 2, 3];
219        let result = v.implode(", ", |n| n.to_string());
220        assert_eq!(result, "1, 2, 3");
221    }
222
223    #[test]
224    fn test_implode_single_element() {
225        let v = vec![42];
226        let result = v.implode(", ", |n| n.to_string());
227        assert_eq!(result, "42");
228    }
229
230    #[test]
231    fn test_sum_by() {
232        let v = vec![1, 2, 3, 4];
233        let sum: i32 = v.sum_by(|n| *n);
234        assert_eq!(sum, 10);
235    }
236
237    #[test]
238    fn test_max_by_key() {
239        let v = vec![3, 1, 4, 1, 5, 9];
240        let max = v.max_by_key(|n| *n);
241        assert_eq!(max, Some(&9));
242    }
243
244    #[test]
245    fn test_min_by_key() {
246        let v = vec![3, 1, 4, 1, 5, 9];
247        let min = v.min_by_key(|n| *n);
248        assert_eq!(min, Some(&1));
249    }
250
251    #[test]
252    fn test_empty_collection() {
253        let v: Vec<i32> = vec![];
254        assert!(v.max_by_key(|n| *n).is_none());
255        assert!(v.min_by_key(|n| *n).is_none());
256        let sum: i32 = v.sum_by(|n| *n);
257        assert_eq!(sum, 0);
258    }
259
260    #[test]
261    fn test_chunk_larger_than_len() {
262        let v = vec![1, 2];
263        let chunks = v.chunk(5);
264        assert_eq!(chunks.len(), 1);
265        assert_eq!(chunks[0], vec![1, 2]);
266    }
267
268    #[test]
269    fn test_chunk_empty() {
270        let v: Vec<i32> = vec![];
271        let chunks = v.chunk(2);
272        assert!(chunks.is_empty());
273    }
274
275    #[test]
276    fn test_chunk_usize_max() {
277        let v = vec![1, 2, 3];
278        let chunks = v.chunk(usize::MAX);
279        assert_eq!(chunks.len(), 1);
280        assert_eq!(chunks[0], vec![1, 2, 3]);
281    }
282
283    struct DummyResource {
284        id: i32,
285    }
286
287    impl crate::resource::ApiResource for DummyResource {
288        fn to_array(&self) -> serde_json::Value {
289            crate::_serde_json::json!({ "id": self.id })
290        }
291    }
292
293    #[test]
294    fn test_collection_resource() {
295        let vec = vec![DummyResource { id: 1 }, DummyResource { id: 2 }];
296        let res = vec.collection_resource();
297        assert_eq!(res, crate::_serde_json::json!([{"id": 1}, {"id": 2}]));
298    }
299}