Skip to main content

paramodel_elements/
types.rs

1// Copyright (c) Jonathan Shook
2// SPDX-License-Identifier: Apache-2.0
3
4//! Element type descriptors and the registry that dispatches by type.
5//!
6//! Per SRD-0007 D15, `ElementTypeDescriptor` carries the minimal
7//! metadata needed to validate element construction: which labels are
8//! required, forbidden, or warning-worthy for elements of this type,
9//! plus the infrastructure flag the planner uses.
10//!
11//! `ElementTypeDescriptorRegistry` is the host-provided trait;
12//! embedding systems (hyperplane and peers) implement it to expose
13//! their type catalogues. [`OpenRegistry`] is a permissive default that
14//! accepts any type id — useful in tests.
15
16use std::collections::{BTreeMap, BTreeSet};
17
18use crate::{LabelKey, name_type};
19
20name_type! {
21    /// Canonical identifier for an element type (e.g. `"service"`,
22    /// `"command"`, `"node"`).
23    pub struct TypeId { kind: "TypeId" }
24}
25
26/// Metadata the compiler uses to validate an element of this type.
27#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize, bon::Builder)]
28pub struct ElementTypeDescriptor {
29    /// Canonical type id.
30    pub type_id: TypeId,
31
32    /// Labels that elements of this type *must* carry.
33    #[builder(default)]
34    pub required_labels: BTreeSet<LabelKey>,
35
36    /// Labels that are forbidden on elements of this type, keyed by
37    /// label with a message explaining why.
38    #[builder(default)]
39    pub forbidden_labels: BTreeMap<LabelKey, String>,
40
41    /// Labels whose presence triggers a warning (not an error).
42    #[builder(default)]
43    pub label_warnings: BTreeMap<LabelKey, String>,
44
45    /// `true` if this type represents infrastructure (a node).
46    #[builder(default)]
47    pub provides_infrastructure: bool,
48}
49
50/// Host-provided catalogue of element-type descriptors.
51///
52/// Only place in the elements layer where `dyn Trait` appears; it's an
53/// embedding-system service, not part of the element algebra itself.
54pub trait ElementTypeDescriptorRegistry: Send + Sync + std::fmt::Debug + 'static {
55    /// Every descriptor the registry knows about.
56    fn descriptors(&self) -> Vec<ElementTypeDescriptor>;
57
58    /// Type aliases that resolve to canonical type ids. Default empty.
59    fn type_aliases(&self) -> BTreeMap<TypeId, TypeId> {
60        BTreeMap::new()
61    }
62
63    /// Set of valid canonical type ids (derived from [`Self::descriptors`]).
64    fn valid_type_ids(&self) -> BTreeSet<TypeId> {
65        self.descriptors()
66            .into_iter()
67            .map(|d| d.type_id)
68            .collect()
69    }
70
71    /// Look up one descriptor by canonical type id.
72    fn descriptor(&self, type_id: &TypeId) -> Option<ElementTypeDescriptor> {
73        self.descriptors()
74            .into_iter()
75            .find(|d| &d.type_id == type_id)
76    }
77
78    /// `true` when any registered descriptor carries
79    /// `provides_infrastructure`.
80    fn has_infrastructure_type(&self) -> bool {
81        self.descriptors()
82            .iter()
83            .any(|d| d.provides_infrastructure)
84    }
85}
86
87/// Permissive default registry — accepts any type id as valid.
88///
89/// Intended for tests and mock hosts. Real hosts supply their own
90/// implementation that catalogues the types they can materialise.
91#[derive(Debug, Default)]
92pub struct OpenRegistry;
93
94impl OpenRegistry {
95    /// Construct a new open registry.
96    #[must_use]
97    pub const fn new() -> Self {
98        Self
99    }
100}
101
102impl ElementTypeDescriptorRegistry for OpenRegistry {
103    fn descriptors(&self) -> Vec<ElementTypeDescriptor> {
104        // The open registry reports no descriptors. `descriptor` below
105        // is overridden to synthesise one on demand so any incoming
106        // type id validates.
107        Vec::new()
108    }
109
110    fn valid_type_ids(&self) -> BTreeSet<TypeId> {
111        BTreeSet::new()
112    }
113
114    fn descriptor(&self, type_id: &TypeId) -> Option<ElementTypeDescriptor> {
115        Some(
116            ElementTypeDescriptor::builder()
117                .type_id(type_id.clone())
118                .build(),
119        )
120    }
121
122    fn has_infrastructure_type(&self) -> bool {
123        false
124    }
125}
126
127#[cfg(test)]
128mod tests {
129    use super::*;
130
131    #[test]
132    fn type_id_validates() {
133        TypeId::new("service").unwrap();
134        assert!(TypeId::new("").is_err());
135    }
136
137    #[test]
138    fn descriptor_builder_default_fields() {
139        let d = ElementTypeDescriptor::builder()
140            .type_id(TypeId::new("service").unwrap())
141            .build();
142        assert!(d.required_labels.is_empty());
143        assert!(d.forbidden_labels.is_empty());
144        assert!(d.label_warnings.is_empty());
145        assert!(!d.provides_infrastructure);
146    }
147
148    #[test]
149    fn open_registry_accepts_any_type() {
150        let r = OpenRegistry::new();
151        let id = TypeId::new("whatever").unwrap();
152        let d = r.descriptor(&id).unwrap();
153        assert_eq!(d.type_id, id);
154        assert!(!r.has_infrastructure_type());
155    }
156
157    #[derive(Debug)]
158    struct FixedRegistry {
159        descriptors: Vec<ElementTypeDescriptor>,
160    }
161
162    impl ElementTypeDescriptorRegistry for FixedRegistry {
163        fn descriptors(&self) -> Vec<ElementTypeDescriptor> {
164            self.descriptors.clone()
165        }
166    }
167
168    #[test]
169    fn custom_registry_dispatches_by_type_id() {
170        let reg = FixedRegistry {
171            descriptors: vec![
172                ElementTypeDescriptor::builder()
173                    .type_id(TypeId::new("service").unwrap())
174                    .provides_infrastructure(false)
175                    .build(),
176                ElementTypeDescriptor::builder()
177                    .type_id(TypeId::new("node").unwrap())
178                    .provides_infrastructure(true)
179                    .build(),
180            ],
181        };
182        assert!(reg.descriptor(&TypeId::new("service").unwrap()).is_some());
183        assert!(reg.descriptor(&TypeId::new("node").unwrap()).is_some());
184        assert!(reg.descriptor(&TypeId::new("absent").unwrap()).is_none());
185        assert!(reg.has_infrastructure_type());
186    }
187
188    #[test]
189    fn descriptor_serde_roundtrip() {
190        let d = ElementTypeDescriptor::builder()
191            .type_id(TypeId::new("service").unwrap())
192            .required_labels(std::iter::once(LabelKey::new("name").unwrap()).collect())
193            .build();
194        let json = serde_json::to_string(&d).unwrap();
195        let back: ElementTypeDescriptor = serde_json::from_str(&json).unwrap();
196        assert_eq!(d, back);
197    }
198}