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 chunks = Vec::with_capacity(self.len().div_ceil(size));
90        let mut vec = self;
91
92        while !vec.is_empty() {
93            let rem = vec.len() % size;
94            let take = if rem == 0 { size } else { rem };
95            let start = vec.len() - take;
96            let chunk = vec.split_off(start);
97            chunks.push(chunk);
98        }
99        chunks.reverse();
100
101        chunks
102    }
103
104    fn implode<F>(&self, separator: &str, f: F) -> String
105    where
106        F: Fn(&T) -> String,
107    {
108        // Pre-allocate to reduce intermediate allocations
109        let mut result = String::with_capacity(self.len() * 16);
110        let mut iter = self.iter();
111        if let Some(first) = iter.next() {
112            result.push_str(&f(first));
113            for item in iter {
114                result.push_str(separator);
115                result.push_str(&f(item));
116            }
117        }
118        result
119    }
120
121    fn sum_by<N, F>(&self, f: F) -> N
122    where
123        F: Fn(&T) -> N,
124        N: std::iter::Sum,
125    {
126        self.iter().map(f).sum()
127    }
128
129    fn max_by_key<K, F>(&self, f: F) -> Option<&T>
130    where
131        F: Fn(&T) -> K,
132        K: Ord,
133    {
134        self.iter().max_by_key(|item| f(*item))
135    }
136
137    fn min_by_key<K, F>(&self, f: F) -> Option<&T>
138    where
139        F: Fn(&T) -> K,
140        K: Ord,
141    {
142        self.iter().min_by_key(|item| f(*item))
143    }
144
145    #[cfg_attr(test, mutants::skip)]
146    fn collection_resource(&self) -> serde_json::Value
147    where
148        T: crate::resource::ApiResource,
149    {
150        crate::resource::ResourceCollection::new(self).resolve()
151    }
152}
153
154#[cfg(test)]
155mod tests {
156    use super::*;
157
158    #[test]
159    fn test_key_by() {
160        let v = vec![(1u32, "a"), (2, "b"), (3, "c")];
161        let map = v.key_by(|(k, _)| *k);
162        assert_eq!(map[&1].1, "a");
163        assert_eq!(map[&3].1, "c");
164    }
165
166    #[test]
167    fn test_map() {
168        let v = vec![1, 2, 3];
169        let mapped = v.map(|x| x * 2);
170        assert_eq!(mapped, vec![2, 4, 6]);
171    }
172
173    #[test]
174    fn test_filter() {
175        let v = vec![1, 2, 3, 4];
176        let filtered = v.filter(|x| x % 2 == 0);
177        assert_eq!(filtered, vec![2, 4]);
178    }
179
180    #[test]
181    fn test_chunk_even() {
182        let v = vec![1, 2, 3, 4];
183        let chunks = v.chunk(2);
184        assert_eq!(chunks.len(), 2);
185        assert_eq!(chunks[0], vec![1, 2]);
186        assert_eq!(chunks[1], vec![3, 4]);
187    }
188
189    #[test]
190    fn test_chunk_with_remainder() {
191        let v = vec![1, 2, 3, 4, 5];
192        let chunks = v.chunk(2);
193        assert_eq!(chunks.len(), 3);
194        assert_eq!(chunks[2], vec![5]);
195    }
196
197    #[test]
198    fn test_chunk_zero_returns_all() {
199        let v = vec![1, 2, 3];
200        let chunks = v.chunk(0);
201        assert_eq!(chunks.len(), 1);
202        assert_eq!(chunks[0], vec![1, 2, 3]);
203    }
204
205    #[test]
206    fn test_implode() {
207        let v = vec![1, 2, 3];
208        let result = v.implode(", ", |n| n.to_string());
209        assert_eq!(result, "1, 2, 3");
210    }
211
212    #[test]
213    fn test_implode_single_element() {
214        let v = vec![42];
215        let result = v.implode(", ", |n| n.to_string());
216        assert_eq!(result, "42");
217    }
218
219    #[test]
220    fn test_sum_by() {
221        let v = vec![1, 2, 3, 4];
222        let sum: i32 = v.sum_by(|n| *n);
223        assert_eq!(sum, 10);
224    }
225
226    #[test]
227    fn test_max_by_key() {
228        let v = vec![3, 1, 4, 1, 5, 9];
229        let max = v.max_by_key(|n| *n);
230        assert_eq!(max, Some(&9));
231    }
232
233    #[test]
234    fn test_min_by_key() {
235        let v = vec![3, 1, 4, 1, 5, 9];
236        let min = v.min_by_key(|n| *n);
237        assert_eq!(min, Some(&1));
238    }
239
240    #[test]
241    fn test_empty_collection() {
242        let v: Vec<i32> = vec![];
243        assert!(v.max_by_key(|n| *n).is_none());
244        assert!(v.min_by_key(|n| *n).is_none());
245        let sum: i32 = v.sum_by(|n| *n);
246        assert_eq!(sum, 0);
247    }
248
249    #[test]
250    fn test_chunk_larger_than_len() {
251        let v = vec![1, 2];
252        let chunks = v.chunk(5);
253        assert_eq!(chunks.len(), 1);
254        assert_eq!(chunks[0], vec![1, 2]);
255    }
256
257    #[test]
258    fn test_chunk_empty() {
259        let v: Vec<i32> = vec![];
260        let chunks = v.chunk(2);
261        assert!(chunks.is_empty());
262    }
263
264    #[test]
265    fn test_chunk_usize_max() {
266        let v = vec![1, 2, 3];
267        let chunks = v.chunk(usize::MAX);
268        assert_eq!(chunks.len(), 1);
269        assert_eq!(chunks[0], vec![1, 2, 3]);
270    }
271}