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(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#[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#[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#[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#[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#[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#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
569pub struct Schema {
570 #[serde(default)]
571 pub types: BTreeMap<String, TypeSchema>,
572}
573
574#[derive(Debug, Clone, Serialize, Deserialize, Eq)]
576pub struct Object {
577 pub uid: Uid,
579 #[serde(rename = "type", alias = "kind")]
581 pub type_name: TypeName,
582 pub key: Key,
584 #[serde(default, rename = "attrs")]
586 pub attrs: JsonMap,
587 #[serde(skip)]
589 pub source: Option<SourceLocation>,
590}
591
592impl PartialEq for Object {
593 fn eq(&self, other: &Self) -> bool {
594 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 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 pub fn with_source(mut self, source: SourceLocation) -> Self {
653 self.source = Some(source);
654 self
655 }
656}
657
658#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
660pub struct Inventory {
661 pub schema: Schema,
663 #[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); }
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 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 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 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 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)); k.insert("b".to_string(), serde_json::json!(2.001)); k.insert(
1220 "c".to_string(),
1221 serde_json::json!({"d".to_string(): 2.99999999999}), );
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 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 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}