url_cleaner_engine/glue/regex.rs
1//! A lazily compiled wrapper around [`Regex`].
2
3use std::str::FromStr;
4use std::sync::OnceLock;
5
6use serde::{Serialize, Deserialize};
7use regex::Regex;
8
9use crate::util::*;
10
11pub mod regex_parts;
12pub use regex_parts::*;
13
14/// A lazily compiled [`Regex`].
15/// # Examples
16/// ```
17/// use url_cleaner_engine::glue::*;
18///
19/// let regex = RegexWrapper::from("abc");
20/// assert!(regex.get().unwrap().is_match("abc"));
21///
22/// // Trying to set flags like `/.../i` isn't supported by the regex crate, and so isn't supported here.
23/// assert_eq!(RegexWrapper::from("/abc/i").parts().pattern, "/abc/i");
24/// ```
25#[derive(Clone, Debug, Serialize, Deserialize, Suitability)]
26#[serde(from = "RegexParts", into = "RegexParts")]
27pub struct RegexWrapper {
28 /// The lazily initialized [`Regex`].
29 #[suitable(always)]
30 regex: OnceLock<Regex>,
31 /// The [`RegexParts`] to compile [`Self::regex`] from.
32 parts: RegexParts
33}
34
35impl RegexWrapper {
36 /// Gets the [`RegexParts`] this uses to compile its [`Regex`].
37 /// # Examples
38 /// ```
39 /// use url_cleaner_engine::glue::*;
40 ///
41 /// let regex = RegexWrapper::from(".*=.*");
42 /// assert_eq!(regex.parts(), &RegexParts {pattern: ".*=.*".into(), config: Default::default()});
43 /// ```
44 pub fn parts(&self) -> &RegexParts {
45 &self.parts
46 }
47
48 /// Get the compiled [`Regex`] if it's been compiled.
49 /// # Examples
50 /// ```
51 /// use url_cleaner_engine::glue::*;
52 ///
53 /// let regex = RegexWrapper::from(".*=.*");
54 /// assert!(regex.get_no_compile().is_none());
55 /// regex.get().unwrap();
56 /// assert!(regex.get_no_compile().is_some());
57 /// ```
58 pub fn get_no_compile(&self) -> Option<&Regex> {
59 self.regex.get()
60 }
61
62 /// Gets the compiled [`Regex`] or, if it hasn't been compiled, compiles it.
63 ///
64 /// Currently, if a call to [`Self::get`] returns an error, the next call will attempt to compile the [`Regex`] *again*.
65 ///
66 /// Given this should be pretty rare and the cost of storing the error is pretty high, I choose to consider a reasonable tradeoff.
67 /// # Errors
68 /// If the cache is unset and the call to [`RegexParts::build`] returns an error, that error is returned.
69 /// # Examples
70 /// ```
71 /// use url_cleaner_engine::glue::*;
72 ///
73 /// let regex = RegexWrapper::from(".*=.*");
74 /// assert!(regex.get_no_compile().is_none());
75 /// regex.get().unwrap();
76 /// assert!(regex.get_no_compile().is_some());
77 /// ```
78 pub fn get(&self) -> Result<&Regex, regex::Error> {
79 if let Some(regex) = self.regex.get() {
80 Ok(regex)
81 } else {
82 let temp = self.parts.build()?;
83 Ok(self.regex.get_or_init(|| temp))
84 }
85 }
86}
87
88impl From<RegexParts> for RegexWrapper {
89 fn from(parts: RegexParts) -> Self {
90 Self {
91 regex: OnceLock::new(),
92 parts
93 }
94 }
95}
96
97impl FromStr for RegexWrapper {
98 type Err = std::convert::Infallible;
99
100 /// Creates a new [`RegexParts`] and uses that.
101 ///
102 /// The regex is only compiled later.
103 fn from_str(s: &str) -> Result<Self, Self::Err> {
104 Ok(RegexParts::from(s).into())
105 }
106}
107
108impl From<&str> for RegexWrapper {
109
110 /// Creates a new [`RegexParts`] and uses that.
111 ///
112 /// The regex is only compiled later.
113 fn from(s: &str) -> Self {
114 RegexParts::from(s).into()
115 }
116}
117
118impl PartialEq for RegexWrapper {
119 /// Whether or not `self` and/or `other` have their [`Regex`] cached is not considered.
120 fn eq(&self, other: &Self) -> bool {
121 self.parts.eq(&other.parts)
122 }
123}
124impl Eq for RegexWrapper {}
125
126impl From<RegexWrapper> for RegexParts {
127 fn from(value: RegexWrapper) -> Self {
128 value.parts
129 }
130}
131
132impl AsRef<RegexParts> for RegexWrapper {
133 fn as_ref(&self) -> &RegexParts {
134 &self.parts
135 }
136}
137
138impl TryFrom<RegexWrapper> for Regex {
139 type Error = regex::Error;
140
141 /// Does not re-compile or clone the [`Regex`] if it's already cached. It simply takes it.
142 fn try_from(value: RegexWrapper) -> Result<Self, Self::Error> {
143 if let Some(regex) = value.regex.into_inner() {
144 Ok(regex)
145 } else {
146 value.parts.build()
147 }
148 }
149}