1use super::info::{NoNamespaceSchemaLocationHint, SchemaLocationHint};
34use crate::builder::SchemaSetBuilder;
35use crate::error::SchemaError;
36
37#[derive(Debug, Default)]
39pub struct HintLoadResult {
40 pub loaded_count: usize,
42 pub skipped_count: usize,
44 pub errors: Vec<SchemaError>,
46}
47
48pub fn load_hints_into_builder(
65 builder: &mut SchemaSetBuilder,
66 schema_location_hints: &[SchemaLocationHint],
67 no_namespace_hints: &[NoNamespaceSchemaLocationHint],
68) -> HintLoadResult {
69 let mut result = HintLoadResult::default();
70
71 for hint in schema_location_hints {
72 try_load_hint(builder, &hint.location, &hint.base_uri, &mut result);
73 }
74
75 for hint in no_namespace_hints {
76 try_load_hint(builder, &hint.location, &hint.base_uri, &mut result);
77 }
78
79 result
80}
81
82fn try_load_hint(
83 builder: &mut SchemaSetBuilder,
84 location: &str,
85 base_uri: &str,
86 result: &mut HintLoadResult,
87) {
88 match builder.try_add_relative(location, base_uri) {
89 Ok(true) => {
90 result.loaded_count += 1;
91 }
92 Ok(false) => {
93 result.skipped_count += 1;
95 }
96 Err(e) => {
97 result.errors.push(e);
98 result.skipped_count += 1;
99 }
100 }
101}
102
103#[derive(Debug, Default)]
118pub struct EnrichmentOutcome {
119 pub schema_set: Option<crate::schema::SchemaSet>,
121 pub hint_errors: Vec<SchemaError>,
124 pub compile_error: Option<SchemaError>,
126}
127
128impl EnrichmentOutcome {
129 pub fn is_no_op(&self) -> bool {
132 self.schema_set.is_none()
133 && self.hint_errors.is_empty()
134 && self.compile_error.is_none()
135 }
136
137 pub fn schema_set_or<'a>(
141 &'a self,
142 original: &'a crate::schema::SchemaSet,
143 ) -> &'a crate::schema::SchemaSet {
144 self.schema_set.as_ref().unwrap_or(original)
145 }
146}
147
148pub fn enrich_schema_set(
174 original: &crate::schema::SchemaSet,
175 schema_location_hints: &[SchemaLocationHint],
176 no_namespace_hints: &[NoNamespaceSchemaLocationHint],
177) -> EnrichmentOutcome {
178 if schema_location_hints.is_empty() && no_namespace_hints.is_empty() {
179 return EnrichmentOutcome::default();
180 }
181
182 let mut builder = if original.xsd_version == crate::schema::model::XsdVersion::V1_1 {
183 SchemaSetBuilder::xsd11()
184 } else {
185 SchemaSetBuilder::new()
186 };
187
188 builder.add_from(original);
189 let hint_result =
190 load_hints_into_builder(&mut builder, schema_location_hints, no_namespace_hints);
191
192 match builder.compile() {
193 Ok(compiled) => EnrichmentOutcome {
194 schema_set: Some(compiled.into_schema_set()),
195 hint_errors: hint_result.errors,
196 compile_error: None,
197 },
198 Err(e) => EnrichmentOutcome {
199 schema_set: None,
200 hint_errors: hint_result.errors,
201 compile_error: Some(e),
202 },
203 }
204}
205
206
207#[cfg(test)]
208mod tests {
209 use super::*;
210 use crate::builder::SchemaSetBuilder;
211 use crate::validation::info::{NoNamespaceSchemaLocationHint, SchemaLocationHint};
212
213 #[test]
214 fn test_load_hints_empty() {
215 let mut builder = SchemaSetBuilder::new();
216 let result = load_hints_into_builder(&mut builder, &[], &[]);
217 assert_eq!(result.loaded_count, 0);
218 assert_eq!(result.skipped_count, 0);
219 assert!(result.errors.is_empty());
220 }
221
222 #[test]
223 fn test_load_hints_nonexistent_file_is_nonfatal() {
224 let mut builder = SchemaSetBuilder::new();
225 let hints = vec![SchemaLocationHint {
226 namespace: "urn:test".to_string(),
227 location: "nonexistent_schema_abc123.xsd".to_string(),
228 base_uri: String::new(),
229 }];
230 let result = load_hints_into_builder(&mut builder, &hints, &[]);
231 assert_eq!(result.loaded_count, 0);
232 assert_eq!(result.skipped_count, 1);
233 assert_eq!(result.errors.len(), 1);
234 }
235
236 #[test]
237 fn test_load_no_namespace_hints_nonexistent_is_nonfatal() {
238 let mut builder = SchemaSetBuilder::new();
239 let hints = vec![NoNamespaceSchemaLocationHint {
240 location: "nonexistent_schema_abc123.xsd".to_string(),
241 base_uri: String::new(),
242 }];
243 let result = load_hints_into_builder(&mut builder, &[], &hints);
244 assert_eq!(result.loaded_count, 0);
245 assert_eq!(result.skipped_count, 1);
246 assert_eq!(result.errors.len(), 1);
247 }
248
249 #[test]
250 fn test_duplicate_hints_counted_as_skipped() {
251 let xsd = r#"<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
253 <xs:element name="root" type="xs:string"/>
254 </xs:schema>"#;
255 let mut builder = SchemaSetBuilder::new()
256 .add_source(xsd, "http://example.com/dedup.xsd")
257 .unwrap();
258
259 let hints = vec![SchemaLocationHint {
263 namespace: "".to_string(),
264 location: "http://example.com/dedup.xsd".to_string(),
265 base_uri: String::new(),
266 }];
267 let result = load_hints_into_builder(&mut builder, &hints, &[]);
268 assert_eq!(result.loaded_count, 0, "duplicate should not be loaded");
269 assert_eq!(result.skipped_count, 1, "duplicate should be skipped");
270 }
274
275 #[test]
276 fn test_add_source_normalizes_for_dedup() {
277 let xsd = r#"<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
280 <xs:element name="root" type="xs:string"/>
281 </xs:schema>"#;
282 let cwd = std::env::current_dir().unwrap();
283 let mut builder = SchemaSetBuilder::new()
284 .add_source(xsd, "schemas/test.xsd")
285 .unwrap();
286
287 let instance_base = cwd
288 .join("schemas")
289 .join("instance.xml")
290 .to_string_lossy()
291 .into_owned();
292 let hints = vec![SchemaLocationHint {
293 namespace: "".to_string(),
294 location: "test.xsd".to_string(),
295 base_uri: instance_base,
296 }];
297 let result = load_hints_into_builder(&mut builder, &hints, &[]);
298 assert_eq!(
299 result.loaded_count, 0,
300 "hint resolving to already-loaded URI should not reload"
301 );
302 assert_eq!(result.skipped_count, 1);
303 }
304
305 #[test]
306 fn test_enrich_schema_set_no_hints_is_no_op() {
307 let xsd = r#"<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
308 <xs:element name="root" type="xs:string"/>
309 </xs:schema>"#;
310 let compiled = SchemaSetBuilder::new()
311 .add_source(xsd, "test.xsd")
312 .unwrap()
313 .compile()
314 .unwrap();
315
316 let outcome = enrich_schema_set(compiled.schema_set(), &[], &[]);
317 assert!(
318 outcome.is_no_op(),
319 "should be a no-op when no hints are provided"
320 );
321 assert!(outcome.schema_set.is_none());
322 assert!(outcome.compile_error.is_none());
323 assert!(outcome.hint_errors.is_empty());
324 }
325
326 #[test]
327 fn test_enrich_schema_set_preserves_original_elements() {
328 let dir = std::env::temp_dir().join("xsd_hint_test_enrich");
330 let _ = std::fs::create_dir_all(&dir);
331 let schema_path = dir.join("base.xsd");
332 std::fs::write(
333 &schema_path,
334 r#"<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
335 <xs:element name="root" type="xs:string"/>
336 </xs:schema>"#,
337 )
338 .unwrap();
339
340 let compiled = SchemaSetBuilder::new()
341 .add("", &schema_path.to_string_lossy())
342 .unwrap()
343 .compile()
344 .unwrap();
345 let original = compiled.schema_set();
346
347 let hints = vec![SchemaLocationHint {
351 namespace: "urn:test".to_string(),
352 location: "nonexistent_42.xsd".to_string(),
353 base_uri: String::new(),
354 }];
355
356 let outcome = enrich_schema_set(original, &hints, &[]);
357 assert!(
358 outcome.schema_set.is_some(),
359 "should return Some even if hint fails"
360 );
361 assert!(
362 !outcome.hint_errors.is_empty(),
363 "hint load failure must be surfaced in hint_errors"
364 );
365 assert!(
366 outcome.compile_error.is_none(),
367 "recompile of the original schemas should still succeed"
368 );
369
370 let enriched = outcome.schema_set.unwrap();
371 let name = enriched.name_table.add("root");
372 assert!(
373 enriched.lookup_element(None, name).is_some(),
374 "original element 'root' should still be present after enrichment"
375 );
376
377 let _ = std::fs::remove_dir_all(&dir);
378 }
379
380 #[test]
381 fn test_enrich_schema_set_preserves_xsd_version() {
382 let dir = std::env::temp_dir().join("xsd_hint_test_version");
383 let _ = std::fs::create_dir_all(&dir);
384 let schema_path = dir.join("test.xsd");
385 std::fs::write(
386 &schema_path,
387 r#"<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
388 <xs:element name="root" type="xs:string"/>
389 </xs:schema>"#,
390 )
391 .unwrap();
392
393 let compiled = SchemaSetBuilder::xsd11()
394 .add("", &schema_path.to_string_lossy())
395 .unwrap()
396 .compile()
397 .unwrap();
398 let original = compiled.schema_set();
399 assert_eq!(original.xsd_version, crate::schema::model::XsdVersion::V1_1);
400
401 let hints = vec![SchemaLocationHint {
402 namespace: "urn:test".to_string(),
403 location: "nonexistent_42.xsd".to_string(),
404 base_uri: String::new(),
405 }];
406 let enriched = enrich_schema_set(original, &hints, &[])
407 .schema_set
408 .unwrap();
409 assert_eq!(
410 enriched.xsd_version,
411 crate::schema::model::XsdVersion::V1_1,
412 "enriched set should preserve XSD 1.1 version"
413 );
414
415 let _ = std::fs::remove_dir_all(&dir);
416 }
417
418 #[test]
419 fn test_enrich_schema_set_surfaces_compile_error() {
420 let dir = std::env::temp_dir().join("xsd_hint_test_compile_err");
424 let _ = std::fs::create_dir_all(&dir);
425 let primary = dir.join("primary.xsd");
426 std::fs::write(
427 &primary,
428 r#"<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"
429 targetNamespace="urn:test">
430 <xs:element name="root" type="xs:string"/>
431 </xs:schema>"#,
432 )
433 .unwrap();
434 let conflict = dir.join("conflict.xsd");
435 std::fs::write(
436 &conflict,
437 r#"<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"
438 targetNamespace="urn:test">
439 <xs:element name="root" type="xs:int"/>
440 </xs:schema>"#,
441 )
442 .unwrap();
443
444 let compiled = SchemaSetBuilder::new()
445 .add("urn:test", &primary.to_string_lossy())
446 .unwrap()
447 .compile()
448 .unwrap();
449 let original = compiled.schema_set();
450
451 let hints = vec![SchemaLocationHint {
452 namespace: "urn:test".to_string(),
453 location: conflict.to_string_lossy().into_owned(),
454 base_uri: String::new(),
455 }];
456 let outcome = enrich_schema_set(original, &hints, &[]);
457
458 assert!(
462 outcome.schema_set.is_none() || outcome.compile_error.is_none(),
463 "outcome must be internally consistent: {outcome:?}"
464 );
465 if outcome.schema_set.is_none() {
466 assert!(
467 outcome.compile_error.is_some() || !outcome.hint_errors.is_empty(),
468 "if no enriched set is produced, the failure reason must be \
469 surfaced via compile_error or hint_errors, got: {outcome:?}"
470 );
471 }
472
473 let _ = std::fs::remove_dir_all(&dir);
474 }
475
476 #[test]
477 fn test_add_from_seeds_builder_with_loaded_locations() {
478 let dir = std::env::temp_dir().join("xsd_hint_test_add_from");
479 let _ = std::fs::create_dir_all(&dir);
480 let schema_path = dir.join("original.xsd");
481 std::fs::write(
482 &schema_path,
483 r#"<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
484 <xs:element name="root" type="xs:string"/>
485 </xs:schema>"#,
486 )
487 .unwrap();
488
489 let compiled = SchemaSetBuilder::new()
490 .add("", &schema_path.to_string_lossy())
491 .unwrap()
492 .compile()
493 .unwrap();
494
495 let mut builder = SchemaSetBuilder::new();
496 builder.add_from(compiled.schema_set());
497
498 assert!(builder.schema_count() > 0, "add_from should load schemas");
500
501 let _ = std::fs::remove_dir_all(&dir);
502 }
503}