Skip to main content

phoxal_model/component/
mod.rs

1//! Canonical component facts used after authored documents are loaded.
2//!
3//! Runtime consumers use these unversioned values through [`crate::Robot`].
4
5pub mod capability;
6
7use std::collections::BTreeMap;
8use std::fmt;
9
10use capability::Capability;
11
12#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)]
13#[serde(deny_unknown_fields)]
14pub struct Component {
15    capabilities: BTreeMap<String, Capability>,
16}
17
18impl Component {
19    #[doc(hidden)]
20    pub fn __new(capabilities: BTreeMap<String, Capability>) -> Self {
21        Self { capabilities }
22    }
23
24    #[must_use]
25    pub fn capability(&self, capability_id: &str) -> Option<&Capability> {
26        self.capabilities.get(capability_id)
27    }
28
29    pub fn capabilities(&self) -> impl ExactSizeIterator<Item = (&str, &Capability)> {
30        self.capabilities
31            .iter()
32            .map(|(id, capability)| (id.as_str(), capability))
33    }
34}
35
36#[derive(
37    serde::Serialize, serde::Deserialize, Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash,
38)]
39#[serde(try_from = "String", into = "String")]
40pub struct CapabilityRef {
41    pub component_id: String,
42    pub capability_id: String,
43}
44
45impl CapabilityRef {
46    #[must_use]
47    pub fn new(component_id: impl Into<String>, capability_id: impl Into<String>) -> Self {
48        Self {
49            component_id: component_id.into(),
50            capability_id: capability_id.into(),
51        }
52    }
53}
54
55impl fmt::Display for CapabilityRef {
56    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
57        write!(formatter, "{}.{}", self.component_id, self.capability_id)
58    }
59}
60
61impl std::str::FromStr for CapabilityRef {
62    type Err = crate::ModelError;
63
64    fn from_str(value: &str) -> Result<Self, Self::Err> {
65        let (component_id, capability_id) = value.split_once('.').ok_or_else(|| {
66            crate::ModelError::Invalid(format!(
67                "capability reference '{value}' must use component.capability"
68            ))
69        })?;
70        if !is_valid_token(component_id) || !is_valid_token(capability_id) {
71            return Err(crate::ModelError::Invalid(format!(
72                "capability reference '{value}' is not normalized"
73            )));
74        }
75        Ok(Self::new(component_id, capability_id))
76    }
77}
78
79impl TryFrom<String> for CapabilityRef {
80    type Error = crate::ModelError;
81
82    fn try_from(value: String) -> Result<Self, Self::Error> {
83        value.parse()
84    }
85}
86
87impl From<CapabilityRef> for String {
88    fn from(value: CapabilityRef) -> Self {
89        value.to_string()
90    }
91}
92
93#[must_use]
94pub fn is_valid_token(value: &str) -> bool {
95    let trimmed = value.trim();
96    !trimmed.is_empty()
97        && trimmed.chars().all(|character| {
98            character.is_ascii_lowercase()
99                || character.is_ascii_digit()
100                || character == '_'
101                || character == '-'
102        })
103}