1use crate::client::Client;
13use crate::error::{Error, Result};
14use crate::internal::{apply_pagination, push_opt};
15use crate::pagination::{FetchFn, Page, PageStream};
16use crate::resources::entity_subresources::EntitySubresourceOptions;
17use crate::resources::idv_subresources::IdvSubresourceOptions;
18use crate::Record;
19use bon::Builder;
20use std::collections::BTreeMap;
21use std::sync::Arc;
22
23#[derive(Debug, Clone, Default, Builder, PartialEq, Eq)]
29#[non_exhaustive]
30pub struct ListLcatsOptions {
31 #[builder(into)]
33 pub uei: Option<String>,
34 #[builder(into)]
36 pub idv_key: Option<String>,
37
38 #[builder(into)]
41 pub page: Option<u32>,
42 #[builder(into)]
44 pub limit: Option<u32>,
45 #[builder(into)]
47 pub cursor: Option<String>,
48 #[builder(into)]
50 pub shape: Option<String>,
51 #[builder(default)]
53 pub flat: bool,
54 #[builder(default)]
56 pub flat_lists: bool,
57
58 #[builder(into)]
61 pub search: Option<String>,
62 #[builder(into)]
64 pub ordering: Option<String>,
65
66 #[builder(default)]
68 pub extra: BTreeMap<String, String>,
69}
70
71#[derive(Debug, Clone, PartialEq, Eq)]
77enum LcatsTarget<'a> {
78 Entity(&'a str),
79 Idv(&'a str),
80}
81
82impl ListLcatsOptions {
83 fn target(&self) -> Option<LcatsTarget<'_>> {
86 if let Some(uei) = self.uei.as_deref().filter(|s| !s.is_empty()) {
87 return Some(LcatsTarget::Entity(uei));
88 }
89 if let Some(key) = self.idv_key.as_deref().filter(|s| !s.is_empty()) {
90 return Some(LcatsTarget::Idv(key));
91 }
92 None
93 }
94
95 fn to_entity_opts(&self) -> EntitySubresourceOptions {
97 EntitySubresourceOptions {
98 page: self.page,
99 limit: self.limit,
100 cursor: self.cursor.clone(),
101 shape: self.shape.clone(),
102 flat: self.flat,
103 flat_lists: self.flat_lists,
104 joiner: None,
105 ordering: self.ordering.clone(),
106 search: self.search.clone(),
107 extra: self.extra.clone(),
108 }
109 }
110
111 fn to_idv_opts(&self) -> IdvSubresourceOptions {
113 IdvSubresourceOptions {
114 page: self.page,
115 limit: self.limit,
116 cursor: self.cursor.clone(),
117 shape: self.shape.clone(),
118 flat: self.flat,
119 flat_lists: self.flat_lists,
120 joiner: None,
121 ordering: self.ordering.clone(),
122 search: self.search.clone(),
123 extra: self.extra.clone(),
124 }
125 }
126
127 fn to_query(&self) -> Vec<(String, String)> {
131 let mut q = Vec::new();
132 apply_pagination(
133 &mut q,
134 self.page,
135 self.limit,
136 self.cursor.as_deref(),
137 self.shape.as_deref(),
138 self.flat,
139 self.flat_lists,
140 );
141 push_opt(&mut q, "search", self.search.as_deref());
142 push_opt(&mut q, "ordering", self.ordering.as_deref());
143 for (k, v) in &self.extra {
144 if !v.is_empty() {
145 q.push((k.clone(), v.clone()));
146 }
147 }
148 q
149 }
150}
151
152fn validation_missing_owner() -> Error {
153 Error::Validation {
154 message: "list_lcats: one of uei or idv_key is required".into(),
155 response: None,
156 }
157}
158
159impl Client {
160 pub async fn list_lcats(&self, opts: ListLcatsOptions) -> Result<Page<Record>> {
171 match opts.target().ok_or_else(validation_missing_owner)? {
172 LcatsTarget::Entity(uei) => {
173 let sub_opts = opts.to_entity_opts();
174 self.list_entity_lcats(uei, sub_opts).await
175 }
176 LcatsTarget::Idv(key) => {
177 let sub_opts = opts.to_idv_opts();
178 self.list_idv_lcats(key, sub_opts).await
179 }
180 }
181 }
182
183 pub fn iterate_lcats(&self, opts: ListLcatsOptions) -> PageStream<Record> {
187 let opts = Arc::new(opts);
188 let fetch: FetchFn<Record> = Box::new(move |client, page, cursor| {
189 let mut next = (*opts).clone();
190 next.page = page;
191 next.cursor = cursor;
192 Box::pin(async move { client.list_lcats(next).await })
193 });
194 PageStream::new(self.clone(), fetch)
195 }
196}
197
198#[cfg(test)]
199mod tests {
200 use super::*;
201
202 fn get_q(q: &[(String, String)], k: &str) -> Option<String> {
203 q.iter().find(|(kk, _)| kk == k).map(|(_, v)| v.clone())
204 }
205
206 #[test]
209 fn target_uei_dispatches_to_entity() {
210 let opts = ListLcatsOptions::builder().uei("UEI123").build();
211 assert_eq!(opts.target(), Some(LcatsTarget::Entity("UEI123")));
212 }
213
214 #[test]
215 fn target_idv_key_dispatches_to_idv() {
216 let opts = ListLcatsOptions::builder().idv_key("IDV-001").build();
217 assert_eq!(opts.target(), Some(LcatsTarget::Idv("IDV-001")));
218 }
219
220 #[test]
221 fn target_both_set_uei_wins() {
222 let opts = ListLcatsOptions::builder().uei("U1").idv_key("I1").build();
223 assert_eq!(opts.target(), Some(LcatsTarget::Entity("U1")));
224 }
225
226 #[test]
227 fn target_neither_set_returns_none() {
228 let opts = ListLcatsOptions::default();
229 assert!(opts.target().is_none());
230 }
231
232 #[test]
233 fn target_treats_empty_strings_as_unset() {
234 let opts = ListLcatsOptions::builder()
235 .uei(String::new())
236 .idv_key(String::new())
237 .build();
238 assert!(opts.target().is_none());
239 }
240
241 #[tokio::test]
244 async fn list_lcats_requires_owner_default() {
245 let c = Client::builder().api_key("k").build().expect("client");
246 let err = c
247 .list_lcats(ListLcatsOptions::default())
248 .await
249 .expect_err("must error");
250 match err {
251 Error::Validation { message, .. } => {
252 assert!(message.contains("uei") && message.contains("idv_key"));
253 }
254 other => panic!("expected Validation, got {other:?}"),
255 }
256 }
257
258 #[tokio::test]
259 async fn list_lcats_requires_owner_explicit_empty() {
260 let c = Client::builder().api_key("k").build().expect("client");
261 let opts = ListLcatsOptions::builder()
262 .uei(String::new())
263 .idv_key(String::new())
264 .build();
265 let err = c.list_lcats(opts).await.expect_err("must error");
266 assert!(matches!(err, Error::Validation { .. }));
267 }
268
269 #[test]
272 fn forwards_search_and_ordering() {
273 let opts = ListLcatsOptions::builder()
274 .uei("UEI123")
275 .search("software engineer")
276 .ordering("labor_category")
277 .build();
278 let q = opts.to_query();
279 assert_eq!(get_q(&q, "search").as_deref(), Some("software engineer"));
280 assert_eq!(get_q(&q, "ordering").as_deref(), Some("labor_category"));
281 }
282
283 #[test]
284 fn forwards_pagination_and_shape() {
285 let opts = ListLcatsOptions::builder()
286 .uei("UEI123")
287 .page(2u32)
288 .limit(50u32)
289 .shape("labor_category,rate")
290 .flat(true)
291 .build();
292 let q = opts.to_query();
293 assert_eq!(get_q(&q, "page").as_deref(), Some("2"));
294 assert_eq!(get_q(&q, "limit").as_deref(), Some("50"));
295 assert_eq!(get_q(&q, "shape").as_deref(), Some("labor_category,rate"));
296 assert_eq!(get_q(&q, "flat").as_deref(), Some("true"));
297 }
298
299 #[test]
300 fn extra_keys_pass_through() {
301 let mut extra = BTreeMap::new();
302 extra.insert("custom".into(), "value".into());
303 let opts = ListLcatsOptions::builder().uei("UEI1").extra(extra).build();
304 let q = opts.to_query();
305 assert_eq!(get_q(&q, "custom").as_deref(), Some("value"));
306 }
307}