Skip to main content

navcore_signalk_client/
server_info.rs

1//! Fetching the server's own identity off the wire.
2//!
3//! `GET /signalk` -- the root Signal K itself sits on, before any API
4//! version -- answers with the server implementation's own id and
5//! version, nothing about a boat: confirmed against a real server,
6//! `{"server": {"id":"signalk-server-node","version":"2.31.1"},
7//! "endpoints": {...}}`. This is the one figure that says which
8//! server, not which vessel, a client has found -- two installations
9//! serving the same boat's data (one built into an onboard device, and
10//! a shore-side test instance) are otherwise indistinguishable from
11//! the outside.
12
13use std::time::Duration;
14
15use crate::access::agent;
16use crate::{ClientError, Trust};
17
18/// Same figure [`crate::units::fetch_active`] uses, for the same reason:
19/// this is a call on a boat's own LAN.
20const TIMEOUT: Duration = Duration::from_secs(10);
21
22/// What a Signal K server says about itself at its own root.
23#[derive(Debug, Clone, PartialEq, Eq)]
24pub struct ServerInfo {
25    /// The server implementation's own id, e.g. `"signalk-server-node"`.
26    pub id: String,
27    /// The server's own version string, e.g. `"2.31.1"`.
28    pub version: String,
29}
30
31/// Asks the server what it is.
32///
33/// No token: the same "server-wide, not the vessel's own" reasoning
34/// [`crate::units::fetch_active`]'s own doc gives, and confirmed the
35/// same way -- this answers without one on a server with security on.
36///
37/// # Errors
38///
39/// If the server cannot be reached, or its root does not carry a
40/// `server.id` and `server.version` the way every Signal K server this
41/// crate has met does.
42pub fn fetch(http_base: &str, trust: &Trust) -> Result<ServerInfo, ClientError> {
43    let answer: serde_json::Value = agent(trust)?
44        .get(format!("{http_base}/signalk"))
45        .config()
46        .timeout_global(Some(TIMEOUT))
47        .build()
48        .call()
49        .map_err(|error| ClientError::Http(error.to_string()))?
50        .body_mut()
51        .read_json()
52        .map_err(|error| ClientError::Http(error.to_string()))?;
53
54    parse(&answer)
55}
56
57/// The read half of [`fetch`], apart from it so it can be tested without
58/// a server: `answer` is the root's own JSON body.
59fn parse(answer: &serde_json::Value) -> Result<ServerInfo, ClientError> {
60    let field = |name: &'static str| {
61        answer
62            .get("server")
63            .and_then(|server| server.get(name))
64            .and_then(serde_json::Value::as_str)
65            .map(str::to_owned)
66            .ok_or_else(|| ClientError::Protocol(format!("no \"server.{name}\" at the root")))
67    };
68
69    Ok(ServerInfo {
70        id: field("id")?,
71        version: field("version")?,
72    })
73}
74
75#[cfg(test)]
76mod tests {
77    use super::*;
78
79    #[test]
80    fn a_real_servers_own_root_is_read() {
81        // Confirmed live against this workspace's own dev signalk-server.
82        let answer = serde_json::json!({
83            "endpoints": {"v1": {"version": "2.31.1"}},
84            "server": {"id": "signalk-server-node", "version": "2.31.1"},
85        });
86        assert_eq!(
87            parse(&answer).unwrap(),
88            ServerInfo { id: "signalk-server-node".to_owned(), version: "2.31.1".to_owned() }
89        );
90    }
91
92    #[test]
93    fn a_root_with_no_server_object_is_a_protocol_error() {
94        let answer = serde_json::json!({"endpoints": {}});
95        assert!(matches!(parse(&answer), Err(ClientError::Protocol(_))));
96    }
97}