use std::collections::BTreeMap;
use serde::{Deserialize, Serialize};
pub const LICENSES_PATH: &str = "/data/api/v1/licenses";
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct LicenseStatusWire {
#[serde(default)]
pub cloud: Vec<serde_json::Value>,
#[serde(default)]
pub certificate: Vec<serde_json::Value>,
#[serde(default)]
pub hardware: Vec<LicenseHardware>,
#[serde(default)]
pub embedded: Vec<serde_json::Value>,
#[serde(default)]
pub leased: Vec<serde_json::Value>,
#[serde(default)]
pub effective: LicenseEffective,
#[serde(
rename = "leasedUnactivateTimeoutMS",
alias = "leased_unactivate_timeout_ms",
default,
skip_serializing_if = "Option::is_none"
)]
pub leased_unactivate_timeout_ms: Option<i64>,
#[serde(default)]
pub busy: bool,
#[serde(flatten)]
pub extra: BTreeMap<String, serde_json::Value>,
}
impl LicenseStatusWire {
pub fn item_count(&self) -> usize {
self.hardware.iter().map(|entry| entry.items.len()).sum()
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct LicenseHardware {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub key: Option<String>,
#[serde(default)]
pub items: Vec<LicenseItem>,
#[serde(flatten)]
pub extra: BTreeMap<String, serde_json::Value>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct LicenseItem {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub title: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub version: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub details: Option<serde_json::Value>,
#[serde(flatten)]
pub extra: BTreeMap<String, serde_json::Value>,
}
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
pub struct LicenseEffective {
#[serde(
rename = "lastUpdated",
alias = "last_updated_ms",
default,
skip_serializing_if = "Option::is_none"
)]
pub last_updated_ms: Option<i64>,
#[serde(flatten)]
pub extra: BTreeMap<String, serde_json::Value>,
}
#[cfg(test)]
mod tests {
use super::{LicenseHardware, LicenseStatusWire};
#[test]
fn license_parses_the_live_8_3_6_capture() {
let wire: LicenseStatusWire = serde_json::from_value(serde_json::json!({
"cloud": [],
"certificate": [],
"hardware": [],
"embedded": [],
"leased": [],
"effective": { "lastUpdated": 1788748551614i64 },
"leasedUnactivateTimeoutMS": 7500,
"busy": false
}))
.expect("the live 8.3.6 shape must parse");
assert!(wire.cloud.is_empty());
assert!(wire.hardware.is_empty());
assert_eq!(
wire.effective.last_updated_ms,
Some(1_788_748_551_614),
"epoch MILLISECONDS (magnitude cross-check)"
);
assert_eq!(wire.leased_unactivate_timeout_ms, Some(7500));
assert!(!wire.busy);
assert_eq!(wire.item_count(), 0, "fresh trial rig: zero items");
}
#[test]
fn license_parses_the_live_8_3_3_capture() {
let wire: LicenseStatusWire = serde_json::from_value(serde_json::json!({
"cloud": [],
"certificate": [],
"hardware": [],
"embedded": [],
"leased": [],
"effective": { "lastUpdated": 1788748731641i64 },
"leasedUnactivateTimeoutMS": 7500,
"busy": false
}))
.expect("the live 8.3.3 shape must parse");
assert_eq!(wire.effective.last_updated_ms, Some(1_788_748_731_641));
}
#[test]
fn license_parses_a_populated_hardware_entry_leniently() {
let wire: LicenseStatusWire = serde_json::from_value(serde_json::json!({
"hardware": [{
"key": "0123456789",
"items": [{
"name": "ignition",
"title": "Ignition",
"version": "8.3.6",
"details": { "state": "Active", "expirationDate": null,
"whateverFuturePoint": {"nested": true} }
}]
}]
}))
.expect("a populated hardware entry must parse leniently");
assert_eq!(wire.item_count(), 1);
let entry: &LicenseHardware = &wire.hardware[0];
assert_eq!(entry.key.as_deref(), Some("0123456789"));
let item = &entry.items[0];
assert_eq!(item.name.as_deref(), Some("ignition"));
assert_eq!(item.title.as_deref(), Some("Ignition"));
assert_eq!(item.version.as_deref(), Some("8.3.6"));
assert!(
item.details.is_some(),
"details rides passthrough, never modeled"
);
}
#[test]
fn license_unknown_keys_ride_extra() {
let wire: LicenseStatusWire = serde_json::from_value(serde_json::json!({
"cloud": [],
"certificate": [],
"hardware": [],
"embedded": [],
"leased": [],
"effective": { "lastUpdated": 1788748551614i64 },
"leasedUnactivateTimeoutMS": 7500,
"busy": false,
"brandNewPointReleaseKey": {"future": true}
}))
.expect("unknown keys must never refuse the parse");
assert_eq!(
wire.extra.get("brandNewPointReleaseKey"),
Some(&serde_json::json!({"future": true})),
"the unknown key rides flatten passthrough"
);
}
}