domainstack_wasm/
lib.rs

1//! # domainstack-wasm
2//!
3//! WASM browser validation for domainstack. Run the same validation logic
4//! in both browser and server.
5//!
6//! ## Quick Start
7//!
8//! ```javascript
9//! import { createValidator } from '@domainstack/wasm';
10//!
11//! const validator = await createValidator();
12//! const result = validator.validate('Booking', {
13//!   guest_email: 'invalid',
14//!   rooms: 15
15//! });
16//!
17//! if (!result.ok) {
18//!   result.errors?.forEach(e => console.log(e.path, e.message));
19//! }
20//! ```
21//!
22//! ## Architecture
23//!
24//! This crate provides a thin WASM layer over domainstack validation.
25//! Types are registered at compile time, and validation is dispatched
26//! by type name at runtime.
27
28use serde::Serialize;
29use std::collections::HashMap;
30use wasm_bindgen::prelude::*;
31
32// Re-export for use in proc macro generated code
33pub use domainstack::{Validate, ValidationError};
34pub use serde::de::DeserializeOwned;
35
36/// Initialize panic hook for better error messages in browser console.
37/// Call this once at application startup.
38#[wasm_bindgen(start)]
39pub fn init() {
40    #[cfg(feature = "console_error_panic_hook")]
41    console_error_panic_hook::set_once();
42}
43
44// ============================================================================
45// WASM-Serializable Types
46// ============================================================================
47
48/// Result of a validation operation.
49///
50/// This is the ONLY type returned to JavaScript. No nulls, no exceptions.
51#[derive(Debug, Clone, Serialize)]
52pub struct ValidationResult {
53    /// Whether validation passed
54    pub ok: bool,
55
56    /// Validation violations (present when ok=false and validation failed)
57    #[serde(skip_serializing_if = "Option::is_none")]
58    pub errors: Option<Vec<WasmViolation>>,
59
60    /// System error (present when ok=false and a system error occurred)
61    #[serde(skip_serializing_if = "Option::is_none")]
62    pub error: Option<SystemError>,
63}
64
65impl ValidationResult {
66    /// Create a successful result
67    pub fn success() -> Self {
68        Self {
69            ok: true,
70            errors: None,
71            error: None,
72        }
73    }
74
75    /// Create a validation failure result
76    pub fn validation_failed(violations: Vec<WasmViolation>) -> Self {
77        Self {
78            ok: false,
79            errors: Some(violations),
80            error: None,
81        }
82    }
83
84    /// Create a system error result
85    pub fn system_error(code: &'static str, message: String) -> Self {
86        Self {
87            ok: false,
88            errors: None,
89            error: Some(SystemError { code, message }),
90        }
91    }
92}
93
94/// A validation violation, serializable for WASM.
95#[derive(Debug, Clone, Serialize)]
96pub struct WasmViolation {
97    /// Field path (e.g., "guest_email", "rooms\[0\].adults")
98    pub path: String,
99
100    /// Error code (e.g., "invalid_email", "out_of_range")
101    pub code: String,
102
103    /// Human-readable message
104    pub message: String,
105
106    /// Additional metadata
107    #[serde(skip_serializing_if = "Option::is_none")]
108    pub meta: Option<HashMap<String, String>>,
109}
110
111impl From<&domainstack::Violation> for WasmViolation {
112    fn from(v: &domainstack::Violation) -> Self {
113        let meta = if v.meta.is_empty() {
114            None
115        } else {
116            Some(
117                v.meta
118                    .iter()
119                    .map(|(k, v)| (k.to_string(), v.to_string()))
120                    .collect(),
121            )
122        };
123
124        Self {
125            path: v.path.to_string(),
126            code: v.code.to_string(),
127            message: v.message.clone(),
128            meta,
129        }
130    }
131}
132
133/// System-level error (not a validation failure).
134#[derive(Debug, Clone, Serialize)]
135pub struct SystemError {
136    /// Error code: "unknown_type" | "parse_error" | "internal_error"
137    pub code: &'static str,
138
139    /// Human-readable error message
140    pub message: String,
141}
142
143// ============================================================================
144// Dispatch Error
145// ============================================================================
146
147/// Internal error type for dispatch operations
148pub enum DispatchError {
149    /// Type name not found in registry
150    UnknownType,
151
152    /// JSON parsing failed
153    ParseError(String),
154
155    /// Validation failed (boxed to reduce enum size)
156    Validation(Box<ValidationError>),
157}
158
159// ============================================================================
160// Type Registry
161// ============================================================================
162
163/// Function signature for type validators
164pub type ValidateFn = fn(&str) -> Result<(), DispatchError>;
165
166/// Registry of type validators.
167///
168/// In production, this is populated by the `#[wasm_validate]` proc macro.
169/// For now, users must manually register types.
170pub struct TypeRegistry {
171    validators: HashMap<&'static str, ValidateFn>,
172}
173
174impl TypeRegistry {
175    /// Create an empty registry
176    pub fn new() -> Self {
177        Self {
178            validators: HashMap::new(),
179        }
180    }
181
182    /// Register a type validator
183    pub fn register<T>(&mut self, type_name: &'static str)
184    where
185        T: Validate + DeserializeOwned + 'static,
186    {
187        self.validators.insert(type_name, validate_json::<T>);
188    }
189
190    /// Validate JSON against a registered type
191    pub fn validate(&self, type_name: &str, json: &str) -> Result<(), DispatchError> {
192        match self.validators.get(type_name) {
193            Some(validate_fn) => validate_fn(json),
194            None => Err(DispatchError::UnknownType),
195        }
196    }
197
198    /// Check if a type is registered
199    pub fn has_type(&self, type_name: &str) -> bool {
200        self.validators.contains_key(type_name)
201    }
202
203    /// List all registered type names
204    pub fn type_names(&self) -> Vec<&'static str> {
205        self.validators.keys().copied().collect()
206    }
207}
208
209impl Default for TypeRegistry {
210    fn default() -> Self {
211        Self::new()
212    }
213}
214
215/// Validate JSON for a specific type
216fn validate_json<T>(json: &str) -> Result<(), DispatchError>
217where
218    T: Validate + DeserializeOwned,
219{
220    let value: T =
221        serde_json::from_str(json).map_err(|e| DispatchError::ParseError(e.to_string()))?;
222    value
223        .validate()
224        .map_err(|e| DispatchError::Validation(Box::new(e)))
225}
226
227// ============================================================================
228// Global Registry
229// ============================================================================
230
231use std::cell::RefCell;
232
233thread_local! {
234    static REGISTRY: RefCell<TypeRegistry> = RefCell::new(TypeRegistry::new());
235}
236
237/// Register a type for WASM validation.
238///
239/// # Example
240///
241/// ```ignore
242/// use domainstack::prelude::*;
243/// use domainstack_wasm::register_type;
244///
245/// #[derive(Validate, Deserialize)]
246/// struct Booking {
247///     #[validate(email)]
248///     guest_email: String,
249/// }
250///
251/// // Call once at initialization
252/// register_type::<Booking>("Booking");
253/// ```
254pub fn register_type<T>(type_name: &'static str)
255where
256    T: Validate + DeserializeOwned + 'static,
257{
258    REGISTRY.with(|r| r.borrow_mut().register::<T>(type_name));
259}
260
261/// Check if a type is registered
262pub fn is_type_registered(type_name: &str) -> bool {
263    REGISTRY.with(|r| r.borrow().has_type(type_name))
264}
265
266/// Get list of all registered type names
267pub fn registered_types() -> Vec<&'static str> {
268    REGISTRY.with(|r| r.borrow().type_names())
269}
270
271// ============================================================================
272// WASM Entry Points
273// ============================================================================
274
275/// Validate JSON data against a registered type.
276///
277/// # Arguments
278///
279/// * `type_name` - The name of the type to validate against (e.g., "Booking")
280/// * `json` - JSON string to validate
281///
282/// # Returns
283///
284/// A `ValidationResult` object (always, never throws):
285/// - `{ ok: true }` - Validation passed
286/// - `{ ok: false, errors: [...] }` - Validation failed
287/// - `{ ok: false, error: { code, message } }` - System error
288#[wasm_bindgen]
289pub fn validate(type_name: &str, json: &str) -> JsValue {
290    let result = REGISTRY.with(|r| r.borrow().validate(type_name, json));
291
292    let validation_result = match result {
293        Ok(()) => ValidationResult::success(),
294        Err(DispatchError::UnknownType) => {
295            ValidationResult::system_error("unknown_type", format!("Unknown type: {}", type_name))
296        }
297        Err(DispatchError::ParseError(msg)) => ValidationResult::system_error("parse_error", msg),
298        Err(DispatchError::Validation(err)) => {
299            let violations = err.violations.iter().map(WasmViolation::from).collect();
300            ValidationResult::validation_failed(violations)
301        }
302    };
303
304    // Serialize to JsValue, with fallback for serialization errors
305    serde_wasm_bindgen::to_value(&validation_result).unwrap_or_else(|_| {
306        let fallback = ValidationResult::system_error(
307            "internal_error",
308            "Failed to serialize validation result".to_string(),
309        );
310        serde_wasm_bindgen::to_value(&fallback).unwrap()
311    })
312}
313
314/// Validate a JavaScript object against a registered type.
315///
316/// This is a convenience wrapper that accepts a JS object directly
317/// instead of a JSON string.
318///
319/// # Arguments
320///
321/// * `type_name` - The name of the type to validate against
322/// * `value` - JavaScript object to validate
323#[wasm_bindgen]
324pub fn validate_object(type_name: &str, value: JsValue) -> JsValue {
325    // Serialize JS object to JSON string
326    let json = match js_sys::JSON::stringify(&value) {
327        Ok(s) => s.as_string().unwrap_or_default(),
328        Err(_) => {
329            let result = ValidationResult::system_error(
330                "parse_error",
331                "Failed to serialize JavaScript object to JSON".to_string(),
332            );
333            return serde_wasm_bindgen::to_value(&result).unwrap();
334        }
335    };
336
337    validate(type_name, &json)
338}
339
340/// Get list of registered type names.
341///
342/// Useful for debugging and introspection.
343#[wasm_bindgen]
344pub fn get_registered_types() -> JsValue {
345    let types = registered_types();
346    serde_wasm_bindgen::to_value(&types).unwrap_or(JsValue::NULL)
347}
348
349/// Check if a type is registered.
350#[wasm_bindgen]
351pub fn has_type(type_name: &str) -> bool {
352    is_type_registered(type_name)
353}
354
355// ============================================================================
356// Builder Pattern for TypeScript
357// ============================================================================
358
359/// Validator instance for TypeScript ergonomics.
360///
361/// ```javascript
362/// const validator = await createValidator();
363/// const result = validator.validate('Booking', { ... });
364/// ```
365#[wasm_bindgen]
366pub struct Validator {
367    // Marker field - registry is global via thread_local
368    _private: (),
369}
370
371#[wasm_bindgen]
372impl Validator {
373    /// Validate JSON string against a type
374    pub fn validate(&self, type_name: &str, json: &str) -> JsValue {
375        validate(type_name, json)
376    }
377
378    /// Validate JS object against a type
379    #[wasm_bindgen(js_name = validateObject)]
380    pub fn validate_object(&self, type_name: &str, value: JsValue) -> JsValue {
381        validate_object(type_name, value)
382    }
383
384    /// Get registered type names
385    #[wasm_bindgen(js_name = getTypes)]
386    pub fn get_types(&self) -> JsValue {
387        get_registered_types()
388    }
389
390    /// Check if type is registered
391    #[wasm_bindgen(js_name = hasType)]
392    pub fn has_type(&self, type_name: &str) -> bool {
393        has_type(type_name)
394    }
395}
396
397/// Create a validator instance.
398///
399/// This is the main entry point for TypeScript/JavaScript.
400///
401/// ```javascript
402/// import { createValidator } from '@domainstack/wasm';
403///
404/// const validator = await createValidator();
405/// ```
406#[wasm_bindgen(js_name = createValidator)]
407pub fn create_validator() -> Validator {
408    Validator { _private: () }
409}
410
411// ============================================================================
412// Tests
413// ============================================================================
414
415#[cfg(test)]
416mod tests {
417    use super::*;
418
419    #[test]
420    fn test_validation_result_success() {
421        let result = ValidationResult::success();
422        assert!(result.ok);
423        assert!(result.errors.is_none());
424        assert!(result.error.is_none());
425    }
426
427    #[test]
428    fn test_validation_result_failure() {
429        let violations = vec![WasmViolation {
430            path: "email".to_string(),
431            code: "invalid_email".to_string(),
432            message: "Invalid email format".to_string(),
433            meta: None,
434        }];
435        let result = ValidationResult::validation_failed(violations);
436        assert!(!result.ok);
437        assert!(result.errors.is_some());
438        assert_eq!(result.errors.as_ref().unwrap().len(), 1);
439        assert!(result.error.is_none());
440    }
441
442    #[test]
443    fn test_validation_result_system_error() {
444        let result =
445            ValidationResult::system_error("unknown_type", "Unknown type: Foo".to_string());
446        assert!(!result.ok);
447        assert!(result.errors.is_none());
448        assert!(result.error.is_some());
449        assert_eq!(result.error.as_ref().unwrap().code, "unknown_type");
450    }
451
452    #[test]
453    fn test_registry_unknown_type() {
454        let registry = TypeRegistry::new();
455        let result = registry.validate("NonExistent", "{}");
456        assert!(matches!(result, Err(DispatchError::UnknownType)));
457    }
458
459    // Integration tests with actual Validate types
460    mod integration {
461        use super::*;
462        use domainstack::Validate;
463        use serde::Deserialize;
464
465        #[derive(Debug, Validate, Deserialize)]
466        struct TestBooking {
467            #[validate(length(min = 1, max = 100))]
468            guest_name: String,
469
470            #[validate(range(min = 1, max = 10))]
471            rooms: u8,
472        }
473
474        #[test]
475        fn test_register_and_validate_success() {
476            let mut registry = TypeRegistry::new();
477            registry.register::<TestBooking>("TestBooking");
478
479            let json = r#"{"guest_name": "John Doe", "rooms": 2}"#;
480            let result = registry.validate("TestBooking", json);
481            assert!(result.is_ok());
482        }
483
484        #[test]
485        fn test_register_and_validate_failure() {
486            let mut registry = TypeRegistry::new();
487            registry.register::<TestBooking>("TestBooking");
488
489            // rooms = 15 is out of range (max = 10)
490            let json = r#"{"guest_name": "John", "rooms": 15}"#;
491            let result = registry.validate("TestBooking", json);
492
493            assert!(matches!(result, Err(DispatchError::Validation(_))));
494            if let Err(DispatchError::Validation(err)) = result {
495                assert!(!err.violations.is_empty());
496                assert_eq!(err.violations[0].path.to_string(), "rooms");
497            }
498        }
499
500        #[test]
501        fn test_parse_error() {
502            let mut registry = TypeRegistry::new();
503            registry.register::<TestBooking>("TestBooking");
504
505            let json = r#"{"guest_name": "John", "rooms": "not a number"}"#;
506            let result = registry.validate("TestBooking", json);
507
508            assert!(matches!(result, Err(DispatchError::ParseError(_))));
509        }
510
511        #[test]
512        fn test_wasm_violation_from_violation() {
513            let violation = domainstack::Violation {
514                path: domainstack::Path::from("email"),
515                code: "invalid_email",
516                message: "Invalid email format".to_string(),
517                meta: domainstack::Meta::default(),
518            };
519
520            let wasm_violation = WasmViolation::from(&violation);
521            assert_eq!(wasm_violation.path, "email");
522            assert_eq!(wasm_violation.code, "invalid_email");
523            assert_eq!(wasm_violation.message, "Invalid email format");
524            assert!(wasm_violation.meta.is_none());
525        }
526
527        #[test]
528        fn test_wasm_violation_with_meta() {
529            let mut meta = domainstack::Meta::default();
530            meta.insert("min", "1");
531            meta.insert("max", "10");
532
533            let violation = domainstack::Violation {
534                path: domainstack::Path::from("age"),
535                code: "out_of_range",
536                message: "Must be between 1 and 10".to_string(),
537                meta,
538            };
539
540            let wasm_violation = WasmViolation::from(&violation);
541            assert!(wasm_violation.meta.is_some());
542            let meta = wasm_violation.meta.unwrap();
543            assert_eq!(meta.get("min"), Some(&"1".to_string()));
544            assert_eq!(meta.get("max"), Some(&"10".to_string()));
545        }
546
547        #[test]
548        fn test_multiple_violations() {
549            let mut registry = TypeRegistry::new();
550            registry.register::<TestBooking>("TestBooking");
551
552            // Both fields invalid
553            let json = r#"{"guest_name": "", "rooms": 15}"#;
554            let result = registry.validate("TestBooking", json);
555
556            assert!(matches!(result, Err(DispatchError::Validation(_))));
557            if let Err(DispatchError::Validation(err)) = result {
558                assert_eq!(err.violations.len(), 2);
559            }
560        }
561
562        #[test]
563        fn test_empty_json_object() {
564            let mut registry = TypeRegistry::new();
565            registry.register::<TestBooking>("TestBooking");
566
567            // Missing required fields - should fail parse
568            let json = r#"{}"#;
569            let result = registry.validate("TestBooking", json);
570            assert!(matches!(result, Err(DispatchError::ParseError(_))));
571        }
572
573        #[test]
574        fn test_invalid_json_syntax() {
575            let mut registry = TypeRegistry::new();
576            registry.register::<TestBooking>("TestBooking");
577
578            let json = r#"{ invalid json }"#;
579            let result = registry.validate("TestBooking", json);
580            assert!(matches!(result, Err(DispatchError::ParseError(_))));
581        }
582
583        #[test]
584        fn test_registry_has_type() {
585            let mut registry = TypeRegistry::new();
586            assert!(!registry.has_type("TestBooking"));
587
588            registry.register::<TestBooking>("TestBooking");
589            assert!(registry.has_type("TestBooking"));
590            assert!(!registry.has_type("NonExistent"));
591        }
592
593        #[test]
594        fn test_registry_type_names() {
595            let mut registry = TypeRegistry::new();
596            assert!(registry.type_names().is_empty());
597
598            registry.register::<TestBooking>("TestBooking");
599            let names = registry.type_names();
600            assert_eq!(names.len(), 1);
601            assert!(names.contains(&"TestBooking"));
602        }
603
604        // Test nested validation
605        #[derive(Debug, Validate, Deserialize)]
606        struct TestAddress {
607            #[validate(length(min = 1, max = 100))]
608            street: String,
609
610            #[validate(length(min = 1, max = 50))]
611            city: String,
612        }
613
614        #[derive(Debug, Validate, Deserialize)]
615        struct TestPerson {
616            #[validate(length(min = 1, max = 50))]
617            name: String,
618
619            #[validate(nested)]
620            address: TestAddress,
621        }
622
623        #[test]
624        fn test_nested_validation_with_path() {
625            let mut registry = TypeRegistry::new();
626            registry.register::<TestPerson>("TestPerson");
627
628            // Invalid city in nested address
629            let json = r#"{"name": "John", "address": {"street": "123 Main", "city": ""}}"#;
630            let result = registry.validate("TestPerson", json);
631
632            assert!(matches!(result, Err(DispatchError::Validation(_))));
633            if let Err(DispatchError::Validation(err)) = result {
634                assert!(!err.violations.is_empty());
635                // Path should include nested field
636                let path = err.violations[0].path.to_string();
637                assert!(path.contains("address") || path.contains("city"));
638            }
639        }
640    }
641
642    // Tests for global registry functions
643    mod global_registry {
644        use super::*;
645        use serde::Deserialize;
646
647        #[derive(Debug, Validate, Deserialize)]
648        struct GlobalTestType {
649            #[validate(length(min = 1))]
650            name: String,
651        }
652
653        #[test]
654        fn test_global_register_and_check() {
655            // Register type globally
656            register_type::<GlobalTestType>("GlobalTestType");
657
658            // Check it's registered
659            assert!(is_type_registered("GlobalTestType"));
660            assert!(!is_type_registered("NotRegistered"));
661
662            // Check it appears in list
663            let types = registered_types();
664            assert!(types.contains(&"GlobalTestType"));
665        }
666    }
667
668    // Tests for ValidationResult constructors
669    mod validation_result {
670        use super::*;
671
672        #[test]
673        fn test_success_serialization() {
674            let result = ValidationResult::success();
675            let json = serde_json::to_string(&result).unwrap();
676            assert!(json.contains("\"ok\":true"));
677            assert!(!json.contains("errors"));
678            assert!(!json.contains("error"));
679        }
680
681        #[test]
682        fn test_validation_failed_serialization() {
683            let violations = vec![
684                WasmViolation {
685                    path: "field1".to_string(),
686                    code: "error1".to_string(),
687                    message: "Error 1".to_string(),
688                    meta: None,
689                },
690                WasmViolation {
691                    path: "field2".to_string(),
692                    code: "error2".to_string(),
693                    message: "Error 2".to_string(),
694                    meta: None,
695                },
696            ];
697            let result = ValidationResult::validation_failed(violations);
698            let json = serde_json::to_string(&result).unwrap();
699            assert!(json.contains("\"ok\":false"));
700            assert!(json.contains("\"errors\""));
701            assert!(json.contains("field1"));
702            assert!(json.contains("field2"));
703        }
704
705        #[test]
706        fn test_system_error_serialization() {
707            let result = ValidationResult::system_error("parse_error", "Invalid JSON".to_string());
708            let json = serde_json::to_string(&result).unwrap();
709            assert!(json.contains("\"ok\":false"));
710            assert!(json.contains("\"error\""));
711            assert!(json.contains("parse_error"));
712            assert!(json.contains("Invalid JSON"));
713        }
714
715        #[test]
716        fn test_empty_violations_list() {
717            let result = ValidationResult::validation_failed(vec![]);
718            let json = serde_json::to_string(&result).unwrap();
719            assert!(json.contains("\"ok\":false"));
720            assert!(json.contains("\"errors\":[]"));
721        }
722
723        #[test]
724        fn test_violation_with_meta_serialization() {
725            let mut meta = HashMap::new();
726            meta.insert("min".to_string(), "1".to_string());
727            meta.insert("max".to_string(), "10".to_string());
728
729            let violations = vec![WasmViolation {
730                path: "count".to_string(),
731                code: "out_of_range".to_string(),
732                message: "Must be between 1 and 10".to_string(),
733                meta: Some(meta),
734            }];
735            let result = ValidationResult::validation_failed(violations);
736            let json = serde_json::to_string(&result).unwrap();
737            assert!(json.contains("\"meta\""));
738            assert!(json.contains("\"min\":\"1\""));
739            assert!(json.contains("\"max\":\"10\""));
740        }
741
742        #[test]
743        fn test_special_characters_in_message() {
744            let result = ValidationResult::system_error(
745                "parse_error",
746                r#"Expected "}" at line 1, column 5"#.to_string(),
747            );
748            let json = serde_json::to_string(&result).unwrap();
749            assert!(json.contains("parse_error"));
750            // Message should be properly escaped - verify valid JSON by parsing as Value
751            assert!(serde_json::from_str::<serde_json::Value>(&json).is_ok());
752        }
753    }
754
755    // Tests for TypeRegistry edge cases
756    mod registry_edge_cases {
757        use super::*;
758        use serde::Deserialize;
759
760        #[derive(Debug, Validate, Deserialize)]
761        struct TypeA {
762            #[validate(length(min = 1))]
763            name: String,
764        }
765
766        #[derive(Debug, Validate, Deserialize)]
767        struct TypeB {
768            #[validate(range(min = 0, max = 100))]
769            value: i32,
770        }
771
772        #[test]
773        fn test_registry_default_impl() {
774            let registry = TypeRegistry::default();
775            assert!(registry.type_names().is_empty());
776        }
777
778        #[test]
779        fn test_multiple_types_registration() {
780            let mut registry = TypeRegistry::new();
781            registry.register::<TypeA>("TypeA");
782            registry.register::<TypeB>("TypeB");
783
784            assert!(registry.has_type("TypeA"));
785            assert!(registry.has_type("TypeB"));
786            assert_eq!(registry.type_names().len(), 2);
787        }
788
789        #[test]
790        fn test_type_overwriting() {
791            let mut registry = TypeRegistry::new();
792            registry.register::<TypeA>("SharedName");
793
794            // Validate with TypeA rules (length validation)
795            let result = registry.validate("SharedName", r#"{"name": ""}"#);
796            assert!(matches!(result, Err(DispatchError::Validation(_))));
797
798            // Overwrite with TypeB
799            registry.register::<TypeB>("SharedName");
800
801            // Now validates with TypeB rules (range validation)
802            let result = registry.validate("SharedName", r#"{"value": 50}"#);
803            assert!(result.is_ok());
804        }
805
806        #[test]
807        fn test_empty_type_name() {
808            let mut registry = TypeRegistry::new();
809            registry.register::<TypeA>("");
810
811            assert!(registry.has_type(""));
812            let result = registry.validate("", r#"{"name": "test"}"#);
813            assert!(result.is_ok());
814        }
815
816        #[test]
817        fn test_type_name_with_special_characters() {
818            let mut registry = TypeRegistry::new();
819            registry.register::<TypeA>("Type::With::Colons");
820            registry.register::<TypeB>("Type<Generic>");
821
822            assert!(registry.has_type("Type::With::Colons"));
823            assert!(registry.has_type("Type<Generic>"));
824        }
825
826        #[test]
827        fn test_validate_with_whitespace_in_json() {
828            let mut registry = TypeRegistry::new();
829            registry.register::<TypeA>("TypeA");
830
831            let json = r#"
832                {
833                    "name": "test"
834                }
835            "#;
836            let result = registry.validate("TypeA", json);
837            assert!(result.is_ok());
838        }
839
840        #[test]
841        fn test_validate_with_extra_fields_in_json() {
842            let mut registry = TypeRegistry::new();
843            registry.register::<TypeA>("TypeA");
844
845            // Extra fields should be ignored (default serde behavior)
846            let json = r#"{"name": "test", "extra": "ignored"}"#;
847            let result = registry.validate("TypeA", json);
848            assert!(result.is_ok());
849        }
850
851        #[test]
852        fn test_unicode_in_validation_values() {
853            let mut registry = TypeRegistry::new();
854            registry.register::<TypeA>("TypeA");
855
856            let json = r#"{"name": "日本語テスト 🎉"}"#;
857            let result = registry.validate("TypeA", json);
858            assert!(result.is_ok());
859        }
860
861        #[test]
862        fn test_null_value_in_json() {
863            let mut registry = TypeRegistry::new();
864            registry.register::<TypeA>("TypeA");
865
866            let json = r#"{"name": null}"#;
867            let result = registry.validate("TypeA", json);
868            // String field can't be null, should fail parse
869            assert!(matches!(result, Err(DispatchError::ParseError(_))));
870        }
871
872        #[test]
873        fn test_empty_string_validation() {
874            let mut registry = TypeRegistry::new();
875            registry.register::<TypeA>("TypeA");
876
877            let json = r#"{"name": ""}"#;
878            let result = registry.validate("TypeA", json);
879            // Empty string fails min length 1 validation
880            assert!(matches!(result, Err(DispatchError::Validation(_))));
881        }
882    }
883
884    // Tests for WasmViolation conversion edge cases
885    mod wasm_violation_edge_cases {
886        use super::*;
887
888        #[test]
889        fn test_violation_with_empty_path() {
890            let violation = domainstack::Violation {
891                path: domainstack::Path::root(),
892                code: "invalid",
893                message: "Invalid".to_string(),
894                meta: domainstack::Meta::default(),
895            };
896
897            let wasm_violation = WasmViolation::from(&violation);
898            assert_eq!(wasm_violation.path, "");
899        }
900
901        #[test]
902        fn test_violation_with_complex_nested_path() {
903            let path = domainstack::Path::root()
904                .field("orders")
905                .index(0)
906                .field("items")
907                .index(5)
908                .field("variant");
909
910            let violation = domainstack::Violation {
911                path,
912                code: "invalid",
913                message: "Invalid".to_string(),
914                meta: domainstack::Meta::default(),
915            };
916
917            let wasm_violation = WasmViolation::from(&violation);
918            assert_eq!(wasm_violation.path, "orders[0].items[5].variant");
919        }
920
921        #[test]
922        fn test_violation_with_special_chars_in_code() {
923            let violation = domainstack::Violation {
924                path: domainstack::Path::from("field"),
925                code: "error_code_with_underscores",
926                message: "Error".to_string(),
927                meta: domainstack::Meta::default(),
928            };
929
930            let wasm_violation = WasmViolation::from(&violation);
931            assert_eq!(wasm_violation.code, "error_code_with_underscores");
932        }
933
934        #[test]
935        fn test_violation_preserves_long_message() {
936            let long_message = "A".repeat(1000);
937            let violation = domainstack::Violation {
938                path: domainstack::Path::from("field"),
939                code: "error",
940                message: long_message.clone(),
941                meta: domainstack::Meta::default(),
942            };
943
944            let wasm_violation = WasmViolation::from(&violation);
945            assert_eq!(wasm_violation.message, long_message);
946        }
947
948        #[test]
949        fn test_violation_meta_numeric_values() {
950            let mut meta = domainstack::Meta::default();
951            meta.insert("min", 1);
952            meta.insert("max", 100);
953            meta.insert("actual", 150);
954
955            let violation = domainstack::Violation {
956                path: domainstack::Path::from("field"),
957                code: "out_of_range",
958                message: "Out of range".to_string(),
959                meta,
960            };
961
962            let wasm_violation = WasmViolation::from(&violation);
963            let meta = wasm_violation.meta.unwrap();
964            // Numeric values should be converted to strings
965            assert_eq!(meta.get("min"), Some(&"1".to_string()));
966            assert_eq!(meta.get("max"), Some(&"100".to_string()));
967            assert_eq!(meta.get("actual"), Some(&"150".to_string()));
968        }
969    }
970
971    // Tests for dispatch error handling
972    mod dispatch_error_tests {
973        use super::*;
974        use serde::Deserialize;
975
976        #[derive(Debug, Validate, Deserialize)]
977        #[allow(dead_code)]
978        struct SimpleType {
979            value: i32,
980        }
981
982        #[test]
983        fn test_dispatch_error_unknown_type_message() {
984            let registry = TypeRegistry::new();
985            let result = registry.validate("DoesNotExist", "{}");
986
987            match result {
988                Err(DispatchError::UnknownType) => {}
989                _ => panic!("Expected UnknownType error"),
990            }
991        }
992
993        #[test]
994        fn test_dispatch_error_parse_error_contains_details() {
995            let mut registry = TypeRegistry::new();
996            registry.register::<SimpleType>("SimpleType");
997
998            let result = registry.validate("SimpleType", r#"{"value": "not_a_number"}"#);
999
1000            match result {
1001                Err(DispatchError::ParseError(msg)) => {
1002                    assert!(!msg.is_empty());
1003                }
1004                _ => panic!("Expected ParseError"),
1005            }
1006        }
1007
1008        #[test]
1009        fn test_dispatch_error_validation_boxed() {
1010            let mut registry = TypeRegistry::new();
1011
1012            #[derive(Debug, Validate, Deserialize)]
1013            struct AlwaysInvalid {
1014                #[validate(range(min = 10, max = 5))] // Invalid range, will fail
1015                value: i32,
1016            }
1017
1018            registry.register::<AlwaysInvalid>("AlwaysInvalid");
1019
1020            // Any value should trigger validation error
1021            let result = registry.validate("AlwaysInvalid", r#"{"value": 7}"#);
1022
1023            match result {
1024                Err(DispatchError::Validation(boxed_err)) => {
1025                    assert!(!boxed_err.violations.is_empty());
1026                }
1027                _ => panic!("Expected Validation error"),
1028            }
1029        }
1030    }
1031}