1use serde_json::{Map, Value};
15
16use crate::error::{ErrorCode, ProviderError};
17
18pub const MAX_POINTER_LEN: usize = 128;
24
25pub const DEFAULT_MAX_EXPANSIONS: usize = 32;
36
37pub const MAX_STRUCTURAL_DEPTH: usize = 256;
43
44pub const ANNOTATIONS: &[&str] = &[
49 "$comment",
50 "default",
51 "deprecated",
52 "description",
53 "examples",
54 "readOnly",
55 "title",
56 "writeOnly",
57];
58
59#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
65#[non_exhaustive]
66pub enum DialectError {
67 #[error("{pointer}: the branches of {keyword} were not provably disjoint")]
70 UnprovenUnion {
71 pointer: String,
73 keyword: String,
75 },
76 #[error("{pointer}: $ref carries the constraining sibling {keyword}")]
79 ConstrainingRefSibling {
80 pointer: String,
82 keyword: String,
84 },
85 #[error("{pointer}: $ref is not a local definition reference")]
88 UnresolvableRef {
89 pointer: String,
91 },
92 #[error("{pointer}: the definition is recursive and cannot be inlined")]
95 RecursiveRef {
96 pointer: String,
98 },
99 #[error("{pointer}: the schema goes deeper than {limit} levels")]
102 TooDeep {
103 pointer: String,
105 limit: usize,
107 },
108 #[error("{pointer}: expected an object sub-schema")]
111 NotAnObject {
112 pointer: String,
114 },
115}
116
117impl DialectError {
118 #[must_use]
120 pub fn pointer(&self) -> &str {
121 match self {
122 Self::UnprovenUnion { pointer, .. }
123 | Self::ConstrainingRefSibling { pointer, .. }
124 | Self::UnresolvableRef { pointer }
125 | Self::RecursiveRef { pointer }
126 | Self::TooDeep { pointer, .. }
127 | Self::NotAnObject { pointer } => pointer,
128 }
129 }
130
131 #[must_use]
133 pub fn keyword(&self) -> &str {
134 match self {
135 Self::UnprovenUnion { keyword, .. } | Self::ConstrainingRefSibling { keyword, .. } => {
136 keyword
137 }
138 Self::UnresolvableRef { .. } | Self::RecursiveRef { .. } => "$ref",
139 Self::TooDeep { .. } => "depth",
140 Self::NotAnObject { .. } => "schema",
141 }
142 }
143
144 #[must_use]
146 pub const fn as_str(&self) -> &'static str {
147 match self {
148 Self::UnprovenUnion { .. } => "unproven_union",
149 Self::ConstrainingRefSibling { .. } => "constraining_ref_sibling",
150 Self::UnresolvableRef { .. } => "unresolvable_ref",
151 Self::RecursiveRef { .. } => "recursive_ref",
152 Self::TooDeep { .. } => "too_deep",
153 Self::NotAnObject { .. } => "not_an_object",
154 }
155 }
156
157 #[must_use]
160 pub fn code(&self) -> ErrorCode {
161 ErrorCode::new(self.as_str())
162 }
163}
164
165impl From<DialectError> for ProviderError {
166 fn from(value: DialectError) -> Self {
174 Self::unsupported("schema_dialect").with_code(value.as_str())
175 }
176}
177
178fn child(pointer: &str, segment: &str) -> String {
184 let escaped = segment.replace('~', "~0").replace('/', "~1");
185 format!("{pointer}/{escaped}")
186}
187
188fn short(pointer: &str) -> String {
190 if pointer.is_empty() {
191 return String::from("/");
192 }
193 if pointer.len() <= MAX_POINTER_LEN {
194 return pointer.to_owned();
195 }
196 let mut end = MAX_POINTER_LEN;
197 while end > 0 && !pointer.is_char_boundary(end) {
198 end -= 1;
199 }
200 format!("{}…", &pointer[..end])
201}
202
203#[derive(Debug, Clone, PartialEq, Eq)]
208#[non_exhaustive]
209pub enum Disjointness {
210 ByConstant,
213 ByType,
215 ByDiscriminant {
218 property: String,
220 },
221 ByDiscriminantPairs,
230}
231
232#[derive(Debug, Clone, Copy, PartialEq, Eq)]
237struct TypeSet(u8);
238
239impl TypeSet {
240 const NULL: u8 = 1;
241 const BOOLEAN: u8 = 1 << 1;
242 const INTEGER: u8 = 1 << 2;
243 const NUMBER: u8 = 1 << 3;
244 const STRING: u8 = 1 << 4;
245 const ARRAY: u8 = 1 << 5;
246 const OBJECT: u8 = 1 << 6;
247 const ALL: u8 = 0b0111_1111;
248
249 const fn all() -> Self {
250 Self(Self::ALL)
251 }
252
253 const fn empty() -> Self {
254 Self(0)
255 }
256
257 const fn is_empty(self) -> bool {
258 self.0 == 0
259 }
260
261 fn named(name: &str) -> Self {
262 Self(match name {
263 "null" => Self::NULL,
264 "boolean" => Self::BOOLEAN,
265 "integer" => Self::INTEGER,
269 "number" => Self::NUMBER | Self::INTEGER,
270 "string" => Self::STRING,
271 "array" => Self::ARRAY,
272 "object" => Self::OBJECT,
273 _ => 0,
274 })
275 }
276
277 fn of_value(value: &Value) -> Self {
278 Self(match value {
279 Value::Null => Self::NULL,
280 Value::Bool(_) => Self::BOOLEAN,
281 Value::Number(number) if number.is_f64() => Self::NUMBER,
282 Value::Number(_) => Self::INTEGER,
283 Value::String(_) => Self::STRING,
284 Value::Array(_) => Self::ARRAY,
285 Value::Object(_) => Self::OBJECT,
286 })
287 }
288
289 const fn union(self, other: Self) -> Self {
290 Self(self.0 | other.0)
291 }
292
293 const fn intersects(self, other: Self) -> bool {
294 self.0 & other.0 != 0
295 }
296}
297
298fn type_set(schema: &Value) -> TypeSet {
303 let Some(object) = schema.as_object() else {
304 return TypeSet::all();
305 };
306 if let Some(constant) = object.get("const") {
307 return TypeSet::of_value(constant);
308 }
309 if let Some(Value::Array(values)) = object.get("enum") {
310 return values.iter().fold(TypeSet::empty(), |acc, value| {
311 acc.union(TypeSet::of_value(value))
312 });
313 }
314 match object.get("type") {
315 Some(Value::String(name)) => TypeSet::named(name),
316 Some(Value::Array(names)) => names.iter().fold(TypeSet::empty(), |acc, name| {
317 name.as_str()
318 .map_or(TypeSet::all(), |name| acc.union(TypeSet::named(name)))
319 }),
320 _ => TypeSet::all(),
321 }
322}
323
324fn literals(schema: &Value) -> Option<Vec<&Value>> {
330 let object = schema.as_object()?;
331 if let Some(constant) = object.get("const") {
332 return Some(vec![constant]);
333 }
334 match object.get("enum") {
335 Some(Value::Array(values)) if !values.is_empty() => Some(values.iter().collect()),
336 _ => None,
337 }
338}
339
340fn literals_disjoint(left: &[&Value], right: &[&Value]) -> bool {
342 !left.iter().any(|one| right.contains(one))
343}
344
345fn required_names(schema: &Value) -> Vec<&str> {
347 schema
348 .get("required")
349 .and_then(Value::as_array)
350 .map(|names| names.iter().filter_map(Value::as_str).collect())
351 .unwrap_or_default()
352}
353
354#[must_use]
384pub fn prove_disjoint(branches: &[Value]) -> Option<Disjointness> {
385 if branches.len() < 2 {
386 return Some(Disjointness::ByConstant);
387 }
388
389 let pinned: Option<Vec<Vec<&Value>>> = branches.iter().map(literals).collect();
391 if let Some(sets) = pinned
392 && pairwise(&sets, |left, right| literals_disjoint(left, right))
393 {
394 return Some(Disjointness::ByConstant);
395 }
396
397 let types: Vec<TypeSet> = branches.iter().map(type_set).collect();
399 if !types.iter().any(|set| set.is_empty())
400 && pairwise(&types, |left, right| !left.intersects(*right))
401 {
402 return Some(Disjointness::ByType);
403 }
404
405 for candidate in required_names(&branches[0]) {
409 let pinned: Option<Vec<Vec<&Value>>> = branches
410 .iter()
411 .map(|branch| pinned_property(branch, candidate))
412 .collect();
413 if let Some(sets) = pinned
414 && pairwise(&sets, |left, right| literals_disjoint(left, right))
415 {
416 return Some(Disjointness::ByDiscriminant {
417 property: candidate.to_owned(),
418 });
419 }
420 }
421
422 let candidates: Vec<&str> = required_names(&branches[0]);
427 if !candidates.is_empty()
428 && pairwise(branches, |left, right| {
429 candidates.iter().any(|candidate| {
430 match (
431 pinned_property(left, candidate),
432 pinned_property(right, candidate),
433 ) {
434 (Some(one), Some(other)) => literals_disjoint(&one, &other),
435 _ => false,
436 }
437 })
438 })
439 {
440 return Some(Disjointness::ByDiscriminantPairs);
441 }
442
443 None
444}
445
446fn pinned_property<'a>(branch: &'a Value, property: &str) -> Option<Vec<&'a Value>> {
448 if !required_names(branch).contains(&property) {
449 return None;
450 }
451 branch.get("properties")?.get(property).and_then(literals)
452}
453
454fn pairwise<T>(items: &[T], holds: impl Fn(&T, &T) -> bool) -> bool {
456 items
457 .iter()
458 .enumerate()
459 .all(|(index, left)| items[index + 1..].iter().all(|right| holds(left, right)))
460}
461
462pub fn narrow_unions(schema: &Value) -> Result<Value, DialectError> {
486 fn walk(node: &Value, pointer: &str) -> Result<Value, DialectError> {
487 match node {
488 Value::Object(map) => {
489 let mut out = Map::with_capacity(map.len());
490 for (key, value) in map {
491 let child = child(pointer, key);
492 if key == "oneOf" {
493 let branches =
494 value.as_array().ok_or_else(|| DialectError::NotAnObject {
495 pointer: short(&child),
496 })?;
497 if prove_disjoint(branches).is_none() {
498 return Err(DialectError::UnprovenUnion {
499 pointer: short(pointer),
500 keyword: String::from("oneOf"),
501 });
502 }
503 out.insert(String::from("anyOf"), walk(value, &child)?);
504 } else {
505 out.insert(key.clone(), walk(value, &child)?);
506 }
507 }
508 Ok(Value::Object(out))
509 }
510 Value::Array(items) => items
511 .iter()
512 .enumerate()
513 .map(|(index, item)| walk(item, &child(pointer, &index.to_string())))
514 .collect::<Result<Vec<_>, _>>()
515 .map(Value::Array),
516 other => Ok(other.clone()),
517 }
518 }
519 walk(schema, "")
520}
521
522pub fn lift_ref_siblings(schema: &Value) -> Result<Value, DialectError> {
541 fn walk(node: &Value, pointer: &str) -> Result<Value, DialectError> {
542 match node {
543 Value::Object(map) if map.contains_key("$ref") && map.len() > 1 => {
544 let mut out = Map::with_capacity(map.len());
545 for key in map.keys().filter(|key| key.as_str() != "$ref") {
546 if !ANNOTATIONS.contains(&key.as_str()) {
547 return Err(DialectError::ConstrainingRefSibling {
548 pointer: short(pointer),
549 keyword: key.clone(),
550 });
551 }
552 }
553 for (key, value) in map {
554 if key == "$ref" {
555 continue;
556 }
557 out.insert(key.clone(), value.clone());
558 }
559 out.insert(
560 String::from("anyOf"),
561 Value::Array(vec![serde_json::json!({"$ref": map["$ref"].clone()})]),
562 );
563 Ok(Value::Object(out))
564 }
565 Value::Object(map) => map
566 .iter()
567 .map(|(key, value)| {
568 walk(value, &child(pointer, key)).map(|value| (key.clone(), value))
569 })
570 .collect::<Result<Map<_, _>, _>>()
571 .map(Value::Object),
572 Value::Array(items) => items
573 .iter()
574 .enumerate()
575 .map(|(index, item)| walk(item, &child(pointer, &index.to_string())))
576 .collect::<Result<Vec<_>, _>>()
577 .map(Value::Array),
578 other => Ok(other.clone()),
579 }
580 }
581 walk(schema, "")
582}
583
584#[derive(Debug, Clone, PartialEq, Eq, Default)]
587pub struct Closure {
588 pub schema: Value,
590 pub forced_required: Vec<String>,
594}
595
596#[must_use]
627pub fn close_objects(schema: &Value) -> Closure {
628 fn walk(node: &Value, pointer: &str, forced: &mut Vec<String>) -> Value {
629 match node {
630 Value::Object(map) => {
631 let mut out = Map::with_capacity(map.len() + 2);
632 for (key, value) in map {
633 out.insert(key.clone(), walk(value, &child(pointer, key), forced));
634 }
635 if let Some(Value::Object(properties)) = map.get("properties") {
636 let already: Vec<&str> = required_names(node);
637 for name in properties.keys() {
638 if !already.contains(&name.as_str()) {
639 forced.push(short(&child(&child(pointer, "properties"), name)));
640 }
641 }
642 out.insert(
643 String::from("required"),
644 Value::Array(
645 properties
646 .keys()
647 .map(|name| Value::String(name.clone()))
648 .collect(),
649 ),
650 );
651 out.insert(String::from("additionalProperties"), Value::Bool(false));
652 }
653 Value::Object(out)
654 }
655 Value::Array(items) => Value::Array(
656 items
657 .iter()
658 .enumerate()
659 .map(|(index, item)| walk(item, &child(pointer, &index.to_string()), forced))
660 .collect(),
661 ),
662 other => other.clone(),
663 }
664 }
665 let mut forced_required = Vec::new();
666 let schema = walk(schema, "", &mut forced_required);
667 forced_required.sort();
671 Closure {
672 schema,
673 forced_required,
674 }
675}
676
677pub fn inline_definitions(schema: &Value, max_expansions: usize) -> Result<Value, DialectError> {
704 const TABLES: [&str; 2] = ["$defs", "definitions"];
706
707 struct Inliner<'a> {
708 tables: Vec<&'a Map<String, Value>>,
709 max_expansions: usize,
710 }
711
712 impl Inliner<'_> {
713 fn resolve(&self, reference: &str) -> Option<&Value> {
715 let rest = reference.strip_prefix("#/")?;
716 let (table, name) = rest.split_once('/')?;
717 if !TABLES.contains(&table) || name.contains('/') {
718 return None;
719 }
720 self.tables.iter().find_map(|found| found.get(name))
721 }
722
723 fn walk(
724 &self,
725 node: &Value,
726 pointer: &str,
727 depth: usize,
728 open: &mut Vec<String>,
729 ) -> Result<Value, DialectError> {
730 if open.len() > self.max_expansions {
736 return Err(DialectError::TooDeep {
737 pointer: short(pointer),
738 limit: self.max_expansions,
739 });
740 }
741 if depth > MAX_STRUCTURAL_DEPTH {
742 return Err(DialectError::TooDeep {
743 pointer: short(pointer),
744 limit: MAX_STRUCTURAL_DEPTH,
745 });
746 }
747 match node {
748 Value::Object(map) if map.contains_key("$ref") => {
749 let reference =
750 map["$ref"]
751 .as_str()
752 .ok_or_else(|| DialectError::UnresolvableRef {
753 pointer: short(pointer),
754 })?;
755 if open.iter().any(|seen| seen == reference) {
756 return Err(DialectError::RecursiveRef {
757 pointer: short(pointer),
758 });
759 }
760 let target =
761 self.resolve(reference)
762 .ok_or_else(|| DialectError::UnresolvableRef {
763 pointer: short(pointer),
764 })?;
765 for key in map.keys() {
766 if key == "$ref" || TABLES.contains(&key.as_str()) {
767 continue;
771 }
772 if !ANNOTATIONS.contains(&key.as_str()) {
773 return Err(DialectError::ConstrainingRefSibling {
774 pointer: short(pointer),
775 keyword: key.clone(),
776 });
777 }
778 }
779 open.push(reference.to_owned());
780 let expanded = self.walk(target, pointer, depth + 1, open);
781 open.pop();
782 let mut expanded = match expanded? {
783 Value::Object(body) => body,
784 other => return Ok(other),
785 };
786 for (key, value) in map {
789 if key != "$ref" && !TABLES.contains(&key.as_str()) {
790 expanded.insert(key.clone(), value.clone());
791 }
792 }
793 Ok(Value::Object(expanded))
794 }
795 Value::Object(map) => map
796 .iter()
797 .filter(|(key, _)| !TABLES.contains(&key.as_str()))
798 .map(|(key, value)| {
799 self.walk(value, &child(pointer, key), depth + 1, open)
800 .map(|value| (key.clone(), value))
801 })
802 .collect::<Result<Map<_, _>, _>>()
803 .map(Value::Object),
804 Value::Array(items) => items
805 .iter()
806 .enumerate()
807 .map(|(index, item)| {
808 self.walk(item, &child(pointer, &index.to_string()), depth + 1, open)
809 })
810 .collect::<Result<Vec<_>, _>>()
811 .map(Value::Array),
812 other => Ok(other.clone()),
813 }
814 }
815 }
816
817 let tables = TABLES
818 .iter()
819 .filter_map(|name| schema.get(*name).and_then(Value::as_object))
820 .collect();
821 let inliner = Inliner {
822 tables,
823 max_expansions,
824 };
825 inliner.walk(schema, "", 0, &mut Vec::new())
826}
827
828#[must_use]
841pub fn collapse_literal_union(branches: &[Value]) -> Option<(Vec<Value>, String)> {
842 let mut values = Vec::with_capacity(branches.len());
843 let mut lines = Vec::new();
844 for branch in branches {
845 let pinned = literals(branch)?;
846 if pinned.len() != 1 {
847 return None;
848 }
849 let value = pinned[0];
850 if let (Some(name), Some(doc)) = (
851 value.as_str(),
852 branch.get("description").and_then(Value::as_str),
853 ) {
854 lines.push(format!("«{name}»: {doc}"));
855 }
856 values.push(value.clone());
857 }
858 Some((values, lines.join("\n")))
859}
860
861#[cfg(test)]
862mod tests {
863 use super::*;
864 use serde_json::json;
865
866 #[test]
867 fn a_tagged_enum_is_disjoint_by_its_tag() {
868 let branches = [
869 json!({"type": "object", "required": ["kind", "value"],
870 "properties": {"kind": {"const": "token"}, "value": {"type": "string"}}}),
871 json!({"type": "object", "required": ["kind"],
872 "properties": {"kind": {"const": "new_case"}}}),
873 ];
874 assert_eq!(
875 prove_disjoint(&branches),
876 Some(Disjointness::ByDiscriminant {
877 property: "kind".to_owned()
878 })
879 );
880 }
881
882 #[test]
889 fn branches_sharing_a_tag_are_separated_by_a_second_field() {
890 let branches = [
891 json!({"type": "object", "required": ["kind", "operation"],
892 "properties": {"kind": {"const": "apply"},
893 "operation": {"enum": ["a.set", "a.clear"]}}}),
894 json!({"type": "object", "required": ["kind", "operation"],
895 "properties": {"kind": {"const": "apply"},
896 "operation": {"enum": ["a.create"]}}}),
897 json!({"type": "object", "required": ["kind"],
898 "properties": {"kind": {"const": "start"}}}),
899 ];
900 assert_eq!(
901 prove_disjoint(&branches),
902 Some(Disjointness::ByDiscriminantPairs)
903 );
904 }
905
906 #[test]
909 fn a_shared_tag_with_an_overlapping_second_field_is_not_disjoint() {
910 let branches = [
911 json!({"type": "object", "required": ["kind", "operation"],
912 "properties": {"kind": {"const": "apply"},
913 "operation": {"enum": ["a.set", "a.clear"]}}}),
914 json!({"type": "object", "required": ["kind", "operation"],
915 "properties": {"kind": {"const": "apply"},
916 "operation": {"enum": ["a.clear"]}}}),
917 ];
918 assert_eq!(prove_disjoint(&branches), None);
919 }
920
921 #[test]
922 fn a_repeated_tag_is_not_disjoint() {
923 let branches = [
925 json!({"type": "object", "required": ["kind"],
926 "properties": {"kind": {"const": "token"}}}),
927 json!({"type": "object", "required": ["kind"],
928 "properties": {"kind": {"const": "token"}}}),
929 ];
930 assert_eq!(prove_disjoint(&branches), None);
931 }
932
933 #[test]
934 fn a_tag_that_one_branch_leaves_optional_is_not_disjoint() {
935 let branches = [
938 json!({"type": "object", "required": ["kind"],
939 "properties": {"kind": {"const": "a"}}}),
940 json!({"type": "object", "properties": {"kind": {"const": "b"}}}),
941 ];
942 assert_eq!(prove_disjoint(&branches), None);
943 }
944
945 #[test]
946 fn distinct_constants_are_disjoint_and_shared_ones_are_not() {
947 assert_eq!(
948 prove_disjoint(&[json!({"const": "a"}), json!({"const": "b"})]),
949 Some(Disjointness::ByConstant)
950 );
951 assert_eq!(
952 prove_disjoint(&[json!({"enum": ["a", "b"]}), json!({"enum": ["b", "c"]})]),
953 None
954 );
955 }
956
957 #[test]
958 fn distinct_types_are_disjoint_but_number_contains_integer() {
959 assert_eq!(
960 prove_disjoint(&[json!({"type": "string"}), json!({"type": "object"})]),
961 Some(Disjointness::ByType)
962 );
963 assert_eq!(
965 prove_disjoint(&[json!({"type": "number"}), json!({"type": "integer"})]),
966 None
967 );
968 }
969
970 #[test]
971 fn an_unconstrained_branch_defeats_every_proof() {
972 assert_eq!(
973 prove_disjoint(&[json!({"type": "string"}), json!({})]),
974 None
975 );
976 }
977
978 #[test]
979 fn narrowing_rewrites_what_it_proves_and_refuses_what_it_cannot() {
980 let proven = narrow_unions(&json!({
981 "properties": {"t": {"oneOf": [{"const": "a"}, {"const": "b"}]}}
982 }))
983 .expect("provably disjoint");
984 assert_eq!(
985 proven["properties"]["t"]["anyOf"],
986 json!([{"const": "a"}, {"const": "b"}])
987 );
988
989 let error = narrow_unions(&json!({
990 "properties": {"t": {"oneOf": [{"type": "object"}, {"type": "object"}]}}
991 }))
992 .expect_err("two open objects overlap");
993 assert_eq!(error.keyword(), "oneOf");
994 assert_eq!(error.pointer(), "/properties/t");
995 assert_eq!(error.as_str(), "unproven_union");
996 }
997
998 #[test]
999 fn a_ref_keeps_its_description_and_refuses_a_constraint() {
1000 let lifted = lift_ref_siblings(&json!({
1001 "properties": {"a": {"$ref": "#/$defs/X", "description": "the a"}}
1002 }))
1003 .expect("an annotation lifts");
1004 assert_eq!(
1005 lifted["properties"]["a"],
1006 json!({"description": "the a", "anyOf": [{"$ref": "#/$defs/X"}]})
1007 );
1008
1009 let error = lift_ref_siblings(&json!({
1010 "properties": {"a": {"$ref": "#/$defs/X", "minLength": 3}}
1011 }))
1012 .expect_err("a constraint cannot be lifted");
1013 assert_eq!(error.keyword(), "minLength");
1014 assert_eq!(error.as_str(), "constraining_ref_sibling");
1015 }
1016
1017 #[test]
1018 fn closing_forces_every_property_and_names_what_it_forced() {
1019 let closed = close_objects(&json!({
1020 "type": "object",
1021 "required": ["a"],
1022 "properties": {
1023 "a": {"type": "string"},
1024 "b": {"type": ["string", "null"]},
1025 "c": {"type": "object", "properties": {"d": {"type": "string"}}}
1026 }
1027 }));
1028 assert_eq!(closed.schema["required"], json!(["a", "b", "c"]));
1029 assert_eq!(closed.schema["additionalProperties"], json!(false));
1030 assert_eq!(
1031 closed.schema["properties"]["c"]["additionalProperties"],
1032 json!(false)
1033 );
1034 assert_eq!(
1035 closed.forced_required,
1036 vec![
1037 "/properties/b".to_owned(),
1038 "/properties/c".to_owned(),
1039 "/properties/c/properties/d".to_owned()
1040 ]
1041 );
1042 }
1043
1044 #[test]
1045 fn inlining_substitutes_a_definition_and_prefers_the_field_documentation() {
1046 let inlined = inline_definitions(
1047 &json!({
1048 "type": "object",
1049 "properties": {"a": {"$ref": "#/$defs/Name", "description": "this field"}},
1050 "$defs": {"Name": {"type": "string", "description": "the type", "minLength": 1}}
1051 }),
1052 DEFAULT_MAX_EXPANSIONS,
1053 )
1054 .expect("acyclic");
1055 assert_eq!(
1056 inlined["properties"]["a"],
1057 json!({"type": "string", "description": "this field", "minLength": 1}),
1058 "the constraint survives and the field's own sentence wins"
1059 );
1060 assert!(inlined.get("$defs").is_none(), "the table is consumed");
1061 }
1062
1063 #[test]
1064 fn inlining_refuses_a_recursive_definition_rather_than_looping() {
1065 let error = inline_definitions(
1066 &json!({
1067 "$ref": "#/$defs/Node",
1068 "$defs": {"Node": {"type": "object", "properties": {"next": {"$ref": "#/$defs/Node"}}}}
1069 }),
1070 DEFAULT_MAX_EXPANSIONS,
1071 )
1072 .expect_err("recursion has no finite expansion");
1073 assert_eq!(error.as_str(), "recursive_ref");
1074 }
1075
1076 #[test]
1077 fn inlining_refuses_a_reference_it_cannot_resolve() {
1078 for reference in [
1079 "https://example.test/schema#/$defs/X",
1080 "#/components/schemas/X",
1081 "#/$defs/Missing",
1082 ] {
1083 let error = inline_definitions(&json!({"$ref": reference}), DEFAULT_MAX_EXPANSIONS)
1084 .expect_err("unresolvable");
1085 assert_eq!(error.as_str(), "unresolvable_ref", "{reference}");
1086 }
1087 }
1088
1089 #[test]
1090 fn a_literal_union_collapses_and_keeps_each_variant_sentence() {
1091 let (values, doc) = collapse_literal_union(&[
1092 json!({"const": "plan", "description": "a finished plan"}),
1093 json!({"const": "read_requests", "description": "reads to perform"}),
1094 ])
1095 .expect("all branches pin one literal");
1096 assert_eq!(values, vec![json!("plan"), json!("read_requests")]);
1097 assert_eq!(
1098 doc,
1099 "«plan»: a finished plan\n«read_requests»: reads to perform"
1100 );
1101
1102 assert!(collapse_literal_union(&[json!({"type": "string"})]).is_none());
1103 }
1104
1105 #[test]
1106 fn a_dialect_failure_is_a_fallback_not_a_retry() {
1107 use crate::error::{ProviderErrorKind, RetryClass};
1108 let error = ProviderError::from(DialectError::UnprovenUnion {
1109 pointer: String::from("/x"),
1110 keyword: String::from("oneOf"),
1111 });
1112 assert!(matches!(
1113 error.kind(),
1114 ProviderErrorKind::Unsupported { .. }
1115 ));
1116 assert_eq!(error.retry_class(), RetryClass::Fallback);
1117 assert_eq!(
1118 error.code().map(|code| code.as_str().to_owned()),
1119 Some("unproven_union".to_owned())
1120 );
1121 }
1122
1123 #[test]
1124 fn an_awkward_property_name_is_escaped_into_the_pointer() {
1125 let error = narrow_unions(&json!({
1128 "properties": {"a/b~c": {"oneOf": [{"type": "object"}, {"type": "object"}]}}
1129 }))
1130 .expect_err("an unprovable union under an awkward name");
1131 assert_eq!(error.pointer(), "/properties/a~1b~0c");
1132 }
1133
1134 #[test]
1135 fn the_limit_counts_expansions_rather_than_json_levels() {
1136 let mut deep = json!({"type": "string"});
1140 for _ in 0..40 {
1141 deep = json!({"type": "array", "items": deep});
1142 }
1143 assert!(
1144 inline_definitions(&deep, 2).is_ok(),
1145 "no references to expand"
1146 );
1147
1148 let chained = json!({
1151 "$ref": "#/$defs/A",
1152 "$defs": {
1153 "A": {"type": "object", "properties": {"b": {"$ref": "#/$defs/B"}}},
1154 "B": {"type": "object", "properties": {"c": {"$ref": "#/$defs/C"}}},
1155 "C": {"type": "string"}
1156 }
1157 });
1158 assert!(inline_definitions(&chained, 8).is_ok());
1159 let error = inline_definitions(&chained, 2).expect_err("three deep");
1160 assert!(
1161 matches!(error, DialectError::TooDeep { limit: 2, .. }),
1162 "{error:?}"
1163 );
1164 }
1165
1166 #[test]
1167 fn a_long_pointer_is_truncated_on_a_character_boundary() {
1168 let deep = format!("/{}", "à".repeat(200));
1169 let error = DialectError::TooDeep {
1170 pointer: short(&deep),
1171 limit: 4,
1172 };
1173 assert!(error.pointer().len() <= MAX_POINTER_LEN + 4);
1174 assert!(error.pointer().ends_with('…'));
1175 }
1176}