Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -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."
Expand Down
80 changes: 69 additions & 11 deletions src/commands/properties.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<String>,
/// Location filter (default all).
#[arg(long)]
location: Option<String>,
Expand All @@ -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 {
Expand All @@ -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
Expand Down Expand Up @@ -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()),
]
);
}
}
Loading