use serde::Serialize;
use std::collections::HashMap;
use wasm_bindgen::prelude::*;
pub use domainstack::{Validate, ValidationError};
pub use serde::de::DeserializeOwned;
#[wasm_bindgen(start)]
pub fn init() {
#[cfg(feature = "console_error_panic_hook")]
console_error_panic_hook::set_once();
}
#[derive(Debug, Clone, Serialize)]
pub struct ValidationResult {
pub ok: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub errors: Option<Vec<WasmViolation>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub error: Option<SystemError>,
}
impl ValidationResult {
pub fn success() -> Self {
Self {
ok: true,
errors: None,
error: None,
}
}
pub fn validation_failed(violations: Vec<WasmViolation>) -> Self {
Self {
ok: false,
errors: Some(violations),
error: None,
}
}
pub fn system_error(code: &'static str, message: String) -> Self {
Self {
ok: false,
errors: None,
error: Some(SystemError { code, message }),
}
}
}
#[derive(Debug, Clone, Serialize)]
pub struct WasmViolation {
pub path: String,
pub code: String,
pub message: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub meta: Option<HashMap<String, String>>,
}
impl From<&domainstack::Violation> for WasmViolation {
fn from(v: &domainstack::Violation) -> Self {
let meta = if v.meta.is_empty() {
None
} else {
Some(
v.meta
.iter()
.map(|(k, v)| (k.to_string(), v.to_string()))
.collect(),
)
};
Self {
path: v.path.to_string(),
code: v.code.to_string(),
message: v.message.clone(),
meta,
}
}
}
#[derive(Debug, Clone, Serialize)]
pub struct SystemError {
pub code: &'static str,
pub message: String,
}
pub enum DispatchError {
UnknownType,
ParseError(String),
Validation(Box<ValidationError>),
}
pub type ValidateFn = fn(&str) -> Result<(), DispatchError>;
pub struct TypeRegistry {
validators: HashMap<&'static str, ValidateFn>,
}
impl TypeRegistry {
pub fn new() -> Self {
Self {
validators: HashMap::new(),
}
}
pub fn register<T>(&mut self, type_name: &'static str)
where
T: Validate + DeserializeOwned + 'static,
{
self.validators.insert(type_name, validate_json::<T>);
}
pub fn validate(&self, type_name: &str, json: &str) -> Result<(), DispatchError> {
match self.validators.get(type_name) {
Some(validate_fn) => validate_fn(json),
None => Err(DispatchError::UnknownType),
}
}
pub fn has_type(&self, type_name: &str) -> bool {
self.validators.contains_key(type_name)
}
pub fn type_names(&self) -> Vec<&'static str> {
self.validators.keys().copied().collect()
}
}
impl Default for TypeRegistry {
fn default() -> Self {
Self::new()
}
}
fn validate_json<T>(json: &str) -> Result<(), DispatchError>
where
T: Validate + DeserializeOwned,
{
let value: T =
serde_json::from_str(json).map_err(|e| DispatchError::ParseError(e.to_string()))?;
value
.validate()
.map_err(|e| DispatchError::Validation(Box::new(e)))
}
use std::cell::RefCell;
thread_local! {
static REGISTRY: RefCell<TypeRegistry> = RefCell::new(TypeRegistry::new());
}
pub fn register_type<T>(type_name: &'static str)
where
T: Validate + DeserializeOwned + 'static,
{
REGISTRY.with(|r| r.borrow_mut().register::<T>(type_name));
}
pub fn is_type_registered(type_name: &str) -> bool {
REGISTRY.with(|r| r.borrow().has_type(type_name))
}
pub fn registered_types() -> Vec<&'static str> {
REGISTRY.with(|r| r.borrow().type_names())
}
#[wasm_bindgen]
pub fn validate(type_name: &str, json: &str) -> JsValue {
let result = REGISTRY.with(|r| r.borrow().validate(type_name, json));
let validation_result = match result {
Ok(()) => ValidationResult::success(),
Err(DispatchError::UnknownType) => {
ValidationResult::system_error("unknown_type", format!("Unknown type: {}", type_name))
}
Err(DispatchError::ParseError(msg)) => ValidationResult::system_error("parse_error", msg),
Err(DispatchError::Validation(err)) => {
let violations = err.violations.iter().map(WasmViolation::from).collect();
ValidationResult::validation_failed(violations)
}
};
serde_wasm_bindgen::to_value(&validation_result).unwrap_or_else(|_| {
let fallback = ValidationResult::system_error(
"internal_error",
"Failed to serialize validation result".to_string(),
);
serde_wasm_bindgen::to_value(&fallback).unwrap()
})
}
#[wasm_bindgen]
pub fn validate_object(type_name: &str, value: JsValue) -> JsValue {
let json = match js_sys::JSON::stringify(&value) {
Ok(s) => s.as_string().unwrap_or_default(),
Err(_) => {
let result = ValidationResult::system_error(
"parse_error",
"Failed to serialize JavaScript object to JSON".to_string(),
);
return serde_wasm_bindgen::to_value(&result).unwrap();
}
};
validate(type_name, &json)
}
#[wasm_bindgen]
pub fn get_registered_types() -> JsValue {
let types = registered_types();
serde_wasm_bindgen::to_value(&types).unwrap_or(JsValue::NULL)
}
#[wasm_bindgen]
pub fn has_type(type_name: &str) -> bool {
is_type_registered(type_name)
}
#[wasm_bindgen]
pub struct Validator {
_private: (),
}
#[wasm_bindgen]
impl Validator {
pub fn validate(&self, type_name: &str, json: &str) -> JsValue {
validate(type_name, json)
}
#[wasm_bindgen(js_name = validateObject)]
pub fn validate_object(&self, type_name: &str, value: JsValue) -> JsValue {
validate_object(type_name, value)
}
#[wasm_bindgen(js_name = getTypes)]
pub fn get_types(&self) -> JsValue {
get_registered_types()
}
#[wasm_bindgen(js_name = hasType)]
pub fn has_type(&self, type_name: &str) -> bool {
has_type(type_name)
}
}
#[wasm_bindgen(js_name = createValidator)]
pub fn create_validator() -> Validator {
Validator { _private: () }
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_validation_result_success() {
let result = ValidationResult::success();
assert!(result.ok);
assert!(result.errors.is_none());
assert!(result.error.is_none());
}
#[test]
fn test_validation_result_failure() {
let violations = vec![WasmViolation {
path: "email".to_string(),
code: "invalid_email".to_string(),
message: "Invalid email format".to_string(),
meta: None,
}];
let result = ValidationResult::validation_failed(violations);
assert!(!result.ok);
assert!(result.errors.is_some());
assert_eq!(result.errors.as_ref().unwrap().len(), 1);
assert!(result.error.is_none());
}
#[test]
fn test_validation_result_system_error() {
let result =
ValidationResult::system_error("unknown_type", "Unknown type: Foo".to_string());
assert!(!result.ok);
assert!(result.errors.is_none());
assert!(result.error.is_some());
assert_eq!(result.error.as_ref().unwrap().code, "unknown_type");
}
#[test]
fn test_registry_unknown_type() {
let registry = TypeRegistry::new();
let result = registry.validate("NonExistent", "{}");
assert!(matches!(result, Err(DispatchError::UnknownType)));
}
mod integration {
use super::*;
use domainstack::Validate;
use serde::Deserialize;
#[derive(Debug, Validate, Deserialize)]
struct TestBooking {
#[validate(length(min = 1, max = 100))]
guest_name: String,
#[validate(range(min = 1, max = 10))]
rooms: u8,
}
#[test]
fn test_register_and_validate_success() {
let mut registry = TypeRegistry::new();
registry.register::<TestBooking>("TestBooking");
let json = r#"{"guest_name": "John Doe", "rooms": 2}"#;
let result = registry.validate("TestBooking", json);
assert!(result.is_ok());
}
#[test]
fn test_register_and_validate_failure() {
let mut registry = TypeRegistry::new();
registry.register::<TestBooking>("TestBooking");
let json = r#"{"guest_name": "John", "rooms": 15}"#;
let result = registry.validate("TestBooking", json);
assert!(matches!(result, Err(DispatchError::Validation(_))));
if let Err(DispatchError::Validation(err)) = result {
assert!(!err.violations.is_empty());
assert_eq!(err.violations[0].path.to_string(), "rooms");
}
}
#[test]
fn test_parse_error() {
let mut registry = TypeRegistry::new();
registry.register::<TestBooking>("TestBooking");
let json = r#"{"guest_name": "John", "rooms": "not a number"}"#;
let result = registry.validate("TestBooking", json);
assert!(matches!(result, Err(DispatchError::ParseError(_))));
}
#[test]
fn test_wasm_violation_from_violation() {
let violation = domainstack::Violation {
path: domainstack::Path::from("email"),
code: "invalid_email",
message: "Invalid email format".to_string(),
meta: domainstack::Meta::default(),
};
let wasm_violation = WasmViolation::from(&violation);
assert_eq!(wasm_violation.path, "email");
assert_eq!(wasm_violation.code, "invalid_email");
assert_eq!(wasm_violation.message, "Invalid email format");
assert!(wasm_violation.meta.is_none());
}
#[test]
fn test_wasm_violation_with_meta() {
let mut meta = domainstack::Meta::default();
meta.insert("min", "1");
meta.insert("max", "10");
let violation = domainstack::Violation {
path: domainstack::Path::from("age"),
code: "out_of_range",
message: "Must be between 1 and 10".to_string(),
meta,
};
let wasm_violation = WasmViolation::from(&violation);
assert!(wasm_violation.meta.is_some());
let meta = wasm_violation.meta.unwrap();
assert_eq!(meta.get("min"), Some(&"1".to_string()));
assert_eq!(meta.get("max"), Some(&"10".to_string()));
}
#[test]
fn test_multiple_violations() {
let mut registry = TypeRegistry::new();
registry.register::<TestBooking>("TestBooking");
let json = r#"{"guest_name": "", "rooms": 15}"#;
let result = registry.validate("TestBooking", json);
assert!(matches!(result, Err(DispatchError::Validation(_))));
if let Err(DispatchError::Validation(err)) = result {
assert_eq!(err.violations.len(), 2);
}
}
#[test]
fn test_empty_json_object() {
let mut registry = TypeRegistry::new();
registry.register::<TestBooking>("TestBooking");
let json = r#"{}"#;
let result = registry.validate("TestBooking", json);
assert!(matches!(result, Err(DispatchError::ParseError(_))));
}
#[test]
fn test_invalid_json_syntax() {
let mut registry = TypeRegistry::new();
registry.register::<TestBooking>("TestBooking");
let json = r#"{ invalid json }"#;
let result = registry.validate("TestBooking", json);
assert!(matches!(result, Err(DispatchError::ParseError(_))));
}
#[test]
fn test_registry_has_type() {
let mut registry = TypeRegistry::new();
assert!(!registry.has_type("TestBooking"));
registry.register::<TestBooking>("TestBooking");
assert!(registry.has_type("TestBooking"));
assert!(!registry.has_type("NonExistent"));
}
#[test]
fn test_registry_type_names() {
let mut registry = TypeRegistry::new();
assert!(registry.type_names().is_empty());
registry.register::<TestBooking>("TestBooking");
let names = registry.type_names();
assert_eq!(names.len(), 1);
assert!(names.contains(&"TestBooking"));
}
#[derive(Debug, Validate, Deserialize)]
struct TestAddress {
#[validate(length(min = 1, max = 100))]
street: String,
#[validate(length(min = 1, max = 50))]
city: String,
}
#[derive(Debug, Validate, Deserialize)]
struct TestPerson {
#[validate(length(min = 1, max = 50))]
name: String,
#[validate(nested)]
address: TestAddress,
}
#[test]
fn test_nested_validation_with_path() {
let mut registry = TypeRegistry::new();
registry.register::<TestPerson>("TestPerson");
let json = r#"{"name": "John", "address": {"street": "123 Main", "city": ""}}"#;
let result = registry.validate("TestPerson", json);
assert!(matches!(result, Err(DispatchError::Validation(_))));
if let Err(DispatchError::Validation(err)) = result {
assert!(!err.violations.is_empty());
let path = err.violations[0].path.to_string();
assert!(path.contains("address") || path.contains("city"));
}
}
}
mod global_registry {
use super::*;
use serde::Deserialize;
#[derive(Debug, Validate, Deserialize)]
struct GlobalTestType {
#[validate(length(min = 1))]
name: String,
}
#[test]
fn test_global_register_and_check() {
register_type::<GlobalTestType>("GlobalTestType");
assert!(is_type_registered("GlobalTestType"));
assert!(!is_type_registered("NotRegistered"));
let types = registered_types();
assert!(types.contains(&"GlobalTestType"));
}
}
mod validation_result {
use super::*;
#[test]
fn test_success_serialization() {
let result = ValidationResult::success();
let json = serde_json::to_string(&result).unwrap();
assert!(json.contains("\"ok\":true"));
assert!(!json.contains("errors"));
assert!(!json.contains("error"));
}
#[test]
fn test_validation_failed_serialization() {
let violations = vec![
WasmViolation {
path: "field1".to_string(),
code: "error1".to_string(),
message: "Error 1".to_string(),
meta: None,
},
WasmViolation {
path: "field2".to_string(),
code: "error2".to_string(),
message: "Error 2".to_string(),
meta: None,
},
];
let result = ValidationResult::validation_failed(violations);
let json = serde_json::to_string(&result).unwrap();
assert!(json.contains("\"ok\":false"));
assert!(json.contains("\"errors\""));
assert!(json.contains("field1"));
assert!(json.contains("field2"));
}
#[test]
fn test_system_error_serialization() {
let result = ValidationResult::system_error("parse_error", "Invalid JSON".to_string());
let json = serde_json::to_string(&result).unwrap();
assert!(json.contains("\"ok\":false"));
assert!(json.contains("\"error\""));
assert!(json.contains("parse_error"));
assert!(json.contains("Invalid JSON"));
}
#[test]
fn test_empty_violations_list() {
let result = ValidationResult::validation_failed(vec![]);
let json = serde_json::to_string(&result).unwrap();
assert!(json.contains("\"ok\":false"));
assert!(json.contains("\"errors\":[]"));
}
#[test]
fn test_violation_with_meta_serialization() {
let mut meta = HashMap::new();
meta.insert("min".to_string(), "1".to_string());
meta.insert("max".to_string(), "10".to_string());
let violations = vec![WasmViolation {
path: "count".to_string(),
code: "out_of_range".to_string(),
message: "Must be between 1 and 10".to_string(),
meta: Some(meta),
}];
let result = ValidationResult::validation_failed(violations);
let json = serde_json::to_string(&result).unwrap();
assert!(json.contains("\"meta\""));
assert!(json.contains("\"min\":\"1\""));
assert!(json.contains("\"max\":\"10\""));
}
#[test]
fn test_special_characters_in_message() {
let result = ValidationResult::system_error(
"parse_error",
r#"Expected "}" at line 1, column 5"#.to_string(),
);
let json = serde_json::to_string(&result).unwrap();
assert!(json.contains("parse_error"));
assert!(serde_json::from_str::<serde_json::Value>(&json).is_ok());
}
}
mod registry_edge_cases {
use super::*;
use serde::Deserialize;
#[derive(Debug, Validate, Deserialize)]
struct TypeA {
#[validate(length(min = 1))]
name: String,
}
#[derive(Debug, Validate, Deserialize)]
struct TypeB {
#[validate(range(min = 0, max = 100))]
value: i32,
}
#[test]
fn test_registry_default_impl() {
let registry = TypeRegistry::default();
assert!(registry.type_names().is_empty());
}
#[test]
fn test_multiple_types_registration() {
let mut registry = TypeRegistry::new();
registry.register::<TypeA>("TypeA");
registry.register::<TypeB>("TypeB");
assert!(registry.has_type("TypeA"));
assert!(registry.has_type("TypeB"));
assert_eq!(registry.type_names().len(), 2);
}
#[test]
fn test_type_overwriting() {
let mut registry = TypeRegistry::new();
registry.register::<TypeA>("SharedName");
let result = registry.validate("SharedName", r#"{"name": ""}"#);
assert!(matches!(result, Err(DispatchError::Validation(_))));
registry.register::<TypeB>("SharedName");
let result = registry.validate("SharedName", r#"{"value": 50}"#);
assert!(result.is_ok());
}
#[test]
fn test_empty_type_name() {
let mut registry = TypeRegistry::new();
registry.register::<TypeA>("");
assert!(registry.has_type(""));
let result = registry.validate("", r#"{"name": "test"}"#);
assert!(result.is_ok());
}
#[test]
fn test_type_name_with_special_characters() {
let mut registry = TypeRegistry::new();
registry.register::<TypeA>("Type::With::Colons");
registry.register::<TypeB>("Type<Generic>");
assert!(registry.has_type("Type::With::Colons"));
assert!(registry.has_type("Type<Generic>"));
}
#[test]
fn test_validate_with_whitespace_in_json() {
let mut registry = TypeRegistry::new();
registry.register::<TypeA>("TypeA");
let json = r#"
{
"name": "test"
}
"#;
let result = registry.validate("TypeA", json);
assert!(result.is_ok());
}
#[test]
fn test_validate_with_extra_fields_in_json() {
let mut registry = TypeRegistry::new();
registry.register::<TypeA>("TypeA");
let json = r#"{"name": "test", "extra": "ignored"}"#;
let result = registry.validate("TypeA", json);
assert!(result.is_ok());
}
#[test]
fn test_unicode_in_validation_values() {
let mut registry = TypeRegistry::new();
registry.register::<TypeA>("TypeA");
let json = r#"{"name": "日本語テスト 🎉"}"#;
let result = registry.validate("TypeA", json);
assert!(result.is_ok());
}
#[test]
fn test_null_value_in_json() {
let mut registry = TypeRegistry::new();
registry.register::<TypeA>("TypeA");
let json = r#"{"name": null}"#;
let result = registry.validate("TypeA", json);
assert!(matches!(result, Err(DispatchError::ParseError(_))));
}
#[test]
fn test_empty_string_validation() {
let mut registry = TypeRegistry::new();
registry.register::<TypeA>("TypeA");
let json = r#"{"name": ""}"#;
let result = registry.validate("TypeA", json);
assert!(matches!(result, Err(DispatchError::Validation(_))));
}
}
mod wasm_violation_edge_cases {
use super::*;
#[test]
fn test_violation_with_empty_path() {
let violation = domainstack::Violation {
path: domainstack::Path::root(),
code: "invalid",
message: "Invalid".to_string(),
meta: domainstack::Meta::default(),
};
let wasm_violation = WasmViolation::from(&violation);
assert_eq!(wasm_violation.path, "");
}
#[test]
fn test_violation_with_complex_nested_path() {
let path = domainstack::Path::root()
.field("orders")
.index(0)
.field("items")
.index(5)
.field("variant");
let violation = domainstack::Violation {
path,
code: "invalid",
message: "Invalid".to_string(),
meta: domainstack::Meta::default(),
};
let wasm_violation = WasmViolation::from(&violation);
assert_eq!(wasm_violation.path, "orders[0].items[5].variant");
}
#[test]
fn test_violation_with_special_chars_in_code() {
let violation = domainstack::Violation {
path: domainstack::Path::from("field"),
code: "error_code_with_underscores",
message: "Error".to_string(),
meta: domainstack::Meta::default(),
};
let wasm_violation = WasmViolation::from(&violation);
assert_eq!(wasm_violation.code, "error_code_with_underscores");
}
#[test]
fn test_violation_preserves_long_message() {
let long_message = "A".repeat(1000);
let violation = domainstack::Violation {
path: domainstack::Path::from("field"),
code: "error",
message: long_message.clone(),
meta: domainstack::Meta::default(),
};
let wasm_violation = WasmViolation::from(&violation);
assert_eq!(wasm_violation.message, long_message);
}
#[test]
fn test_violation_meta_numeric_values() {
let mut meta = domainstack::Meta::default();
meta.insert("min", 1);
meta.insert("max", 100);
meta.insert("actual", 150);
let violation = domainstack::Violation {
path: domainstack::Path::from("field"),
code: "out_of_range",
message: "Out of range".to_string(),
meta,
};
let wasm_violation = WasmViolation::from(&violation);
let meta = wasm_violation.meta.unwrap();
assert_eq!(meta.get("min"), Some(&"1".to_string()));
assert_eq!(meta.get("max"), Some(&"100".to_string()));
assert_eq!(meta.get("actual"), Some(&"150".to_string()));
}
}
mod dispatch_error_tests {
use super::*;
use serde::Deserialize;
#[derive(Debug, Validate, Deserialize)]
#[allow(dead_code)]
struct SimpleType {
value: i32,
}
#[test]
fn test_dispatch_error_unknown_type_message() {
let registry = TypeRegistry::new();
let result = registry.validate("DoesNotExist", "{}");
match result {
Err(DispatchError::UnknownType) => {}
_ => panic!("Expected UnknownType error"),
}
}
#[test]
fn test_dispatch_error_parse_error_contains_details() {
let mut registry = TypeRegistry::new();
registry.register::<SimpleType>("SimpleType");
let result = registry.validate("SimpleType", r#"{"value": "not_a_number"}"#);
match result {
Err(DispatchError::ParseError(msg)) => {
assert!(!msg.is_empty());
}
_ => panic!("Expected ParseError"),
}
}
#[test]
fn test_dispatch_error_validation_boxed() {
let mut registry = TypeRegistry::new();
#[derive(Debug, Validate, Deserialize)]
struct AlwaysInvalid {
#[validate(range(min = 10, max = 5))] value: i32,
}
registry.register::<AlwaysInvalid>("AlwaysInvalid");
let result = registry.validate("AlwaysInvalid", r#"{"value": 7}"#);
match result {
Err(DispatchError::Validation(boxed_err)) => {
assert!(!boxed_err.violations.is_empty());
}
_ => panic!("Expected Validation error"),
}
}
}
}