phoxal_model/component/
mod.rs1pub 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}