1use serde::{Deserialize, Serialize};
4use zlink::{proxy, ReplyError};
5
6#[proxy("io.systemd.Metrics")]
8pub trait Metrics {
9 #[zlink(more)]
12 async fn list(
13 &mut self,
14 ) -> zlink::Result<
15 impl futures_util::Stream<Item = zlink::Result<Result<ListOutput, MetricsError>>>,
16 >;
17 #[zlink(more)]
20 async fn describe(
21 &mut self,
22 ) -> zlink::Result<
23 impl futures_util::Stream<Item = zlink::Result<Result<DescribeOutput, MetricsError>>>,
24 >;
25}
26
27#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
29pub struct ListOutput {
30 pub name: String,
31 pub value: serde_json::Value,
32 pub object: Option<String>,
33 pub fields: Option<std::collections::HashMap<String, serde_json::Value>>,
34}
35
36impl ListOutput {
37 pub fn name(&self) -> &str {
39 &self.name
40 }
41
42 pub fn name_suffix(&self) -> &str {
44 self.name
45 .rsplit_once('.')
46 .map(|(_, suffix)| suffix)
47 .unwrap_or(&self.name)
48 }
49
50 pub fn value(&self) -> &serde_json::Value {
52 &self.value
53 }
54
55 pub fn object(&self) -> Option<&str> {
57 self.object.as_deref()
58 }
59
60 pub fn object_name(&self) -> String {
62 self.object.as_deref().unwrap_or("").to_string()
63 }
64
65 pub fn value_as_string(&self) -> &str {
67 self.value
68 .as_str()
69 .expect("value_as_string called on non-string value; validate metric type first")
70 }
71
72 pub fn value_as_int(&self) -> i64 {
74 self.value
75 .as_i64()
76 .expect("value_as_int called on non-integer value; validate metric type first")
77 }
78
79 pub fn value_as_bool(&self) -> bool {
81 self.value
82 .as_bool()
83 .expect("value_as_bool called on non-boolean value; validate metric type first")
84 }
85
86 pub fn fields(&self) -> Option<&std::collections::HashMap<String, serde_json::Value>> {
88 self.fields.as_ref()
89 }
90
91 pub fn get_field_as_str(&self, field_name: &str) -> Option<&str> {
93 self.fields
94 .as_ref()
95 .and_then(|f| f.get(field_name))
96 .and_then(|v| v.as_str())
97 }
98}
99#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
101pub struct DescribeOutput {
102 pub name: String,
103 pub description: String,
104 pub r#type: MetricFamilyType,
105}
106#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
108#[serde(rename_all = "snake_case")]
109pub enum MetricFamilyType {
110 Counter,
112 Gauge,
114 String,
116}
117
118#[derive(Debug, Clone, PartialEq, ReplyError)]
120#[zlink(interface = "io.systemd.Metrics")]
121pub enum MetricsError {
122 NoSuchMetric,
124}
125
126impl std::fmt::Display for MetricsError {
127 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
128 match self {
129 MetricsError::NoSuchMetric => write!(f, "No such metric found"),
130 }
131 }
132}
133
134impl std::error::Error for MetricsError {}
135
136#[cfg(test)]
137mod tests {
138 use super::*;
139
140 #[test]
141 fn test_object_name_with_value() {
142 let output = ListOutput {
143 name: "test.metric".to_string(),
144 value: serde_json::Value::Null,
145 object: Some("my-service.service".to_string()),
146 fields: None,
147 };
148
149 assert_eq!(output.object_name(), "my-service.service");
150 }
151
152 #[test]
153 fn test_object_name_without_value() {
154 let output = ListOutput {
155 name: "test.metric".to_string(),
156 value: serde_json::Value::Null,
157 object: None,
158 fields: None,
159 };
160
161 assert_eq!(output.object_name(), "");
162 }
163
164 #[test]
165 fn test_object_name_with_empty_string() {
166 let output = ListOutput {
167 name: "test.metric".to_string(),
168 value: serde_json::Value::Null,
169 object: Some("".to_string()),
170 fields: None,
171 };
172
173 assert_eq!(output.object_name(), "");
174 }
175
176 #[test]
177 fn test_object_returns_option() {
178 let output_with_object = ListOutput {
179 name: "test.metric".to_string(),
180 value: serde_json::Value::Null,
181 object: Some("service.service".to_string()),
182 fields: None,
183 };
184
185 let output_without_object = ListOutput {
186 name: "test.metric".to_string(),
187 value: serde_json::Value::Null,
188 object: None,
189 fields: None,
190 };
191
192 assert_eq!(output_with_object.object(), Some("service.service"));
193 assert_eq!(output_without_object.object(), None);
194 }
195
196 #[test]
197 fn test_get_field_as_str_existing_field() {
198 let mut fields = std::collections::HashMap::new();
199 fields.insert("type".to_string(), serde_json::json!("service"));
200 fields.insert("state".to_string(), serde_json::json!("active"));
201
202 let output = ListOutput {
203 name: "test.metric".to_string(),
204 value: serde_json::Value::Null,
205 object: None,
206 fields: Some(fields),
207 };
208
209 assert_eq!(output.get_field_as_str("type"), Some("service"));
210 assert_eq!(output.get_field_as_str("state"), Some("active"));
211 }
212
213 #[test]
214 fn test_get_field_as_str_missing_field() {
215 let fields = std::collections::HashMap::new();
216
217 let output = ListOutput {
218 name: "test.metric".to_string(),
219 value: serde_json::Value::Null,
220 object: None,
221 fields: Some(fields),
222 };
223
224 assert_eq!(output.get_field_as_str("nonexistent"), None);
225 }
226
227 #[test]
228 fn test_get_field_as_str_no_fields() {
229 let output = ListOutput {
230 name: "test.metric".to_string(),
231 value: serde_json::Value::Null,
232 object: None,
233 fields: None,
234 };
235
236 assert_eq!(output.get_field_as_str("type"), None);
237 }
238
239 #[test]
240 fn test_get_field_as_str_non_string_value() {
241 let mut fields = std::collections::HashMap::new();
242 fields.insert("number".to_string(), serde_json::json!(123));
243 fields.insert("bool".to_string(), serde_json::json!(true));
244
245 let output = ListOutput {
246 name: "test.metric".to_string(),
247 value: serde_json::Value::Null,
248 object: None,
249 fields: Some(fields),
250 };
251
252 assert_eq!(output.get_field_as_str("number"), None);
253 assert_eq!(output.get_field_as_str("bool"), None);
254 }
255
256 #[test]
257 fn test_name_suffix() {
258 let output = ListOutput {
259 name: "io.systemd.unit_active_state".to_string(),
260 value: serde_json::Value::Null,
261 object: None,
262 fields: None,
263 };
264
265 assert_eq!(output.name_suffix(), "unit_active_state");
266 }
267
268 #[test]
269 fn test_name_suffix_no_dots() {
270 let output = ListOutput {
271 name: "simple_name".to_string(),
272 value: serde_json::Value::Null,
273 object: None,
274 fields: None,
275 };
276
277 assert_eq!(output.name_suffix(), "simple_name");
278 }
279
280 #[test]
281 fn test_name_suffix_empty() {
282 let output = ListOutput {
283 name: "".to_string(),
284 value: serde_json::Value::Null,
285 object: None,
286 fields: None,
287 };
288
289 assert_eq!(output.name_suffix(), "");
290 }
291
292 #[test]
293 fn test_value_as_string_with_value() {
294 let output = ListOutput {
295 name: "test.metric".to_string(),
296 value: serde_json::json!("active"),
297 object: None,
298 fields: None,
299 };
300
301 assert_eq!(output.value_as_string(), "active");
302 }
303
304 #[test]
305 fn test_value_as_string_empty_string() {
306 let output = ListOutput {
307 name: "test.metric".to_string(),
308 value: serde_json::json!(""),
309 object: None,
310 fields: None,
311 };
312
313 assert_eq!(output.value_as_string(), "");
314 }
315
316 #[test]
317 fn test_value_as_int_with_value() {
318 let output = ListOutput {
319 name: "test.metric".to_string(),
320 value: serde_json::json!(42),
321 object: None,
322 fields: None,
323 };
324
325 assert_eq!(output.value_as_int(), 42);
326 }
327
328 #[test]
329 #[should_panic(expected = "value_as_int called on non-integer value")]
330 fn test_value_as_int_without_value() {
331 let output = ListOutput {
332 name: "test.metric".to_string(),
333 value: serde_json::Value::Null,
334 object: None,
335 fields: None,
336 };
337
338 output.value_as_int();
339 }
340
341 #[test]
342 fn test_value_as_int_zero() {
343 let output = ListOutput {
344 name: "test.metric".to_string(),
345 value: serde_json::json!(0),
346 object: None,
347 fields: None,
348 };
349
350 assert_eq!(output.value_as_int(), 0);
351 }
352
353 #[test]
354 fn test_value_as_int_negative() {
355 let output = ListOutput {
356 name: "test.metric".to_string(),
357 value: serde_json::json!(-5),
358 object: None,
359 fields: None,
360 };
361
362 assert_eq!(output.value_as_int(), -5);
363 }
364
365 #[test]
366 fn test_value_as_int_large_number() {
367 let output = ListOutput {
368 name: "test.metric".to_string(),
369 value: serde_json::json!(9999999999_i64),
370 object: None,
371 fields: None,
372 };
373
374 assert_eq!(output.value_as_int(), 9999999999);
375 }
376
377 #[test]
378 fn test_value_as_bool_true() {
379 let output = ListOutput {
380 name: "test.metric".to_string(),
381 value: serde_json::json!(true),
382 object: None,
383 fields: None,
384 };
385
386 assert_eq!(output.value_as_bool(), true);
387 }
388
389 #[test]
390 fn test_value_as_bool_false() {
391 let output = ListOutput {
392 name: "test.metric".to_string(),
393 value: serde_json::json!(false),
394 object: None,
395 fields: None,
396 };
397
398 assert_eq!(output.value_as_bool(), false);
399 }
400
401 #[test]
402 #[should_panic(expected = "value_as_bool called on non-boolean value")]
403 fn test_value_as_bool_none() {
404 let output = ListOutput {
405 name: "test.metric".to_string(),
406 value: serde_json::Value::Null,
407 object: None,
408 fields: None,
409 };
410
411 output.value_as_bool();
412 }
413}