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