From 75fafc5e011194cc38f341bf572f94fcb1ac374a Mon Sep 17 00:00:00 2001 From: Pistonight Date: Tue, 29 Sep 2026 13:52:39 -0700 Subject: [PATCH 1/2] feat: deserializable regex --- packages/copper/Cargo.toml | 6 ++- packages/copper/src/errhand.rs | 18 +++++++-- packages/copper/src/lib.rs | 2 + packages/copper/src/str/mod.rs | 5 +++ packages/copper/src/str/regex.rs | 66 ++++++++++++++++++++++++++++++++ 5 files changed, 93 insertions(+), 4 deletions(-) create mode 100644 packages/copper/src/str/regex.rs diff --git a/packages/copper/Cargo.toml b/packages/copper/Cargo.toml index f314970..27849aa 100644 --- a/packages/copper/Cargo.toml +++ b/packages/copper/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "pistonite-cu" -version = "0.9.5" +version = "0.9.6" edition = "2024" description = "Battery-included common utils to speed up development of rust tools" repository = "https://github.com/Pistonite/cu" @@ -64,6 +64,7 @@ features = [ "macros", "rt-multi-thread", "time" ] [features] default = [] full = [ + "regex", "cli", "coroutine-heavy", "prompt", @@ -74,6 +75,9 @@ full = [ "derive", ] +# --- Extra bits of utils --- +regex = ["dep:regex"] + # --- Command Line Interface --- print = ["dep:oneshot", "dep:regex", "dep:env_filter", "dep:terminal_size", "dep:unicode-width", "dep:ctrlc"] cli = ["dep:clap", "print"] diff --git a/packages/copper/src/errhand.rs b/packages/copper/src/errhand.rs index 3aa6519..8b8ee62 100644 --- a/packages/copper/src/errhand.rs +++ b/packages/copper/src/errhand.rs @@ -21,7 +21,9 @@ pub use anyhow::{Context, Error, Ok, Result, anyhow as fmterr, bail}; /// - `anyhow::Ok` is `cu::Ok` /// /// Here are custom utilities from `cu` that integrates with `anyhow` -/// - `cu::check!` wraps `.with_context()` +/// - `cu::check!` wraps `.with_context()`. This is the general-purpose way to add context +/// to errors that can happen at runtime even when the program is correct, such as +/// invalid user input or system errors (e.g. file does not exist) /// ```rust /// # use pistonite_cu as cu; /// use cu::pre::*; @@ -45,9 +47,12 @@ pub use anyhow::{Context, Error, Ok, Result, anyhow as fmterr, bail}; /// - [`cu::unimplemented!`](macro@crate::unimplemented) /// and [`cu::unreachable!`](macro@crate::unreachable) /// that are similar to the std macros, but instead of `panic!`, they will `bail!` -/// - [`cu::ensure!`](macro@crate::ensure) is unlike `anyhow::ensure`, that +/// - [`cu::ensure!`](macro@crate::ensure) is an assert-like check for invariants that +/// should almost never fail if the program is correct (i.e. a debugging aid, like `assert!`, +/// but returns an error instead of panicking). Unlike `anyhow::ensure`, /// it evaluates to a `Result<()>` instead of generates a return. /// It also does not automatically generate debug information. +/// Do not use it to validate user input or check for system errors - use `check!` or `bail!` for those. /// - [`cu::some!`] checks an `Option` and returns `Ok(None)` if the option is `None`. /// /// Here are other `anyhow` re-exports that are less commonly used @@ -197,7 +202,14 @@ macro_rules! unreachable { }} } -/// Check if an expression is `true` +/// Check if an expression is `true`, as an assertion +/// +/// This is meant to be assert-like: the condition is an invariant that should +/// almost never fail if everything is correct, so a failure indicates a bug +/// (i.e. it's a debugging aid, like `assert!`, but returns an error instead of panicking). +/// For errors that are expected to happen at runtime, such as invalid user input +/// or system errors (e.g. file does not exist), use [`cu::check!`](macro@crate::check) +/// or `cu::bail!` with a descriptive message instead. /// /// Unlike `anyhow::ensure`, if the condition fail, this will generate an `Error` /// instead of returning an error directly, so you need to add a `?`. diff --git a/packages/copper/src/lib.rs b/packages/copper/src/lib.rs index f2cf304..75d20a9 100644 --- a/packages/copper/src/lib.rs +++ b/packages/copper/src/lib.rs @@ -82,6 +82,8 @@ extern crate self as cu; // --- Basic stuff (no feature needed) --- pub mod str; +#[cfg(feature = "regex")] +pub use str::Regex; pub use str::{ByteFormat, ZString}; mod env_var; diff --git a/packages/copper/src/str/mod.rs b/packages/copper/src/str/mod.rs index 715e20f..2e2b988 100644 --- a/packages/copper/src/str/mod.rs +++ b/packages/copper/src/str/mod.rs @@ -17,3 +17,8 @@ pub use path::PathExtension; // path macro just depends on the std path functions and does not require fs mod path_macro; + +#[cfg(feature = "regex")] +mod regex; +#[cfg(feature = "regex")] +pub use regex::Regex; diff --git a/packages/copper/src/str/regex.rs b/packages/copper/src/str/regex.rs new file mode 100644 index 0000000..9e73703 --- /dev/null +++ b/packages/copper/src/str/regex.rs @@ -0,0 +1,66 @@ +use cu::pre::*; + +/// Deserializable regex +#[derive(Clone)] +pub struct Regex(regex::Regex, String); +impl std::ops::Deref for Regex { + type Target = regex::Regex; + fn deref(&self) -> &Self::Target { + &self.0 + } +} +impl std::ops::DerefMut for Regex { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} +impl std::fmt::Display for Regex { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.0) + } +} +impl std::fmt::Debug for Regex { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "Regex({})", self.0) + } +} +impl Regex { + /// Get the string that this regex was created from + pub fn to_str(&self) -> &str { + &self.1 + } +} +impl From for regex::Regex { + fn from(value: Regex) -> Self { + value.0 + } +} + +#[cfg(feature = "serde")] +impl Serialize for Regex { + fn serialize(&self, s: S) -> Result { + s.serialize_str(&self.1) + } +} + +#[cfg(feature = "serde")] +impl<'de> Deserialize<'de> for Regex { + fn deserialize>(d: D) -> Result { + return d.deserialize_str(Visitor); + struct Visitor; + impl<'de> serde::de::Visitor<'de> for Visitor { + type Value = Regex; + fn expecting(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + write!(f, "a regular expression") + } + fn visit_str(self, v: &str) -> Result { + match regex::Regex::new(v) { + Err(e) => Err(serde::de::Error::custom(format!( + "invalid regular expression '{v}': {e}" + ))), + Ok(x) => Ok(Regex(x, v.to_string())), + } + } + } + } +} From 978f5fec300a721929b2a92a1d95683c77ab9d61 Mon Sep 17 00:00:00 2001 From: Pistonight Date: Tue, 29 Sep 2026 16:00:18 -0700 Subject: [PATCH 2/2] regex constructor --- packages/copper/src/str/regex.rs | 39 ++++++++++++++++++++++++-------- 1 file changed, 30 insertions(+), 9 deletions(-) diff --git a/packages/copper/src/str/regex.rs b/packages/copper/src/str/regex.rs index 9e73703..b3d0742 100644 --- a/packages/copper/src/str/regex.rs +++ b/packages/copper/src/str/regex.rs @@ -2,44 +2,62 @@ use cu::pre::*; /// Deserializable regex #[derive(Clone)] -pub struct Regex(regex::Regex, String); +pub struct Regex { + inner: regex::Regex, + + #[cfg(feature = "serde")] + lit: String, +} +impl Regex { + pub fn new(s: &str) -> cu::Result { + let inner = cu::check!(regex::Regex::new(s), "failed to build regex from {s:?}")?; + #[cfg(feature = "serde")] + let s = Self { + inner, + lit: s.to_string(), + }; + #[cfg(not(feature = "serde"))] + let s = Self { inner }; + Ok(s) + } +} impl std::ops::Deref for Regex { type Target = regex::Regex; fn deref(&self) -> &Self::Target { - &self.0 + &self.inner } } impl std::ops::DerefMut for Regex { fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.0 + &mut self.inner } } impl std::fmt::Display for Regex { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{}", self.0) + write!(f, "{}", self.lit) } } impl std::fmt::Debug for Regex { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "Regex({})", self.0) + write!(f, "Regex({})", self.inner) } } impl Regex { /// Get the string that this regex was created from pub fn to_str(&self) -> &str { - &self.1 + &self.lit } } impl From for regex::Regex { fn from(value: Regex) -> Self { - value.0 + value.inner } } #[cfg(feature = "serde")] impl Serialize for Regex { fn serialize(&self, s: S) -> Result { - s.serialize_str(&self.1) + s.serialize_str(&self.lit) } } @@ -58,7 +76,10 @@ impl<'de> Deserialize<'de> for Regex { Err(e) => Err(serde::de::Error::custom(format!( "invalid regular expression '{v}': {e}" ))), - Ok(x) => Ok(Regex(x, v.to_string())), + Ok(x) => Ok(Regex { + inner: x, + lit: v.to_string(), + }), } } }