llman_core/
schema_utils.rs1use jsonschema::validator_for;
9use schemars::JsonSchema;
10use schemars::generate::SchemaSettings;
11
12const SCHEMA_ERROR_LIMIT: usize = 5;
13
14pub fn generate_schema<T: JsonSchema>() -> schemars::Schema {
17 let mut settings = SchemaSettings::draft07();
18 settings.inline_subschemas = true;
19 settings.into_generator().into_root_schema_for::<T>()
20}
21
22pub fn validate_yaml_value_against<T: JsonSchema>(value: &serde_json::Value) -> Result<(), String> {
32 let schema_value = serde_json::to_value(generate_schema::<T>()).map_err(|e| e.to_string())?;
33 validate_schema_value(&schema_value, value)
34}
35
36pub fn validate_schema_value(
38 schema_value: &serde_json::Value,
39 value: &serde_json::Value,
40) -> Result<(), String> {
41 let validator = validator_for(schema_value).map_err(|e| e.to_string())?;
42 if !validator.is_valid(value) {
43 return Err(format_schema_errors(
44 validator.iter_errors(value).map(|err| err.to_string()),
45 ));
46 }
47 Ok(())
48}
49
50pub fn format_schema_errors<I>(errors: I) -> String
51where
52 I: IntoIterator<Item = String>,
53{
54 let mut iter = errors.into_iter();
55 let mut items = Vec::new();
56 for _ in 0..SCHEMA_ERROR_LIMIT {
57 if let Some(err) = iter.next() {
58 items.push(err);
59 } else {
60 break;
61 }
62 }
63 let remaining = iter.count();
64 if items.is_empty() {
65 return "unknown".to_string();
66 }
67 let mut message = items.join("; ");
68 if remaining > 0 {
69 message.push_str(&format!("; ... (+{remaining} more)"));
70 }
71 message
72}
73
74pub fn schema_header_line(schema_url: &str) -> String {
75 format!("# yaml-language-server: $schema={schema_url}")
76}
77
78pub fn prepend_schema_header(content: &str, schema_url: &str) -> String {
79 let header = schema_header_line(schema_url);
80 if content.is_empty() {
81 return format!("{header}\n");
82 }
83 let newline = if content.contains("\r\n") {
84 "\r\n"
85 } else {
86 "\n"
87 };
88 format!("{header}{newline}{content}")
89}
90
91pub fn apply_schema_header_to_content(content: &str, schema_url: &str) -> (String, bool) {
92 let header = schema_header_line(schema_url);
93 if content.is_empty() {
94 return (format!("{header}\n"), true);
95 }
96 let newline = if content.contains("\r\n") {
97 "\r\n"
98 } else {
99 "\n"
100 };
101 let has_trailing = content.ends_with('\n') || content.ends_with("\r\n");
102 let all_lines = content.lines().collect::<Vec<_>>();
103
104 let mut header_end = 0;
107 while header_end < all_lines.len() {
108 let line = all_lines[header_end];
109 if line.trim().is_empty() || line.trim_start().starts_with('#') {
110 header_end += 1;
111 continue;
112 }
113 break;
114 }
115
116 let mut normalized_header_lines = Vec::new();
117 for line in &all_lines[..header_end] {
118 if line
119 .trim_start()
120 .starts_with("# yaml-language-server: $schema=")
121 {
122 continue;
123 }
124 normalized_header_lines.push((*line).to_string());
125 }
126
127 let mut out_lines = Vec::with_capacity(all_lines.len() + 1);
128 out_lines.push(header);
129 out_lines.extend(normalized_header_lines);
130 out_lines.extend(
131 all_lines[header_end..]
132 .iter()
133 .map(|line| (*line).to_string()),
134 );
135 let mut updated = out_lines.join(newline);
136 if has_trailing {
137 updated.push_str(newline);
138 }
139 let changed = updated != content;
140 (updated, changed)
141}
142
143#[cfg(test)]
144mod tests {
145 use super::*;
146
147 #[test]
148 fn apply_schema_header_inserts_before_doc_start() {
149 let content = "---\nversion: \"0.1\"\n";
150 let (updated, changed) =
151 apply_schema_header_to_content(content, "https://example.com/g.json");
152 assert!(changed);
153 assert!(updated.starts_with("# yaml-language-server: $schema="));
154 assert!(updated.contains("\n---\n"));
155 }
156
157 #[test]
158 fn apply_schema_header_replaces_existing() {
159 let content =
160 "# yaml-language-server: $schema=https://example.com/old.json\nversion: \"0.1\"\n";
161 let (updated, changed) =
162 apply_schema_header_to_content(content, "https://example.com/g.json");
163 assert!(changed);
164 assert!(updated.starts_with(&schema_header_line("https://example.com/g.json")));
165 assert!(!updated.contains("old.json"));
166 }
167
168 #[test]
169 fn apply_schema_header_does_not_delete_late_schema_headers() {
170 let content = "# comment\n# yaml-language-server: $schema=https://example.com/old.json\nkey: value\n# yaml-language-server: $schema=https://example.com/keep.json\n".to_string();
171 let (updated, changed) =
172 apply_schema_header_to_content(&content, "https://example.com/g.json");
173 assert!(changed);
174 assert!(updated.starts_with(&schema_header_line("https://example.com/g.json")));
175 assert!(updated.contains("https://example.com/keep.json"));
176 }
177}