use serde::{Deserialize, Serialize};
use zlink::{proxy, ReplyError};
#[proxy("io.systemd.Metrics")]
pub trait Metrics {
#[zlink(more)]
async fn list(
&mut self,
) -> zlink::Result<
impl futures_util::Stream<Item = zlink::Result<Result<ListOutput, MetricsError>>>,
>;
#[zlink(more)]
async fn describe(
&mut self,
) -> zlink::Result<
impl futures_util::Stream<Item = zlink::Result<Result<DescribeOutput, MetricsError>>>,
>;
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct ListOutput {
pub name: String,
pub value: serde_json::Value,
pub object: Option<String>,
pub fields: Option<std::collections::HashMap<String, serde_json::Value>>,
}
impl ListOutput {
pub fn name(&self) -> &str {
&self.name
}
pub fn name_suffix(&self) -> &str {
self.name
.rsplit_once('.')
.map(|(_, suffix)| suffix)
.unwrap_or(&self.name)
}
pub fn value(&self) -> &serde_json::Value {
&self.value
}
pub fn object(&self) -> Option<&str> {
self.object.as_deref()
}
pub fn object_name(&self) -> String {
self.object.as_deref().unwrap_or("").to_string()
}
pub fn value_as_string(&self) -> &str {
self.value
.as_str()
.expect("value_as_string called on non-string value; validate metric type first")
}
pub fn value_as_int(&self) -> i64 {
self.value
.as_i64()
.expect("value_as_int called on non-integer value; validate metric type first")
}
pub fn value_as_bool(&self) -> bool {
self.value
.as_bool()
.expect("value_as_bool called on non-boolean value; validate metric type first")
}
pub fn fields(&self) -> Option<&std::collections::HashMap<String, serde_json::Value>> {
self.fields.as_ref()
}
pub fn get_field_as_str(&self, field_name: &str) -> Option<&str> {
self.fields
.as_ref()
.and_then(|f| f.get(field_name))
.and_then(|v| v.as_str())
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct DescribeOutput {
pub name: String,
pub description: String,
pub r#type: MetricFamilyType,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "snake_case")]
pub enum MetricFamilyType {
Counter,
Gauge,
String,
}
#[derive(Debug, Clone, PartialEq, ReplyError)]
#[zlink(interface = "io.systemd.Metrics")]
pub enum MetricsError {
NoSuchMetric,
}
impl std::fmt::Display for MetricsError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
MetricsError::NoSuchMetric => write!(f, "No such metric found"),
}
}
}
impl std::error::Error for MetricsError {}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_object_name_with_value() {
let output = ListOutput {
name: "test.metric".to_string(),
value: serde_json::Value::Null,
object: Some("my-service.service".to_string()),
fields: None,
};
assert_eq!(output.object_name(), "my-service.service");
}
#[test]
fn test_object_name_without_value() {
let output = ListOutput {
name: "test.metric".to_string(),
value: serde_json::Value::Null,
object: None,
fields: None,
};
assert_eq!(output.object_name(), "");
}
#[test]
fn test_object_name_with_empty_string() {
let output = ListOutput {
name: "test.metric".to_string(),
value: serde_json::Value::Null,
object: Some("".to_string()),
fields: None,
};
assert_eq!(output.object_name(), "");
}
#[test]
fn test_object_returns_option() {
let output_with_object = ListOutput {
name: "test.metric".to_string(),
value: serde_json::Value::Null,
object: Some("service.service".to_string()),
fields: None,
};
let output_without_object = ListOutput {
name: "test.metric".to_string(),
value: serde_json::Value::Null,
object: None,
fields: None,
};
assert_eq!(output_with_object.object(), Some("service.service"));
assert_eq!(output_without_object.object(), None);
}
#[test]
fn test_get_field_as_str_existing_field() {
let mut fields = std::collections::HashMap::new();
fields.insert("type".to_string(), serde_json::json!("service"));
fields.insert("state".to_string(), serde_json::json!("active"));
let output = ListOutput {
name: "test.metric".to_string(),
value: serde_json::Value::Null,
object: None,
fields: Some(fields),
};
assert_eq!(output.get_field_as_str("type"), Some("service"));
assert_eq!(output.get_field_as_str("state"), Some("active"));
}
#[test]
fn test_get_field_as_str_missing_field() {
let fields = std::collections::HashMap::new();
let output = ListOutput {
name: "test.metric".to_string(),
value: serde_json::Value::Null,
object: None,
fields: Some(fields),
};
assert_eq!(output.get_field_as_str("nonexistent"), None);
}
#[test]
fn test_get_field_as_str_no_fields() {
let output = ListOutput {
name: "test.metric".to_string(),
value: serde_json::Value::Null,
object: None,
fields: None,
};
assert_eq!(output.get_field_as_str("type"), None);
}
#[test]
fn test_get_field_as_str_non_string_value() {
let mut fields = std::collections::HashMap::new();
fields.insert("number".to_string(), serde_json::json!(123));
fields.insert("bool".to_string(), serde_json::json!(true));
let output = ListOutput {
name: "test.metric".to_string(),
value: serde_json::Value::Null,
object: None,
fields: Some(fields),
};
assert_eq!(output.get_field_as_str("number"), None);
assert_eq!(output.get_field_as_str("bool"), None);
}
#[test]
fn test_name_suffix() {
let output = ListOutput {
name: "io.systemd.unit_active_state".to_string(),
value: serde_json::Value::Null,
object: None,
fields: None,
};
assert_eq!(output.name_suffix(), "unit_active_state");
}
#[test]
fn test_name_suffix_no_dots() {
let output = ListOutput {
name: "simple_name".to_string(),
value: serde_json::Value::Null,
object: None,
fields: None,
};
assert_eq!(output.name_suffix(), "simple_name");
}
#[test]
fn test_name_suffix_empty() {
let output = ListOutput {
name: "".to_string(),
value: serde_json::Value::Null,
object: None,
fields: None,
};
assert_eq!(output.name_suffix(), "");
}
#[test]
fn test_value_as_string_with_value() {
let output = ListOutput {
name: "test.metric".to_string(),
value: serde_json::json!("active"),
object: None,
fields: None,
};
assert_eq!(output.value_as_string(), "active");
}
#[test]
fn test_value_as_string_empty_string() {
let output = ListOutput {
name: "test.metric".to_string(),
value: serde_json::json!(""),
object: None,
fields: None,
};
assert_eq!(output.value_as_string(), "");
}
#[test]
fn test_value_as_int_with_value() {
let output = ListOutput {
name: "test.metric".to_string(),
value: serde_json::json!(42),
object: None,
fields: None,
};
assert_eq!(output.value_as_int(), 42);
}
#[test]
#[should_panic(expected = "value_as_int called on non-integer value")]
fn test_value_as_int_without_value() {
let output = ListOutput {
name: "test.metric".to_string(),
value: serde_json::Value::Null,
object: None,
fields: None,
};
output.value_as_int();
}
#[test]
fn test_value_as_int_zero() {
let output = ListOutput {
name: "test.metric".to_string(),
value: serde_json::json!(0),
object: None,
fields: None,
};
assert_eq!(output.value_as_int(), 0);
}
#[test]
fn test_value_as_int_negative() {
let output = ListOutput {
name: "test.metric".to_string(),
value: serde_json::json!(-5),
object: None,
fields: None,
};
assert_eq!(output.value_as_int(), -5);
}
#[test]
fn test_value_as_int_large_number() {
let output = ListOutput {
name: "test.metric".to_string(),
value: serde_json::json!(9999999999_i64),
object: None,
fields: None,
};
assert_eq!(output.value_as_int(), 9999999999);
}
#[test]
fn test_value_as_bool_true() {
let output = ListOutput {
name: "test.metric".to_string(),
value: serde_json::json!(true),
object: None,
fields: None,
};
assert_eq!(output.value_as_bool(), true);
}
#[test]
fn test_value_as_bool_false() {
let output = ListOutput {
name: "test.metric".to_string(),
value: serde_json::json!(false),
object: None,
fields: None,
};
assert_eq!(output.value_as_bool(), false);
}
#[test]
#[should_panic(expected = "value_as_bool called on non-boolean value")]
fn test_value_as_bool_none() {
let output = ListOutput {
name: "test.metric".to_string(),
value: serde_json::Value::Null,
object: None,
fields: None,
};
output.value_as_bool();
}
}