Skip to main content

powerio_core/
component_id.rs

1use std::fmt;
2
3use serde::{Deserialize, Serialize};
4
5use crate::Error;
6use crate::validation::valid_nonempty_text;
7
8/// Stable identity of one component in a PowerIO value.
9///
10/// The component type qualifies the source supplied or PowerIO assigned local
11/// identity. This keeps, for example, a load named `main` distinct from a
12/// generator named `main` without exposing a table row position.
13#[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    /// Construct a component identity from its structural type and local
22    /// identity.
23    ///
24    /// # Errors
25    /// Either part is empty, contains NUL, or exceeds the common identifier
26    /// bound.
27    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    /// The component's structural type, such as `load` or `switch`.
46    #[must_use]
47    pub fn component_type(&self) -> &str {
48        &self.component_type
49    }
50
51    /// The identity within that component type.
52    #[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}