1#[derive(Debug, Clone, PartialEq, Eq)]
2pub struct Str {
3 inner: String,
4}
5
6unsafe impl Send for Str {}
7unsafe impl Sync for Str {}
8
9impl Str {
10 pub fn of(s: impl Into<String>) -> Self {
11 Self { inner: s.into() }
12 }
13
14 pub fn slug(mut self) -> Self {
15 self.inner = crate::string::slug(&self.inner, '-');
16 self
17 }
18
19 pub fn snake(mut self) -> Self {
20 self.inner = crate::string::to_snake_case(&self.inner);
21 self
22 }
23
24 pub fn camel(mut self) -> Self {
25 self.inner = crate::string::to_camel_case(&self.inner);
26 self
27 }
28
29 pub fn pascal(mut self) -> Self {
30 self.inner = crate::string::to_pascal_case(&self.inner);
31 self
32 }
33
34 pub fn kebab(mut self) -> Self {
35 self.inner = crate::string::to_kebab_case(&self.inner);
36 self
37 }
38
39 pub fn title(mut self) -> Self {
40 self.inner = crate::string::to_title_case(&self.inner);
41 self
42 }
43
44 pub fn upper(mut self) -> Self {
45 self.inner = crate::string::to_upper(&self.inner);
46 self
47 }
48
49 pub fn lower(mut self) -> Self {
50 self.inner = crate::string::to_lower(&self.inner);
51 self
52 }
53
54 pub fn trim(mut self) -> Self {
55 self.inner = self.inner.trim().to_string();
56 self
57 }
58
59 pub fn ltrim(mut self) -> Self {
60 self.inner = self.inner.trim_start().to_string();
61 self
62 }
63
64 pub fn rtrim(mut self) -> Self {
65 self.inner = self.inner.trim_end().to_string();
66 self
67 }
68
69 pub fn squish(mut self) -> Self {
70 self.inner = crate::string::squish(&self.inner);
71 self
72 }
73
74 pub fn truncate(mut self, limit: usize) -> Self {
75 self.inner = crate::string::truncate(&self.inner, limit);
76 self
77 }
78
79 pub fn reverse(mut self) -> Self {
80 self.inner = crate::string::reverse(&self.inner);
81 self
82 }
83
84 pub fn repeat(mut self, times: usize) -> Self {
85 self.inner = crate::string::repeat(&self.inner, times);
86 self
87 }
88
89 pub fn append(mut self, s: &str) -> Self {
90 self.inner.push_str(s);
91 self
92 }
93
94 pub fn prepend(mut self, s: &str) -> Self {
95 self.inner = format!("{}{}", s, self.inner);
96 self
97 }
98
99 pub fn replace(mut self, from: &str, to: &str) -> Self {
100 self.inner = self.inner.replace(from, to);
101 self
102 }
103
104 pub fn replace_first(mut self, from: &str, to: &str) -> Self {
105 self.inner = crate::string::replace_first(&self.inner, from, to);
106 self
107 }
108
109 pub fn replace_last(mut self, from: &str, to: &str) -> Self {
110 self.inner = crate::string::replace_last(&self.inner, from, to);
111 self
112 }
113
114 pub fn finish(mut self, cap: &str) -> Self {
115 self.inner = crate::string::finish(&self.inner, cap);
116 self
117 }
118
119 pub fn ensure_start(mut self, prefix: &str) -> Self {
120 self.inner = crate::string::ensure_start(&self.inner, prefix);
121 self
122 }
123
124 pub fn wrap(mut self, before: &str, after: &str) -> Self {
125 self.inner = crate::string::wrap(&self.inner, before, after);
126 self
127 }
128
129 pub fn pad_left(mut self, n: usize) -> Self {
130 self.inner = crate::string::pad_left(&self.inner, n, ' ');
131 self
132 }
133
134 pub fn pad_right(mut self, n: usize) -> Self {
135 self.inner = crate::string::pad_right(&self.inner, n, ' ');
136 self
137 }
138
139 pub fn pad_both(mut self, n: usize) -> Self {
140 self.inner = crate::string::pad_both(&self.inner, n, ' ');
141 self
142 }
143
144 pub fn mask(mut self, mask_char: char, from: usize) -> Self {
145 self.inner = crate::string::mask(&self.inner, mask_char, from);
146 self
147 }
148
149 pub fn escape_html(mut self) -> Self {
150 self.inner = escape_html_impl(&self.inner);
151 self
152 }
153
154 pub fn when(self, condition: bool, f: impl FnOnce(Self) -> Self) -> Self {
155 if condition {
156 f(self)
157 } else {
158 self
159 }
160 }
161
162 pub fn when_empty(self, f: impl FnOnce(Self) -> Self) -> Self {
163 if self.inner.is_empty() {
164 f(self)
165 } else {
166 self
167 }
168 }
169
170 pub fn when_not_empty(self, f: impl FnOnce(Self) -> Self) -> Self {
171 if !self.inner.is_empty() {
172 f(self)
173 } else {
174 self
175 }
176 }
177
178 pub fn when_contains(self, needle: &str, f: impl FnOnce(Self) -> Self) -> Self {
179 if self.inner.contains(needle) {
180 f(self)
181 } else {
182 self
183 }
184 }
185
186 pub fn when_starts_with(self, prefix: &str, f: impl FnOnce(Self) -> Self) -> Self {
187 if self.inner.starts_with(prefix) {
188 f(self)
189 } else {
190 self
191 }
192 }
193
194 pub fn when_ends_with(self, suffix: &str, f: impl FnOnce(Self) -> Self) -> Self {
195 if self.inner.ends_with(suffix) {
196 f(self)
197 } else {
198 self
199 }
200 }
201
202 pub fn tap(self, f: impl FnOnce(&str)) -> Self {
203 f(&self.inner);
204 self
205 }
206
207 pub fn pipe<F: FnOnce(String) -> String>(self, f: F) -> Self {
208 Self {
209 inner: f(self.inner),
210 }
211 }
212
213 #[allow(clippy::inherent_to_string)]
214 pub fn to_string(self) -> String {
215 self.inner
216 }
217
218 pub fn len(&self) -> usize {
219 self.inner.len()
220 }
221
222 pub fn is_empty(&self) -> bool {
223 self.inner.is_empty()
224 }
225
226 pub fn contains(&self, needle: &str) -> bool {
227 self.inner.contains(needle)
228 }
229
230 pub fn starts_with(&self, prefix: &str) -> bool {
231 self.inner.starts_with(prefix)
232 }
233
234 pub fn ends_with(&self, suffix: &str) -> bool {
235 self.inner.ends_with(suffix)
236 }
237
238 pub fn word_count(&self) -> usize {
239 crate::string::word_count(&self.inner)
240 }
241
242 pub fn to_base64(self) -> String {
243 to_base64(&self.inner)
244 }
245
246 pub fn split(self, delimiter: &str) -> Vec<String> {
247 self.inner.split(delimiter).map(|s| s.to_string()).collect()
248 }
249
250 pub fn exactly(&self, other: &str) -> bool {
251 self.inner == other
252 }
253
254 pub fn value(self) -> String {
255 self.inner
256 }
257}
258
259fn escape_html_impl(s: &str) -> String {
260 s.replace('&', "&")
261 .replace('<', "<")
262 .replace('>', ">")
263 .replace('"', """)
264 .replace('\'', "'")
265}
266
267pub fn to_base64(s: &str) -> String {
268 use base64::Engine;
269 base64::engine::general_purpose::STANDARD.encode(s.as_bytes())
270}
271
272#[cfg(feature = "json")]
273pub fn escape_html(s: &str) -> String {
274 escape_html_impl(s)
275}
276
277#[cfg(test)]
278mod tests {
279 use super::*;
280
281 #[test]
282 fn fluent_basic() {
283 let result = Str::of("hello world").trim().snake().value();
284 assert_eq!(result, "hello_world");
285 }
286
287 #[test]
288 fn fluent_when() {
289 let result = Str::of("hello").when(false, |s| s.append(" world")).value();
290 assert_eq!(result, "hello");
291
292 let result = Str::of("hello").when(true, |s| s.append(" world")).value();
293 assert_eq!(result, "hello world");
294 }
295
296 #[test]
297 fn fluent_when_empty() {
298 let result = Str::of("").when_empty(|s| s.append("default")).value();
299 assert_eq!(result, "default");
300
301 let result = Str::of("hello").when_empty(|s| s.append("default")).value();
302 assert_eq!(result, "hello");
303 }
304
305 #[test]
306 fn fluent_tap() {
307 let mut seen = String::new();
308 let _ = Str::of("test").tap(|s| seen.push_str(s));
309 assert_eq!(seen, "test");
310 }
311
312 #[test]
313 fn fluent_pipe() {
314 let result = Str::of("hello").pipe(|s| s.to_uppercase()).value();
315 assert_eq!(result, "HELLO");
316 }
317
318 #[test]
319 fn to_base64_basic() {
320 assert_eq!(to_base64("hello"), "aGVsbG8=");
321 }
322
323 #[test]
324 fn escape_html_basic() {
325 #[cfg(feature = "json")]
326 {
327 assert_eq!(escape_html("<div>"), "<div>");
328 }
329 }
330}