daedalus_data/
descriptor.rs

1use serde::{Deserialize, Serialize};
2
3use crate::errors::{DataError, DataErrorCode, DataResult};
4use crate::model::{TypeExpr, Value, ValueType};
5
6/// GPU-related hints carried on descriptors.
7///
8/// ```
9/// use daedalus_data::descriptor::{GpuHints, MemoryLocation};
10/// let hints = GpuHints { requires_gpu: false, preferred_memory: Some(MemoryLocation::Host) };
11/// assert_eq!(hints.preferred_memory, Some(MemoryLocation::Host));
12/// ```
13#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, Default)]
14pub struct GpuHints {
15    pub requires_gpu: bool,
16    pub preferred_memory: Option<MemoryLocation>,
17}
18
19/// Memory location hint for GPU-aware values.
20///
21/// ```
22/// use daedalus_data::descriptor::MemoryLocation;
23/// let loc = MemoryLocation::Device;
24/// assert_eq!(loc, MemoryLocation::Device);
25/// ```
26#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
27pub enum MemoryLocation {
28    Host,
29    Device,
30    Shared,
31}
32
33/// Descriptor for values/types.
34///
35/// ```
36/// use daedalus_data::descriptor::{DataDescriptor, DescriptorId, DescriptorVersion};
37/// let desc = DataDescriptor {
38///     id: DescriptorId::new("example"),
39///     version: DescriptorVersion::new("1.0.0"),
40///     label: None,
41///     settable: false,
42///     default: None,
43///     schema: None,
44///     codecs: vec![],
45///     converters: vec![],
46///     feature_flags: vec![],
47///     gpu: None,
48///     type_expr: None,
49/// };
50/// assert_eq!(desc.id.0, "example");
51/// ```
52#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
53pub struct DataDescriptor {
54    pub id: DescriptorId,
55    pub version: DescriptorVersion,
56    pub label: Option<String>,
57    pub settable: bool,
58    pub default: Option<Value>,
59    pub schema: Option<String>,
60    pub codecs: Vec<String>,
61    pub converters: Vec<String>,
62    pub feature_flags: Vec<String>,
63    pub gpu: Option<GpuHints>,
64    pub type_expr: Option<TypeExpr>,
65}
66
67impl DataDescriptor {
68    /// Validate the descriptor, including type/default compatibility.
69    ///
70    /// ```
71    /// use daedalus_data::descriptor::{DataDescriptor, DescriptorId, DescriptorVersion};
72    /// let desc = DataDescriptor {
73    ///     id: DescriptorId::new("example"),
74    ///     version: DescriptorVersion::new("1.0"),
75    ///     label: None,
76    ///     settable: false,
77    ///     default: None,
78    ///     schema: None,
79    ///     codecs: vec![],
80    ///     converters: vec![],
81    ///     feature_flags: vec![],
82    ///     gpu: None,
83    ///     type_expr: None,
84    /// };
85    /// desc.validate().unwrap();
86    /// ```
87    pub fn validate(&self) -> DataResult<()> {
88        self.id.validate()?;
89        self.version.validate()?;
90        if let Some(default) = &self.default {
91            if self.type_expr.is_none() {
92                return Err(DataError::new(
93                    DataErrorCode::InvalidDescriptor,
94                    "type_expr is required when default is present",
95                ));
96            }
97            validate_default(self.type_expr.as_ref().unwrap(), default)?;
98        }
99        Ok(())
100    }
101
102    /// Deterministic ordering for codecs/converters/feature flags.
103    ///
104    /// ```
105    /// use daedalus_data::descriptor::{DataDescriptor, DescriptorId, DescriptorVersion};
106    /// let desc = DataDescriptor {
107    ///     id: DescriptorId::new("id"),
108    ///     version: DescriptorVersion::new("1.0"),
109    ///     label: None,
110    ///     settable: false,
111    ///     default: None,
112    ///     schema: None,
113    ///     codecs: vec!["b".into(), "a".into()],
114    ///     converters: vec!["y".into(), "x".into()],
115    ///     feature_flags: vec!["b".into(), "a".into()],
116    ///     gpu: None,
117    ///     type_expr: None,
118    /// };
119    /// let sorted = desc.normalize();
120    /// assert_eq!(sorted.codecs, vec!["a", "b"]);
121    /// ```
122    pub fn normalize(mut self) -> Self {
123        self.codecs.sort();
124        self.converters.sort();
125        self.feature_flags.sort();
126        self
127    }
128}
129
130/// Builder to construct descriptors with deterministic ordering.
131///
132/// ```
133/// use daedalus_data::descriptor::{DescriptorBuilder, GpuHints, MemoryLocation};
134/// use daedalus_data::errors::DataResult;
135/// use daedalus_data::model::{TypeExpr, Value, ValueType};
136///
137/// fn build_descriptor() -> DataResult<()> {
138///     let desc = DescriptorBuilder::new("example", "1.0.0")
139///         .label("Example")
140///         .settable(true)
141///         .type_expr(TypeExpr::Scalar(ValueType::String))
142///         .default(Value::String("hi".into()))
143///         .codec("json")
144///         .feature_flag("core")
145///         .gpu_hints(GpuHints { requires_gpu: false, preferred_memory: Some(MemoryLocation::Host) })
146///         .build()?;
147///     assert_eq!(desc.codecs, vec!["json"]);
148///     Ok(())
149/// }
150/// ```
151pub struct DescriptorBuilder {
152    inner: DataDescriptor,
153}
154
155impl DescriptorBuilder {
156    pub fn new(id: impl Into<String>, version: impl Into<String>) -> Self {
157        Self {
158            inner: DataDescriptor {
159                id: DescriptorId::new(id.into()),
160                version: DescriptorVersion::new(version.into()),
161                label: None,
162                settable: false,
163                default: None,
164                schema: None,
165                codecs: Vec::new(),
166                converters: Vec::new(),
167                feature_flags: Vec::new(),
168                gpu: None,
169                type_expr: None,
170            },
171        }
172    }
173
174    pub fn label(mut self, label: impl Into<String>) -> Self {
175        self.inner.label = Some(label.into());
176        self
177    }
178
179    pub fn settable(mut self, settable: bool) -> Self {
180        self.inner.settable = settable;
181        self
182    }
183
184    pub fn default(mut self, default: Value) -> Self {
185        self.inner.default = Some(default);
186        self
187    }
188
189    pub fn schema(mut self, schema: impl Into<String>) -> Self {
190        self.inner.schema = Some(schema.into());
191        self
192    }
193
194    pub fn codec(mut self, codec: impl Into<String>) -> Self {
195        self.inner.codecs.push(codec.into());
196        self
197    }
198
199    pub fn converter(mut self, conv: impl Into<String>) -> Self {
200        self.inner.converters.push(conv.into());
201        self
202    }
203
204    pub fn feature_flag(mut self, flag: impl Into<String>) -> Self {
205        self.inner.feature_flags.push(flag.into());
206        self
207    }
208
209    pub fn gpu_hints(mut self, hints: GpuHints) -> Self {
210        self.inner.gpu = Some(hints);
211        self
212    }
213
214    pub fn type_expr(mut self, ty: TypeExpr) -> Self {
215        self.inner.type_expr = Some(ty.normalize());
216        self
217    }
218
219    pub fn build(self) -> DataResult<DataDescriptor> {
220        let desc = self.inner.normalize();
221        desc.validate()?;
222        Ok(desc)
223    }
224}
225
226/// Descriptor for a type expression with associated metadata.
227///
228/// ```
229/// use daedalus_data::descriptor::{DataDescriptor, DescriptorId, DescriptorVersion, TypeDescriptor};
230/// use daedalus_data::model::{TypeExpr, ValueType};
231/// let desc = DataDescriptor {
232///     id: DescriptorId::new("demo"),
233///     version: DescriptorVersion::new("1.0.0"),
234///     label: None,
235///     settable: false,
236///     default: None,
237///     schema: None,
238///     codecs: vec![],
239///     converters: vec![],
240///     feature_flags: vec![],
241///     gpu: None,
242///     type_expr: None,
243/// };
244/// let typed = TypeDescriptor { ty: TypeExpr::Scalar(ValueType::Int), descriptor: desc };
245/// assert!(matches!(typed.ty, TypeExpr::Scalar(_)));
246/// ```
247#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
248pub struct TypeDescriptor {
249    pub ty: TypeExpr,
250    pub descriptor: DataDescriptor,
251}
252
253/// Strongly typed descriptor id with basic namespace validation.
254///
255/// ```
256/// use daedalus_data::descriptor::DescriptorId;
257/// let id = DescriptorId::namespaced("sensor", "temp");
258/// assert_eq!(id.0, "sensor.temp");
259/// ```
260#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize, Hash)]
261pub struct DescriptorId(pub String);
262
263impl DescriptorId {
264    pub fn new(id: impl Into<String>) -> Self {
265        Self(id.into())
266    }
267
268    pub fn namespaced(namespace: impl Into<String>, name: impl Into<String>) -> Self {
269        let ns = namespace.into();
270        let name = name.into();
271        if ns.is_empty() {
272            return Self(name);
273        }
274        Self(format!("{ns}.{name}"))
275    }
276    pub fn validate(&self) -> DataResult<()> {
277        if self.0.is_empty() {
278            return Err(DataError::new(
279                DataErrorCode::InvalidDescriptor,
280                "id must not be empty",
281            ));
282        }
283        if !self.0.chars().all(|c| {
284            c.is_ascii_lowercase() || c.is_ascii_digit() || c == '.' || c == '_' || c == '-'
285        }) {
286            return Err(DataError::new(
287                DataErrorCode::InvalidDescriptor,
288                "id must be lowercase/digit/._-",
289            ));
290        }
291        Ok(())
292    }
293}
294
295/// Strongly typed semantic version string.
296///
297/// ```
298/// use daedalus_data::descriptor::DescriptorVersion;
299/// let ver = DescriptorVersion::new("1.2.3");
300/// assert_eq!(ver.0, "1.2.3");
301/// ```
302#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize, Hash)]
303pub struct DescriptorVersion(pub String);
304
305impl DescriptorVersion {
306    pub fn new(v: impl Into<String>) -> Self {
307        Self(v.into())
308    }
309    pub fn validate(&self) -> DataResult<()> {
310        let parts: Vec<_> = self.0.split('.').collect();
311        if parts.len() < 2 {
312            return Err(DataError::new(
313                DataErrorCode::InvalidDescriptor,
314                "version must be at least major.minor",
315            ));
316        }
317        if parts
318            .iter()
319            .any(|p| p.is_empty() || p.chars().any(|c| !c.is_ascii_digit()))
320        {
321            return Err(DataError::new(
322                DataErrorCode::InvalidDescriptor,
323                "version segments must be numeric",
324            ));
325        }
326        Ok(())
327    }
328}
329
330fn validate_default(ty: &TypeExpr, value: &Value) -> DataResult<()> {
331    match (ty, value) {
332        (TypeExpr::Scalar(ValueType::Unit), Value::Unit) => Ok(()),
333        (TypeExpr::Scalar(ValueType::Bool), Value::Bool(_)) => Ok(()),
334        (TypeExpr::Scalar(ValueType::I32 | ValueType::U32 | ValueType::Int), Value::Int(_)) => {
335            Ok(())
336        }
337        (TypeExpr::Scalar(ValueType::F32 | ValueType::Float), Value::Float(_)) => Ok(()),
338        (TypeExpr::Scalar(ValueType::String), Value::String(_)) => Ok(()),
339        (TypeExpr::Scalar(ValueType::Bytes), Value::Bytes(_)) => Ok(()),
340        (TypeExpr::Optional(inner), v) => validate_default(inner, v),
341        (TypeExpr::List(inner), Value::List(items)) => {
342            for v in items {
343                validate_default(inner, v)?;
344            }
345            Ok(())
346        }
347        (TypeExpr::Map(k_ty, v_ty), Value::Map(entries)) => {
348            for (k, v) in entries {
349                validate_default(k_ty, k)?;
350                validate_default(v_ty, v)?;
351            }
352            Ok(())
353        }
354        (TypeExpr::Tuple(types), Value::Tuple(values)) => {
355            if types.len() != values.len() {
356                return Err(DataError::new(
357                    DataErrorCode::InvalidType,
358                    "tuple length mismatch",
359                ));
360            }
361            for (t, v) in types.iter().zip(values.iter()) {
362                validate_default(t, v)?;
363            }
364            Ok(())
365        }
366        (TypeExpr::Struct(fields), Value::Struct(values)) => {
367            if fields.len() != values.len() {
368                return Err(DataError::new(
369                    DataErrorCode::InvalidType,
370                    "struct field count mismatch",
371                ));
372            }
373            for (field, val) in fields.iter().zip(values.iter()) {
374                if field.name != val.name {
375                    return Err(DataError::new(
376                        DataErrorCode::InvalidType,
377                        "struct field name mismatch",
378                    ));
379                }
380                validate_default(&field.ty, &val.value)?;
381            }
382            Ok(())
383        }
384        (TypeExpr::Enum(variants), Value::Enum(ev)) => {
385            let variant = variants.iter().find(|v| v.name == ev.name).ok_or_else(|| {
386                DataError::new(DataErrorCode::InvalidType, "enum variant not found")
387            })?;
388            match (&variant.ty, &ev.value) {
389                (None, None) => Ok(()),
390                (Some(t), Some(v)) => validate_default(t, v),
391                (None, Some(_)) | (Some(_), None) => Err(DataError::new(
392                    DataErrorCode::InvalidType,
393                    "enum payload mismatch",
394                )),
395            }
396        }
397        _ => Err(DataError::new(
398            DataErrorCode::InvalidType,
399            "default does not match type_expr",
400        )),
401    }
402}
403
404#[cfg(test)]
405mod tests {
406    use super::*;
407    use crate::model::{StructField, StructFieldValue};
408
409    #[test]
410    fn normalize_sorts_fields() {
411        let desc = DataDescriptor {
412            id: DescriptorId::new("id"),
413            version: DescriptorVersion::new("v1"),
414            label: None,
415            settable: true,
416            default: None,
417            schema: None,
418            codecs: vec!["b".into(), "a".into()],
419            converters: vec!["y".into(), "x".into()],
420            feature_flags: vec!["f2".into(), "f1".into()],
421            gpu: None,
422            type_expr: None,
423        }
424        .normalize();
425        assert_eq!(desc.codecs, vec!["a", "b"]);
426        assert_eq!(desc.converters, vec!["x", "y"]);
427        assert_eq!(desc.feature_flags, vec!["f1", "f2"]);
428    }
429
430    #[test]
431    fn serde_preserves_sorted_order() {
432        let desc = DescriptorBuilder::new("id", "1.0")
433            .codec("z")
434            .codec("a")
435            .converter("b")
436            .converter("a")
437            .feature_flag("beta")
438            .feature_flag("alpha")
439            .build()
440            .expect("build");
441        let json = serde_json::to_string(&desc).unwrap();
442        assert!(json.find("a").unwrap() < json.find("z").unwrap());
443        assert!(json.find("alpha").unwrap() < json.find("beta").unwrap());
444    }
445
446    /// Minimal registry fixture showing `(id, version)` uniqueness and conflict diagnostics.
447    ///
448    /// ```
449    /// use std::collections::HashMap;
450    /// use daedalus_data::descriptor::{DataDescriptor, DescriptorBuilder, DescriptorId, DescriptorVersion};
451    ///
452    /// #[derive(Default)]
453    /// struct Registry {
454    ///     entries: HashMap<(DescriptorId, DescriptorVersion), DataDescriptor>,
455    /// }
456    ///
457    /// impl Registry {
458    ///     fn register(&mut self, desc: DataDescriptor) -> Result<(), String> {
459    ///         let key = (desc.id.clone(), desc.version.clone());
460    ///         if self.entries.contains_key(&key) {
461    ///             return Err(format!("duplicate descriptor {:?},{}", key.0, key.1));
462    ///         }
463    ///         self.entries.insert(key, desc);
464    ///         Ok(())
465    ///     }
466    /// }
467    ///
468    /// let mut reg = Registry::default();
469    /// let desc = DescriptorBuilder::new("sensor.temp", "1.0.0").build().unwrap();
470    /// reg.register(desc.clone()).unwrap();
471    /// assert!(reg.register(desc).is_err());
472    /// ```
473    #[test]
474    fn registry_fixture_compiles() {
475        // Doc-test above is the primary fixture; keep this test as a placeholder.
476    }
477
478    #[test]
479    fn golden_descriptor_serialization_is_stable() {
480        let desc = DescriptorBuilder::new("id", "1.0")
481            .label("Example")
482            .settable(true)
483            .codec("json")
484            .converter("int_to_string")
485            .feature_flag("core")
486            .build()
487            .expect("build");
488        let json = serde_json::to_string(&desc).unwrap();
489        assert_eq!(
490            json,
491            r#"{"id":"id","version":"1.0","label":"Example","settable":true,"default":null,"schema":null,"codecs":["json"],"converters":["int_to_string"],"feature_flags":["core"],"gpu":null,"type_expr":null}"#
492        );
493    }
494
495    #[test]
496    fn validates_default_against_type() {
497        let desc = DescriptorBuilder::new("id", "1.0")
498            .type_expr(TypeExpr::Scalar(ValueType::String))
499            .default(Value::String("ok".into()))
500            .build()
501            .unwrap();
502        assert_eq!(desc.id.0, "id");
503
504        let err = DescriptorBuilder::new("id2", "1.0")
505            .type_expr(TypeExpr::Scalar(ValueType::Int))
506            .default(Value::Bool(true))
507            .build()
508            .unwrap_err();
509        assert_eq!(err.code(), DataErrorCode::InvalidType);
510
511        let err = DescriptorBuilder::new("id3", "1.0")
512            .type_expr(TypeExpr::Struct(vec![StructField {
513                name: "a".into(),
514                ty: TypeExpr::Scalar(ValueType::Int),
515            }]))
516            .default(Value::Struct(vec![StructFieldValue {
517                name: "a".into(),
518                value: Value::Int(1),
519            }]))
520            .build()
521            .unwrap();
522        assert_eq!(err.id.0, "id3");
523    }
524}