diff --git a/Cargo.lock b/Cargo.lock index 4e85aa8..53d29f8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1148,7 +1148,7 @@ checksum = "11d3d7f243d5c5a8b9bb5d6dd2b1602c0cb0b9db1621bafc7ed66e35ff9fe092" [[package]] name = "lofty-cli" -version = "0.1.6" +version = "0.1.7" dependencies = [ "assert_cmd", "clap", diff --git a/Cargo.toml b/Cargo.toml index df4e4d4..8bbb544 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "lofty-cli" -version = "0.1.6" +version = "0.1.7" edition = "2021" rust-version = "1.88" description = "Interact with the Lofty (lofty.ai) fractional real-estate marketplace API — market data, order books, and market-making analysis. Keychain-secured, agent-friendly." diff --git a/src/commands/properties.rs b/src/commands/properties.rs index d82dae2..c013281 100644 --- a/src/commands/properties.rs +++ b/src/commands/properties.rs @@ -17,9 +17,12 @@ pub enum Cmd { /// Items per page (max 200). #[arg(long, default_value_t = 50)] page_size: u32, - /// RESIDENTIAL, COMMERCIAL, or ALL. - #[arg(long, default_value = "ALL")] - property_type: String, + /// Filter by property type. Values are upstream-defined and passed + /// through verbatim (observed: "single family", "vacation rental", + /// "duplex", "triplex", "fourplex", "mixed use", "commercial"). + /// Omit to list every type. + #[arg(long)] + property_type: Option, /// Location filter (default all). #[arg(long)] location: Option, @@ -32,6 +35,30 @@ pub enum Cmd { Trades { property_id: String }, } +/// Query for the marketplace listing. `propertyType` is omitted entirely when +/// no filter is given: the upstream enum changed out from under the old `ALL` +/// sentinel (every documented value now 400s with `invalid_property_type`), +/// and omission already means "all types". A user-supplied value is passed +/// through verbatim so new upstream values work without a CLI release. +fn list_query( + page: u32, + page_size: u32, + property_type: Option<&str>, + location: Option<&str>, +) -> Vec<(&'static str, String)> { + let mut q = vec![ + ("page", page.to_string()), + ("pageSize", page_size.to_string()), + ]; + if let Some(pt) = property_type { + q.push(("propertyType", pt.to_string())); + } + if let Some(loc) = location { + q.push(("location", loc.to_string())); + } + q +} + pub fn run(ctx: &Ctx, cmd: &Cmd) -> Result<(), CliError> { let client = ctx.client()?; match cmd { @@ -41,14 +68,12 @@ pub fn run(ctx: &Ctx, cmd: &Cmd) -> Result<(), CliError> { property_type, location, } => { - let mut q = vec![ - ("page", page.to_string()), - ("pageSize", page_size.to_string()), - ("propertyType", property_type.clone()), - ]; - if let Some(loc) = location { - q.push(("location", loc.clone())); - } + let q = list_query( + *page, + *page_size, + property_type.as_deref(), + location.as_deref(), + ); let payload = client.get("/public/v1/properties", &q)?; emit(ctx, "properties-list", payload, |v| { let props = v @@ -122,3 +147,36 @@ pub fn run(ctx: &Ctx, cmd: &Cmd) -> Result<(), CliError> { } } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn omits_property_type_when_no_filter_is_given() { + // Sending any sentinel (the old default was `ALL`) now 400s upstream; + // "all types" must be expressed by leaving the parameter off. + let q = list_query(1, 50, None, None); + assert_eq!( + q, + vec![("page", "1".to_string()), ("pageSize", "50".to_string())] + ); + } + + #[test] + fn passes_a_property_type_through_verbatim() { + // Upstream values contain spaces and are lowercase ("vacation rental"); + // no client-side casing, mapping, or validation — unknown-to-us values + // must reach the API untouched so new upstream types just work. + let q = list_query(2, 10, Some("vacation rental"), Some("Tiffin, OH")); + assert_eq!( + q, + vec![ + ("page", "2".to_string()), + ("pageSize", "10".to_string()), + ("propertyType", "vacation rental".to_string()), + ("location", "Tiffin, OH".to_string()), + ] + ); + } +}