powerio_core/
component_id.rs1use std::fmt;
2
3use serde::{Deserialize, Serialize};
4
5use crate::Error;
6use crate::validation::valid_nonempty_text;
7
8#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
14#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
15pub struct ComponentId {
16 component_type: Box<str>,
17 local_id: Box<str>,
18}
19
20impl ComponentId {
21 pub fn new(
28 component_type: impl Into<String>,
29 local_id: impl Into<String>,
30 ) -> Result<Self, Error> {
31 let component_type = component_type.into();
32 let local_id = local_id.into();
33 if !valid_nonempty_text(&component_type) || !valid_nonempty_text(&local_id) {
34 return Err(Error::new(
35 &crate::codes::VALIDATE_COMPONENT_INVALID_ID,
36 "a component type and local identity must both be nonempty and bounded",
37 ));
38 }
39 Ok(Self {
40 component_type: component_type.into_boxed_str(),
41 local_id: local_id.into_boxed_str(),
42 })
43 }
44
45 #[must_use]
47 pub fn component_type(&self) -> &str {
48 &self.component_type
49 }
50
51 #[must_use]
53 pub fn local_id(&self) -> &str {
54 &self.local_id
55 }
56}
57
58impl fmt::Display for ComponentId {
59 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
60 write!(formatter, "{}/{}", self.component_type, self.local_id)
61 }
62}
63
64#[cfg(test)]
65mod tests {
66 use super::*;
67
68 #[test]
69 fn identity_is_type_qualified() {
70 let load = ComponentId::new("load", "main").unwrap();
71 let generator = ComponentId::new("generator", "main").unwrap();
72 assert_ne!(load, generator);
73 assert_eq!(load.component_type(), "load");
74 assert_eq!(load.local_id(), "main");
75 assert_eq!(load.to_string(), "load/main");
76 }
77
78 #[test]
79 fn invalid_parts_are_rejected() {
80 assert!(ComponentId::new("", "main").is_err());
81 assert!(ComponentId::new("load", "").is_err());
82 assert!(ComponentId::new("load", "bad\0id").is_err());
83 }
84}