jsonschema_explain/
schema.rs1use jsonschema_schema::{Schema, SchemaValue, ref_name};
2
3use crate::fmt::{Fmt, format_type};
4
5pub fn resolve_ref<'a>(sv: &'a SchemaValue, root: &'a SchemaValue) -> &'a SchemaValue {
10 let Some(schema) = sv.as_schema() else {
11 return sv;
12 };
13 let Some(ref ref_str) = schema.ref_ else {
14 return sv;
15 };
16 let fragment = if let Some(path) = ref_str.strip_prefix('#') {
17 path
18 } else if let Some(pos) = ref_str.find('#') {
19 &ref_str[pos + 1..]
20 } else {
21 return sv;
22 };
23 if let Ok(resolved) = jsonschema_schema::navigate_pointer(root, root, fragment) {
24 return resolved;
25 }
26 sv
27}
28
29pub fn navigate_pointer<'a>(
35 schema: &'a SchemaValue,
36 root: &'a SchemaValue,
37 pointer: &str,
38) -> Result<&'a SchemaValue, String> {
39 jsonschema_schema::navigate_pointer(schema, root, pointer)
40}
41
42pub(crate) fn required_set(schema: &Schema) -> Vec<String> {
44 schema.required_set().to_vec()
45}
46
47pub(crate) fn schema_type_str(schema: &Schema) -> Option<String> {
49 schema.type_str()
50}
51
52pub(crate) fn get_description(schema: &Schema) -> Option<&str> {
54 schema.description()
55}
56
57pub(crate) fn variant_summary(variant: &SchemaValue, root: &SchemaValue, f: &Fmt<'_>) -> String {
59 let resolved_sv = resolve_ref(variant, root);
60 let Some(resolved) = resolved_sv.as_schema() else {
61 return format!("{}(schema){}", f.dim, f.reset);
62 };
63
64 let dep = if resolved.is_deprecated() {
65 format!(" {}[DEPRECATED]{}", f.dim, f.reset)
66 } else {
67 String::new()
68 };
69
70 if let Some(title) = resolved.title.as_deref() {
72 let ty = schema_type_str(resolved).unwrap_or_default();
73 if ty.is_empty() {
74 return format!("{}{title}{}{dep}", f.bold, f.reset);
75 }
76 return format!(
77 "{}{title}{}{dep} ({})",
78 f.bold,
79 f.reset,
80 format_type(&ty, f)
81 );
82 }
83
84 if let Some(schema) = variant.as_schema()
86 && let Some(ref r) = schema.ref_
87 {
88 if r.starts_with("#/") {
89 return format!("{}{}{}{dep}", f.cyan, ref_name(r), f.reset);
90 }
91 return format!("{}(see: {r}){}{dep}", f.dim, f.reset);
92 }
93
94 if let Some(desc) = get_description(resolved) {
95 let first_line = first_sentence(desc);
96 let ty = schema_type_str(resolved).unwrap_or_default();
97 let rendered = if f.is_color() {
98 markdown_to_ansi::render_inline(first_line, &f.md_opts(None))
99 } else {
100 first_line.to_string()
101 };
102 if ty.is_empty() {
103 return format!("{rendered}{dep}");
104 }
105 return format!("{} - {rendered}{dep}", format_type(&ty, f));
106 }
107
108 if let Some(ty) = schema_type_str(resolved) {
109 return format!("{}{dep}", format_type(&ty, f));
110 }
111
112 if let Some(ref pat) = resolved.pattern {
114 return format!("pattern: {}{pat}{}{dep}", f.magenta, f.reset);
115 }
116
117 format!("{}(schema){}{dep}", f.dim, f.reset)
118}
119
120fn first_sentence(desc: &str) -> &str {
122 let trimmed = desc.trim();
124 if let Some(pos) = trimmed.find("\n\n") {
125 let first = trimmed[..pos].trim();
126 if !first.is_empty() {
127 return first;
128 }
129 }
130 if let Some(pos) = trimmed.find('\n') {
131 let first = trimmed[..pos].trim();
132 if !first.is_empty() {
133 return first;
134 }
135 }
136 trimmed
137}
138
139#[cfg(test)]
140#[allow(clippy::unwrap_used)]
141mod tests {
142 use super::*;
143 use serde_json::json;
144
145 fn sv(val: serde_json::Value) -> SchemaValue {
147 SchemaValue::Schema(Box::new(jsonschema_migrate::migrate(val).unwrap()))
148 }
149
150 #[test]
153 fn navigate_empty_pointer_returns_schema() {
154 let schema = sv(json!({"type": "object"}));
155 let result = navigate_pointer(&schema, &schema, "").unwrap();
156 assert!(result.as_schema().is_some());
157 }
158
159 #[test]
160 fn navigate_root_slash_returns_schema() {
161 let schema = sv(json!({"type": "object"}));
162 let result = navigate_pointer(&schema, &schema, "/").unwrap();
163 assert!(result.as_schema().is_some());
164 }
165
166 #[test]
167 fn navigate_single_segment() {
168 let schema = sv(json!({
169 "properties": {
170 "name": { "type": "string" }
171 }
172 }));
173 let result = navigate_pointer(&schema, &schema, "/properties/name").unwrap();
174 let s = result.as_schema().unwrap();
175 assert!(s.type_str().as_deref() == Some("string"));
176 }
177
178 #[test]
179 fn navigate_nested_segments() {
180 let schema = sv(json!({
181 "properties": {
182 "name": { "type": "string", "description": "The name" }
183 }
184 }));
185 let result = navigate_pointer(&schema, &schema, "/properties/name").unwrap();
186 let s = result.as_schema().unwrap();
187 assert_eq!(s.description.as_deref(), Some("The name"));
188 }
189
190 #[test]
191 fn navigate_resolves_ref_at_each_step() {
192 let schema = sv(json!({
193 "properties": {
194 "item": { "$ref": "#/$defs/Item" }
195 },
196 "$defs": {
197 "Item": {
198 "type": "object",
199 "description": "An item"
200 }
201 }
202 }));
203 let result = navigate_pointer(&schema, &schema, "/properties/item").unwrap();
204 let s = result.as_schema().unwrap();
205 assert_eq!(s.description.as_deref(), Some("An item"));
206 }
207
208 #[test]
209 fn navigate_through_ref_then_deeper() {
210 let schema = sv(json!({
211 "properties": {
212 "config": { "$ref": "#/$defs/Config" }
213 },
214 "$defs": {
215 "Config": {
216 "type": "object",
217 "properties": {
218 "debug": { "type": "boolean" }
219 }
220 }
221 }
222 }));
223 let result =
224 navigate_pointer(&schema, &schema, "/properties/config/properties/debug").unwrap();
225 let s = result.as_schema().unwrap();
226 assert!(s.type_str().as_deref() == Some("boolean"));
227 }
228
229 #[test]
230 fn navigate_array_index() {
231 let schema = sv(json!({
232 "oneOf": [
233 { "type": "string" },
234 { "type": "integer" }
235 ]
236 }));
237 let result = navigate_pointer(&schema, &schema, "/oneOf/1").unwrap();
238 let s = result.as_schema().unwrap();
239 assert!(s.type_str().as_deref() == Some("integer"));
240 }
241
242 #[test]
243 fn navigate_missing_segment_errors() {
244 let schema = sv(json!({"type": "object"}));
245 let err = navigate_pointer(&schema, &schema, "/nonexistent").unwrap_err();
246 assert!(err.contains("nonexistent"), "error was: {err}");
247 }
248
249 #[test]
250 fn navigate_defs_directly() {
251 let schema = sv(json!({
252 "$defs": {
253 "Foo": { "type": "string" }
254 }
255 }));
256 let result = navigate_pointer(&schema, &schema, "/$defs/Foo").unwrap();
257 let s = result.as_schema().unwrap();
258 assert!(s.type_str().as_deref() == Some("string"));
259 }
260
261 #[test]
264 fn resolve_ref_no_ref_returns_self() {
265 let schema = sv(json!({"type": "string"}));
266 let result = resolve_ref(&schema, &schema);
267 assert!(result.as_schema().is_some());
268 }
269
270 #[test]
271 fn resolve_ref_follows_local_ref() {
272 let root = sv(json!({
273 "$defs": {
274 "Name": { "type": "string" }
275 }
276 }));
277 let schema = sv(json!({"$ref": "#/$defs/Name"}));
278 let resolved = resolve_ref(&schema, &root);
279 let s = resolved.as_schema().unwrap();
280 assert!(s.type_str().as_deref() == Some("string"));
281 }
282
283 #[test]
284 fn resolve_ref_missing_target_returns_self() {
285 let root = sv(json!({"$defs": {}}));
286 let schema = sv(json!({"$ref": "#/$defs/Missing"}));
287 let resolved = resolve_ref(&schema, &root);
288 let s = resolved.as_schema().unwrap();
289 assert!(s.ref_.is_some());
290 }
291
292 #[test]
293 fn resolve_ref_external_ref_returns_self() {
294 let root = sv(json!({}));
295 let schema = sv(json!({"$ref": "https://example.com/schema.json"}));
296 let resolved = resolve_ref(&schema, &root);
297 let s = resolved.as_schema().unwrap();
298 assert!(s.ref_.is_some());
299 }
300}