use super::{ApiVersion, Inner, transport::Request};
use crate::error::{Error, Result};
use crate::model::system::V3Version;
const MIN_V3: (u32, u32, u32) = (3, 0, 3);
#[derive(Debug, Clone, Default, PartialEq, Eq)]
#[non_exhaustive]
pub struct ServerInfo {
pub version: Option<String>,
pub api_v3_version: Option<String>,
pub storage: Option<String>,
}
impl ServerInfo {
#[must_use]
pub fn supports_v3(&self) -> bool {
self.api_v3_version
.as_deref()
.and_then(parse_semver)
.is_some_and(|v| v >= MIN_V3)
}
}
fn parse_semver(s: &str) -> Option<(u32, u32, u32)> {
let core = s.split(['-', '+']).next()?;
let mut parts = core.split('.').map(|p| p.trim().parse::<u32>().ok());
Some((
parts.next()??,
parts.next().flatten().unwrap_or(0),
parts.next().flatten().unwrap_or(0),
))
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum Api {
V1,
V3,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum Support {
Both,
V1Only,
V3Only,
}
impl Inner {
pub(crate) async fn server_info(&self) -> Result<&ServerInfo> {
self.server.get_or_try_init(|| self.discover()).await
}
async fn discover(&self) -> Result<ServerInfo> {
let probe = Request::get("api/v3/version", "api/v3/version").anonymous();
match self.execute(probe).await {
Ok(resp) => {
let v: V3Version = resp.v3()?;
Ok(ServerInfo {
version: v.version,
api_v3_version: v.api_version,
storage: v.storage.map(|s| {
[s.storage, s.version]
.into_iter()
.flatten()
.collect::<Vec<_>>()
.join(" ")
}),
})
}
Err(Error::NotFound) => {
let status = Request::get("api/v1/status.json", "api/v1/status.json");
let version = match self.execute(status).await {
Ok(resp) => resp
.json::<serde_json::Value>()
.ok()
.and_then(|v| v.get("version")?.as_str().map(str::to_owned)),
Err(_) => None,
};
Ok(ServerInfo {
version,
..ServerInfo::default()
})
}
Err(err) => Err(err),
}
}
pub(crate) async fn pick_api(&self, support: Support, feature: &'static str) -> Result<Api> {
let unsupported = |reason: &str| Error::Unsupported {
feature,
reason: reason.to_owned(),
};
match support {
Support::V1Only => Ok(Api::V1),
Support::V3Only => {
if self.credentials.is_api_secret() {
return Err(unsupported(
"API v3 does not accept API_SECRET; use an access token",
));
}
if self.api == ApiVersion::V1 {
return Err(unsupported("the client is pinned to API v1"));
}
if self.server_info().await?.supports_v3() {
Ok(Api::V3)
} else {
Err(unsupported(
"this Nightscout has no API v3 (upgrade to 14.1 or later)",
))
}
}
Support::Both => match self.api {
ApiVersion::V1 => Ok(Api::V1),
ApiVersion::V3 if self.credentials.is_api_secret() => Err(unsupported(
"API v3 does not accept API_SECRET; use an access token",
)),
ApiVersion::V3 => Ok(Api::V3),
ApiVersion::Auto => {
if self.credentials.is_access_token() && self.server_info().await?.supports_v3()
{
Ok(Api::V3)
} else {
Ok(Api::V1)
}
}
},
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn v3_version_gate() {
let info = |v: &str| ServerInfo {
api_v3_version: Some(v.into()),
..ServerInfo::default()
};
assert!(info("3.0.5").supports_v3());
assert!(info("3.0.3").supports_v3());
assert!(!info("3.0.2-alpha").supports_v3());
assert!(info("3.1").supports_v3());
assert!(!ServerInfo::default().supports_v3());
}
}