use serde::{Deserialize, Serialize};
pub const MIN_GATEWAY: &str = "8.3.1";
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct GatewayInfo {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub redundancy_role: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub edition: Option<String>,
#[serde(rename = "ignitionVersion", alias = "version")]
pub ignition_version: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub jvm_version: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub license: Option<LicenseInfo>,
#[serde(skip, default)]
pub endpoint: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct LicenseInfo {
pub mode: String,
#[serde(
rename = "expirationDate",
alias = "expiration_date",
default,
skip_serializing_if = "Option::is_none"
)]
pub expiration_date: Option<String>,
}
pub fn below_minimum(raw: &str) -> bool {
let clean = raw.trim().split(['-', ' ']).next().unwrap_or(raw);
let normalized = if clean.matches('.').count() == 1 {
format!("{clean}.0")
} else {
clean.to_string()
};
match semver::Version::parse(&normalized) {
Ok(version) => {
version < semver::Version::parse(MIN_GATEWAY).expect("MIN_GATEWAY is valid semver")
}
Err(_) => true,
}
}
#[cfg(test)]
mod tests {
use super::{GatewayInfo, LicenseInfo, MIN_GATEWAY, below_minimum};
#[test]
fn below_minimum_matrix() {
let cases = [
("8.3.1", false), ("8.3.2", false), ("8.3.0", true), ("8.1.10", true), ("8.3", true), ("8.3.1-SNAPSHOT.20260801", false), (" 8.3.2 ", false), ("garbage", true), ("", true), ("9.0.0", false), ("8.3.6 (b2026042713)", false), ("8.3.6", false), ];
for (raw, expected) in cases {
assert_eq!(
below_minimum(raw),
expected,
"below_minimum({raw:?}) must be {expected}"
);
}
assert_eq!(MIN_GATEWAY, "8.3.1");
}
#[test]
fn gateway_info_parses_live_and_legacy_field_names() {
let live = serde_json::json!({
"name": "ign-live",
"redundancyRole": "Independent",
"edition": "standard",
"ignitionVersion": "8.3.6 (b2026042713)",
"jvmVersion": "17.0.11",
"license": {"mode": "Trial", "expirationDate": "2026-08-21"}
});
let info: GatewayInfo =
serde_json::from_value(live).expect("live ignitionVersion shape parses");
assert_eq!(info.ignition_version, "8.3.6 (b2026042713)");
assert_eq!(info.name.as_deref(), Some("ign-live"));
assert_eq!(info.license.as_ref().expect("license").mode, "Trial");
let legacy = serde_json::json!({"version": "8.3.2"});
let info: GatewayInfo =
serde_json::from_value(legacy).expect("legacy `version` name still parses (alias)");
assert_eq!(info.ignition_version, "8.3.2");
}
#[test]
fn gateway_info_serializes_under_the_gateway_native_key() {
let info = GatewayInfo {
name: Some("ign-live-rig".into()),
redundancy_role: Some("Independent".into()),
edition: Some("standard".into()),
ignition_version: "8.3.6 (b2026042713)".into(),
jvm_version: None,
license: Some(LicenseInfo {
mode: "Trial".into(),
expiration_date: None,
}),
endpoint: None,
};
let json = serde_json::to_value(&info).expect("serialize");
assert_eq!(json["ignitionVersion"], "8.3.6 (b2026042713)");
assert_eq!(json["name"], "ign-live-rig");
assert_eq!(json["license"]["mode"], "Trial");
assert!(
json.get("endpoint").is_none(),
"endpoint is never serialized (CORE-05 skip)"
);
}
}