1use indexmap::IndexMap;
2use serde::{Deserialize, Serialize};
3use serde_json::Value;
4
5use crate::ext_lintel::LintelExt;
6use crate::ext_taplo::TaploSchemaExt;
7
8#[derive(Debug, Clone, Serialize, Deserialize)]
10#[serde(untagged)]
11pub enum SchemaValue {
12 Bool(bool),
13 Schema(Box<Schema>),
14}
15
16#[derive(Debug, Clone, Serialize, Deserialize)]
18#[serde(untagged)]
19pub enum TypeValue {
20 Single(String),
21 Union(Vec<String>),
22}
23
24#[derive(Debug, Clone, Default, Serialize, Deserialize)]
26pub struct Schema {
27 #[serde(rename = "$schema", skip_serializing_if = "Option::is_none")]
29 pub schema: Option<String>,
30 #[serde(rename = "$id", skip_serializing_if = "Option::is_none")]
31 pub id: Option<String>,
32 #[serde(rename = "$ref", skip_serializing_if = "Option::is_none")]
33 pub ref_: Option<String>,
34 #[serde(rename = "$anchor", skip_serializing_if = "Option::is_none")]
35 pub anchor: Option<String>,
36 #[serde(rename = "$dynamicRef", skip_serializing_if = "Option::is_none")]
37 pub dynamic_ref: Option<String>,
38 #[serde(rename = "$dynamicAnchor", skip_serializing_if = "Option::is_none")]
39 pub dynamic_anchor: Option<String>,
40 #[serde(rename = "$comment", skip_serializing_if = "Option::is_none")]
41 pub comment: Option<String>,
42 #[serde(rename = "$defs", skip_serializing_if = "Option::is_none")]
43 pub defs: Option<IndexMap<String, SchemaValue>>,
44
45 #[serde(skip_serializing_if = "Option::is_none")]
47 pub title: Option<String>,
48 #[serde(skip_serializing_if = "Option::is_none")]
49 pub description: Option<String>,
50 #[serde(
51 rename = "markdownDescription",
52 skip_serializing_if = "Option::is_none"
53 )]
54 pub markdown_description: Option<String>,
55 #[serde(skip_serializing_if = "Option::is_none")]
56 pub default: Option<Value>,
57 #[serde(skip_serializing_if = "Option::is_none")]
58 pub deprecated: Option<bool>,
59 #[serde(rename = "readOnly", skip_serializing_if = "Option::is_none")]
60 pub read_only: Option<bool>,
61 #[serde(rename = "writeOnly", skip_serializing_if = "Option::is_none")]
62 pub write_only: Option<bool>,
63 #[serde(skip_serializing_if = "Option::is_none")]
64 pub examples: Option<Vec<Value>>,
65
66 #[serde(rename = "type", skip_serializing_if = "Option::is_none")]
68 pub type_: Option<TypeValue>,
69 #[serde(rename = "enum", skip_serializing_if = "Option::is_none")]
70 pub enum_: Option<Vec<Value>>,
71 #[serde(rename = "const", skip_serializing_if = "Option::is_none")]
72 pub const_: Option<Value>,
73
74 #[serde(skip_serializing_if = "Option::is_none")]
76 pub properties: Option<IndexMap<String, SchemaValue>>,
77 #[serde(rename = "patternProperties", skip_serializing_if = "Option::is_none")]
78 pub pattern_properties: Option<IndexMap<String, SchemaValue>>,
79 #[serde(
80 rename = "additionalProperties",
81 skip_serializing_if = "Option::is_none"
82 )]
83 pub additional_properties: Option<Box<SchemaValue>>,
84 #[serde(skip_serializing_if = "Option::is_none")]
85 pub required: Option<Vec<String>>,
86 #[serde(rename = "propertyNames", skip_serializing_if = "Option::is_none")]
87 pub property_names: Option<Box<SchemaValue>>,
88 #[serde(rename = "minProperties", skip_serializing_if = "Option::is_none")]
89 pub min_properties: Option<u64>,
90 #[serde(rename = "maxProperties", skip_serializing_if = "Option::is_none")]
91 pub max_properties: Option<u64>,
92 #[serde(
93 rename = "unevaluatedProperties",
94 skip_serializing_if = "Option::is_none"
95 )]
96 pub unevaluated_properties: Option<Box<SchemaValue>>,
97
98 #[serde(skip_serializing_if = "Option::is_none")]
100 pub items: Option<Box<SchemaValue>>,
101 #[serde(rename = "prefixItems", skip_serializing_if = "Option::is_none")]
102 pub prefix_items: Option<Vec<SchemaValue>>,
103 #[serde(skip_serializing_if = "Option::is_none")]
104 pub contains: Option<Box<SchemaValue>>,
105 #[serde(rename = "minContains", skip_serializing_if = "Option::is_none")]
106 pub min_contains: Option<u64>,
107 #[serde(rename = "maxContains", skip_serializing_if = "Option::is_none")]
108 pub max_contains: Option<u64>,
109 #[serde(rename = "minItems", skip_serializing_if = "Option::is_none")]
110 pub min_items: Option<u64>,
111 #[serde(rename = "maxItems", skip_serializing_if = "Option::is_none")]
112 pub max_items: Option<u64>,
113 #[serde(rename = "uniqueItems", skip_serializing_if = "Option::is_none")]
114 pub unique_items: Option<bool>,
115 #[serde(rename = "unevaluatedItems", skip_serializing_if = "Option::is_none")]
116 pub unevaluated_items: Option<Box<SchemaValue>>,
117
118 #[serde(skip_serializing_if = "Option::is_none")]
120 pub minimum: Option<Value>,
121 #[serde(skip_serializing_if = "Option::is_none")]
122 pub maximum: Option<Value>,
123 #[serde(rename = "exclusiveMinimum", skip_serializing_if = "Option::is_none")]
124 pub exclusive_minimum: Option<Value>,
125 #[serde(rename = "exclusiveMaximum", skip_serializing_if = "Option::is_none")]
126 pub exclusive_maximum: Option<Value>,
127 #[serde(rename = "multipleOf", skip_serializing_if = "Option::is_none")]
128 pub multiple_of: Option<Value>,
129
130 #[serde(rename = "minLength", skip_serializing_if = "Option::is_none")]
132 pub min_length: Option<u64>,
133 #[serde(rename = "maxLength", skip_serializing_if = "Option::is_none")]
134 pub max_length: Option<u64>,
135 #[serde(skip_serializing_if = "Option::is_none")]
136 pub pattern: Option<String>,
137 #[serde(skip_serializing_if = "Option::is_none")]
138 pub format: Option<String>,
139
140 #[serde(rename = "allOf", skip_serializing_if = "Option::is_none")]
142 pub all_of: Option<Vec<SchemaValue>>,
143 #[serde(rename = "anyOf", skip_serializing_if = "Option::is_none")]
144 pub any_of: Option<Vec<SchemaValue>>,
145 #[serde(rename = "oneOf", skip_serializing_if = "Option::is_none")]
146 pub one_of: Option<Vec<SchemaValue>>,
147 #[serde(skip_serializing_if = "Option::is_none")]
148 pub not: Option<Box<SchemaValue>>,
149
150 #[serde(rename = "if", skip_serializing_if = "Option::is_none")]
152 pub if_: Option<Box<SchemaValue>>,
153 #[serde(rename = "then", skip_serializing_if = "Option::is_none")]
154 pub then_: Option<Box<SchemaValue>>,
155 #[serde(rename = "else", skip_serializing_if = "Option::is_none")]
156 pub else_: Option<Box<SchemaValue>>,
157
158 #[serde(rename = "dependentRequired", skip_serializing_if = "Option::is_none")]
160 pub dependent_required: Option<IndexMap<String, Vec<String>>>,
161 #[serde(rename = "dependentSchemas", skip_serializing_if = "Option::is_none")]
162 pub dependent_schemas: Option<IndexMap<String, SchemaValue>>,
163
164 #[serde(rename = "contentMediaType", skip_serializing_if = "Option::is_none")]
166 pub content_media_type: Option<String>,
167 #[serde(rename = "contentEncoding", skip_serializing_if = "Option::is_none")]
168 pub content_encoding: Option<String>,
169 #[serde(rename = "contentSchema", skip_serializing_if = "Option::is_none")]
170 pub content_schema: Option<Box<SchemaValue>>,
171
172 #[serde(rename = "x-taplo", skip_serializing_if = "Option::is_none")]
174 pub x_taplo: Option<TaploSchemaExt>,
175 #[serde(rename = "x-taplo-info", skip_serializing_if = "Option::is_none")]
176 pub x_taplo_info: Option<Value>,
177 #[serde(rename = "x-lintel", skip_serializing_if = "Option::is_none")]
178 pub x_lintel: Option<LintelExt>,
179 #[serde(
180 rename = "x-tombi-toml-version",
181 skip_serializing_if = "Option::is_none"
182 )]
183 pub x_tombi_toml_version: Option<String>,
184 #[serde(
185 rename = "x-tombi-table-keys-order",
186 skip_serializing_if = "Option::is_none"
187 )]
188 pub x_tombi_table_keys_order: Option<Value>,
189 #[serde(
190 rename = "x-tombi-additional-key-label",
191 skip_serializing_if = "Option::is_none"
192 )]
193 pub x_tombi_additional_key_label: Option<String>,
194 #[serde(
195 rename = "x-tombi-array-values-order",
196 skip_serializing_if = "Option::is_none"
197 )]
198 pub x_tombi_array_values_order: Option<Value>,
199
200 #[serde(flatten)]
202 pub extra: IndexMap<String, Value>,
203}
204
205impl SchemaValue {
206 pub fn as_schema(&self) -> Option<&Schema> {
208 match self {
209 Self::Schema(s) => Some(s),
210 Self::Bool(_) => None,
211 }
212 }
213}
214
215impl Schema {
216 pub fn from_value(value: Value) -> Result<Self, serde_json::Error> {
222 serde_json::from_value(value)
223 }
224
225 pub fn description(&self) -> Option<&str> {
227 self.markdown_description
228 .as_deref()
229 .or(self.description.as_deref())
230 }
231
232 pub fn required_set(&self) -> &[String] {
234 self.required.as_deref().unwrap_or_default()
235 }
236
237 pub fn is_deprecated(&self) -> bool {
239 self.deprecated.unwrap_or(false)
240 }
241
242 pub fn type_str(&self) -> Option<String> {
244 schema_type_str(self)
245 }
246
247 pub fn get_keyword(&self, key: &str) -> Option<&SchemaValue> {
252 match key {
253 "items" => self.items.as_deref(),
254 "contains" => self.contains.as_deref(),
255 "additionalProperties" => self.additional_properties.as_deref(),
256 "propertyNames" => self.property_names.as_deref(),
257 "unevaluatedProperties" => self.unevaluated_properties.as_deref(),
258 "unevaluatedItems" => self.unevaluated_items.as_deref(),
259 "not" => self.not.as_deref(),
260 "if" => self.if_.as_deref(),
261 "then" => self.then_.as_deref(),
262 "else" => self.else_.as_deref(),
263 "contentSchema" => self.content_schema.as_deref(),
264 _ => None,
265 }
266 }
267
268 pub fn get_map_entry(&self, keyword: &str, key: &str) -> Option<&SchemaValue> {
273 match keyword {
274 "properties" => self.properties.as_ref()?.get(key),
275 "patternProperties" => self.pattern_properties.as_ref()?.get(key),
276 "$defs" => self.defs.as_ref()?.get(key),
277 "dependentSchemas" => self.dependent_schemas.as_ref()?.get(key),
278 _ => None,
279 }
280 }
281
282 pub fn get_array_entry(&self, keyword: &str, index: usize) -> Option<&SchemaValue> {
284 match keyword {
285 "allOf" => self.all_of.as_ref()?.get(index),
286 "anyOf" => self.any_of.as_ref()?.get(index),
287 "oneOf" => self.one_of.as_ref()?.get(index),
288 "prefixItems" => self.prefix_items.as_ref()?.get(index),
289 _ => None,
290 }
291 }
292}
293
294fn schema_type_str(schema: &Schema) -> Option<String> {
296 if let Some(ref ty) = schema.type_ {
298 return match ty {
299 TypeValue::Single(s) if s == "array" => {
300 let item_ty = schema
301 .items
302 .as_ref()
303 .and_then(|sv| sv.as_schema())
304 .and_then(schema_type_str);
305 match item_ty {
306 Some(item_ty) => Some(format!("{item_ty}[]")),
307 None => Some("array".to_string()),
308 }
309 }
310 TypeValue::Single(s) => Some(s.clone()),
311 TypeValue::Union(arr) => Some(arr.join(" | ")),
312 };
313 }
314
315 if let Some(ref r) = schema.ref_ {
317 return Some(ref_name(r).to_string());
318 }
319
320 for variants in [&schema.one_of, &schema.any_of].into_iter().flatten() {
322 let types: Vec<String> = variants
323 .iter()
324 .filter_map(|v| match v {
325 SchemaValue::Schema(s) => {
326 schema_type_str(s).or_else(|| s.ref_.as_ref().map(|r| ref_name(r).to_string()))
327 }
328 SchemaValue::Bool(_) => None,
329 })
330 .collect();
331 if !types.is_empty() {
332 return Some(types.join(" | "));
333 }
334 }
335
336 if let Some(ref c) = schema.const_ {
338 return Some(format!("const: {c}"));
339 }
340
341 if schema.enum_.is_some() {
343 return Some("enum".to_string());
344 }
345
346 None
347}
348
349pub fn ref_name(ref_str: &str) -> &str {
351 ref_str.rsplit('/').next().unwrap_or(ref_str)
352}
353
354pub fn resolve_ref<'a>(schema: &'a Schema, root: &'a Schema) -> &'a Schema {
359 if let Some(ref ref_str) = schema.ref_
360 && let Some(path) = ref_str.strip_prefix("#/")
361 {
362 let Ok(root_value) = serde_json::to_value(root) else {
364 return schema;
365 };
366 let mut current = &root_value;
367 for segment in path.split('/') {
368 let decoded = segment.replace("~1", "/").replace("~0", "~");
369 match current.get(&decoded) {
370 Some(next) => current = next,
371 None => return schema,
372 }
373 }
374 let _ = current;
379 return schema;
380 }
381 schema
382}
383
384pub fn navigate_pointer<'a>(
393 schema: &'a SchemaValue,
394 root: &'a SchemaValue,
395 pointer: &str,
396) -> Result<&'a SchemaValue, String> {
397 let path = pointer.strip_prefix('/').unwrap_or(pointer);
398 if path.is_empty() {
399 return Ok(schema);
400 }
401
402 let mut current = resolve_schema_value_ref(schema, root);
403 let mut segments = path.split('/').peekable();
404
405 while let Some(segment) = segments.next() {
406 let decoded = segment.replace("~1", "/").replace("~0", "~");
407 current = resolve_schema_value_ref(current, root);
408
409 let Some(schema) = current.as_schema() else {
410 return Err(format!(
411 "cannot resolve segment '{decoded}' in pointer '{pointer}'"
412 ));
413 };
414
415 if is_map_keyword(&decoded) {
417 let key_segment = segments
418 .next()
419 .ok_or_else(|| format!("expected key after '{decoded}' in pointer '{pointer}'"))?;
420 let key = key_segment.replace("~1", "/").replace("~0", "~");
421 if let Some(entry) = schema.get_map_entry(&decoded, &key) {
422 current = entry;
423 continue;
424 }
425 return Err(format!(
426 "cannot resolve segment '{key}' in '{decoded}' in pointer '{pointer}'"
427 ));
428 }
429
430 if is_array_keyword(&decoded) {
432 let idx_segment = segments.next().ok_or_else(|| {
433 format!("expected index after '{decoded}' in pointer '{pointer}'")
434 })?;
435 let idx: usize = idx_segment.parse().map_err(|_| {
436 format!("expected numeric index after '{decoded}', got '{idx_segment}'")
437 })?;
438 if let Some(entry) = schema.get_array_entry(&decoded, idx) {
439 current = entry;
440 continue;
441 }
442 return Err(format!(
443 "index {idx} out of bounds in '{decoded}' in pointer '{pointer}'"
444 ));
445 }
446
447 if let Some(sv) = schema.get_keyword(&decoded) {
449 current = sv;
450 continue;
451 }
452
453 if let Some(sv) = schema.get_map_entry_by_pointer_segment(&decoded) {
456 current = sv;
457 continue;
458 }
459
460 if let Ok(idx) = decoded.parse::<usize>() {
462 let found = ["allOf", "anyOf", "oneOf", "prefixItems"]
463 .iter()
464 .find_map(|kw| schema.get_array_entry(kw, idx));
465 if let Some(entry) = found {
466 current = entry;
467 continue;
468 }
469 }
470
471 return Err(format!(
472 "cannot resolve segment '{decoded}' in pointer '{pointer}'"
473 ));
474 }
475
476 Ok(resolve_schema_value_ref(current, root))
477}
478
479fn is_map_keyword(segment: &str) -> bool {
481 matches!(
482 segment,
483 "properties" | "patternProperties" | "$defs" | "dependentSchemas"
484 )
485}
486
487fn is_array_keyword(segment: &str) -> bool {
489 matches!(segment, "allOf" | "anyOf" | "oneOf" | "prefixItems")
490}
491
492fn resolve_schema_value_ref<'a>(sv: &'a SchemaValue, root: &'a SchemaValue) -> &'a SchemaValue {
494 let Some(schema) = sv.as_schema() else {
495 return sv;
496 };
497 if let Some(ref ref_str) = schema.ref_
498 && let Some(path) = ref_str.strip_prefix("#/")
499 {
500 let mut current = root;
501 let mut segments = path.split('/').peekable();
502 while let Some(segment) = segments.next() {
503 let decoded = segment.replace("~1", "/").replace("~0", "~");
504 let Some(inner) = current.as_schema() else {
505 return sv;
506 };
507
508 if is_map_keyword(&decoded) {
510 let Some(key_segment) = segments.next() else {
511 return sv;
512 };
513 let key = key_segment.replace("~1", "/").replace("~0", "~");
514 match inner.get_map_entry(&decoded, &key) {
515 Some(n) => current = n,
516 None => return sv,
517 }
518 continue;
519 }
520
521 if is_array_keyword(&decoded) {
523 let Some(idx_segment) = segments.next() else {
524 return sv;
525 };
526 let Ok(idx) = idx_segment.parse::<usize>() else {
527 return sv;
528 };
529 match inner.get_array_entry(&decoded, idx) {
530 Some(n) => current = n,
531 None => return sv,
532 }
533 continue;
534 }
535
536 if let Some(n) = inner.get_keyword(&decoded) {
538 current = n;
539 continue;
540 }
541
542 if let Some(n) = inner.get_map_entry_by_pointer_segment(&decoded) {
544 current = n;
545 continue;
546 }
547
548 return sv;
549 }
550 return current;
551 }
552 sv
553}
554
555impl Schema {
556 fn get_map_entry_by_pointer_segment(&self, segment: &str) -> Option<&SchemaValue> {
560 self.properties
564 .as_ref()
565 .and_then(|m| m.get(segment))
566 .or_else(|| {
567 self.pattern_properties
568 .as_ref()
569 .and_then(|m| m.get(segment))
570 })
571 .or_else(|| self.defs.as_ref().and_then(|m| m.get(segment)))
572 .or_else(|| self.dependent_schemas.as_ref().and_then(|m| m.get(segment)))
573 }
574}
575
576#[cfg(test)]
577#[allow(clippy::unwrap_used)]
578mod tests {
579 use super::*;
580 use serde_json::json;
581
582 #[test]
583 fn round_trip_simple_schema() {
584 let json = json!({
585 "type": "object",
586 "title": "Test",
587 "properties": {
588 "name": { "type": "string" }
589 }
590 });
591 let schema: Schema = serde_json::from_value(json.clone()).unwrap();
592 assert_eq!(schema.title.as_deref(), Some("Test"));
593 assert!(schema.properties.is_some());
594
595 let back = serde_json::to_value(&schema).unwrap();
596 assert_eq!(back["type"], "object");
597 assert_eq!(back["title"], "Test");
598 }
599
600 #[test]
601 fn bool_schema_value() {
602 let json = json!(true);
603 let sv: SchemaValue = serde_json::from_value(json).unwrap();
604 assert!(matches!(sv, SchemaValue::Bool(true)));
605 assert!(sv.as_schema().is_none());
606 }
607
608 #[test]
609 fn schema_value_object() {
610 let json = json!({"type": "string"});
611 let sv: SchemaValue = serde_json::from_value(json).unwrap();
612 let s = sv.as_schema().unwrap();
613 assert!(matches!(s.type_, Some(TypeValue::Single(ref t)) if t == "string"));
614 }
615
616 #[test]
617 fn type_value_single() {
618 let json = json!("string");
619 let tv: TypeValue = serde_json::from_value(json).unwrap();
620 assert!(matches!(tv, TypeValue::Single(ref s) if s == "string"));
621 }
622
623 #[test]
624 fn type_value_union() {
625 let json = json!(["string", "null"]);
626 let tv: TypeValue = serde_json::from_value(json).unwrap();
627 assert!(matches!(tv, TypeValue::Union(ref v) if v.len() == 2));
628 }
629
630 #[test]
631 fn description_prefers_markdown() {
632 let schema = Schema {
633 description: Some("plain".into()),
634 markdown_description: Some("**rich**".into()),
635 ..Default::default()
636 };
637 assert_eq!(schema.description(), Some("**rich**"));
638 }
639
640 #[test]
641 fn description_falls_back() {
642 let schema = Schema {
643 description: Some("plain".into()),
644 ..Default::default()
645 };
646 assert_eq!(schema.description(), Some("plain"));
647 }
648
649 #[test]
650 fn type_str_simple() {
651 let schema = Schema {
652 type_: Some(TypeValue::Single("string".into())),
653 ..Default::default()
654 };
655 assert_eq!(schema.type_str().as_deref(), Some("string"));
656 }
657
658 #[test]
659 fn type_str_union() {
660 let schema = Schema {
661 type_: Some(TypeValue::Union(vec!["string".into(), "null".into()])),
662 ..Default::default()
663 };
664 assert_eq!(schema.type_str().as_deref(), Some("string | null"));
665 }
666
667 #[test]
668 fn type_str_array_with_items() {
669 let items = SchemaValue::Schema(Box::new(Schema {
670 type_: Some(TypeValue::Single("string".into())),
671 ..Default::default()
672 }));
673 let schema = Schema {
674 type_: Some(TypeValue::Single("array".into())),
675 items: Some(Box::new(items)),
676 ..Default::default()
677 };
678 assert_eq!(schema.type_str().as_deref(), Some("string[]"));
679 }
680
681 #[test]
682 fn type_str_ref() {
683 let schema = Schema {
684 ref_: Some("#/$defs/Foo".into()),
685 ..Default::default()
686 };
687 assert_eq!(schema.type_str().as_deref(), Some("Foo"));
688 }
689
690 #[test]
691 fn is_deprecated_default_false() {
692 let schema = Schema::default();
693 assert!(!schema.is_deprecated());
694 }
695
696 #[test]
697 fn is_deprecated_true() {
698 let schema = Schema {
699 deprecated: Some(true),
700 ..Default::default()
701 };
702 assert!(schema.is_deprecated());
703 }
704
705 #[test]
706 fn required_set_empty() {
707 let schema = Schema::default();
708 assert!(schema.required_set().is_empty());
709 }
710
711 #[test]
712 fn required_set_values() {
713 let schema = Schema {
714 required: Some(vec!["a".into(), "b".into()]),
715 ..Default::default()
716 };
717 assert_eq!(schema.required_set(), &["a", "b"]);
718 }
719
720 #[test]
721 fn extra_fields_preserved() {
722 let json = json!({
723 "type": "object",
724 "x-custom": "value",
725 "x-another": 42
726 });
727 let schema: Schema = serde_json::from_value(json).unwrap();
728 assert_eq!(schema.extra.get("x-custom").unwrap(), "value");
729 assert_eq!(schema.extra.get("x-another").unwrap(), 42);
730 }
731
732 #[test]
733 fn x_taplo_deserialization() {
734 let json = json!({
735 "type": "object",
736 "x-taplo": {
737 "hidden": true,
738 "docs": {
739 "main": "Main docs"
740 }
741 }
742 });
743 let schema: Schema = serde_json::from_value(json).unwrap();
744 let taplo = schema.x_taplo.unwrap();
745 assert_eq!(taplo.hidden, Some(true));
746 assert_eq!(taplo.docs.unwrap().main.as_deref(), Some("Main docs"));
747 }
748
749 #[test]
750 fn x_lintel_deserialization() {
751 let json = json!({
752 "type": "object",
753 "x-lintel": {
754 "source": "https://example.com/schema.json",
755 "sourceSha256": "abc123"
756 }
757 });
758 let schema: Schema = serde_json::from_value(json).unwrap();
759 let lintel = schema.x_lintel.unwrap();
760 assert_eq!(
761 lintel.source.as_deref(),
762 Some("https://example.com/schema.json")
763 );
764 assert_eq!(lintel.source_sha256.as_deref(), Some("abc123"));
765 }
766
767 #[test]
768 fn navigate_pointer_empty() {
769 let sv = SchemaValue::Schema(Box::new(Schema {
770 type_: Some(TypeValue::Single("object".into())),
771 ..Default::default()
772 }));
773 let result = navigate_pointer(&sv, &sv, "").unwrap();
774 assert!(result.as_schema().is_some());
775 }
776
777 #[test]
778 fn navigate_pointer_properties() {
779 let name_schema = SchemaValue::Schema(Box::new(Schema {
780 type_: Some(TypeValue::Single("string".into())),
781 ..Default::default()
782 }));
783 let mut props = IndexMap::new();
784 props.insert("name".into(), name_schema);
785 let root = SchemaValue::Schema(Box::new(Schema {
786 properties: Some(props),
787 ..Default::default()
788 }));
789 let result = navigate_pointer(&root, &root, "/properties/name").unwrap();
790 let s = result.as_schema().unwrap();
791 assert!(matches!(s.type_, Some(TypeValue::Single(ref t)) if t == "string"));
792 }
793
794 #[test]
795 fn navigate_pointer_resolves_ref() {
796 let item_schema = SchemaValue::Schema(Box::new(Schema {
797 type_: Some(TypeValue::Single("object".into())),
798 description: Some("An item".into()),
799 ..Default::default()
800 }));
801 let ref_schema = SchemaValue::Schema(Box::new(Schema {
802 ref_: Some("#/$defs/Item".into()),
803 ..Default::default()
804 }));
805 let mut defs = IndexMap::new();
806 defs.insert("Item".into(), item_schema);
807 let mut props = IndexMap::new();
808 props.insert("item".into(), ref_schema);
809 let root = SchemaValue::Schema(Box::new(Schema {
810 properties: Some(props),
811 defs: Some(defs),
812 ..Default::default()
813 }));
814 let result = navigate_pointer(&root, &root, "/properties/item").unwrap();
815 let s = result.as_schema().unwrap();
816 assert_eq!(s.description.as_deref(), Some("An item"));
817 }
818
819 #[test]
820 fn navigate_pointer_bad_segment_errors() {
821 let sv = SchemaValue::Schema(Box::default());
822 let err = navigate_pointer(&sv, &sv, "/nonexistent").unwrap_err();
823 assert!(err.contains("nonexistent"));
824 }
825
826 #[test]
827 fn parse_cargo_fixture() {
828 let content =
829 std::fs::read_to_string("../jsonschema-migrate/tests/fixtures/cargo.json").unwrap();
830 let value: Value = serde_json::from_str(&content).unwrap();
831 let mut migrated = value;
832 jsonschema_migrate::migrate_to_2020_12(&mut migrated);
833 let schema: Schema = serde_json::from_value(migrated).unwrap();
834 assert!(schema.title.is_some() || schema.type_.is_some());
835 if schema.x_taplo.is_some() {
837 }
839 }
840}