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