1use bon::Builder;
9
10#[derive(Debug, Clone, Default, Builder, PartialEq, Eq)]
17#[non_exhaustive]
18pub struct ListOptions {
19 #[builder(into)]
22 pub page: Option<u32>,
23
24 #[builder(into)]
26 pub limit: Option<u32>,
27
28 #[builder(into)]
31 pub cursor: Option<String>,
32
33 #[builder(into)]
36 pub shape: Option<String>,
37
38 #[builder(default)]
40 pub flat: bool,
41
42 #[builder(default)]
45 pub flat_lists: bool,
46}
47
48impl ListOptions {
49 pub(crate) fn apply(&self, q: &mut Vec<(String, String)>) {
52 apply_pagination(
53 q,
54 self.page,
55 self.limit,
56 self.cursor.as_deref(),
57 self.shape.as_deref(),
58 self.flat,
59 self.flat_lists,
60 );
61 }
62}
63
64pub(crate) fn apply_pagination(
67 q: &mut Vec<(String, String)>,
68 page: Option<u32>,
69 limit: Option<u32>,
70 cursor: Option<&str>,
71 shape: Option<&str>,
72 flat: bool,
73 flat_lists: bool,
74) {
75 if let Some(c) = cursor.filter(|s| !s.is_empty()) {
76 q.push(("cursor".into(), c.into()));
77 } else if let Some(p) = page.filter(|p| *p > 0) {
78 q.push(("page".into(), p.to_string()));
79 }
80 if let Some(l) = limit.filter(|l| *l > 0) {
81 q.push(("limit".into(), l.to_string()));
82 }
83 if let Some(s) = shape.filter(|s| !s.is_empty()) {
84 q.push(("shape".into(), s.into()));
85 }
86 if flat {
87 q.push(("flat".into(), "true".into()));
88 }
89 if flat_lists {
90 q.push(("flat_lists".into(), "true".into()));
91 }
92}
93
94pub(crate) fn push_opt(q: &mut Vec<(String, String)>, key: &str, value: Option<&str>) {
96 if let Some(v) = value.filter(|v| !v.is_empty()) {
97 q.push((key.into(), v.into()));
98 }
99}
100
101pub(crate) fn push_opt_bool(q: &mut Vec<(String, String)>, key: &str, value: Option<bool>) {
103 if let Some(v) = value {
104 q.push((key.into(), v.to_string()));
105 }
106}
107
108pub(crate) fn push_opt_u32(q: &mut Vec<(String, String)>, key: &str, value: Option<u32>) {
110 if let Some(v) = value.filter(|n| *n > 0) {
111 q.push((key.into(), v.to_string()));
112 }
113}
114
115pub(crate) fn first_non_empty<'a>(values: &[Option<&'a str>]) -> Option<&'a str> {
117 values.iter().copied().flatten().find(|s| !s.is_empty())
118}
119
120#[cfg(test)]
121mod tests {
122 use super::*;
123
124 #[test]
125 fn list_options_cursor_wins_over_page() {
126 let opts = ListOptions::builder()
127 .page(2u32)
128 .cursor("abc".to_string())
129 .build();
130 let mut q = Vec::new();
131 opts.apply(&mut q);
132 assert!(q.contains(&("cursor".into(), "abc".into())));
133 assert!(!q.iter().any(|(k, _)| k == "page"));
134 }
135
136 #[test]
137 fn list_options_only_emits_set_fields() {
138 let opts = ListOptions::builder().limit(25u32).build();
139 let mut q = Vec::new();
140 opts.apply(&mut q);
141 assert_eq!(q, vec![("limit".to_string(), "25".to_string())]);
142 }
143
144 #[test]
145 fn list_options_flat_emits_string() {
146 let opts = ListOptions::builder().flat(true).flat_lists(true).build();
147 let mut q = Vec::new();
148 opts.apply(&mut q);
149 assert!(q.contains(&("flat".into(), "true".into())));
150 assert!(q.contains(&("flat_lists".into(), "true".into())));
151 }
152
153 #[test]
154 fn first_non_empty_skips_empties() {
155 assert_eq!(
156 first_non_empty(&[Some(""), Some("a"), Some("b")]),
157 Some("a")
158 );
159 assert_eq!(first_non_empty(&[None, None]), None);
160 assert_eq!(first_non_empty(&[Some("")]), None);
161 }
162}