1pub mod errors;
100pub mod types;
101pub mod validation;
102
103use std::collections::HashMap;
104use serde_json::Value;
105
106use crate::validation::{
108 primitive::{
109 string::StringValidator, integer::IntegerValidator, boolean::BooleanValidator,
110 bytes::BytesValidator, blob::BlobValidator, cid_link::CidLinkValidator,
111 null::NullValidator,
112 },
113 field::{
114 object::ObjectValidator, array::ArrayValidator, union::UnionValidator,
115 reference::RefValidator,
116 },
117 meta::{
118 token::TokenValidator, unknown::UnknownValidator,
119 },
120 primary::{
121 record::RecordValidator, query::QueryValidator, procedure::ProcedureValidator,
122 subscription::SubscriptionValidator,
123 },
124};
125
126pub use errors::ValidationError;
127pub use types::{LexiconDoc, StringFormat};
128pub use validation::{ValidationContext, ValidationContextBuilder, Validator};
129
130pub fn validate(lexicons: Vec<Value>) -> Result<(), HashMap<String, Vec<String>>> {
198 use validation::{ValidationContext, primary, field, primitive, meta};
199
200 let mut all_errors = HashMap::new();
201
202 let ctx = match ValidationContext::builder().with_lexicons(lexicons).and_then(|b| b.build()) {
204 Ok(context) => context,
205 Err(_) => {
206 let mut errors = HashMap::new();
207 errors.insert("context".to_string(), vec!["Failed to build validation context".to_string()]);
208 return Err(errors);
209 }
210 };
211
212 for (lexicon_id, lexicon_doc) in &ctx.lexicons {
214 let mut lexicon_errors = Vec::new();
215
216 if let Some(defs) = lexicon_doc.defs.as_object() {
218 for (def_name, def_value) in defs {
219 let def_context = ctx.with_current_lexicon(lexicon_id)
220 .with_path(&format!("{}#{}", lexicon_id, def_name));
221
222 if let Some(type_str) = def_value.get("type").and_then(|t| t.as_str()) {
223 let validation_result = match type_str {
224 "record" => primary::record::RecordValidator.validate(def_value, &def_context),
226 "query" => primary::query::QueryValidator.validate(def_value, &def_context),
227 "procedure" => primary::procedure::ProcedureValidator.validate(def_value, &def_context),
228 "subscription" => primary::subscription::SubscriptionValidator.validate(def_value, &def_context),
229
230 "object" => field::object::ObjectValidator.validate(def_value, &def_context),
232 "array" => field::array::ArrayValidator.validate(def_value, &def_context),
233 "union" => field::union::UnionValidator.validate(def_value, &def_context),
234 "ref" => field::reference::RefValidator.validate(def_value, &def_context),
235 "string" => primitive::string::StringValidator.validate(def_value, &def_context),
240 "integer" => primitive::integer::IntegerValidator.validate(def_value, &def_context),
241 "boolean" => primitive::boolean::BooleanValidator.validate(def_value, &def_context),
242 "bytes" => primitive::bytes::BytesValidator.validate(def_value, &def_context),
243 "blob" => primitive::blob::BlobValidator.validate(def_value, &def_context),
244 "cid-link" => primitive::cid_link::CidLinkValidator.validate(def_value, &def_context),
245 "null" => primitive::null::NullValidator.validate(def_value, &def_context),
246
247 "token" => meta::token::TokenValidator.validate(def_value, &def_context),
249 "unknown" => meta::unknown::UnknownValidator.validate(def_value, &def_context),
250
251 _ => Err(ValidationError::InvalidSchema(format!("Unknown type: {}", type_str))),
252 };
253
254 if let Err(error) = validation_result {
255 lexicon_errors.push(error.to_string());
256 }
257 } else {
258 lexicon_errors.push(format!("Definition '{}' missing 'type' field", def_name));
259 }
260 }
261 }
262
263 if !lexicon_errors.is_empty() {
264 all_errors.insert(lexicon_id.clone(), lexicon_errors);
265 }
266 }
267
268 if all_errors.is_empty() {
269 Ok(())
270 } else {
271 Err(all_errors)
272 }
273}
274
275pub fn validate_record(
318 lexicons: Vec<Value>,
319 collection: &str,
320 record: Value
321) -> Result<(), ValidationError> {
322 let ctx = ValidationContext::builder()
324 .with_lexicons(lexicons)
325 .map_err(|_| ValidationError::InvalidSchema("Failed to build validation context".to_string()))?
326 .build()
327 .map_err(|_| ValidationError::InvalidSchema("Failed to build validation context".to_string()))?;
328
329 let lexicon = ctx.get_lexicon(collection)
331 .ok_or_else(|| ValidationError::LexiconNotFound(collection.to_string()))?;
332
333 let main_def = lexicon.defs.as_object()
335 .and_then(|defs| defs.get("main"))
336 .ok_or_else(|| ValidationError::InvalidSchema(format!(
337 "Lexicon '{}' missing main definition", collection
338 )))?;
339
340 let type_str = main_def.get("type")
342 .and_then(|t| t.as_str())
343 .ok_or_else(|| ValidationError::InvalidSchema(format!(
344 "Lexicon '{}' main definition missing type", collection
345 )))?;
346
347 if type_str != "record" {
348 return Err(ValidationError::InvalidSchema(format!(
349 "Lexicon '{}' main definition is not a record type", collection
350 )));
351 }
352
353 let record_schema = main_def.get("record")
355 .ok_or_else(|| ValidationError::InvalidSchema(format!(
356 "Record definition '{}' missing 'record' field", collection
357 )))?;
358
359 validate_data_against_schema(&record, record_schema, &ctx.with_current_lexicon(collection).with_path(collection))
361}
362
363fn validate_data_against_schema(data: &Value, schema: &Value, ctx: &ValidationContext) -> Result<(), ValidationError> {
365 use crate::validation::Validator;
366
367 if let Some(type_str) = schema.get("type").and_then(|t| t.as_str()) {
368 match type_str {
369 "string" => StringValidator.validate_data(data, schema, ctx),
371 "integer" => IntegerValidator.validate_data(data, schema, ctx),
372 "boolean" => BooleanValidator.validate_data(data, schema, ctx),
373 "bytes" => BytesValidator.validate_data(data, schema, ctx),
374 "blob" => BlobValidator.validate_data(data, schema, ctx),
375 "cid-link" => CidLinkValidator.validate_data(data, schema, ctx),
376 "null" => NullValidator.validate_data(data, schema, ctx),
377
378 "object" => ObjectValidator.validate_data(data, schema, ctx),
380 "array" => ArrayValidator.validate_data(data, schema, ctx),
381 "union" => UnionValidator.validate_data(data, schema, ctx),
382 "ref" => RefValidator.validate_data(data, schema, ctx),
383
384 "token" => TokenValidator.validate_data(data, schema, ctx),
386 "unknown" => UnknownValidator.validate_data(data, schema, ctx),
387
388 "record" => RecordValidator.validate_data(data, schema, ctx),
390 "query" => QueryValidator.validate_data(data, schema, ctx),
391 "procedure" => ProcedureValidator.validate_data(data, schema, ctx),
392 "subscription" => SubscriptionValidator.validate_data(data, schema, ctx),
393
394 _ => Err(ValidationError::InvalidSchema(format!(
396 "Unknown schema type '{}' at '{}'", type_str, ctx.path()
397 ))),
398 }
399 } else {
400 Err(ValidationError::InvalidSchema(format!(
401 "Schema missing type field at '{}'", ctx.path()
402 )))
403 }
404}
405
406pub fn is_valid_nsid(nsid: &str) -> bool {
431 use regex::Regex;
432 let nsid_regex = Regex::new(
434 r"^[a-zA-Z]([a-zA-Z0-9-]*[a-zA-Z0-9])?(\.[a-zA-Z]([a-zA-Z0-9-]*[a-zA-Z0-9])?){2,}$",
435 )
436 .unwrap();
437 nsid_regex.is_match(nsid)
438}
439
440#[cfg(feature = "wasm")]
442mod wasm_bindings {
443 use super::*;
444 use wasm_bindgen::prelude::*;
445 use crate::validation::ValidationContext;
446
447 #[cfg(feature = "console_error_panic_hook")]
454 #[wasm_bindgen(start)]
455 pub fn main() {
456 console_error_panic_hook::set_once();
457 }
458
459 #[wasm_bindgen]
461 pub struct WasmLexiconValidator {
462 context: ValidationContext,
463 }
464
465 #[wasm_bindgen]
466 impl WasmLexiconValidator {
467 #[wasm_bindgen(constructor)]
470 pub fn new(lexicons_json: &str) -> Result<WasmLexiconValidator, JsValue> {
471 let lexicons: Vec<Value> = serde_json::from_str(lexicons_json)
472 .map_err(|e| JsValue::from_str(&format!("Failed to parse lexicons JSON: {}", e)))?;
473
474 let context = ValidationContext::builder()
475 .with_lexicons(lexicons)
476 .map_err(|e| JsValue::from_str(&format!("Failed to build validation context: {}", e)))?
477 .build()
478 .map_err(|e| JsValue::from_str(&format!("Failed to build validation context: {}", e)))?;
479
480 Ok(WasmLexiconValidator { context })
481 }
482
483 #[wasm_bindgen]
486 pub fn validate_record(&self, collection: &str, record_json: &str) -> Result<(), JsValue> {
487 let record: Value = serde_json::from_str(record_json)
488 .map_err(|e| JsValue::from_str(&format!("Failed to parse record JSON: {}", e)))?;
489
490 validate_data_against_schema(&record, &self.get_record_schema(collection)?, &self.context.with_path(collection))
491 .map_err(|e| JsValue::from_str(&e.to_string()))
492 }
493
494 #[wasm_bindgen]
497 pub fn validate_lexicons(&self) -> String {
498 let mut results = HashMap::new();
499
500 for (lexicon_id, lexicon_doc) in &self.context.lexicons {
501 let mut lexicon_errors = Vec::new();
502
503 if let Some(defs) = lexicon_doc.defs.as_object() {
504 for (def_name, def_value) in defs {
505 let def_context = self.context.with_path(&format!("{}#{}", lexicon_id, def_name));
506
507 if let Some(type_str) = def_value.get("type").and_then(|t| t.as_str()) {
508 let validation_result = validate_definition_by_type(def_value, type_str, &def_context);
509
510 if let Err(error) = validation_result {
511 lexicon_errors.push(format!("{}#{}: {}", lexicon_id, def_name, error));
512 }
513 } else {
514 lexicon_errors.push(format!("{}#{}: Missing 'type' field", lexicon_id, def_name));
515 }
516 }
517 }
518
519 if !lexicon_errors.is_empty() {
520 results.insert(lexicon_id.clone(), lexicon_errors);
521 }
522 }
523
524 serde_json::to_string(&results).unwrap_or_else(|_| "{}".to_string())
525 }
526
527 #[wasm_bindgen]
530 pub fn check_references(&self) -> String {
531 let mut unresolved_refs = Vec::new();
532
533 for (lexicon_id, lexicon_doc) in &self.context.lexicons {
534 if let Some(defs) = lexicon_doc.defs.as_object() {
535 for (def_name, def_value) in defs {
536 collect_unresolved_references(def_value, &format!("{}#{}", lexicon_id, def_name), &self.context, &mut unresolved_refs);
537 }
538 }
539 }
540
541 serde_json::to_string(&unresolved_refs).unwrap_or_else(|_| "[]".to_string())
542 }
543
544 #[wasm_bindgen]
547 pub fn get_error_context(&self, path: &str) -> String {
548 let ctx = self.context.with_path(path);
549 serde_json::json!({
550 "path": ctx.path(),
551 "current_lexicon": ctx.current_lexicon_id,
552 "has_circular_reference": !ctx.reference_stack.is_empty(),
553 "reference_stack": ctx.reference_stack
554 }).to_string()
555 }
556
557 fn get_record_schema(&self, collection: &str) -> Result<Value, JsValue> {
559 let lexicon = self.context.get_lexicon(collection)
560 .ok_or_else(|| JsValue::from_str(&format!("Lexicon not found: {}", collection)))?;
561
562 let main_def = lexicon.defs.as_object()
563 .and_then(|defs| defs.get("main"))
564 .ok_or_else(|| JsValue::from_str(&format!("Missing main definition in lexicon: {}", collection)))?;
565
566 let record_schema = main_def.get("record")
567 .ok_or_else(|| JsValue::from_str(&format!("Missing record schema in lexicon: {}", collection)))?;
568
569 Ok(record_schema.clone())
570 }
571 }
572
573 fn validate_definition_by_type(def_value: &Value, type_str: &str, ctx: &ValidationContext) -> Result<(), ValidationError> {
575 use crate::validation::Validator;
576
577 match type_str {
578 "record" => RecordValidator.validate(def_value, ctx),
580 "query" => QueryValidator.validate(def_value, ctx),
581 "procedure" => ProcedureValidator.validate(def_value, ctx),
582 "subscription" => SubscriptionValidator.validate(def_value, ctx),
583
584 "object" => ObjectValidator.validate(def_value, ctx),
586 "array" => ArrayValidator.validate(def_value, ctx),
587 "union" => UnionValidator.validate(def_value, ctx),
588 "ref" => RefValidator.validate(def_value, ctx),
589
590 "string" => StringValidator.validate(def_value, ctx),
592 "integer" => IntegerValidator.validate(def_value, ctx),
593 "boolean" => BooleanValidator.validate(def_value, ctx),
594 "bytes" => BytesValidator.validate(def_value, ctx),
595 "blob" => BlobValidator.validate(def_value, ctx),
596 "cid-link" => CidLinkValidator.validate(def_value, ctx),
597 "null" => NullValidator.validate(def_value, ctx),
598
599 "token" => TokenValidator.validate(def_value, ctx),
601 "unknown" => UnknownValidator.validate(def_value, ctx),
602
603 _ => Err(ValidationError::InvalidSchema(format!("Unknown type: {}", type_str))),
604 }
605 }
606
607 fn collect_unresolved_references(value: &Value, path: &str, ctx: &ValidationContext, unresolved: &mut Vec<String>) {
609 match value {
610 Value::Object(obj) => {
611 if let Some(ref_str) = obj.get("$ref").and_then(|r| r.as_str()) {
612 if ctx.resolve_reference(ref_str).is_err() {
613 unresolved.push(format!("{}: {}", path, ref_str));
614 }
615 }
616
617 for (key, val) in obj {
618 collect_unresolved_references(val, &format!("{}.{}", path, key), ctx, unresolved);
619 }
620 },
621 Value::Array(arr) => {
622 for (i, val) in arr.iter().enumerate() {
623 collect_unresolved_references(val, &format!("{}[{}]", path, i), ctx, unresolved);
624 }
625 },
626 _ => {}
627 }
628 }
629
630 #[wasm_bindgen]
633 pub fn validate_lexicons_and_get_errors(lexicons_json: &str) -> String {
634 match validate_lexicons_from_json(lexicons_json) {
635 Ok(_) => "{}".to_string(), Err(errors) => serde_json::to_string(&errors).unwrap_or_else(|_| "{}".to_string())
637 }
638 }
639
640 fn validate_lexicons_from_json(lexicons_json: &str) -> Result<(), HashMap<String, Vec<String>>> {
642 let lexicons: Vec<Value> = serde_json::from_str(lexicons_json)
643 .map_err(|e| {
644 let mut error_map = HashMap::new();
645 error_map.insert("parse_error".to_string(), vec![format!("Failed to parse lexicons JSON: {}", e)]);
646 error_map
647 })?;
648
649 validate(lexicons)
650 }
651
652 #[wasm_bindgen]
654 pub fn validate_string_format(value: &str, format: &str) -> Result<(), JsValue> {
655 use crate::validation::primitive::string::StringValidator;
656 use crate::StringFormat;
657
658 let format_enum = format.parse::<StringFormat>()
659 .map_err(|_| JsValue::from_str(&format!("Unknown format: {}", format)))?;
660
661 let validator = StringValidator;
662
663 let is_valid = match format_enum {
665 StringFormat::DateTime => validator.is_valid_rfc3339_datetime(value),
666 StringFormat::Uri => validator.is_valid_uri(value),
667 StringFormat::AtUri => validator.is_valid_at_uri(value),
668 StringFormat::Did => validator.is_valid_did(value),
669 StringFormat::Handle => validator.is_valid_handle(value),
670 StringFormat::AtIdentifier => {
671 validator.is_valid_did(value) || validator.is_valid_handle(value)
672 },
673 StringFormat::Nsid => crate::is_valid_nsid(value),
674 StringFormat::Cid => validator.is_valid_cid(value),
675 StringFormat::Language => validator.is_valid_language_tag(value),
676 StringFormat::Tid => validator.is_valid_tid(value),
677 StringFormat::RecordKey => validator.is_valid_record_key(value),
678 };
679
680 if is_valid {
681 Ok(())
682 } else {
683 Err(JsValue::from_str(&format!("Invalid {} format: {}", format, value)))
684 }
685 }
686
687 #[wasm_bindgen]
689 pub fn is_valid_nsid(nsid: &str) -> bool {
690 crate::is_valid_nsid(nsid)
691 }
692}
693
694#[cfg(feature = "wasm")]
696pub use wasm_bindings::*;
697
698#[cfg(test)]
699mod test_multiple_errors_example {
700 use super::*;
701 use serde_json::json;
702
703 #[test]
704 fn test_multiple_error_collection_comprehensive() {
705 let lexicons = vec![
706 json!({
708 "lexicon": 1,
709 "id": "com.example.post",
710 "defs": {
711 "main": {
712 "type": "record",
713 "key": "tid",
714 },
716 "metadata": {
717 "type": "badtype" },
719 "config": {
720 "description": "Some config"
722 }
723 }
724 }),
725
726 json!({
728 "lexicon": 1,
729 "id": "com.example.profile",
730 "defs": {
731 "main": {
732 "type": "record",
733 "key": "tid",
734 "record": {
735 "type": "object",
736 "properties": {
737 "avatar": {
738 "type": "ref",
739 "$ref": "com.missing.lexicon#image" }
741 }
742 }
743 },
744 "settings": {
745 "type": "object",
746 "properties": {
747 "theme": {
748 "type": "invalidtype" }
750 }
751 }
752 }
753 }),
754
755 json!({
757 "lexicon": 1,
758 "id": "com.example.valid",
759 "defs": {
760 "main": {
761 "type": "record",
762 "key": "tid",
763 "record": {
764 "type": "object",
765 "properties": {
766 "text": {"type": "string"}
767 }
768 }
769 }
770 }
771 })
772 ];
773
774 match validate(lexicons) {
775 Ok(_) => panic!("Expected validation errors but validation passed"),
776 Err(errors) => {
777 assert!(errors.contains_key("com.example.post"), "Should have errors for com.example.post");
779 assert!(errors.contains_key("com.example.profile"), "Should have errors for com.example.profile");
780 assert!(!errors.contains_key("com.example.valid"), "Should NOT have errors for valid lexicon");
781
782 assert_eq!(errors["com.example.post"].len(), 3, "com.example.post should have exactly 3 errors");
784 assert_eq!(errors["com.example.profile"].len(), 2, "com.example.profile should have exactly 2 errors");
785
786 let post_errors = &errors["com.example.post"];
788 assert!(post_errors.iter().any(|e| e.contains("missing 'type' field")),
789 "Should contain missing type field error");
790 assert!(post_errors.iter().any(|e| e.contains("missing required 'record' field")),
791 "Should contain missing record field error");
792 assert!(post_errors.iter().any(|e| e.contains("Unknown type: badtype")),
793 "Should contain unknown type error");
794
795 let profile_errors = &errors["com.example.profile"];
796 assert!(profile_errors.iter().any(|e| e.contains("Unknown schema type 'ref'")),
797 "Should contain unknown ref type error");
798 assert!(profile_errors.iter().any(|e| e.contains("invalidtype")),
799 "Should contain invalidtype error");
800
801 let total_errors: usize = errors.values().map(|v| v.len()).sum();
803 assert_eq!(total_errors, 5, "Should have collected all 5 errors total");
804 assert_eq!(errors.len(), 2, "Should have errors for exactly 2 lexicons");
805 }
806 }
807 }
808
809 #[test]
810 fn test_broken_reference_validation() {
811 let broken_lexicon = json!({
812 "lexicon": 1,
813 "id": "test.broken.ref",
814 "defs": {
815 "main": {
816 "type": "object",
817 "properties": {
818 "items": {
819 "type": "array",
820 "items": {
821 "type": "ref",
822 "ref": "#nonExistentDef"
823 }
824 }
825 }
826 },
827 "existingDef": {
828 "type": "string"
829 }
830 }
831 });
832
833 let result = validate(vec![broken_lexicon]);
834
835 assert!(result.is_err(), "Validation should fail for broken reference");
837
838 if let Err(errors) = result {
839 assert!(errors.contains_key("test.broken.ref"), "Should have errors for the test lexicon");
840 let error_messages = errors.get("test.broken.ref").unwrap();
841 assert!(!error_messages.is_empty(), "Should have at least one error");
842
843 let has_ref_error = error_messages.iter().any(|msg|
845 msg.contains("non-existent") || msg.contains("nonExistentDef")
846 );
847 assert!(has_ref_error, "Should have error about non-existent reference. Got: {:?}", error_messages);
848 }
849 }
850}
851