1use std::thread;
18
19use signalk_client::ClientError;
20use signalk_client::resources::{Client, Route as WireRoute, RoutePoint as WireRoutePoint};
21
22use nav_math::METRES_PER_NM;
23
24use crate::{Route, Waypoint};
25
26pub trait ResourcesBackend {
32 fn list_routes(&self) -> Result<Vec<WireRoute>, ClientError>;
37 fn publish_route(&self, route: &WireRoute) -> Result<String, ClientError>;
42 fn delete_route(&self, uuid: &str) -> Result<(), ClientError>;
47}
48
49impl ResourcesBackend for Client {
50 fn list_routes(&self) -> Result<Vec<WireRoute>, ClientError> {
51 Self::list_routes(self)
52 }
53
54 fn publish_route(&self, route: &WireRoute) -> Result<String, ClientError> {
55 Self::publish_route(self, route)
56 }
57
58 fn delete_route(&self, uuid: &str) -> Result<(), ClientError> {
59 Self::delete_route(self, uuid)
60 }
61}
62
63pub fn list<B: ResourcesBackend>(backend: &B) -> Result<Vec<Route>, ClientError> {
72 backend
73 .list_routes()?
74 .into_iter()
75 .map(|wire| {
76 let uuid = wire.uuid.ok_or_else(|| ClientError::Protocol("the server listed a route with no uuid".to_owned()))?;
77 Ok(Route {
78 uuid,
79 name: wire.name,
80 description: wire.description,
81 waypoints: wire.points.into_iter().map(|point| Waypoint { position: point.position, name: point.name }).collect(),
82 })
83 })
84 .collect()
85}
86
87pub fn save<B: ResourcesBackend>(backend: &B, route: &Route) -> Result<(), ClientError> {
94 let wire = WireRoute {
95 uuid: Some(route.uuid.clone()),
96 name: route.name.clone(),
97 description: route.description.clone(),
98 distance_m: Some(route.distance_nm() * METRES_PER_NM),
104 points: route
105 .waypoints
106 .iter()
107 .map(|waypoint| WireRoutePoint { position: waypoint.position, name: waypoint.name.clone() })
108 .collect(),
109 };
110 backend.publish_route(&wire).map(|_id| ())
111}
112
113pub fn delete<B: ResourcesBackend>(backend: &B, uuid: &str) -> Result<(), ClientError> {
120 backend.delete_route(uuid)
121}
122
123pub fn spawn_list(client: Client, on_done: impl FnOnce(Result<Vec<Route>, ClientError>) + Send + 'static) {
127 thread::spawn(move || on_done(list(&client)));
128}
129
130pub fn spawn_save(client: Client, route: Route, on_done: impl FnOnce(Result<(), ClientError>) + Send + 'static) {
132 thread::spawn(move || on_done(save(&client, &route)));
133}
134
135pub fn spawn_delete(client: Client, uuid: String, on_done: impl FnOnce(Result<(), ClientError>) + Send + 'static) {
137 thread::spawn(move || on_done(delete(&client, &uuid)));
138}
139
140#[cfg(test)]
141mod tests {
142 use super::*;
143 use nav_math::Position;
144 use std::cell::RefCell;
145
146 #[derive(Default)]
150 struct FakeBackend {
151 listed: Vec<WireRoute>,
152 publish_result: RefCell<Option<Result<String, ClientError>>>,
153 delete_result: RefCell<Option<Result<(), ClientError>>>,
154 published: RefCell<Vec<WireRoute>>,
155 deleted: RefCell<Vec<String>>,
156 }
157
158 impl ResourcesBackend for FakeBackend {
159 fn list_routes(&self) -> Result<Vec<WireRoute>, ClientError> {
160 Ok(self.listed.clone())
161 }
162
163 fn publish_route(&self, route: &WireRoute) -> Result<String, ClientError> {
164 self.published.borrow_mut().push(route.clone());
165 self.publish_result.borrow_mut().take().expect("a scripted publish result")
166 }
167
168 fn delete_route(&self, uuid: &str) -> Result<(), ClientError> {
169 self.deleted.borrow_mut().push(uuid.to_owned());
170 self.delete_result.borrow_mut().take().expect("a scripted delete result")
171 }
172 }
173
174 fn wire_route(uuid: &str, name: &str) -> WireRoute {
175 WireRoute {
176 uuid: Some(uuid.to_owned()),
177 name: Some(name.to_owned()),
178 description: None,
179 distance_m: None,
180 points: vec![
181 WireRoutePoint { position: Position::new(45.5, 13.5), name: Some("Start".to_owned()) },
182 WireRoutePoint { position: Position::new(45.6, 13.6), name: None },
183 ],
184 }
185 }
186
187 #[test]
188 fn list_translates_every_wire_route_and_its_points() {
189 let backend = FakeBackend { listed: vec![wire_route("a", "Piran to Koper")], ..Default::default() };
190 let routes = list(&backend).unwrap();
191 assert_eq!(routes.len(), 1);
192 assert_eq!(routes[0].uuid, "a");
193 assert_eq!(routes[0].name.as_deref(), Some("Piran to Koper"));
194 assert_eq!(routes[0].waypoints.len(), 2);
195 assert_eq!(routes[0].waypoints[0].name.as_deref(), Some("Start"));
196 assert_eq!(routes[0].waypoints[1].name, None);
197 }
198
199 #[test]
200 fn a_listed_route_with_no_uuid_is_a_protocol_error() {
201 let backend = FakeBackend {
202 listed: vec![WireRoute { uuid: None, name: None, description: None, distance_m: None, points: vec![] }],
203 ..Default::default()
204 };
205 assert!(matches!(list(&backend), Err(ClientError::Protocol(_))));
206 }
207
208 #[test]
209 fn save_always_writes_with_the_routes_own_id() {
210 let backend = FakeBackend { publish_result: RefCell::new(Some(Ok("a".to_owned()))), ..Default::default() };
211 let route = Route::new("a", vec![Waypoint::at(Position::new(45.5, 13.5))]);
212 save(&backend, &route).unwrap();
213 assert_eq!(backend.published.borrow()[0].uuid.as_deref(), Some("a"));
214 }
215
216 #[test]
217 fn save_sends_the_routes_own_distance_so_other_signal_k_clients_can_show_it() {
218 let backend = FakeBackend { publish_result: RefCell::new(Some(Ok("a".to_owned()))), ..Default::default() };
219 let route = Route::new(
220 "a",
221 vec![
222 Waypoint::at(Position::new(45.0, 13.0)),
223 Waypoint::at(Position::new(45.0, 13.0292)), ],
225 );
226 save(&backend, &route).unwrap();
227 let sent = backend.published.borrow()[0].distance_m.expect("a distance was sent");
228 assert!((sent - route.distance_nm() * METRES_PER_NM).abs() < 0.01, "{sent}");
229 }
230
231 #[test]
232 fn save_propagates_a_refusal() {
233 let backend = FakeBackend {
234 publish_result: RefCell::new(Some(Err(ClientError::Protocol("Invalid route".to_owned())))),
235 ..Default::default()
236 };
237 let route = Route::new("a", vec![Waypoint::at(Position::new(45.5, 13.5))]);
238 assert!(save(&backend, &route).is_err());
239 }
240
241 #[test]
242 fn delete_calls_the_backend_with_the_given_uuid() {
243 let backend = FakeBackend { delete_result: RefCell::new(Some(Ok(()))), ..Default::default() };
244 delete(&backend, "a").unwrap();
245 assert_eq!(backend.deleted.borrow()[0], "a");
246 }
247
248 #[test]
249 fn delete_propagates_a_refusal() {
250 let backend = FakeBackend {
251 delete_result: RefCell::new(Some(Err(ClientError::Protocol("nope".to_owned())))),
252 ..Default::default()
253 };
254 assert!(delete(&backend, "a").is_err());
255 }
256}