use crate::client::Client;
use crate::error::{Error, Result};
use crate::internal::{apply_pagination, push_opt};
use crate::pagination::{FetchFn, Page, PageStream};
use crate::resources::entity_subresources::EntitySubresourceOptions;
use crate::resources::idv_subresources::IdvSubresourceOptions;
use crate::Record;
use bon::Builder;
use std::collections::BTreeMap;
use std::sync::Arc;
#[derive(Debug, Clone, Default, Builder, PartialEq, Eq)]
#[non_exhaustive]
pub struct ListLcatsOptions {
#[builder(into)]
pub uei: Option<String>,
#[builder(into)]
pub idv_key: Option<String>,
#[builder(into)]
pub page: Option<u32>,
#[builder(into)]
pub limit: Option<u32>,
#[builder(into)]
pub cursor: Option<String>,
#[builder(into)]
pub shape: Option<String>,
#[builder(default)]
pub flat: bool,
#[builder(default)]
pub flat_lists: bool,
#[builder(into)]
pub search: Option<String>,
#[builder(into)]
pub ordering: Option<String>,
#[builder(default)]
pub extra: BTreeMap<String, String>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
enum LcatsTarget<'a> {
Entity(&'a str),
Idv(&'a str),
}
impl ListLcatsOptions {
fn target(&self) -> Option<LcatsTarget<'_>> {
if let Some(uei) = self.uei.as_deref().filter(|s| !s.is_empty()) {
return Some(LcatsTarget::Entity(uei));
}
if let Some(key) = self.idv_key.as_deref().filter(|s| !s.is_empty()) {
return Some(LcatsTarget::Idv(key));
}
None
}
fn to_entity_opts(&self) -> EntitySubresourceOptions {
EntitySubresourceOptions {
page: self.page,
limit: self.limit,
cursor: self.cursor.clone(),
shape: self.shape.clone(),
flat: self.flat,
flat_lists: self.flat_lists,
joiner: None,
ordering: self.ordering.clone(),
search: self.search.clone(),
extra: self.extra.clone(),
}
}
fn to_idv_opts(&self) -> IdvSubresourceOptions {
IdvSubresourceOptions {
page: self.page,
limit: self.limit,
cursor: self.cursor.clone(),
shape: self.shape.clone(),
flat: self.flat,
flat_lists: self.flat_lists,
joiner: None,
ordering: self.ordering.clone(),
search: self.search.clone(),
extra: self.extra.clone(),
}
}
fn to_query(&self) -> Vec<(String, String)> {
let mut q = Vec::new();
apply_pagination(
&mut q,
self.page,
self.limit,
self.cursor.as_deref(),
self.shape.as_deref(),
self.flat,
self.flat_lists,
);
push_opt(&mut q, "search", self.search.as_deref());
push_opt(&mut q, "ordering", self.ordering.as_deref());
for (k, v) in &self.extra {
if !v.is_empty() {
q.push((k.clone(), v.clone()));
}
}
q
}
}
fn validation_missing_owner() -> Error {
Error::Validation {
message: "list_lcats: one of uei or idv_key is required".into(),
response: None,
}
}
impl Client {
pub async fn list_lcats(&self, opts: ListLcatsOptions) -> Result<Page<Record>> {
match opts.target().ok_or_else(validation_missing_owner)? {
LcatsTarget::Entity(uei) => {
let sub_opts = opts.to_entity_opts();
self.list_entity_lcats(uei, sub_opts).await
}
LcatsTarget::Idv(key) => {
let sub_opts = opts.to_idv_opts();
self.list_idv_lcats(key, sub_opts).await
}
}
}
pub fn iterate_lcats(&self, opts: ListLcatsOptions) -> PageStream<Record> {
let opts = Arc::new(opts);
let fetch: FetchFn<Record> = Box::new(move |client, page, cursor| {
let mut next = (*opts).clone();
next.page = page;
next.cursor = cursor;
Box::pin(async move { client.list_lcats(next).await })
});
PageStream::new(self.clone(), fetch)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn get_q(q: &[(String, String)], k: &str) -> Option<String> {
q.iter().find(|(kk, _)| kk == k).map(|(_, v)| v.clone())
}
#[test]
fn target_uei_dispatches_to_entity() {
let opts = ListLcatsOptions::builder().uei("UEI123").build();
assert_eq!(opts.target(), Some(LcatsTarget::Entity("UEI123")));
}
#[test]
fn target_idv_key_dispatches_to_idv() {
let opts = ListLcatsOptions::builder().idv_key("IDV-001").build();
assert_eq!(opts.target(), Some(LcatsTarget::Idv("IDV-001")));
}
#[test]
fn target_both_set_uei_wins() {
let opts = ListLcatsOptions::builder().uei("U1").idv_key("I1").build();
assert_eq!(opts.target(), Some(LcatsTarget::Entity("U1")));
}
#[test]
fn target_neither_set_returns_none() {
let opts = ListLcatsOptions::default();
assert!(opts.target().is_none());
}
#[test]
fn target_treats_empty_strings_as_unset() {
let opts = ListLcatsOptions::builder()
.uei(String::new())
.idv_key(String::new())
.build();
assert!(opts.target().is_none());
}
#[tokio::test]
async fn list_lcats_requires_owner_default() {
let c = Client::builder().api_key("k").build().expect("client");
let err = c
.list_lcats(ListLcatsOptions::default())
.await
.expect_err("must error");
match err {
Error::Validation { message, .. } => {
assert!(message.contains("uei") && message.contains("idv_key"));
}
other => panic!("expected Validation, got {other:?}"),
}
}
#[tokio::test]
async fn list_lcats_requires_owner_explicit_empty() {
let c = Client::builder().api_key("k").build().expect("client");
let opts = ListLcatsOptions::builder()
.uei(String::new())
.idv_key(String::new())
.build();
let err = c.list_lcats(opts).await.expect_err("must error");
assert!(matches!(err, Error::Validation { .. }));
}
#[test]
fn forwards_search_and_ordering() {
let opts = ListLcatsOptions::builder()
.uei("UEI123")
.search("software engineer")
.ordering("labor_category")
.build();
let q = opts.to_query();
assert_eq!(get_q(&q, "search").as_deref(), Some("software engineer"));
assert_eq!(get_q(&q, "ordering").as_deref(), Some("labor_category"));
}
#[test]
fn forwards_pagination_and_shape() {
let opts = ListLcatsOptions::builder()
.uei("UEI123")
.page(2u32)
.limit(50u32)
.shape("labor_category,rate")
.flat(true)
.build();
let q = opts.to_query();
assert_eq!(get_q(&q, "page").as_deref(), Some("2"));
assert_eq!(get_q(&q, "limit").as_deref(), Some("50"));
assert_eq!(get_q(&q, "shape").as_deref(), Some("labor_category,rate"));
assert_eq!(get_q(&q, "flat").as_deref(), Some("true"));
}
#[test]
fn extra_keys_pass_through() {
let mut extra = BTreeMap::new();
extra.insert("custom".into(), "value".into());
let opts = ListLcatsOptions::builder().uei("UEI1").extra(extra).build();
let q = opts.to_query();
assert_eq!(get_q(&q, "custom").as_deref(), Some("value"));
}
}