A better Rust ATProto crate

regex-lite usage on wasm

Orual a0914f4b 873b85e9

Changed files
+112 -5
crates
+8
Cargo.lock
··· 2279 2279 "miette", 2280 2280 "n0-future", 2281 2281 "regex", 2282 + "regex-lite", 2282 2283 "reqwest", 2283 2284 "serde", 2284 2285 "serde_html_form", ··· 2368 2369 "p256", 2369 2370 "rand 0.9.2", 2370 2371 "regex", 2372 + "regex-lite", 2371 2373 "reqwest", 2372 2374 "serde", 2373 2375 "serde_html_form", ··· 3880 3882 "memchr", 3881 3883 "regex-syntax", 3882 3884 ] 3885 + 3886 + [[package]] 3887 + name = "regex-lite" 3888 + version = "0.1.8" 3889 + source = "registry+https://github.com/rust-lang/crates.io-index" 3890 + checksum = "8d942b98df5e658f56f20d592c7f868833fe38115e65c33003d8cd224b0155da" 3883 3891 3884 3892 [[package]] 3885 3893 name = "regex-syntax"
+1 -1
Cargo.toml
··· 88 88 jose-jwk = "0.1" 89 89 90 90 # Text processing 91 - regex = "1.11" 91 + regex = { version = "1.11", default-features = false } 92 92 webpage = { version = "2.0", default-features = false }
+2 -1
crates/jacquard-common/Cargo.toml
··· 38 38 multihash = "0.19.3" 39 39 ouroboros = "0.18.5" 40 40 rand = "0.9.2" 41 - regex = "1.11.3" 42 41 serde.workspace = true 43 42 serde_html_form.workspace = true 44 43 serde_json.workspace = true ··· 64 63 getrandom = { version = "0.3.4", features = ["wasm_js"] } 65 64 chrono = { workspace = true, features = ["wasmbind"] } 66 65 getrandom_02 = { package = "getrandom", version = "0.2", features = ["js"] } 66 + regex-lite = "0.1" 67 67 68 68 [target.'cfg(not(target_arch = "wasm32"))'.dependencies] 69 69 reqwest = { workspace = true, optional = true, features = [ "http2", "system-proxy", "rustls-tls"] } 70 70 tokio-util = { version = "0.7.16", features = ["io"] } 71 + regex = { version = "1.11.3", default-features = false, features = ["std", "perf-literal"] } 71 72 72 73 73 74
+3
crates/jacquard-common/src/types/aturi.rs
··· 3 3 use crate::types::recordkey::{RecordKey, Rkey}; 4 4 use crate::types::string::AtStrError; 5 5 use crate::{CowStr, IntoStatic}; 6 + #[cfg(not(target_arch = "wasm32"))] 6 7 use regex::Regex; 8 + #[cfg(target_arch = "wasm32")] 9 + use regex_lite::Regex; 7 10 use serde::Serializer; 8 11 use serde::{Deserialize, Deserializer, Serialize, de::Error}; 9 12 use smol_str::{SmolStr, ToSmolStr};
+3
crates/jacquard-common/src/types/datetime.rs
··· 7 7 use std::{cmp, str::FromStr}; 8 8 9 9 use crate::{CowStr, IntoStatic}; 10 + #[cfg(not(target_arch = "wasm32"))] 10 11 use regex::Regex; 12 + #[cfg(target_arch = "wasm32")] 13 + use regex_lite::Regex; 11 14 12 15 /// Regex for ISO 8601 datetime validation per AT Protocol spec 13 16 pub static ISO8601_REGEX: LazyLock<Regex> = LazyLock::new(|| {
+3
crates/jacquard-common/src/types/did.rs
··· 1 1 use crate::types::string::AtStrError; 2 2 use crate::{CowStr, IntoStatic}; 3 + #[cfg(not(target_arch = "wasm32"))] 3 4 use regex::Regex; 5 + #[cfg(target_arch = "wasm32")] 6 + use regex_lite::Regex; 4 7 use serde::{Deserialize, Deserializer, Serialize, de::Error}; 5 8 use smol_str::{SmolStr, ToSmolStr}; 6 9 use std::fmt;
+3
crates/jacquard-common/src/types/handle.rs
··· 1 1 use crate::types::string::AtStrError; 2 2 use crate::types::{DISALLOWED_TLDS, ends_with}; 3 3 use crate::{CowStr, IntoStatic}; 4 + #[cfg(not(target_arch = "wasm32"))] 4 5 use regex::Regex; 6 + #[cfg(target_arch = "wasm32")] 7 + use regex_lite::Regex; 5 8 use serde::{Deserialize, Deserializer, Serialize, de::Error}; 6 9 use smol_str::{SmolStr, ToSmolStr}; 7 10 use std::fmt;
+3
crates/jacquard-common/src/types/nsid.rs
··· 1 1 use crate::types::recordkey::RecordKeyType; 2 2 use crate::types::string::AtStrError; 3 3 use crate::{CowStr, IntoStatic}; 4 + #[cfg(not(target_arch = "wasm32"))] 4 5 use regex::Regex; 6 + #[cfg(target_arch = "wasm32")] 7 + use regex_lite::Regex; 5 8 use serde::{Deserialize, Deserializer, Serialize, de::Error}; 6 9 use smol_str::{SmolStr, ToSmolStr}; 7 10 use std::fmt;
+3
crates/jacquard-common/src/types/recordkey.rs
··· 1 1 use crate::types::Literal; 2 2 use crate::types::string::AtStrError; 3 3 use crate::{CowStr, IntoStatic}; 4 + #[cfg(not(target_arch = "wasm32"))] 4 5 use regex::Regex; 6 + #[cfg(target_arch = "wasm32")] 7 + use regex_lite::Regex; 5 8 use serde::{Deserialize, Deserializer, Serialize, de::Error}; 6 9 use smol_str::{SmolStr, ToSmolStr}; 7 10 use std::fmt;
+3
crates/jacquard-common/src/types/tid.rs
··· 7 7 use crate::CowStr; 8 8 use crate::types::integer::LimitedU32; 9 9 use crate::types::string::{AtStrError, StrParseKind}; 10 + #[cfg(not(target_arch = "wasm32"))] 10 11 use regex::Regex; 12 + #[cfg(target_arch = "wasm32")] 13 + use regex_lite::Regex; 11 14 12 15 const S32_CHAR: &str = "234567abcdefghijklmnopqrstuvwxyz"; 13 16
+73
crates/jacquard-common/src/xrpc/dyn_req.rs
··· 54 54 } 55 55 } 56 56 } 57 + 58 + pub struct DynResponse { 59 + buffer: Bytes, 60 + status: StatusCode, 61 + } 62 + 63 + impl DynResponse { 64 + pub fn new(buffer: Bytes, status: StatusCode) -> Self { 65 + Self { buffer, status } 66 + } 67 + 68 + /// Parse the response into an owned output 69 + pub fn into_output<R>(self) -> Result<RespOutput<'static, R>, XrpcError<RespErr<'static, R>>> 70 + where 71 + R: XrpcResp, 72 + for<'a> RespOutput<'a, R>: IntoStatic<Output = RespOutput<'static, R>>, 73 + for<'a> RespErr<'a, R>: IntoStatic<Output = RespErr<'static, R>>, 74 + { 75 + fn parse_error<'b, R: XrpcResp>(buffer: &'b [u8]) -> Result<R::Err<'b>, serde_json::Error> { 76 + serde_json::from_slice(buffer) 77 + } 78 + 79 + // 200: parse as output 80 + if self.status.is_success() { 81 + match R::decode_output(&self.buffer) { 82 + Ok(output) => Ok(output.into_static()), 83 + Err(e) => Err(XrpcError::Decode(e)), 84 + } 85 + // 400: try typed XRPC error, fallback to generic error 86 + } else if self.status.as_u16() == 400 { 87 + let error = match parse_error::<R>(&self.buffer) { 88 + Ok(error) => XrpcError::Xrpc(error), 89 + Err(_) => { 90 + // Fallback to generic error (InvalidRequest, ExpiredToken, etc.) 91 + match serde_json::from_slice::<GenericXrpcError>(&self.buffer) { 92 + Ok(mut generic) => { 93 + generic.nsid = R::NSID; 94 + generic.method = ""; // method info only available on request 95 + generic.http_status = self.status; 96 + // Map auth-related errors to AuthError 97 + match generic.error.as_ref() { 98 + "ExpiredToken" => XrpcError::Auth(AuthError::TokenExpired), 99 + "InvalidToken" => XrpcError::Auth(AuthError::InvalidToken), 100 + _ => XrpcError::Generic(generic), 101 + } 102 + } 103 + Err(e) => XrpcError::Decode(DecodeError::Json(e)), 104 + } 105 + } 106 + }; 107 + Err(error.into_static()) 108 + // 401: always auth error 109 + } else { 110 + let error: XrpcError<<R as XrpcResp>::Err<'_>> = 111 + match serde_json::from_slice::<GenericXrpcError>(&self.buffer) { 112 + Ok(mut generic) => { 113 + let status = self.status; 114 + generic.nsid = R::NSID; 115 + generic.method = ""; // method info only available on request 116 + generic.http_status = status; 117 + match generic.error.as_ref() { 118 + "ExpiredToken" => XrpcError::Auth(AuthError::TokenExpired), 119 + "InvalidToken" => XrpcError::Auth(AuthError::InvalidToken), 120 + _ => XrpcError::Auth(AuthError::NotAuthenticated), 121 + } 122 + } 123 + Err(e) => XrpcError::Decode(DecodeError::Json(e)), 124 + }; 125 + 126 + Err(error.into_static()) 127 + } 128 + } 129 + }
+2 -1
crates/jacquard/Cargo.toml
··· 152 152 tokio = { workspace = true, default-features = false, features = ["sync"] } 153 153 url.workspace = true 154 154 smol_str.workspace = true 155 - regex.workspace = true 156 155 webpage.workspace = true 157 156 jose-jwk = { workspace = true, features = ["p256"] } 158 157 tracing = { workspace = true, optional = true } ··· 167 166 "rustls-tls", 168 167 ] } 169 168 tokio = { workspace = true, features = ["macros", "rt-multi-thread", "fs"] } 169 + regex = { workspace = true, default-features = false, features = ["std", "perf-literal", "unicode"] } 170 170 171 171 [target.'cfg(target_family = "wasm")'.dependencies] 172 172 getrandom = { version = "0.2", features = ["js"] } 173 + regex-lite = "0.1" 173 174 174 175 [target.'cfg(all(target_family = "wasm", target_os = "unknown"))'.dependencies] 175 176 gloo-storage = "0.3"
+5 -2
crates/jacquard/src/richtext.rs
··· 20 20 use jacquard_identity::resolver::IdentityError; 21 21 #[cfg(feature = "api_bluesky")] 22 22 use jacquard_identity::resolver::IdentityResolver; 23 - use regex::Regex; 23 + #[cfg(not(target_family = "wasm"))] 24 + use regex::{Regex, Captures}; 25 + #[cfg(target_family = "wasm")] 26 + use regex_lite::{Regex, Captures}; 24 27 use std::marker::PhantomData; 25 28 use std::ops::Range; 26 29 use std::sync::LazyLock; ··· 197 200 /// runs of newlines and invisible chars to at most two newlines. 198 201 fn sanitize_text(text: &str) -> String { 199 202 SANITIZE_NEWLINES_REGEX 200 - .replace_all(text, |caps: &regex::Captures| { 203 + .replace_all(text, |caps: &Captures| { 201 204 let matched = caps.get(0).unwrap().as_str(); 202 205 203 206 // Count newline sequences, treating \r\n as one unit