use crate::client::Client;
use crate::error::{Error, Result};
use crate::internal::{apply_pagination, push_opt, push_opt_u32};
use crate::pagination::{FetchFn, Page, PageStream};
use crate::resources::agencies::urlencoding;
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 ListVehiclesOptions {
#[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 joiner: Option<String>,
#[builder(into)]
pub search: Option<String>,
#[builder(into)]
pub vehicle_type: Option<String>,
#[builder(into)]
pub type_of_idc: Option<String>,
#[builder(into)]
pub contract_type: Option<String>,
#[builder(into)]
pub set_aside: Option<String>,
#[builder(into)]
pub who_can_use: Option<String>,
#[builder(into)]
pub naics_code: Option<String>,
#[builder(into)]
pub psc_code: Option<String>,
#[builder(into)]
pub program_acronym: Option<String>,
#[builder(into)]
pub agency: Option<String>,
#[builder(into)]
pub organization_id: Option<String>,
#[builder(into)]
pub total_obligated_min: Option<String>,
#[builder(into)]
pub total_obligated_max: Option<String>,
#[builder(into)]
pub idv_count_min: Option<u32>,
#[builder(into)]
pub idv_count_max: Option<u32>,
#[builder(into)]
pub order_count_min: Option<u32>,
#[builder(into)]
pub order_count_max: Option<u32>,
#[builder(into)]
pub fiscal_year: Option<String>,
#[builder(into)]
pub award_date_after: Option<String>,
#[builder(into)]
pub award_date_before: Option<String>,
#[builder(into)]
pub last_date_to_order_after: Option<String>,
#[builder(into)]
pub last_date_to_order_before: Option<String>,
#[builder(into)]
pub ordering: Option<String>,
#[builder(default)]
pub extra: BTreeMap<String, String>,
}
impl ListVehiclesOptions {
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, "joiner", self.joiner.as_deref());
push_opt(&mut q, "search", self.search.as_deref());
push_opt(&mut q, "vehicle_type", self.vehicle_type.as_deref());
push_opt(&mut q, "type_of_idc", self.type_of_idc.as_deref());
push_opt(&mut q, "contract_type", self.contract_type.as_deref());
push_opt(&mut q, "set_aside", self.set_aside.as_deref());
push_opt(&mut q, "who_can_use", self.who_can_use.as_deref());
push_opt(&mut q, "naics_code", self.naics_code.as_deref());
push_opt(&mut q, "psc_code", self.psc_code.as_deref());
push_opt(&mut q, "program_acronym", self.program_acronym.as_deref());
push_opt(&mut q, "agency", self.agency.as_deref());
push_opt(&mut q, "organization_id", self.organization_id.as_deref());
push_opt(
&mut q,
"total_obligated_min",
self.total_obligated_min.as_deref(),
);
push_opt(
&mut q,
"total_obligated_max",
self.total_obligated_max.as_deref(),
);
push_opt_u32(&mut q, "idv_count_min", self.idv_count_min);
push_opt_u32(&mut q, "idv_count_max", self.idv_count_max);
push_opt_u32(&mut q, "order_count_min", self.order_count_min);
push_opt_u32(&mut q, "order_count_max", self.order_count_max);
push_opt(&mut q, "fiscal_year", self.fiscal_year.as_deref());
push_opt(&mut q, "award_date_after", self.award_date_after.as_deref());
push_opt(
&mut q,
"award_date_before",
self.award_date_before.as_deref(),
);
push_opt(
&mut q,
"last_date_to_order_after",
self.last_date_to_order_after.as_deref(),
);
push_opt(
&mut q,
"last_date_to_order_before",
self.last_date_to_order_before.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
}
}
#[derive(Debug, Clone, Default, Builder, PartialEq, Eq)]
#[non_exhaustive]
pub struct GetVehicleOptions {
#[builder(into)]
pub shape: Option<String>,
#[builder(default)]
pub flat: bool,
#[builder(default)]
pub flat_lists: bool,
}
impl GetVehicleOptions {
fn to_query(&self) -> Vec<(String, String)> {
let mut q = Vec::new();
push_opt(&mut q, "shape", self.shape.as_deref());
if self.flat {
q.push(("flat".into(), "true".into()));
}
if self.flat_lists {
q.push(("flat_lists".into(), "true".into()));
}
q
}
}
impl Client {
pub async fn list_vehicles(&self, opts: ListVehiclesOptions) -> Result<Page<Record>> {
let q = opts.to_query();
let bytes = self.get_bytes("/api/vehicles/", &q).await?;
Page::decode(&bytes)
}
pub async fn get_vehicle(&self, uuid: &str, opts: Option<GetVehicleOptions>) -> Result<Record> {
if uuid.is_empty() {
return Err(Error::Validation {
message: "get_vehicle: uuid is required".into(),
response: None,
});
}
let q = opts.unwrap_or_default().to_query();
let path = format!("/api/vehicles/{}/", urlencoding(uuid));
self.get_json::<Record>(&path, &q).await
}
pub fn iterate_vehicles(&self, opts: ListVehiclesOptions) -> 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_vehicles(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 list_vehicles_emits_string_filters() {
let opts = ListVehiclesOptions::builder()
.search("oasis")
.vehicle_type("IDC")
.type_of_idc("GWAC")
.set_aside("8A")
.naics_code("541512")
.psc_code("D302")
.program_acronym("OASIS")
.agency("9700")
.fiscal_year("2024")
.ordering("total_obligated")
.build();
let q = opts.to_query();
assert_eq!(get_q(&q, "search").as_deref(), Some("oasis"));
assert_eq!(get_q(&q, "vehicle_type").as_deref(), Some("IDC"));
assert_eq!(get_q(&q, "type_of_idc").as_deref(), Some("GWAC"));
assert_eq!(get_q(&q, "set_aside").as_deref(), Some("8A"));
assert_eq!(get_q(&q, "naics_code").as_deref(), Some("541512"));
assert_eq!(get_q(&q, "psc_code").as_deref(), Some("D302"));
assert_eq!(get_q(&q, "program_acronym").as_deref(), Some("OASIS"));
assert_eq!(get_q(&q, "agency").as_deref(), Some("9700"));
assert_eq!(get_q(&q, "fiscal_year").as_deref(), Some("2024"));
assert_eq!(get_q(&q, "ordering").as_deref(), Some("total_obligated"));
}
#[test]
fn list_vehicles_emits_int_counts() {
let opts = ListVehiclesOptions::builder()
.idv_count_min(3u32)
.idv_count_max(50u32)
.order_count_min(100u32)
.order_count_max(9999u32)
.build();
let q = opts.to_query();
assert_eq!(get_q(&q, "idv_count_min").as_deref(), Some("3"));
assert_eq!(get_q(&q, "idv_count_max").as_deref(), Some("50"));
assert_eq!(get_q(&q, "order_count_min").as_deref(), Some("100"));
assert_eq!(get_q(&q, "order_count_max").as_deref(), Some("9999"));
}
#[test]
fn list_vehicles_zero_counts_omitted() {
let opts = ListVehiclesOptions::builder()
.idv_count_min(0u32)
.order_count_max(0u32)
.build();
let q = opts.to_query();
assert!(!q.iter().any(|(k, _)| k == "idv_count_min"));
assert!(!q.iter().any(|(k, _)| k == "order_count_max"));
}
#[test]
fn list_vehicles_extra_map() {
let mut extra = BTreeMap::new();
extra.insert("version".to_string(), "v2".to_string());
let opts = ListVehiclesOptions::builder().extra(extra).build();
let q = opts.to_query();
assert_eq!(get_q(&q, "version").as_deref(), Some("v2"));
}
#[test]
fn get_vehicle_options_emits_shape_and_flat() {
let opts = GetVehicleOptions::builder()
.shape("vehicles(minimal)")
.flat(true)
.build();
let q = opts.to_query();
assert_eq!(get_q(&q, "shape").as_deref(), Some("vehicles(minimal)"));
assert_eq!(get_q(&q, "flat").as_deref(), Some("true"));
assert!(!q.iter().any(|(k, _)| k == "flat_lists"));
}
#[tokio::test]
async fn get_vehicle_empty_uuid_returns_validation() {
let client = Client::builder().api_key("x").build().expect("build");
let err = client.get_vehicle("", None).await.expect_err("must error");
match err {
Error::Validation { message, .. } => {
assert!(message.contains("uuid"));
}
other => panic!("expected Validation, got {other:?}"),
}
}
}