Skip to main content

gitlab/api/groups/
groups.rs

1// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
2// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
3// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
4// option. This file may not be copied, modified, or distributed
5// except according to those terms.
6
7use std::collections::{BTreeMap, BTreeSet};
8
9use chrono::NaiveDate;
10use derive_builder::Builder;
11
12use crate::api::common::{AccessLevel, SortOrder};
13use crate::api::endpoint_prelude::*;
14use crate::api::ParamValue;
15
16/// Keys group results may be ordered by.
17#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
18#[non_exhaustive]
19pub enum GroupOrderBy {
20    /// Order by the name of the group.
21    #[default]
22    Name,
23    /// Order by the full path of the group.
24    Path,
25    /// Order by the group ID.
26    Id,
27    /// Order by similarity (only relevant for searches).
28    Similarity,
29}
30
31impl GroupOrderBy {
32    fn use_keyset_pagination(self) -> bool {
33        matches!(self, GroupOrderBy::Name)
34    }
35
36    /// The ordering as a query parameter.
37    fn as_str(self) -> &'static str {
38        match self {
39            GroupOrderBy::Name => "name",
40            GroupOrderBy::Path => "path",
41            GroupOrderBy::Id => "id",
42            GroupOrderBy::Similarity => "similarity",
43        }
44    }
45}
46
47impl ParamValue<'static> for GroupOrderBy {
48    fn as_value(&self) -> Cow<'static, str> {
49        self.as_str().into()
50    }
51}
52
53/// Filter groups by visibility level.
54#[derive(Debug, Clone, Copy, PartialEq, Eq)]
55#[non_exhaustive]
56pub enum GroupVisibilityFilter {
57    /// Public groups.
58    Public,
59    /// Internal groups.
60    Internal,
61    /// Private groups.
62    Private,
63}
64
65impl GroupVisibilityFilter {
66    fn as_str(self) -> &'static str {
67        match self {
68            GroupVisibilityFilter::Public => "public",
69            GroupVisibilityFilter::Internal => "internal",
70            GroupVisibilityFilter::Private => "private",
71        }
72    }
73}
74
75impl ParamValue<'static> for GroupVisibilityFilter {
76    fn as_value(&self) -> Cow<'static, str> {
77        self.as_str().into()
78    }
79}
80
81/// Query for groups on an instance.
82#[derive(Debug, Builder, Clone)]
83#[builder(setter(strip_option))]
84pub struct Groups<'a> {
85    /// Search for groups using a query string.
86    ///
87    /// The search query will be escaped automatically.
88    #[builder(setter(into), default)]
89    search: Option<Cow<'a, str>>,
90
91    /// Skip groups with the given IDs.
92    #[builder(setter(name = "_skip_groups"), default, private)]
93    skip_groups: BTreeSet<u64>,
94    /// Show all groups with access.
95    ///
96    /// Note that the default for this endpoint differs based on whether the API caller has
97    /// administrator privileges or not.
98    #[builder(default)]
99    all_available: Option<bool>,
100    /// Filter owned by those owned by the API caller.
101    #[builder(default)]
102    owned: Option<bool>,
103    /// Filter groups by those where the API caller has a minimum access level.
104    #[builder(default)]
105    min_access_level: Option<AccessLevel>,
106    /// Only return top-level groups.
107    #[builder(default)]
108    top_level_only: Option<bool>,
109    /// The storage shard used by the group.
110    #[builder(setter(into), default)]
111    repository_storage: Option<Cow<'a, str>>,
112
113    /// Include project statistics in the results.
114    #[builder(default)]
115    statistics: Option<bool>,
116    /// Filter by visibility.
117    #[builder(default)]
118    visibility: Option<GroupVisibilityFilter>,
119    /// Filter by date when the group was marked for deletion.
120    #[builder(default)]
121    marked_for_deletion_on: Option<NaiveDate>,
122    /// Include custom attributes in the response.
123    #[builder(default)]
124    with_custom_attributes: Option<bool>,
125    /// Search for projects with a given custom attribute set.
126    #[builder(setter(name = "_custom_attributes"), default, private)]
127    custom_attributes: BTreeMap<Cow<'a, str>, Cow<'a, str>>,
128
129    /// Order results by a given key.
130    #[builder(default)]
131    order_by: Option<GroupOrderBy>,
132    /// The sort order for returned results.
133    #[builder(default)]
134    sort: Option<SortOrder>,
135}
136
137impl<'a> Groups<'a> {
138    /// Create a builder for the endpoint.
139    pub fn builder() -> GroupsBuilder<'a> {
140        GroupsBuilder::default()
141    }
142}
143
144impl<'a> GroupsBuilder<'a> {
145    /// Skip the given group ID.
146    pub fn skip_group(&mut self, group: u64) -> &mut Self {
147        self.skip_groups
148            .get_or_insert_with(BTreeSet::new)
149            .insert(group);
150        self
151    }
152
153    /// Skip the given group IDs.
154    pub fn skip_groups<I>(&mut self, iter: I) -> &mut Self
155    where
156        I: Iterator<Item = u64>,
157    {
158        self.skip_groups
159            .get_or_insert_with(BTreeSet::new)
160            .extend(iter);
161        self
162    }
163
164    /// Add a custom attribute search parameter.
165    pub fn custom_attribute<K, V>(&mut self, key: K, value: V) -> &mut Self
166    where
167        K: Into<Cow<'a, str>>,
168        V: Into<Cow<'a, str>>,
169    {
170        self.custom_attributes
171            .get_or_insert_with(BTreeMap::new)
172            .insert(key.into(), value.into());
173        self
174    }
175
176    /// Add multiple custom attribute search parameters.
177    pub fn custom_attributes<I, K, V>(&mut self, iter: I) -> &mut Self
178    where
179        I: Iterator<Item = (K, V)>,
180        K: Into<Cow<'a, str>>,
181        V: Into<Cow<'a, str>>,
182    {
183        self.custom_attributes
184            .get_or_insert_with(BTreeMap::new)
185            .extend(iter.map(|(k, v)| (k.into(), v.into())));
186        self
187    }
188}
189
190impl Endpoint for Groups<'_> {
191    fn method(&self) -> Method {
192        Method::GET
193    }
194
195    fn endpoint(&self) -> Cow<'static, str> {
196        "groups".into()
197    }
198
199    fn parameters(&self) -> QueryParams<'_> {
200        let mut params = QueryParams::default();
201
202        params
203            .push_opt("search", self.search.as_ref())
204            .extend(
205                self.skip_groups
206                    .iter()
207                    .map(|&value| ("skip_groups[]", value)),
208            )
209            .push_opt("all_available", self.all_available)
210            .push_opt("owned", self.owned)
211            .push_opt(
212                "min_access_level",
213                self.min_access_level.map(|level| level.as_u64()),
214            )
215            .push_opt("top_level_only", self.top_level_only)
216            .push_opt("repository_storage", self.repository_storage.as_ref())
217            .push_opt("statistics", self.statistics)
218            .push_opt("visibility", self.visibility)
219            .push_opt("marked_for_deletion_on", self.marked_for_deletion_on)
220            .push_opt("with_custom_attributes", self.with_custom_attributes)
221            .extend(
222                self.custom_attributes
223                    .iter()
224                    .map(|(key, value)| (format!("custom_attributes[{key}]"), value)),
225            )
226            .push_opt("order_by", self.order_by)
227            .push_opt("sort", self.sort);
228
229        params
230    }
231}
232
233impl Pageable for Groups<'_> {
234    fn use_keyset_pagination(&self) -> bool {
235        self.order_by.unwrap_or_default().use_keyset_pagination()
236            && self.sort.map_or(true, |sort| sort == SortOrder::Ascending)
237    }
238}
239
240#[cfg(test)]
241mod tests {
242    use chrono::NaiveDate;
243
244    use crate::api::common::{AccessLevel, SortOrder};
245    use crate::api::groups::{GroupOrderBy, GroupVisibilityFilter, Groups};
246    use crate::api::{self, Pageable, Query};
247    use crate::test::client::{ExpectedUrl, SingleTestClient};
248
249    #[test]
250    fn order_by_default() {
251        assert_eq!(GroupOrderBy::default(), GroupOrderBy::Name);
252    }
253
254    #[test]
255    fn order_by_as_str() {
256        let items = &[
257            (GroupOrderBy::Name, "name"),
258            (GroupOrderBy::Path, "path"),
259            (GroupOrderBy::Id, "id"),
260            (GroupOrderBy::Similarity, "similarity"),
261        ];
262
263        for (i, s) in items {
264            assert_eq!(i.as_str(), *s);
265        }
266    }
267
268    #[test]
269    fn order_by_use_keyset_pagination() {
270        let items = &[
271            (GroupOrderBy::Name, true),
272            (GroupOrderBy::Path, false),
273            (GroupOrderBy::Id, false),
274            (GroupOrderBy::Similarity, false),
275        ];
276
277        for (i, k) in items {
278            assert_eq!(i.use_keyset_pagination(), *k);
279        }
280    }
281
282    #[test]
283    fn visibility_filter_as_str() {
284        let items = &[
285            (GroupVisibilityFilter::Public, "public"),
286            (GroupVisibilityFilter::Internal, "internal"),
287            (GroupVisibilityFilter::Private, "private"),
288        ];
289
290        for (i, s) in items {
291            assert_eq!(i.as_str(), *s);
292        }
293    }
294
295    #[test]
296    fn defaults_are_sufficient() {
297        Groups::builder().build().unwrap();
298    }
299
300    #[test]
301    fn endpoint_use_keyset_pagination() {
302        let items = &[
303            ((GroupOrderBy::Name, SortOrder::Ascending), true),
304            ((GroupOrderBy::Name, SortOrder::Descending), false),
305            ((GroupOrderBy::Path, SortOrder::Ascending), false),
306            ((GroupOrderBy::Path, SortOrder::Descending), false),
307            ((GroupOrderBy::Id, SortOrder::Ascending), false),
308            ((GroupOrderBy::Id, SortOrder::Descending), false),
309            ((GroupOrderBy::Similarity, SortOrder::Ascending), false),
310            ((GroupOrderBy::Similarity, SortOrder::Descending), false),
311        ];
312
313        for ((o, s), k) in items {
314            let endpoint = Groups::builder().order_by(*o).sort(*s).build().unwrap();
315            assert_eq!(endpoint.use_keyset_pagination(), *k);
316        }
317    }
318
319    #[test]
320    fn endpoint() {
321        let endpoint = ExpectedUrl::builder().endpoint("groups").build().unwrap();
322        let client = SingleTestClient::new_raw(endpoint, "");
323
324        let endpoint = Groups::builder().build().unwrap();
325        api::ignore(endpoint).query(&client).unwrap();
326    }
327
328    #[test]
329    fn endpoint_search() {
330        let endpoint = ExpectedUrl::builder()
331            .endpoint("groups")
332            .add_query_params(&[("search", "query")])
333            .build()
334            .unwrap();
335        let client = SingleTestClient::new_raw(endpoint, "");
336
337        let endpoint = Groups::builder().search("query").build().unwrap();
338        api::ignore(endpoint).query(&client).unwrap();
339    }
340
341    #[test]
342    fn endpoint_skip_group() {
343        let endpoint = ExpectedUrl::builder()
344            .endpoint("groups")
345            .add_query_params(&[("skip_groups[]", "1")])
346            .build()
347            .unwrap();
348        let client = SingleTestClient::new_raw(endpoint, "");
349
350        let endpoint = Groups::builder().skip_group(1).build().unwrap();
351        api::ignore(endpoint).query(&client).unwrap();
352    }
353
354    #[test]
355    fn endpoint_skip_groups() {
356        let endpoint = ExpectedUrl::builder()
357            .endpoint("groups")
358            .add_query_params(&[("skip_groups[]", "1"), ("skip_groups[]", "2")])
359            .build()
360            .unwrap();
361        let client = SingleTestClient::new_raw(endpoint, "");
362
363        let endpoint = Groups::builder()
364            .skip_group(1)
365            .skip_groups([1, 2].iter().copied())
366            .build()
367            .unwrap();
368        api::ignore(endpoint).query(&client).unwrap();
369    }
370
371    #[test]
372    fn endpoint_all_available() {
373        let endpoint = ExpectedUrl::builder()
374            .endpoint("groups")
375            .add_query_params(&[("all_available", "true")])
376            .build()
377            .unwrap();
378        let client = SingleTestClient::new_raw(endpoint, "");
379
380        let endpoint = Groups::builder().all_available(true).build().unwrap();
381        api::ignore(endpoint).query(&client).unwrap();
382    }
383
384    #[test]
385    fn endpoint_owned() {
386        let endpoint = ExpectedUrl::builder()
387            .endpoint("groups")
388            .add_query_params(&[("owned", "false")])
389            .build()
390            .unwrap();
391        let client = SingleTestClient::new_raw(endpoint, "");
392
393        let endpoint = Groups::builder().owned(false).build().unwrap();
394        api::ignore(endpoint).query(&client).unwrap();
395    }
396
397    #[test]
398    fn endpoint_min_access_level() {
399        let endpoint = ExpectedUrl::builder()
400            .endpoint("groups")
401            .add_query_params(&[("min_access_level", "30")])
402            .build()
403            .unwrap();
404        let client = SingleTestClient::new_raw(endpoint, "");
405
406        let endpoint = Groups::builder()
407            .min_access_level(AccessLevel::Developer)
408            .build()
409            .unwrap();
410        api::ignore(endpoint).query(&client).unwrap();
411    }
412
413    #[test]
414    fn endpoint_top_level_only() {
415        let endpoint = ExpectedUrl::builder()
416            .endpoint("groups")
417            .add_query_params(&[("top_level_only", "true")])
418            .build()
419            .unwrap();
420        let client = SingleTestClient::new_raw(endpoint, "");
421
422        let endpoint = Groups::builder().top_level_only(true).build().unwrap();
423        api::ignore(endpoint).query(&client).unwrap();
424    }
425
426    #[test]
427    fn endpoint_repository_storage() {
428        let endpoint = ExpectedUrl::builder()
429            .endpoint("groups")
430            .add_query_params(&[("repository_storage", "default")])
431            .build()
432            .unwrap();
433        let client = SingleTestClient::new_raw(endpoint, "");
434
435        let endpoint = Groups::builder()
436            .repository_storage("default")
437            .build()
438            .unwrap();
439        api::ignore(endpoint).query(&client).unwrap();
440    }
441
442    #[test]
443    fn endpoint_statistics() {
444        let endpoint = ExpectedUrl::builder()
445            .endpoint("groups")
446            .add_query_params(&[("statistics", "false")])
447            .build()
448            .unwrap();
449        let client = SingleTestClient::new_raw(endpoint, "");
450
451        let endpoint = Groups::builder().statistics(false).build().unwrap();
452        api::ignore(endpoint).query(&client).unwrap();
453    }
454
455    #[test]
456    fn endpoint_visibility() {
457        let endpoint = ExpectedUrl::builder()
458            .endpoint("groups")
459            .add_query_params(&[("visibility", "internal")])
460            .build()
461            .unwrap();
462        let client = SingleTestClient::new_raw(endpoint, "");
463
464        let endpoint = Groups::builder()
465            .visibility(GroupVisibilityFilter::Internal)
466            .build()
467            .unwrap();
468        api::ignore(endpoint).query(&client).unwrap();
469    }
470
471    #[test]
472    fn endpoint_marked_for_deletion_on() {
473        let endpoint = ExpectedUrl::builder()
474            .endpoint("groups")
475            .add_query_params(&[("marked_for_deletion_on", "2024-01-01")])
476            .build()
477            .unwrap();
478        let client = SingleTestClient::new_raw(endpoint, "");
479
480        let endpoint = Groups::builder()
481            .marked_for_deletion_on(NaiveDate::from_ymd_opt(2024, 1, 1).unwrap())
482            .build()
483            .unwrap();
484        api::ignore(endpoint).query(&client).unwrap();
485    }
486
487    #[test]
488    fn endpoint_with_custom_attributes() {
489        let endpoint = ExpectedUrl::builder()
490            .endpoint("groups")
491            .add_query_params(&[("with_custom_attributes", "true")])
492            .build()
493            .unwrap();
494        let client = SingleTestClient::new_raw(endpoint, "");
495
496        let endpoint = Groups::builder()
497            .with_custom_attributes(true)
498            .build()
499            .unwrap();
500        api::ignore(endpoint).query(&client).unwrap();
501    }
502
503    #[test]
504    fn endpoint_custom_attributes() {
505        let endpoint = ExpectedUrl::builder()
506            .endpoint("groups")
507            .add_query_params(&[
508                ("custom_attributes[key]", "value"),
509                ("custom_attributes[key2]", "value"),
510                ("custom_attributes[key3]", "value&value"),
511            ])
512            .build()
513            .unwrap();
514        let client = SingleTestClient::new_raw(endpoint, "");
515
516        let endpoint = Groups::builder()
517            .custom_attribute("key", "value")
518            .custom_attributes([("key2", "value"), ("key3", "value&value")].iter().cloned())
519            .build()
520            .unwrap();
521        api::ignore(endpoint).query(&client).unwrap();
522    }
523
524    #[test]
525    fn endpoint_order_by() {
526        let endpoint = ExpectedUrl::builder()
527            .endpoint("groups")
528            .add_query_params(&[("order_by", "path")])
529            .build()
530            .unwrap();
531        let client = SingleTestClient::new_raw(endpoint, "");
532
533        let endpoint = Groups::builder()
534            .order_by(GroupOrderBy::Path)
535            .build()
536            .unwrap();
537        api::ignore(endpoint).query(&client).unwrap();
538    }
539
540    #[test]
541    fn endpoint_sort() {
542        let endpoint = ExpectedUrl::builder()
543            .endpoint("groups")
544            .add_query_params(&[("sort", "asc")])
545            .build()
546            .unwrap();
547        let client = SingleTestClient::new_raw(endpoint, "");
548
549        let endpoint = Groups::builder()
550            .sort(SortOrder::Ascending)
551            .build()
552            .unwrap();
553        api::ignore(endpoint).query(&client).unwrap();
554    }
555}