diff --git a/.cargo/config.toml b/.cargo/config.toml index 7496ab2..9f0a63e 100644 --- a/.cargo/config.toml +++ b/.cargo/config.toml @@ -1,3 +1,6 @@ [alias] xtask = "run --quiet --manifest-path ./xtask/Cargo.toml --" x = "run --quiet --manifest-path ./xtask/Cargo.toml --" + +[target.wasm32-unknown-unknown] +runner = 'wasm-bindgen-test-runner' diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 33db915..0e51582 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -36,6 +36,34 @@ permissions: contents: read jobs: + test-wasm: + name: Test WASM + runs-on: ubuntu-20.04 + + steps: + - name: Checkout repository + uses: actions/checkout@v3 + + - name: Install Rust + uses: dtolnay/rust-toolchain@v1 + with: + toolchain: stable + targets: wasm32-unknown-unknown + + - name: Cache + uses: Swatinem/rust-cache@v2 + with: + shared-key: "rust-stable-test" + + - name: Install wasm-bindgen-cli + run: cargo install --locked wasm-bindgen-cli + + - name: Build xtask + run: cargo build --manifest-path ./xtask/Cargo.toml + + - name: Run tests + run: cargo x test --wasm + test: name: Test runs-on: ${{ matrix.os }} diff --git a/garde/Cargo.toml b/garde/Cargo.toml index c1228e0..47d50e8 100644 --- a/garde/Cargo.toml +++ b/garde/Cargo.toml @@ -30,6 +30,7 @@ email = ["regex"] email-idna = ["dep:idna"] regex = ["dep:regex", "dep:once_cell", "garde_derive?/regex"] pattern = ["regex"] # for backward compatibility with <0.14.0 +js-sys = ["dep:js-sys"] [dependencies] garde_derive = { version = "0.16.0", path = "../garde_derive", optional = true, default-features = false } @@ -47,12 +48,19 @@ regex = { version = "1", default-features = false, features = [ once_cell = { version = "1", optional = true } idna = { version = "0.3", optional = true } +[target.'cfg(all(target_arch = "wasm32", target_os = "unknown"))'.dependencies] +js-sys = { version = "0.3", optional = true } + [dev-dependencies] trybuild = { version = "1.0" } insta = { version = "1.29" } owo-colors = { version = "3.5.0" } glob = "0.3.1" +[target.'cfg(all(target_arch = "wasm32", target_os = "unknown"))'.dev-dependencies] +wasm-bindgen-test = "0.3.38" + +[target.'cfg(not(all(target_arch = "wasm32", target_os = "unknown")))'.dev-dependencies] criterion = "0.4" [[bench]] diff --git a/garde/src/rules/email.rs b/garde/src/rules/email.rs index 9f95a44..e5deaf1 100644 --- a/garde/src/rules/email.rs +++ b/garde/src/rules/email.rs @@ -15,12 +15,22 @@ use std::fmt::Display; use std::str::FromStr; -use once_cell::sync::Lazy; -use regex::Regex; - +use super::pattern::Matcher; use super::AsStr; use crate::error::Error; +macro_rules! init_regex { + ($var:ident => $p:literal) => { + #[cfg(not(all(feature = "js-sys", target_arch = "wasm32", target_os = "unknown")))] + static $var: $crate::rules::pattern::regex::StaticPattern = + $crate::rules::pattern::regex::init_pattern!($p); + + #[cfg(all(feature = "js-sys", target_arch = "wasm32", target_os = "unknown"))] + static $var: $crate::rules::pattern::regex_js_sys::StaticPattern = + $crate::rules::pattern::regex_js_sys::init_pattern!($p); + }; +} + pub fn apply(v: &T, _: ()) -> Result<(), Error> { if let Err(e) = v.validate_email() { return Err(Error::new(format!("not a valid email: {e}"))); @@ -90,8 +100,11 @@ pub fn parse_email(s: &str) -> Result<(), InvalidEmail> { if user.len() > 64 { return Err(InvalidEmail::UserLengthExceeded); } - static USER_RE: Lazy = - Lazy::new(|| Regex::new(r"(?i-u)^[a-z0-9.!#$%&'*+/=?^_`{|}~-]+\z").unwrap()); + + init_regex! { + USER_RE => r"(?i-u)^[a-z0-9.!#$%&'*+/=?^_`{|}~-]+\z" + } + if !USER_RE.is_match(user) { return Err(InvalidEmail::InvalidUser); } @@ -123,9 +136,9 @@ pub fn parse_email(s: &str) -> Result<(), InvalidEmail> { } fn is_valid_domain(domain: &str) -> bool { - static DOMAIN_NAME_RE: Lazy = Lazy::new(|| { - Regex::new(r"(?i-u)^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?(?:\.[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?)*$").unwrap() - }); + init_regex! { + DOMAIN_NAME_RE => r"(?i-u)^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?(?:\.[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?)*$" + }; if DOMAIN_NAME_RE.is_match(domain) { return true; diff --git a/garde/src/rules/pattern.rs b/garde/src/rules/pattern.rs index f4561fd..d4da3a4 100644 --- a/garde/src/rules/pattern.rs +++ b/garde/src/rules/pattern.rs @@ -69,6 +69,68 @@ impl Pattern for Option { } } +#[cfg(all( + feature = "regex", + feature = "js-sys", + target_arch = "wasm32", + target_os = "unknown" +))] +#[doc(hidden)] +pub mod regex_js_sys { + pub use ::js_sys::RegExp; + + use super::*; + + impl Matcher for RegExp { + fn is_match(&self, haystack: &str) -> bool { + self.test(haystack) + } + } + + impl AsStr for RegExp { + fn as_str(&self) -> &str { + "[Not supported in JS]" + } + } + + pub struct SyncWrapper(T); + + impl SyncWrapper { + /// Safety: You have to ensure that this value is never shared or sent between threads unless the inner value supports it + pub const unsafe fn new(inner: T) -> Self { + Self(inner) + } + } + + impl AsStr for SyncWrapper { + fn as_str(&self) -> &str { + self.0.as_str() + } + } + + impl Matcher for SyncWrapper { + fn is_match(&self, haystack: &str) -> bool { + self.0.is_match(haystack) + } + } + + unsafe impl Send for SyncWrapper {} + unsafe impl Sync for SyncWrapper {} + + pub type StaticPattern = once_cell::sync::Lazy>; + + #[macro_export] + macro_rules! __init_js_sys_pattern { + ($pat:literal) => { + $crate::rules::pattern::regex_js_sys::StaticPattern::new(|| { + // Safety: `wasm32-unknown-unknown` is inherently single-threaded. Therefore `Send` and `Sync` aren't really relevant + unsafe { $crate::rules::pattern::regex_js_sys::SyncWrapper::new(::js_sys::RegExp::new($pat, "u")) } + }) + }; + } + pub use crate::__init_js_sys_pattern as init_pattern; +} + #[cfg(feature = "regex")] #[doc(hidden)] pub mod regex { diff --git a/garde/tests/rules/pattern.rs b/garde/tests/rules/pattern.rs index c00b97a..2abadc5 100644 --- a/garde/tests/rules/pattern.rs +++ b/garde/tests/rules/pattern.rs @@ -1,9 +1,10 @@ -use once_cell::sync::Lazy; use regex::Regex; use super::util; mod sub { + use once_cell::sync::Lazy; + use super::*; pub static LAZY_RE: Lazy = Lazy::new(|| Regex::new(r"^abcd|efgh$").unwrap()); @@ -24,11 +25,21 @@ struct Test<'a> { inner: &'a [&'a str], } +#[cfg(not(all(feature = "js-sys", target_arch = "wasm32", target_os = "unknown")))] fn create_regex() -> Regex { Regex::new(r"^abcd|efgh$").unwrap() } -#[test] +#[cfg(all(feature = "js-sys", target_arch = "wasm32", target_os = "unknown"))] +fn create_regex() -> ::js_sys::RegExp { + ::js_sys::RegExp::new(r"^abcd|efgh$", "u") +} + +#[cfg_attr(not(all(target_arch = "wasm32", target_os = "unknown")), test)] +#[cfg_attr( + all(target_arch = "wasm32", target_os = "unknown"), + wasm_bindgen_test::wasm_bindgen_test +)] fn pattern_valid() { util::check_ok( &[ @@ -49,6 +60,7 @@ fn pattern_valid() { ) } +#[cfg(not(all(target_arch = "wasm32", target_os = "unknown")))] #[test] fn pattern_invalid() { util::check_fail!( diff --git a/garde_derive/src/emit.rs b/garde_derive/src/emit.rs index 0a47e4a..a1269e8 100644 --- a/garde_derive/src/emit.rs +++ b/garde_derive/src/emit.rs @@ -241,8 +241,22 @@ impl<'a> ToTokens for Rules<'a> { Pattern(pat) => match pat { model::ValidatePattern::Expr(expr) => quote_spanned!(expr.span() => (&#expr,)), model::ValidatePattern::Lit(s) => quote!({ + #[cfg(not(all( + feature = "js-sys", + target_arch = "wasm32", + target_os = "unknown" + )))] static PATTERN: ::garde::rules::pattern::regex::StaticPattern = ::garde::rules::pattern::regex::init_pattern!(#s); + + #[cfg(all( + feature = "js-sys", + target_arch = "wasm32", + target_os = "unknown" + ))] + static PATTERN: ::garde::rules::pattern::regex_js_sys::StaticPattern = + ::garde::rules::pattern::regex_js_sys::init_pattern!(#s); + (&PATTERN,) }), }, diff --git a/xtask/src/task/test.rs b/xtask/src/task/test.rs index 7491957..52c19fa 100644 --- a/xtask/src/task/test.rs +++ b/xtask/src/task/test.rs @@ -14,6 +14,11 @@ pub struct Test { targets: Vec, #[argp(switch, description = "Run insta with --review")] review: bool, + #[argp( + switch, + description = "Run the tests for the `wasm32-unknown-unknown` platform" + )] + wasm: bool, } #[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] @@ -57,7 +62,9 @@ impl argp::FromArgValue for Target { impl Test { pub fn run(mut self) -> Result { let review = self.review; - let commands = if self.targets.is_empty() { + let mut commands = if self.targets.is_empty() && self.wasm { + vec![unit(), ui(review), rules(review)] + } else if self.targets.is_empty() { vec![unit(), ui(review), rules(review), axum()] } else { self.targets.sort(); @@ -73,6 +80,12 @@ impl Test { .collect() }; + if self.wasm { + commands.iter_mut().for_each(|cmd| { + cmd.args(["--target", "wasm32-unknown-unknown"]); + }); + } + for command in commands { command.run()?; }