···60 return Err("Reference cannot be empty".to_string());
61 }
6263- if ref_str.starts_with('#') {
64 // Local reference
65- let def_name = &ref_str[1..];
66 if def_name.is_empty() {
67 return Err("Local reference must have a definition name after #".to_string());
68 }
···105 // First validate syntax
106 Self::validate_ref_syntax(ref_str)?;
107108- if ref_str.starts_with('#') {
109 // Local reference
110- let def_name = ref_str[1..].to_string();
111 Ok(ParsedReference::Local(def_name))
112 } else if ref_str.contains('#') {
113 // Global reference with fragment
···358 ))
359 })?;
360361- // TODO: Validate that the reference can be resolved
362- // This would require access to the reference resolution system
000000000000000363364- Ok(())
0000000000365 }
366367 /// Validates runtime data against a reference schema definition
···456 .with_lexicons(vec![json!({
457 "lexicon": 1,
458 "id": "com.example.test",
459- "defs": { "main": ref_def.clone() }
000000460 })])
461 .unwrap()
462 .build()
463- .unwrap();
0464465 let validator = RefValidator;
466 assert!(validator.validate(&ref_def, &ctx).is_ok());
···474 });
475476 let ctx = ValidationContext::builder()
477- .with_lexicons(vec![json!({
478- "lexicon": 1,
479- "id": "com.example.test",
480- "defs": { "main": ref_def.clone() }
481- })])
0000000000000482 .unwrap()
483 .build()
484- .unwrap();
0485486 let validator = RefValidator;
487 assert!(validator.validate(&ref_def, &ctx).is_ok());
···495 });
496497 let ctx = ValidationContext::builder()
000000000000000000000000000000000000498 .with_lexicons(vec![json!({
499 "lexicon": 1,
500 "id": "com.example.test",
···502 })])
503 .unwrap()
504 .build()
505- .unwrap();
0506507 let validator = RefValidator;
508- assert!(validator.validate(&ref_def, &ctx).is_ok());
509 }
510511 #[test]
512- fn test_missing_ref_field() {
513 let ref_def = json!({
514- "type": "ref"
0515 });
516517 let ctx = ValidationContext::builder()
···522 })])
523 .unwrap()
524 .build()
525- .unwrap();
0526527 let validator = RefValidator;
528- assert!(validator.validate(&ref_def, &ctx).is_err());
000000000529 }
530531 #[test]
···60 return Err("Reference cannot be empty".to_string());
61 }
6263+ if let Some(def_name) = ref_str.strip_prefix('#') {
64 // Local reference
065 if def_name.is_empty() {
66 return Err("Local reference must have a definition name after #".to_string());
67 }
···104 // First validate syntax
105 Self::validate_ref_syntax(ref_str)?;
106107+ if let Some(def_name) = ref_str.strip_prefix('#') {
108 // Local reference
109+ let def_name = def_name.to_string();
110 Ok(ParsedReference::Local(def_name))
111 } else if ref_str.contains('#') {
112 // Global reference with fragment
···357 ))
358 })?;
359360+ // Parse and validate that the reference can be resolved
361+ let parsed_ref = Self::parse_reference(ref_str).map_err(|e| {
362+ ValidationError::InvalidSchema(format!(
363+ "Ref '{}' has invalid reference: {}",
364+ def_name, e
365+ ))
366+ })?;
367+368+ // Attempt to resolve the reference to verify it exists
369+ // Ensure we have a current lexicon context for resolving local references
370+ ctx.current_lexicon_id()
371+ .ok_or_else(|| {
372+ ValidationError::InvalidSchema(format!(
373+ "Cannot validate ref '{}': no current lexicon context",
374+ def_name
375+ ))
376+ })?;
377378+ match Self::resolve_reference(&parsed_ref, ctx) {
379+ Ok(_) => Ok(()), // Reference resolved successfully
380+ Err(ValidationError::DataValidation(msg)) if msg.contains("not found") => {
381+ // Convert data validation error to schema validation error for definition validation
382+ Err(ValidationError::InvalidSchema(format!(
383+ "Ref '{}' references non-existent definition: {}",
384+ def_name, ref_str
385+ )))
386+ }
387+ Err(e) => Err(e), // Pass through other errors
388+ }
389 }
390391 /// Validates runtime data against a reference schema definition
···480 .with_lexicons(vec![json!({
481 "lexicon": 1,
482 "id": "com.example.test",
483+ "defs": {
484+ "main": ref_def.clone(),
485+ "commonObject": {
486+ "type": "object",
487+ "properties": {}
488+ }
489+ }
490 })])
491 .unwrap()
492 .build()
493+ .unwrap()
494+ .with_current_lexicon("com.example.test");
495496 let validator = RefValidator;
497 assert!(validator.validate(&ref_def, &ctx).is_ok());
···505 });
506507 let ctx = ValidationContext::builder()
508+ .with_lexicons(vec![
509+ json!({
510+ "lexicon": 1,
511+ "id": "com.example.test",
512+ "defs": { "main": ref_def.clone() }
513+ }),
514+ json!({
515+ "lexicon": 1,
516+ "id": "com.example.defs",
517+ "defs": {
518+ "main": { "type": "object" },
519+ "commonObject": {
520+ "type": "object",
521+ "properties": {}
522+ }
523+ }
524+ })
525+ ])
526 .unwrap()
527 .build()
528+ .unwrap()
529+ .with_current_lexicon("com.example.test");
530531 let validator = RefValidator;
532 assert!(validator.validate(&ref_def, &ctx).is_ok());
···540 });
541542 let ctx = ValidationContext::builder()
543+ .with_lexicons(vec![
544+ json!({
545+ "lexicon": 1,
546+ "id": "com.example.test",
547+ "defs": { "main": ref_def.clone() }
548+ }),
549+ json!({
550+ "lexicon": 1,
551+ "id": "com.example.record",
552+ "defs": {
553+ "main": {
554+ "type": "record",
555+ "record": {
556+ "type": "object",
557+ "properties": {}
558+ }
559+ }
560+ }
561+ })
562+ ])
563+ .unwrap()
564+ .build()
565+ .unwrap()
566+ .with_current_lexicon("com.example.test");
567+568+ let validator = RefValidator;
569+ assert!(validator.validate(&ref_def, &ctx).is_ok());
570+ }
571+572+ #[test]
573+ fn test_missing_ref_field() {
574+ let ref_def = json!({
575+ "type": "ref"
576+ });
577+578+ let ctx = ValidationContext::builder()
579 .with_lexicons(vec![json!({
580 "lexicon": 1,
581 "id": "com.example.test",
···583 })])
584 .unwrap()
585 .build()
586+ .unwrap()
587+ .with_current_lexicon("com.example.test");
588589 let validator = RefValidator;
590+ assert!(validator.validate(&ref_def, &ctx).is_err());
591 }
592593 #[test]
594+ fn test_ref_to_nonexistent_definition() {
595 let ref_def = json!({
596+ "type": "ref",
597+ "ref": "#nonExistentDef"
598 });
599600 let ctx = ValidationContext::builder()
···605 })])
606 .unwrap()
607 .build()
608+ .unwrap()
609+ .with_current_lexicon("com.example.test");
610611 let validator = RefValidator;
612+ let result = validator.validate(&ref_def, &ctx);
613+ assert!(result.is_err());
614+615+ // Check that the error message mentions the non-existent definition
616+ if let Err(ValidationError::InvalidSchema(msg)) = result {
617+ assert!(msg.contains("non-existent definition"));
618+ assert!(msg.contains("#nonExistentDef"));
619+ } else {
620+ panic!("Expected InvalidSchema error for non-existent reference");
621+ }
622 }
623624 #[test]
+21-8
packages/lexicon-rs/src/validation/field/union.rs
···46 }
4748 // Handle local reference patterns (#ref)
49- if reference.starts_with('#') {
50- let ref_name = &reference[1..];
51 // Match bare name against local ref
52 if type_name == ref_name {
53 return true;
···59 }
6061 // Handle implicit #main patterns
62- if type_name.ends_with("#main") {
63- let base_type = &type_name[..type_name.len() - 5]; // Remove "#main"
64 if reference == base_type {
65 return true;
66 }
···211 }
212 }
213214- // TODO: Validate that each reference can be resolved
215- // This would require access to the reference resolution system
00000000000000216217 Ok(())
218 }
···922923 // String that should violate shortString maxLength constraint
924 // Currently passes because ObjectValidator doesn't validate property constraints yet
925- let invalid_short = json!({
926 "$type": "shortString",
927 "text": "This string is way too long for the short constraint"
928 });
929 // assert!(validator.validate_data(&invalid_short, &union_schema, &ctx).is_err()); // TODO: Enable when ObjectValidator supports constraints
930931 // String that should violate longString minLength constraint
932- let invalid_long = json!({
933 "$type": "longString",
934 "text": "short"
935 });
···46 }
4748 // Handle local reference patterns (#ref)
49+ if let Some(ref_name) = reference.strip_prefix('#') {
050 // Match bare name against local ref
51 if type_name == ref_name {
52 return true;
···58 }
5960 // Handle implicit #main patterns
61+ if let Some(base_type) = type_name.strip_suffix("#main") {
62+ // Remove "#main"
63 if reference == base_type {
64 return true;
65 }
···210 }
211 }
212213+ // Validate that each reference can be resolved
214+ use crate::validation::field::reference::RefValidator;
215+ let ref_validator = RefValidator;
216+217+ for (i, ref_item) in refs_array.iter().enumerate() {
218+ if let Some(ref_str) = ref_item.as_str() {
219+ // Create a temporary ref schema for validation
220+ let temp_ref_schema = serde_json::json!({
221+ "type": "ref",
222+ "ref": ref_str
223+ });
224+225+ let ref_ctx = ctx.with_path(&format!("{}.refs[{}]", def_name, i));
226+ ref_validator.validate(&temp_ref_schema, &ref_ctx)?;
227+ }
228+ }
229230 Ok(())
231 }
···935936 // String that should violate shortString maxLength constraint
937 // Currently passes because ObjectValidator doesn't validate property constraints yet
938+ let _invalid_short = json!({
939 "$type": "shortString",
940 "text": "This string is way too long for the short constraint"
941 });
942 // assert!(validator.validate_data(&invalid_short, &union_schema, &ctx).is_err()); // TODO: Enable when ObjectValidator supports constraints
943944 // String that should violate longString minLength constraint
945+ let _invalid_long = json!({
946 "$type": "longString",
947 "text": "short"
948 });
···47 /// * `Ok(StringFormat)` if the format is valid
48 /// * `Err(ValidationError)` if the format is unknown
49 fn validate_format_constraint(def_name: &str, format_str: &str) -> Result<StringFormat, ValidationError> {
50- StringFormat::from_str(format_str).ok_or_else(|| {
51 ValidationError::InvalidSchema(format!(
52 "String '{}' has unknown format '{}'. Valid formats: datetime, uri, at-uri, did, handle, at-identifier, nsid, cid, language, tid, record-key",
53 def_name, format_str
···377 // Validate format constraints
378 if let Some(format_value) = schema.get("format") {
379 if let Some(format_str) = format_value.as_str() {
380- if let Some(format) = StringFormat::from_str(format_str) {
381 self.validate_string_format(data_str, format, ctx)?;
382 }
383 }
···47 /// * `Ok(StringFormat)` if the format is valid
48 /// * `Err(ValidationError)` if the format is unknown
49 fn validate_format_constraint(def_name: &str, format_str: &str) -> Result<StringFormat, ValidationError> {
50+ format_str.parse::<StringFormat>().map_err(|_| {
51 ValidationError::InvalidSchema(format!(
52 "String '{}' has unknown format '{}'. Valid formats: datetime, uri, at-uri, did, handle, at-identifier, nsid, cid, language, tid, record-key",
53 def_name, format_str
···377 // Validate format constraints
378 if let Some(format_value) = schema.get("format") {
379 if let Some(format_str) = format_value.as_str() {
380+ if let Ok(format) = format_str.parse::<StringFormat>() {
381 self.validate_string_format(data_str, format, ctx)?;
382 }
383 }
+3-7
packages/lexicon-rs/src/validation/resolution.rs
···35 ));
36 }
3738- let (lexicon_id, def_name) = if reference.starts_with('#') {
39 // Local reference within current lexicon
40- let def_name = &reference[1..];
41 if def_name.is_empty() {
42 return Err(ValidationError::InvalidSchema(
43 "Local reference cannot be just '#'".to_string()
···245 .map(|r| {
246 if r.starts_with('#') {
247 format!("{}{}", lexicon_id, r)
248- } else if !r.contains('#') {
249- r // Already a full reference to main
250 } else {
251 r // Already a full reference
252 }
···262 let mut rec_stack = HashSet::new();
263264 for def_id in dependency_graph.keys() {
265- if !visited.contains(def_id) {
266- if has_cycle_dfs(def_id, &dependency_graph, &mut visited, &mut rec_stack) {
267 return Err(ValidationError::InvalidSchema(format!(
268 "Circular dependency detected involving definition: {}", def_id
269 )));
270 }
271- }
272 }
273274 Ok(())
···35 ));
36 }
3738+ let (lexicon_id, def_name) = if let Some(def_name) = reference.strip_prefix('#') {
39 // Local reference within current lexicon
040 if def_name.is_empty() {
41 return Err(ValidationError::InvalidSchema(
42 "Local reference cannot be just '#'".to_string()
···244 .map(|r| {
245 if r.starts_with('#') {
246 format!("{}{}", lexicon_id, r)
00247 } else {
248 r // Already a full reference
249 }
···259 let mut rec_stack = HashSet::new();
260261 for def_id in dependency_graph.keys() {
262+ if !visited.contains(def_id)
263+ && has_cycle_dfs(def_id, &dependency_graph, &mut visited, &mut rec_stack) {
264 return Err(ValidationError::InvalidSchema(format!(
265 "Circular dependency detected involving definition: {}", def_id
266 )));
267 }
0268 }
269270 Ok(())