115 lines
3.6 KiB
Rust
115 lines
3.6 KiB
Rust
//! The locks: what they report, and putting one in or out of service.
|
|
|
|
use openidconnect::reqwest::Method;
|
|
use serde_json::{Value, json};
|
|
|
|
use tracing::info;
|
|
|
|
use crate::{
|
|
core::models::linka::LockState,
|
|
services::linka::{LinkaError, Result, dry_run, fleet, linka},
|
|
};
|
|
|
|
/// One lock, reduced to what this app shows or acts on
|
|
#[derive(Debug, Clone, PartialEq)]
|
|
pub struct Lock {
|
|
/// The number written on the bike — the name it goes by here too
|
|
pub number: String,
|
|
pub state: LockState,
|
|
pub battery: Option<i32>,
|
|
pub out_of_service: bool,
|
|
}
|
|
|
|
/// Every lock of the account.
|
|
///
|
|
/// The bounding box is the whole planet: the platform filters by map area, and
|
|
/// this app wants the fleet, wherever it happens to be parked.
|
|
pub async fn fetch() -> Result<Vec<Lock>> {
|
|
let answer = fleet(
|
|
"merchantlocks",
|
|
Method::PUT,
|
|
json!({ "longitudeX": -180, "latitudeX": -90, "longitudeY": 180, "latitudeY": 90 }),
|
|
)
|
|
.await?;
|
|
|
|
let locks = answer
|
|
.get("data")
|
|
.and_then(Value::as_array)
|
|
.ok_or_else(|| LinkaError::failed("merchantlocks answered without a list"))?;
|
|
|
|
Ok(locks.iter().filter_map(read).collect())
|
|
}
|
|
|
|
/// A lock with no number is of no use here: it could not be matched to a bike.
|
|
fn read(lock: &Value) -> Option<Lock> {
|
|
let number = match lock.get("lock_number") {
|
|
Some(Value::String(number)) => number.trim().to_owned(),
|
|
Some(Value::Number(number)) => number.to_string(),
|
|
_ => return None,
|
|
};
|
|
Some(Lock {
|
|
number,
|
|
state: lock
|
|
.get("lock_state")
|
|
.and_then(Value::as_str)
|
|
.map_or(LockState::Unknown, LockState::parse),
|
|
battery: lock
|
|
.get("lock_battery_percent")
|
|
.and_then(Value::as_i64)
|
|
.map(|percent| percent as i32),
|
|
out_of_service: lock
|
|
.get("out_of_service")
|
|
.and_then(Value::as_bool)
|
|
.unwrap_or(false),
|
|
})
|
|
}
|
|
|
|
/// Takes the lock of `serial` in or out of service on the platform.
|
|
pub async fn set_service_state(serial: &str, out_of_service: bool) -> Result<()> {
|
|
let linka = linka().ok_or(LinkaError::NotConfigured)?;
|
|
if dry_run() {
|
|
info!("[LINKA] (dry run) would set {serial} out_of_service={out_of_service}");
|
|
return Err(LinkaError::DryRun);
|
|
}
|
|
super::authenticated(
|
|
&linka.service_state_url.clone(),
|
|
Method::POST,
|
|
json!({ "lock_serial_no": serial, "out_of_service": out_of_service }),
|
|
)
|
|
.await
|
|
.map(|_| ())
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn a_lock_is_read_whether_its_number_is_written_as_text_or_as_a_number() {
|
|
let text = read(&json!({
|
|
"lock_number": "3000", "lock_state": "Unlocked",
|
|
"lock_battery_percent": 95, "out_of_service": false
|
|
}))
|
|
.unwrap();
|
|
assert_eq!(text.number, "3000");
|
|
assert_eq!(text.state, LockState::Unlocked);
|
|
assert_eq!(text.battery, Some(95));
|
|
|
|
let number = read(&json!({ "lock_number": 1000, "lock_state": "Locked" })).unwrap();
|
|
assert_eq!(number.number, "1000");
|
|
assert_eq!(number.state, LockState::Locked);
|
|
assert_eq!(number.battery, None);
|
|
assert!(!number.out_of_service);
|
|
}
|
|
|
|
#[test]
|
|
fn a_lock_without_a_number_is_dropped_rather_than_guessed_at() {
|
|
assert!(read(&json!({ "lock_state": "Locked" })).is_none());
|
|
}
|
|
|
|
#[test]
|
|
fn an_unexpected_lock_state_is_reported_as_unknown() {
|
|
let lock = read(&json!({ "lock_number": 1, "lock_state": "Ajar" })).unwrap();
|
|
assert_eq!(lock.state, LockState::Unknown);
|
|
}
|
|
}
|