Skip to main content

alembic_core/
ir.rs

1//! data model types for alembic.
2
3use serde::{Deserialize, Serialize};
4use serde_json::Value;
5use std::collections::BTreeMap;
6use std::fmt;
7use std::hash::{Hash, Hasher};
8use std::ops::{Deref, DerefMut};
9use std::path::PathBuf;
10use uuid::Uuid;
11
12/// source location for tracking where an object was defined.
13#[derive(Debug, Clone, PartialEq, Eq)]
14pub struct SourceLocation {
15    /// path to the source file.
16    pub file: PathBuf,
17    /// line number in the file (1-indexed), if known.
18    pub line: Option<usize>,
19    /// column number in the file (1-indexed), if known.
20    pub column: Option<usize>,
21}
22
23impl SourceLocation {
24    /// create a source location with just a file path.
25    pub fn file(path: impl Into<PathBuf>) -> Self {
26        Self {
27            file: path.into(),
28            line: None,
29            column: None,
30        }
31    }
32
33    /// create a source location with file and line number.
34    pub fn file_line(path: impl Into<PathBuf>, line: usize) -> Self {
35        Self {
36            file: path.into(),
37            line: Some(line),
38            column: None,
39        }
40    }
41}
42
43impl fmt::Display for SourceLocation {
44    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
45        write!(f, "{}", self.file.display())?;
46        if let Some(line) = self.line {
47            write!(f, ":{}", line)?;
48            if let Some(col) = self.column {
49                write!(f, ":{}", col)?;
50            }
51        }
52        Ok(())
53    }
54}
55
56/// stable object identifier (uuid).
57pub type Uid = Uuid;
58
59/// json object wrapper for typed access and stricter boundaries.
60#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize, Eq, Hash)]
61#[serde(transparent)]
62pub struct JsonMap(pub BTreeMap<String, Value>);
63
64impl JsonMap {
65    pub fn into_inner(self) -> BTreeMap<String, Value> {
66        self.0
67    }
68
69    pub fn is_empty(&self) -> bool {
70        self.0.is_empty()
71    }
72
73    pub fn get_str(&self, key: &str) -> Option<&str> {
74        self.get(key)?.as_str()
75    }
76
77    pub fn get_bool(&self, key: &str) -> Option<bool> {
78        self.get(key)?.as_bool()
79    }
80
81    pub fn get_i64(&self, key: &str) -> Option<i64> {
82        self.get(key)?.as_i64()
83    }
84
85    pub fn get_f64(&self, key: &str) -> Option<f64> {
86        self.get(key)?.as_f64()
87    }
88}
89
90impl Deref for JsonMap {
91    type Target = BTreeMap<String, Value>;
92
93    fn deref(&self) -> &Self::Target {
94        &self.0
95    }
96}
97
98impl DerefMut for JsonMap {
99    fn deref_mut(&mut self) -> &mut Self::Target {
100        &mut self.0
101    }
102}
103
104impl From<BTreeMap<String, Value>> for JsonMap {
105    fn from(map: BTreeMap<String, Value>) -> Self {
106        Self(map)
107    }
108}
109
110impl From<JsonMap> for BTreeMap<String, Value> {
111    fn from(map: JsonMap) -> Self {
112        map.0
113    }
114}
115
116/// structured key for object identity.
117#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize, Eq, Hash)]
118#[serde(transparent)]
119pub struct Key(pub BTreeMap<String, Value>);
120
121impl Key {
122    pub fn into_inner(self) -> BTreeMap<String, Value> {
123        self.0
124    }
125
126    pub fn is_empty(&self) -> bool {
127        self.0.is_empty()
128    }
129}
130
131impl Deref for Key {
132    type Target = BTreeMap<String, Value>;
133
134    fn deref(&self) -> &Self::Target {
135        &self.0
136    }
137}
138
139impl DerefMut for Key {
140    fn deref_mut(&mut self) -> &mut Self::Target {
141        &mut self.0
142    }
143}
144
145impl From<BTreeMap<String, Value>> for Key {
146    fn from(map: BTreeMap<String, Value>) -> Self {
147        Self(map)
148    }
149}
150
151impl From<Key> for BTreeMap<String, Value> {
152    fn from(map: Key) -> Self {
153        map.0
154    }
155}
156
157pub fn key_string(key: &Key) -> String {
158    let new_key: Key = Key(key
159        .0
160        .iter()
161        .map(|(k, v)| (k.clone(), canonicalize_number(v.clone())))
162        .collect());
163    serde_json::to_string(&new_key).unwrap_or_default()
164}
165
166/// prefer integer representation if lossless
167fn canonicalize_number(v: Value) -> Value {
168    match v {
169        Value::Number(n) => {
170            if let Some(i) = n.as_i64() {
171                Value::Number(i.into())
172            } else if let Some(u) = n.as_u64() {
173                Value::Number(u.into())
174            } else if let Some(f) = n.as_f64() {
175                let i = f.round() as i64;
176                if f64::abs(i as f64 - f) < 1e-5 {
177                    Value::Number(i.into())
178                } else {
179                    Value::Number(serde_json::Number::from_f64(f).unwrap())
180                }
181            } else {
182                Value::Number(n)
183            }
184        }
185        Value::Object(map) => Value::Object(
186            map.into_iter()
187                .map(|(k, v)| (k, canonicalize_number(v)))
188                .collect(),
189        ),
190        Value::Array(arr) => Value::Array(arr.into_iter().map(canonicalize_number).collect()),
191        other => other,
192    }
193}
194
195pub const ALEMBIC_UID_NAMESPACE: Uuid = Uuid::from_bytes([
196    0x45, 0x93, 0x1a, 0x5f, 0x6c, 0x2b, 0x49, 0x6a, 0x9b, 0x6f, 0x8f, 0x77, 0x7d, 0x4f, 0x3a, 0x1c,
197]);
198
199pub fn uid_v5(type_name: &str, stable: &str) -> Uid {
200    let name = format!("{type_name}:{stable}");
201    Uuid::new_v5(&ALEMBIC_UID_NAMESPACE, name.as_bytes())
202}
203
204/// canonical object type name.
205#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
206#[serde(transparent)]
207pub struct TypeName(String);
208
209impl TypeName {
210    pub fn new(name: impl Into<String>) -> Self {
211        Self(name.into())
212    }
213
214    pub fn as_str(&self) -> &str {
215        &self.0
216    }
217
218    pub fn is_empty(&self) -> bool {
219        self.0.trim().is_empty()
220    }
221}
222
223impl fmt::Display for TypeName {
224    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
225        f.write_str(self.as_str())
226    }
227}
228
229/// field type definition in the schema.
230#[derive(Debug, Clone, PartialEq)]
231pub enum FieldType {
232    String,
233    Text,
234    Int,
235    Float,
236    Bool,
237    Uuid,
238    Date,
239    Datetime,
240    Time,
241    Json,
242    IpAddress,
243    Cidr,
244    Prefix,
245    Mac,
246    Slug,
247    Enum { values: Vec<String> },
248    List { item: Box<FieldType> },
249    Map { value: Box<FieldType> },
250    Ref { target: String },
251    ListRef { target: String },
252}
253
254/// format constraints for string fields.
255#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
256#[serde(rename_all = "snake_case")]
257pub enum FieldFormat {
258    Slug,
259    IpAddress,
260    Cidr,
261    Prefix,
262    Mac,
263    Uuid,
264}
265
266impl Serialize for FieldType {
267    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
268    where
269        S: serde::Serializer,
270    {
271        use serde::ser::SerializeMap;
272        match self {
273            FieldType::String => serializer.serialize_str("string"),
274            FieldType::Text => serializer.serialize_str("text"),
275            FieldType::Int => serializer.serialize_str("int"),
276            FieldType::Float => serializer.serialize_str("float"),
277            FieldType::Bool => serializer.serialize_str("bool"),
278            FieldType::Uuid => serializer.serialize_str("uuid"),
279            FieldType::Date => serializer.serialize_str("date"),
280            FieldType::Datetime => serializer.serialize_str("datetime"),
281            FieldType::Time => serializer.serialize_str("time"),
282            FieldType::Json => serializer.serialize_str("json"),
283            FieldType::IpAddress => serializer.serialize_str("ip_address"),
284            FieldType::Cidr => serializer.serialize_str("cidr"),
285            FieldType::Prefix => serializer.serialize_str("prefix"),
286            FieldType::Mac => serializer.serialize_str("mac"),
287            FieldType::Slug => serializer.serialize_str("slug"),
288            FieldType::Enum { values } => {
289                let mut map = serializer.serialize_map(Some(2))?;
290                map.serialize_entry("type", "enum")?;
291                map.serialize_entry("values", values)?;
292                map.end()
293            }
294            FieldType::List { item } => {
295                let mut map = serializer.serialize_map(Some(2))?;
296                map.serialize_entry("type", "list")?;
297                map.serialize_entry("item", item)?;
298                map.end()
299            }
300            FieldType::Map { value } => {
301                let mut map = serializer.serialize_map(Some(2))?;
302                map.serialize_entry("type", "map")?;
303                map.serialize_entry("value", value)?;
304                map.end()
305            }
306            FieldType::Ref { target } => {
307                let mut map = serializer.serialize_map(Some(2))?;
308                map.serialize_entry("type", "ref")?;
309                map.serialize_entry("target", target)?;
310                map.end()
311            }
312            FieldType::ListRef { target } => {
313                let mut map = serializer.serialize_map(Some(2))?;
314                map.serialize_entry("type", "list_ref")?;
315                map.serialize_entry("target", target)?;
316                map.end()
317            }
318        }
319    }
320}
321
322impl<'de> Deserialize<'de> for FieldType {
323    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
324    where
325        D: serde::Deserializer<'de>,
326    {
327        let value = serde_json::Value::deserialize(deserializer)?;
328        parse_field_type_value(&value).map_err(serde::de::Error::custom)
329    }
330}
331
332fn parse_field_type_value(value: &serde_json::Value) -> Result<FieldType, String> {
333    match value {
334        serde_json::Value::String(raw) => parse_simple_field_type(raw),
335        serde_json::Value::Object(map) => {
336            let raw_type = map
337                .get("type")
338                .and_then(serde_json::Value::as_str)
339                .ok_or_else(|| "field type requires a string 'type' key".to_string())?;
340            match raw_type {
341                "enum" => {
342                    let values = map
343                        .get("values")
344                        .and_then(serde_json::Value::as_array)
345                        .ok_or_else(|| "enum type requires values array".to_string())?
346                        .iter()
347                        .map(|value| {
348                            value
349                                .as_str()
350                                .map(str::to_string)
351                                .ok_or_else(|| "enum values must be strings".to_string())
352                        })
353                        .collect::<Result<Vec<_>, _>>()?;
354                    Ok(FieldType::Enum { values })
355                }
356                "list" => {
357                    let item = map
358                        .get("item")
359                        .ok_or_else(|| "list type requires item".to_string())?;
360                    Ok(FieldType::List {
361                        item: Box::new(parse_field_type_value(item)?),
362                    })
363                }
364                "map" => {
365                    let value = map
366                        .get("value")
367                        .ok_or_else(|| "map type requires value".to_string())?;
368                    Ok(FieldType::Map {
369                        value: Box::new(parse_field_type_value(value)?),
370                    })
371                }
372                "ref" => {
373                    let target = map
374                        .get("target")
375                        .and_then(serde_json::Value::as_str)
376                        .ok_or_else(|| "ref type requires target".to_string())?;
377                    Ok(FieldType::Ref {
378                        target: target.to_string(),
379                    })
380                }
381                "list_ref" => {
382                    let target = map
383                        .get("target")
384                        .and_then(serde_json::Value::as_str)
385                        .ok_or_else(|| "list_ref type requires target".to_string())?;
386                    Ok(FieldType::ListRef {
387                        target: target.to_string(),
388                    })
389                }
390                _ => {
391                    if map.len() != 1 {
392                        return Err(format!("unknown field type {raw_type}"));
393                    }
394                    parse_simple_field_type(raw_type)
395                }
396            }
397        }
398        _ => Err("field type must be a string or map".to_string()),
399    }
400}
401
402fn parse_simple_field_type(raw: &str) -> Result<FieldType, String> {
403    match raw {
404        "string" => Ok(FieldType::String),
405        "text" => Ok(FieldType::Text),
406        "int" => Ok(FieldType::Int),
407        "float" => Ok(FieldType::Float),
408        "bool" => Ok(FieldType::Bool),
409        "uuid" => Ok(FieldType::Uuid),
410        "date" => Ok(FieldType::Date),
411        "datetime" => Ok(FieldType::Datetime),
412        "time" => Ok(FieldType::Time),
413        "json" => Ok(FieldType::Json),
414        "ip_address" => Ok(FieldType::IpAddress),
415        "cidr" => Ok(FieldType::Cidr),
416        "prefix" => Ok(FieldType::Prefix),
417        "mac" => Ok(FieldType::Mac),
418        "slug" => Ok(FieldType::Slug),
419        _ => Err(format!("unknown field type {raw}")),
420    }
421}
422
423fn parse_field_format(raw: &str) -> Result<FieldFormat, String> {
424    match raw {
425        "slug" => Ok(FieldFormat::Slug),
426        "ip_address" => Ok(FieldFormat::IpAddress),
427        "cidr" => Ok(FieldFormat::Cidr),
428        "prefix" => Ok(FieldFormat::Prefix),
429        "mac" => Ok(FieldFormat::Mac),
430        "uuid" => Ok(FieldFormat::Uuid),
431        _ => Err(format!("unknown field format {raw}")),
432    }
433}
434
435/// schema metadata for a single field.
436#[derive(Debug, Clone, PartialEq, Serialize)]
437pub struct FieldSchema {
438    pub r#type: FieldType,
439    #[serde(default)]
440    pub required: bool,
441    #[serde(default)]
442    pub nullable: bool,
443    #[serde(default, skip_serializing_if = "Option::is_none")]
444    pub format: Option<FieldFormat>,
445    #[serde(default, skip_serializing_if = "Option::is_none")]
446    pub pattern: Option<String>,
447    #[serde(default, skip_serializing_if = "Option::is_none")]
448    pub description: Option<String>,
449}
450
451impl<'de> Deserialize<'de> for FieldSchema {
452    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
453    where
454        D: serde::Deserializer<'de>,
455    {
456        let value = serde_json::Value::deserialize(deserializer)?;
457        let map = value
458            .as_object()
459            .ok_or_else(|| serde::de::Error::custom("field schema must be an object"))?;
460
461        let bool_field = |key: &str| -> Result<bool, D::Error> {
462            match map.get(key) {
463                None => Ok(false),
464                Some(value) => value.as_bool().ok_or_else(|| {
465                    serde::de::Error::custom(format!("field schema `{key}` must be a boolean"))
466                }),
467            }
468        };
469        let str_field = |key: &str| -> Result<Option<String>, D::Error> {
470            match map.get(key) {
471                None => Ok(None),
472                Some(value) => value.as_str().map(|s| Some(s.to_string())).ok_or_else(|| {
473                    serde::de::Error::custom(format!("field schema `{key}` must be a string"))
474                }),
475            }
476        };
477
478        let required = bool_field("required")?;
479        let nullable = bool_field("nullable")?;
480        let description = str_field("description")?;
481        let pattern = str_field("pattern")?;
482        let format = str_field("format")?
483            .map(|raw| parse_field_format(&raw).map_err(serde::de::Error::custom))
484            .transpose()?;
485
486        let type_value = map
487            .get("type")
488            .ok_or_else(|| serde::de::Error::custom("field schema requires type"))?;
489        let field_type = match type_value {
490            serde_json::Value::String(raw) => match raw.as_str() {
491                "list" => {
492                    let item = map
493                        .get("item")
494                        .ok_or_else(|| serde::de::Error::custom("list type requires item"))?;
495                    FieldType::List {
496                        item: Box::new(
497                            parse_field_type_value(item).map_err(serde::de::Error::custom)?,
498                        ),
499                    }
500                }
501                "map" => {
502                    let value = map
503                        .get("value")
504                        .ok_or_else(|| serde::de::Error::custom("map type requires value"))?;
505                    FieldType::Map {
506                        value: Box::new(
507                            parse_field_type_value(value).map_err(serde::de::Error::custom)?,
508                        ),
509                    }
510                }
511                "enum" => {
512                    let values = map
513                        .get("values")
514                        .and_then(serde_json::Value::as_array)
515                        .ok_or_else(|| serde::de::Error::custom("enum type requires values"))?
516                        .iter()
517                        .map(|value| {
518                            value.as_str().map(str::to_string).ok_or_else(|| {
519                                serde::de::Error::custom("enum values must be strings")
520                            })
521                        })
522                        .collect::<Result<Vec<_>, _>>()?;
523                    FieldType::Enum { values }
524                }
525                "ref" => {
526                    let target = map
527                        .get("target")
528                        .and_then(serde_json::Value::as_str)
529                        .ok_or_else(|| serde::de::Error::custom("ref type requires target"))?;
530                    FieldType::Ref {
531                        target: target.to_string(),
532                    }
533                }
534                "list_ref" => {
535                    let target = map
536                        .get("target")
537                        .and_then(serde_json::Value::as_str)
538                        .ok_or_else(|| serde::de::Error::custom("list_ref type requires target"))?;
539                    FieldType::ListRef {
540                        target: target.to_string(),
541                    }
542                }
543                _ => parse_simple_field_type(raw).map_err(serde::de::Error::custom)?,
544            },
545            _ => parse_field_type_value(type_value).map_err(serde::de::Error::custom)?,
546        };
547
548        Ok(FieldSchema {
549            r#type: field_type,
550            required,
551            nullable,
552            format,
553            pattern,
554            description,
555        })
556    }
557}
558
559/// schema metadata for a type.
560#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
561pub struct TypeSchema {
562    pub key: BTreeMap<String, FieldSchema>,
563    #[serde(default)]
564    pub fields: BTreeMap<String, FieldSchema>,
565}
566
567/// collection of schema definitions.
568#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
569pub struct Schema {
570    #[serde(default)]
571    pub types: BTreeMap<String, TypeSchema>,
572}
573
574/// object envelope for the ir.
575#[derive(Debug, Clone, Serialize, Deserialize, Eq)]
576pub struct Object {
577    /// stable identifier for the object.
578    pub uid: Uid,
579    /// canonical type for the object.
580    #[serde(rename = "type", alias = "kind")]
581    pub type_name: TypeName,
582    /// structured key used for matching when state is missing.
583    pub key: Key,
584    /// attributes payload for this object.
585    #[serde(default, rename = "attrs")]
586    pub attrs: JsonMap,
587    /// source location where this object was defined (not serialized).
588    #[serde(skip)]
589    pub source: Option<SourceLocation>,
590}
591
592impl PartialEq for Object {
593    fn eq(&self, other: &Self) -> bool {
594        // source location is intentionally excluded from equality
595        self.uid == other.uid
596            && self.type_name == other.type_name
597            && self.key == other.key
598            && self.attrs == other.attrs
599    }
600}
601
602impl Hash for Object {
603    fn hash<H: Hasher>(&self, hasher: &mut H) {
604        self.uid.hash(hasher);
605        self.type_name.hash(hasher);
606        self.key.hash(hasher);
607        self.attrs.hash(hasher);
608    }
609}
610
611#[derive(Debug, Clone, PartialEq, Eq)]
612pub enum ObjectError {
613    MissingType,
614    MissingKey,
615}
616
617impl fmt::Display for ObjectError {
618    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
619        match self {
620            ObjectError::MissingType => f.write_str("object type must be set"),
621            ObjectError::MissingKey => f.write_str("object key must be set"),
622        }
623    }
624}
625
626impl std::error::Error for ObjectError {}
627
628impl Object {
629    /// create an object with a type name.
630    pub fn new(
631        uid: Uid,
632        type_name: TypeName,
633        key: Key,
634        attrs: JsonMap,
635    ) -> Result<Self, ObjectError> {
636        if type_name.is_empty() {
637            return Err(ObjectError::MissingType);
638        }
639        if key.is_empty() {
640            return Err(ObjectError::MissingKey);
641        }
642        Ok(Self {
643            uid,
644            type_name,
645            key,
646            attrs,
647            source: None,
648        })
649    }
650
651    /// set the source location for this object.
652    pub fn with_source(mut self, source: SourceLocation) -> Self {
653        self.source = Some(source);
654        self
655    }
656}
657
658/// top-level inventory of objects.
659#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
660pub struct Inventory {
661    /// schema definitions for type metadata.
662    pub schema: Schema,
663    /// list of objects in this inventory.
664    #[serde(default)]
665    pub objects: Vec<Object>,
666}
667
668#[cfg(test)]
669mod tests {
670    use super::*;
671
672    #[test]
673    fn object_roundtrip_json() {
674        let mut key = BTreeMap::new();
675        key.insert("slug".to_string(), serde_json::json!("fra1"));
676        let mut attrs = BTreeMap::new();
677        attrs.insert("name".to_string(), serde_json::json!("FRA1"));
678        let object = Object::new(
679            Uuid::from_u128(1),
680            TypeName::new("dcim.site"),
681            Key::from(key),
682            attrs.into(),
683        )
684        .unwrap();
685
686        let value = serde_json::to_value(&object).unwrap();
687        let decoded: Object = serde_json::from_value(value).unwrap();
688        assert_eq!(decoded.uid, object.uid);
689        assert_eq!(decoded.type_name, object.type_name);
690        assert_eq!(decoded.key, object.key);
691        assert_eq!(decoded.attrs, object.attrs);
692    }
693
694    #[test]
695    fn field_type_roundtrip() {
696        let cases = vec![
697            FieldType::String,
698            FieldType::Int,
699            FieldType::Enum {
700                values: vec!["a".to_string()],
701            },
702            FieldType::Ref {
703                target: "test".to_string(),
704            },
705            FieldType::List {
706                item: Box::new(FieldType::Bool),
707            },
708        ];
709        for case in cases {
710            let json = serde_json::to_string(&case).unwrap();
711            let back: FieldType = serde_json::from_str(&json).unwrap();
712            assert_eq!(back, case);
713        }
714    }
715
716    #[test]
717    fn json_map_helpers() {
718        let mut map = JsonMap::default();
719        map.insert("s".to_string(), serde_json::json!("val"));
720        map.insert("b".to_string(), serde_json::json!(true));
721        map.insert("i".to_string(), serde_json::json!(123));
722        map.insert("f".to_string(), serde_json::json!(1.23));
723
724        assert_eq!(map.get_str("s"), Some("val"));
725        assert_eq!(map.get_bool("b"), Some(true));
726        assert_eq!(map.get_i64("i"), Some(123));
727        assert_eq!(map.get_f64("f"), Some(1.23));
728
729        assert_eq!(map.get_str("none"), None);
730        assert_eq!(map.get_str("b"), None); // wrong type
731    }
732
733    #[test]
734    fn test_key_string() {
735        let mut k = BTreeMap::new();
736        k.insert("a".to_string(), serde_json::json!(1));
737        k.insert("b".to_string(), serde_json::json!("s"));
738        let key = Key::from(k);
739        let s = key_string(&key);
740        let parsed: serde_json::Value = serde_json::from_str(&s).unwrap();
741        let expected = serde_json::json!({"a": 1, "b": "s"});
742        assert_eq!(parsed, expected);
743    }
744
745    #[test]
746    fn field_schema_deserialization() {
747        // simple type
748        let json = serde_json::json!({ "type": "string" });
749        let schema: FieldSchema = serde_json::from_value(json).unwrap();
750        assert_eq!(schema.r#type, FieldType::String);
751
752        // map type
753        let json = serde_json::json!({
754            "type": "map",
755            "value": "int"
756        });
757        let schema: FieldSchema = serde_json::from_value(json).unwrap();
758        assert_eq!(
759            schema.r#type,
760            FieldType::Map {
761                value: Box::new(FieldType::Int)
762            }
763        );
764
765        // enum type
766        let json = serde_json::json!({
767            "type": "enum",
768            "values": ["a", "b"]
769        });
770        let schema: FieldSchema = serde_json::from_value(json).unwrap();
771        assert_eq!(
772            schema.r#type,
773            FieldType::Enum {
774                values: vec!["a".to_string(), "b".to_string()]
775            }
776        );
777
778        // complex nested
779        let json = serde_json::json!({
780            "type": "list",
781            "item": { "type": "ref", "target": "test" }
782        });
783        let schema: FieldSchema = serde_json::from_value(json).unwrap();
784        assert_eq!(
785            schema.r#type,
786            FieldType::List {
787                item: Box::new(FieldType::Ref {
788                    target: "test".to_string()
789                })
790            }
791        );
792    }
793
794    #[test]
795    fn field_schema_format_and_pattern() {
796        let json = serde_json::json!({
797            "type": "string",
798            "format": "slug",
799            "pattern": "^[a-z0-9-]+$"
800        });
801        let schema: FieldSchema = serde_json::from_value(json).unwrap();
802        assert_eq!(schema.format, Some(FieldFormat::Slug));
803        assert_eq!(schema.pattern.as_deref(), Some("^[a-z0-9-]+$"));
804    }
805
806    #[test]
807    fn test_field_schema_defaults() {
808        let json = serde_json::json!({ "type": "string" });
809        let schema: FieldSchema = serde_json::from_value(json).unwrap();
810        assert!(!schema.required);
811        assert!(!schema.nullable);
812        assert!(schema.format.is_none());
813        assert!(schema.pattern.is_none());
814        assert!(schema.description.is_none());
815    }
816
817    #[test]
818    fn field_type_all_simple_variants() {
819        let simple_types = vec![
820            ("string", FieldType::String),
821            ("int", FieldType::Int),
822            ("float", FieldType::Float),
823            ("bool", FieldType::Bool),
824            ("uuid", FieldType::Uuid),
825            ("date", FieldType::Date),
826            ("datetime", FieldType::Datetime),
827            ("time", FieldType::Time),
828            ("json", FieldType::Json),
829            ("ip_address", FieldType::IpAddress),
830            ("cidr", FieldType::Cidr),
831            ("prefix", FieldType::Prefix),
832            ("mac", FieldType::Mac),
833            ("slug", FieldType::Slug),
834        ];
835        for (name, expected) in simple_types {
836            let json = serde_json::json!({ "type": name });
837            let schema: FieldSchema = serde_json::from_value(json).unwrap();
838            assert_eq!(schema.r#type, expected, "failed for {}", name);
839        }
840    }
841
842    #[test]
843    fn field_type_list_ref() {
844        let json = serde_json::json!({
845            "type": "list_ref",
846            "target": "dcim.device"
847        });
848        let schema: FieldSchema = serde_json::from_value(json).unwrap();
849        assert_eq!(
850            schema.r#type,
851            FieldType::ListRef {
852                target: "dcim.device".to_string()
853            }
854        );
855    }
856
857    #[test]
858    fn field_type_unknown_errors() {
859        let json = serde_json::json!({ "type": "unknown_type" });
860        let result: Result<FieldSchema, _> = serde_json::from_value(json);
861        assert!(result.is_err());
862    }
863
864    #[test]
865    fn field_type_enum_missing_values_errors() {
866        let json = serde_json::json!({ "type": "enum" });
867        let result: Result<FieldSchema, _> = serde_json::from_value(json);
868        assert!(result.is_err());
869    }
870
871    #[test]
872    fn field_type_list_missing_item_errors() {
873        let json = serde_json::json!({ "type": "list" });
874        let result: Result<FieldSchema, _> = serde_json::from_value(json);
875        assert!(result.is_err());
876    }
877
878    #[test]
879    fn field_type_map_missing_value_errors() {
880        let json = serde_json::json!({ "type": "map" });
881        let result: Result<FieldSchema, _> = serde_json::from_value(json);
882        assert!(result.is_err());
883    }
884
885    #[test]
886    fn field_type_ref_missing_target_errors() {
887        let json = serde_json::json!({ "type": "ref" });
888        let result: Result<FieldSchema, _> = serde_json::from_value(json);
889        assert!(result.is_err());
890    }
891
892    #[test]
893    fn object_with_empty_key_errors() {
894        let key = Key::default();
895        let attrs = JsonMap::default();
896        let result = Object::new(Uuid::from_u128(1), TypeName::new("dcim.site"), key, attrs);
897        assert!(result.is_err());
898    }
899
900    #[test]
901    fn object_with_empty_type_errors() {
902        let mut k = BTreeMap::new();
903        k.insert("slug".to_string(), serde_json::json!("x"));
904        let result = Object::new(
905            Uuid::from_u128(1),
906            TypeName::new(""),
907            Key::from(k),
908            JsonMap::default(),
909        );
910        assert_eq!(result.unwrap_err(), ObjectError::MissingType);
911    }
912
913    #[test]
914    fn object_with_whitespace_only_type_errors() {
915        let mut k = BTreeMap::new();
916        k.insert("slug".to_string(), serde_json::json!("x"));
917        let result = Object::new(
918            Uuid::from_u128(1),
919            TypeName::new("   "),
920            Key::from(k),
921            JsonMap::default(),
922        );
923        assert_eq!(result.unwrap_err(), ObjectError::MissingType);
924    }
925
926    #[test]
927    fn object_error_display() {
928        assert_eq!(
929            ObjectError::MissingType.to_string(),
930            "object type must be set"
931        );
932        assert_eq!(
933            ObjectError::MissingKey.to_string(),
934            "object key must be set"
935        );
936    }
937
938    #[test]
939    fn object_equality_ignores_source() {
940        let mut k = BTreeMap::new();
941        k.insert("slug".to_string(), serde_json::json!("x"));
942        let a = Object::new(
943            Uuid::from_u128(1),
944            TypeName::new("dcim.site"),
945            Key::from(k.clone()),
946            JsonMap::default(),
947        )
948        .unwrap()
949        .with_source(SourceLocation::file("a.yaml"));
950        let b = Object::new(
951            Uuid::from_u128(1),
952            TypeName::new("dcim.site"),
953            Key::from(k),
954            JsonMap::default(),
955        )
956        .unwrap()
957        .with_source(SourceLocation::file("b.yaml"));
958        assert_eq!(a, b);
959    }
960
961    #[test]
962    fn object_deserialize_kind_alias() {
963        let json = serde_json::json!({
964            "uid": "00000000-0000-0000-0000-000000000001",
965            "kind": "dcim.site",
966            "key": {"slug": "x"}
967        });
968        let obj: Object = serde_json::from_value(json).unwrap();
969        assert_eq!(obj.type_name.as_str(), "dcim.site");
970    }
971
972    #[test]
973    fn object_source_not_serialized() {
974        let mut k = BTreeMap::new();
975        k.insert("slug".to_string(), serde_json::json!("x"));
976        let obj = Object::new(
977            Uuid::from_u128(1),
978            TypeName::new("dcim.site"),
979            Key::from(k),
980            JsonMap::default(),
981        )
982        .unwrap()
983        .with_source(SourceLocation::file_line("test.yaml", 10));
984        let value = serde_json::to_value(&obj).unwrap();
985        assert!(value.get("source").is_none());
986    }
987
988    #[test]
989    fn source_location_display_file_only() {
990        let loc = SourceLocation::file("test.yaml");
991        assert_eq!(loc.to_string(), "test.yaml");
992        assert!(loc.line.is_none());
993        assert!(loc.column.is_none());
994    }
995
996    #[test]
997    fn source_location_display_file_and_line() {
998        let loc = SourceLocation::file_line("test.yaml", 42);
999        assert_eq!(loc.to_string(), "test.yaml:42");
1000    }
1001
1002    #[test]
1003    fn source_location_display_file_line_column() {
1004        let loc = SourceLocation {
1005            file: "test.yaml".into(),
1006            line: Some(42),
1007            column: Some(7),
1008        };
1009        assert_eq!(loc.to_string(), "test.yaml:42:7");
1010    }
1011
1012    #[test]
1013    fn uid_v5_deterministic() {
1014        let a = uid_v5("dcim.site", "fra1");
1015        let b = uid_v5("dcim.site", "fra1");
1016        assert_eq!(a, b);
1017    }
1018
1019    #[test]
1020    fn uid_v5_different_inputs() {
1021        let a = uid_v5("dcim.site", "fra1");
1022        let b = uid_v5("dcim.site", "fra2");
1023        let c = uid_v5("dcim.device", "fra1");
1024        assert_ne!(a, b);
1025        assert_ne!(a, c);
1026    }
1027
1028    #[test]
1029    fn key_serde_transparent() {
1030        let mut k = BTreeMap::new();
1031        k.insert("slug".to_string(), serde_json::json!("x"));
1032        let key = Key::from(k);
1033        let json = serde_json::to_value(&key).unwrap();
1034        assert_eq!(json, serde_json::json!({"slug": "x"}));
1035        let back: Key = serde_json::from_value(json).unwrap();
1036        assert_eq!(back, key);
1037    }
1038
1039    #[test]
1040    fn field_type_roundtrip_all_complex_variants() {
1041        let cases = vec![
1042            FieldType::Text,
1043            FieldType::Float,
1044            FieldType::Uuid,
1045            FieldType::Date,
1046            FieldType::Datetime,
1047            FieldType::Time,
1048            FieldType::Json,
1049            FieldType::IpAddress,
1050            FieldType::Cidr,
1051            FieldType::Prefix,
1052            FieldType::Mac,
1053            FieldType::Slug,
1054            FieldType::Map {
1055                value: Box::new(FieldType::String),
1056            },
1057            FieldType::ListRef {
1058                target: "dcim.device".to_string(),
1059            },
1060            FieldType::Enum {
1061                values: vec!["active".to_string(), "planned".to_string()],
1062            },
1063            FieldType::List {
1064                item: Box::new(FieldType::List {
1065                    item: Box::new(FieldType::Int),
1066                }),
1067            },
1068        ];
1069        for case in cases {
1070            let json = serde_json::to_string(&case).unwrap();
1071            let back: FieldType = serde_json::from_str(&json).unwrap();
1072            assert_eq!(back, case, "roundtrip failed for {:?}", case);
1073        }
1074    }
1075
1076    #[test]
1077    fn field_schema_with_all_fields_set() {
1078        let json = serde_json::json!({
1079            "type": "string",
1080            "required": true,
1081            "nullable": true,
1082            "format": "slug",
1083            "pattern": "^[a-z]+$",
1084            "description": "a slug field"
1085        });
1086        let schema: FieldSchema = serde_json::from_value(json).unwrap();
1087        assert!(schema.required);
1088        assert!(schema.nullable);
1089        assert_eq!(schema.format, Some(FieldFormat::Slug));
1090        assert_eq!(schema.pattern.as_deref(), Some("^[a-z]+$"));
1091        assert_eq!(schema.description.as_deref(), Some("a slug field"));
1092    }
1093
1094    #[test]
1095    fn field_schema_roundtrip() {
1096        let schema = FieldSchema {
1097            r#type: FieldType::Ref {
1098                target: "dcim.site".to_string(),
1099            },
1100            required: true,
1101            nullable: false,
1102            format: None,
1103            pattern: None,
1104            description: Some("site ref".to_string()),
1105        };
1106        let json = serde_json::to_value(&schema).unwrap();
1107        let back: FieldSchema = serde_json::from_value(json).unwrap();
1108        assert_eq!(back, schema);
1109    }
1110
1111    #[test]
1112    fn field_schema_unknown_format_errors() {
1113        let json = serde_json::json!({
1114            "type": "string",
1115            "format": "nope"
1116        });
1117        let result: Result<FieldSchema, _> = serde_json::from_value(json);
1118        assert!(result.is_err());
1119    }
1120
1121    #[test]
1122    fn field_schema_non_bool_flag_errors() {
1123        let required = serde_json::json!({ "type": "string", "required": "true" });
1124        assert!(serde_json::from_value::<FieldSchema>(required).is_err());
1125        let nullable = serde_json::json!({ "type": "string", "nullable": 1 });
1126        assert!(serde_json::from_value::<FieldSchema>(nullable).is_err());
1127    }
1128
1129    #[test]
1130    fn field_schema_non_string_meta_errors() {
1131        let format = serde_json::json!({ "type": "string", "format": 1 });
1132        assert!(serde_json::from_value::<FieldSchema>(format).is_err());
1133        let pattern = serde_json::json!({ "type": "string", "pattern": true });
1134        assert!(serde_json::from_value::<FieldSchema>(pattern).is_err());
1135    }
1136
1137    #[test]
1138    fn field_type_list_ref_missing_target_errors() {
1139        let json = serde_json::json!({ "type": "list_ref" });
1140        let result: Result<FieldSchema, _> = serde_json::from_value(json);
1141        assert!(result.is_err());
1142    }
1143
1144    #[test]
1145    fn field_type_invalid_value_type_errors() {
1146        let result = parse_field_type_value(&serde_json::json!(42));
1147        assert!(result.is_err());
1148        assert!(result.unwrap_err().contains("string or map"));
1149    }
1150
1151    #[test]
1152    fn field_type_object_simple_fallback() {
1153        let json = serde_json::json!({ "type": "int" });
1154        let schema: FieldSchema = serde_json::from_value(json).unwrap();
1155        assert_eq!(schema.r#type, FieldType::Int);
1156    }
1157
1158    #[test]
1159    fn type_schema_roundtrip() {
1160        let json = serde_json::json!({
1161            "key": {
1162                "slug": { "type": "string" }
1163            },
1164            "fields": {
1165                "name": { "type": "string", "required": true },
1166                "status": { "type": "enum", "values": ["active", "planned"] }
1167            }
1168        });
1169        let schema: TypeSchema = serde_json::from_value(json.clone()).unwrap();
1170        assert!(schema.key.contains_key("slug"));
1171        assert!(schema.fields.contains_key("name"));
1172        assert!(schema.fields.contains_key("status"));
1173        let back = serde_json::to_value(&schema).unwrap();
1174        let back_schema: TypeSchema = serde_json::from_value(back).unwrap();
1175        assert_eq!(back_schema, schema);
1176    }
1177
1178    #[test]
1179    fn inventory_roundtrip() {
1180        let json = serde_json::json!({
1181            "schema": {
1182                "types": {
1183                    "dcim.site": {
1184                        "key": { "slug": { "type": "string" } },
1185                        "fields": { "name": { "type": "string" } }
1186                    }
1187                }
1188            },
1189            "objects": [
1190                {
1191                    "uid": "00000000-0000-0000-0000-000000000001",
1192                    "type": "dcim.site",
1193                    "key": { "slug": "fra1" },
1194                    "attrs": { "name": "FRA1" }
1195                }
1196            ]
1197        });
1198        let inv: Inventory = serde_json::from_value(json).unwrap();
1199        assert_eq!(inv.schema.types.len(), 1);
1200        assert_eq!(inv.objects.len(), 1);
1201        assert_eq!(inv.objects[0].type_name.as_str(), "dcim.site");
1202        let back = serde_json::to_value(&inv).unwrap();
1203        let back_inv: Inventory = serde_json::from_value(back).unwrap();
1204        assert_eq!(back_inv, inv);
1205    }
1206
1207    #[test]
1208    fn key_string_empty() {
1209        let key = Key::default();
1210        let s = key_string(&key);
1211        assert_eq!(s, "{}");
1212    }
1213
1214    #[test]
1215    fn key_string_canonical_form() {
1216        let mut k = BTreeMap::new();
1217        k.insert("a".to_string(), serde_json::json!(1.0000001)); // becomes int
1218        k.insert("b".to_string(), serde_json::json!(2.001)); // kept as float
1219        k.insert(
1220            "c".to_string(),
1221            serde_json::json!({"d".to_string(): 2.99999999999}), // becomes int
1222        );
1223        let key = Key::from(k);
1224        let s = key_string(&key);
1225        assert_eq!(s, "{\"a\":1,\"b\":2.001,\"c\":{\"d\":3}}");
1226    }
1227
1228    #[test]
1229    fn key_string_preserves_large_u64() {
1230        // a u64 above i64::MAX must stay an exact integer; a lossy f64 would
1231        // corrupt the uid derived from the key.
1232        let mut k = BTreeMap::new();
1233        k.insert("id".to_string(), serde_json::json!(u64::MAX));
1234        let s = key_string(&Key::from(k));
1235        assert_eq!(s, "{\"id\":18446744073709551615}");
1236    }
1237
1238    #[test]
1239    fn key_string_distinguishes_near_u64_max() {
1240        // u64::MAX and u64::MAX - 1 round to the same f64, so floating them
1241        // would collapse two distinct keys onto one uid.
1242        let a = {
1243            let mut k = BTreeMap::new();
1244            k.insert("id".to_string(), serde_json::json!(u64::MAX));
1245            key_string(&Key::from(k))
1246        };
1247        let b = {
1248            let mut k = BTreeMap::new();
1249            k.insert("id".to_string(), serde_json::json!(u64::MAX - 1));
1250            key_string(&Key::from(k))
1251        };
1252        assert_ne!(a, b);
1253    }
1254}