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