strck/lib.rs
1//! [![github-img]][github-url] [![crates-img]][crates-url] [![docs-img]][docs-url]
2//!
3//! [github-url]: https://github.com/QnnOkabayashi/strck
4//! [crates-url]: https://crates.io/crates/strck
5//! [docs-url]: crate
6//! [github-img]: https://img.shields.io/badge/github-8da0cb?style=for-the-badge&labelColor=555555&logo=github
7//! [crates-img]: https://img.shields.io/badge/crates.io-fc8d62?style=for-the-badge&labelColor=555555&logo=rust
8//! [docs-img]: https://img.shields.io/badge/docs.rs-66c2a5?style=for-the-badge&labelColor=555555&logoColor=white&logo=data:image/svg+xml;base64,PHN2ZyByb2xlPSJpbWciIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgdmlld0JveD0iMCAwIDUxMiA1MTIiPjxwYXRoIGZpbGw9IiNmNWY1ZjUiIGQ9Ik00ODguNiAyNTAuMkwzOTIgMjE0VjEwNS41YzAtMTUtOS4zLTI4LjQtMjMuNC0zMy43bC0xMDAtMzcuNWMtOC4xLTMuMS0xNy4xLTMuMS0yNS4zIDBsLTEwMCAzNy41Yy0xNC4xIDUuMy0yMy40IDE4LjctMjMuNCAzMy43VjIxNGwtOTYuNiAzNi4yQzkuMyAyNTUuNSAwIDI2OC45IDAgMjgzLjlWMzk0YzAgMTMuNiA3LjcgMjYuMSAxOS45IDMyLjJsMTAwIDUwYzEwLjEgNS4xIDIyLjEgNS4xIDMyLjIgMGwxMDMuOS01MiAxMDMuOSA1MmMxMC4xIDUuMSAyMi4xIDUuMSAzMi4yIDBsMTAwLTUwYzEyLjItNi4xIDE5LjktMTguNiAxOS45LTMyLjJWMjgzLjljMC0xNS05LjMtMjguNC0yMy40LTMzLjd6TTM1OCAyMTQuOGwtODUgMzEuOXYtNjguMmw4NS0zN3Y3My4zek0xNTQgMTA0LjFsMTAyLTM4LjIgMTAyIDM4LjJ2LjZsLTEwMiA0MS40LTEwMi00MS40di0uNnptODQgMjkxLjFsLTg1IDQyLjV2LTc5LjFsODUtMzguOHY3NS40em0wLTExMmwtMTAyIDQxLjQtMTAyLTQxLjR2LS42bDEwMi0zOC4yIDEwMiAzOC4ydi42em0yNDAgMTEybC04NSA0Mi41di03OS4xbDg1LTM4Ljh2NzUuNHptMC0xMTJsLTEwMiA0MS40LTEwMi00MS40di0uNmwxMDItMzguMiAxMDIgMzguMnYuNnoiPjwvcGF0aD48L3N2Zz4K
9//!
10//! Checked owned and borrowed strings.
11//!
12//! # Overview
13//!
14//! The Rust standard library provides the `String` and `str` types, which wrap
15//! `Vec<u8>` and `[u8]` respectively, with the invariant that the contents
16//! are valid UTF-8.
17//!
18//! This crate abstracts the idea of type-level invariants on strings by
19//! introducing the immutable [`Check`] and [`Ck`] types, where the invariants
20//! are determined by a generic [`Invariant`] type parameter. It offers
21//! [`UnicodeIdent`](crate::ident::unicode::UnicodeIdent)
22//! and [`RustIdent`](crate::ident::rust::RustIdent) [`Invariant`]s,
23//! which are enabled by the `ident` feature flag.
24//!
25//! "strck" comes from "str check", similar to how rustc has typeck and
26//! borrowck for type check and borrow check respectively.
27//!
28//! # Motivation
29//!
30//! Libraries working with string-like types with certain properties, like identifiers,
31//! quickly become confusing as `&str` and `String` begin to pollute type signatures
32//! everywhere. One solution is to manually implement an owned checked string type
33//! like [`syn::Ident`] to disambiguate the type signatures and validate the string.
34//! The downside is that new values cannot be created without allocation,
35//! which is unnecessary when only a borrowed version is required.
36//!
37//! `strck` solves this issue by providing a checked borrowed string type, [`Ck`],
38//! alongside a checked owned string type, [`Check`]. These serve as thin wrappers
39//! around `str` and `String`[^1] respectively, and prove at the type level that
40//! the contents satisfy the [`Invariant`] that the wrapper is generic over.
41//!
42//! [^1]: [`Check`] can actually be backed by any `'static + AsRef<str>` type,
43//! but `String` is the default.
44//!
45//! # Use cases
46//!
47//! ### Checked strings without allocating
48//!
49//! The main benefit `strck` offers is validating borrowed strings via the
50//! [`Ck`] type without having to allocate in the result.
51//!
52//! ```rust
53//! use strck::{Ck, IntoCk, ident::rust::RustIdent};
54//!
55//! let this_ident: &Ck<RustIdent> = "this".ck().unwrap();
56//! ```
57//!
58//! ### Checked zero-copy deserialization
59//!
60//! When the `serde` feature flag is enabled, [`Ck`]s can be used to perform
61//! checked zero-copy deserialization, which requires the
62//! [`#[serde(borrow)]`][borrow] attribute.
63//!
64//! ```rust
65//! # use serde::{Serialize, Deserialize};
66//! use strck::{Ck, ident::unicode::UnicodeIdent};
67//!
68//! #[derive(Serialize, Deserialize)]
69//! struct Player<'a> {
70//! #[serde(borrow)]
71//! username: &'a Ck<UnicodeIdent>,
72//! level: u32,
73//! }
74//! ```
75//!
76//! Note that this code sample explicitly uses `Ck<UnicodeIdent>` to demonstrate
77//! that the type is a [`Ck`]. However, `strck` provides [`Ident`] as an
78//! alias for `Ck<UnicodeIdent>`, which should be used in practice.
79//!
80//! ### Infallible parsing
81//!
82//! For types where string validation is relatively cheap but parsing is costly
83//! and fallible, `strck` can be used with a custom [`Invariant`] as an input to
84//! make an infallible parsing function.
85//!
86//! # Postfix construction with `IntoCk` and `IntoCheck`
87//!
88//! This crate exposes two helper traits, [`IntoCk`] and [`IntoCheck`]. When in
89//! scope, the [`.ck()`] and [`.check()`] functions can be used to create
90//! [`Ck`]s and [`Check`]s respectively:
91//!
92//! ```rust
93//! use strck::{IntoCheck, IntoCk, ident::unicode::UnicodeIdent};
94//!
95//! let this_ident = "this".ck::<UnicodeIdent>().unwrap();
96//! let this_foo_ident = format!("{}_foo", this_ident).check::<UnicodeIdent>().unwrap();
97//! ```
98//!
99//! # Feature flags
100//!
101//! * `serde`: Implements `Serialize`/`Deserialize` for [`Check`]s and [`Ck`]s,
102//! where the invariants are checked during deserialization. Disabled by default.
103//!
104//! [`syn::Ident`]: https://docs.rs/syn/latest/syn/struct.Ident.html
105//! [`Ident`]: https://docs.rs/strck/latest/strck/ident/unicode/type.Ident.html
106//! [borrow]: https://serde.rs/lifetimes.html#borrowing-data-in-a-derived-impl
107//! [`.ck()`]: IntoCk::ck
108//! [`.check()`]: IntoCheck::check
109use core::{borrow, cmp, fmt, hash, marker, ops, str};
110
111#[cfg(feature = "ident")]
112pub mod ident;
113mod partial_eq;
114#[cfg(feature = "serde")]
115mod serde;
116
117/// Owned immutable string with invariants.
118///
119/// Similar to how `String` derefs to `&str`, [`Check`] derefs to [`&Ck`](Ck).
120/// This means APIs requiring `&Check<I>` as an argument should instead consider
121/// accepting `&Ck<I>` for more flexibility.
122///
123/// # Buffers
124///
125/// By default, this type is backed by a `String`, but it can also be backed by
126/// any `AsRef<str> + 'static` type. In particular, types like [`SmolStr`] are
127/// good candidates since they're designed to be immutable.
128///
129/// It's recommended to use a type alias when using a custom backing type, since
130/// extra generics can make the type signature long.
131///
132/// [`SmolStr`]: https://docs.rs/smol_str/latest/smol_str/struct.SmolStr.html
133#[derive(Clone)]
134#[repr(transparent)]
135pub struct Check<I: Invariant, B: AsRef<str> + 'static = String> {
136 _marker: marker::PhantomData<I>,
137 buf: B,
138}
139
140/// Borrowed immutable string with invariants.
141///
142/// [`Ck`] is a DST, and therefore must always live behind a pointer. This means
143/// you'll usually see it as `&Ck<I>` in type signatures.
144///
145/// # Deserialization
146///
147/// See the [crate-level documentation] for details on how to use [`Ck`] for
148/// checked zero-copy deserialization.
149///
150/// [crate-level documentation]: crate#checked-zero-copy-deserialization
151#[repr(transparent)]
152pub struct Ck<I: Invariant> {
153 _marker: marker::PhantomData<I>,
154 slice: str,
155}
156
157/// Invariant for a [`Ck`] or [`Check`].
158///
159/// The [`Ck`] and [`Check`] types are checked strings types that make guarantees
160/// about the contents of the string. These guarantees are determined by this
161/// trait, `Invariant` which distinguishes whether or not a string upholds some
162/// arbitrary invariants via the [`Invariant::check`] function. If the `Err` is
163/// returned, then the invariant is broken, and the `Ck` or `Check` generic over
164/// the invariant cannot be constructed.
165///
166/// # Examples
167///
168/// Declaring an invariant that the string contains no whitespace:
169/// ```rust
170/// # use strck::Invariant;
171/// struct NoWhitespace;
172///
173/// impl Invariant for NoWhitespace {
174/// type Error = char;
175///
176/// fn check(slice: &str) -> Result<(), Self::Error> {
177/// match slice.chars().find(|ch| ch.is_whitespace()) {
178/// Some(ch) => Err(ch),
179/// None => Ok(()),
180/// }
181/// }
182/// }
183/// ```
184pub trait Invariant: Sized {
185 /// The type returned in the event that an invariant is broken.
186 ///
187 /// When formatting, `Error` should not be capitalized and should not end
188 /// with a period.
189 type Error: fmt::Display;
190
191 /// Returns `Ok` if the string upholds the invariant, otherwise `Err`.
192 ///
193 /// This function is used internally in [`Check::from_buf`] and [`Ck::from_slice`].
194 fn check(slice: &str) -> Result<(), Self::Error>;
195}
196
197/// Conversion into a [`Ck`].
198pub trait IntoCk: Sized + AsRef<str> {
199 /// Returns a validated [`Ck`] borrowing from `self`.
200 ///
201 /// # Examples
202 ///
203 /// Creating an Rust ident containing `this`:
204 /// ```rust
205 /// use strck::{IntoCk, ident::rust::Ident};
206 ///
207 /// let this_ident: &Ident = "this".ck().unwrap();
208 /// ```
209 fn ck<I: Invariant>(&self) -> Result<&Ck<I>, I::Error>;
210}
211
212impl<T: AsRef<str>> IntoCk for T {
213 fn ck<I: Invariant>(&self) -> Result<&Ck<I>, I::Error> {
214 Ck::from_slice(self.as_ref())
215 }
216}
217
218/// Conversion into a [`Check`].
219pub trait IntoCheck: Sized + AsRef<str> + 'static {
220 /// Returns a validated [`Check`] owning `self`.
221 ///
222 /// Note that [`Check`] uses the input of [`IntoCheck::check`] as its backing
223 /// storage, meaning that `"this".check()` will return a `Check<I, &'static str>`.
224 /// Although this is technically valid, it's _strongly_ recommended to use
225 /// [`Ck`] for string slices instead to avoid confusion.
226 ///
227 /// # Examples
228 ///
229 /// Creating a Unicode ident from a formatted string:
230 /// ```rust
231 /// use strck::{Check, Ck, IntoCheck, ident::unicode::UnicodeIdent};
232 ///
233 /// fn wrapper_name(name: &Ck<UnicodeIdent>) -> Check<UnicodeIdent> {
234 /// format!("lil_{name}").check().unwrap()
235 /// }
236 /// ```
237 fn check<I: Invariant>(self) -> Result<Check<I, Self>, I::Error>;
238}
239
240impl<T: AsRef<str> + 'static> IntoCheck for T {
241 fn check<I: Invariant>(self) -> Result<Check<I, Self>, I::Error> {
242 Check::from_buf(self)
243 }
244}
245
246// impl Check
247
248impl<I: Invariant, B: AsRef<str>> Check<I, B> {
249 /// Returns an `Ok` if the buffer upholds the invariants, otherwise `Err`.
250 pub fn from_buf(buf: B) -> Result<Self, I::Error> {
251 I::check(buf.as_ref())?;
252
253 Ok(Check {
254 _marker: marker::PhantomData,
255 buf,
256 })
257 }
258
259 /// Returns a [`&Ck`](Ck) that borrows from `self`.
260 pub fn as_ck(&self) -> &Ck<I> {
261 // SAFETY: `self` has the same invariants as `&Ck<I>`, and `Ck` has the
262 // same ABI as `str` by `#[repr(transparent)]`.
263 unsafe { core::mem::transmute(self.buf.as_ref()) }
264 }
265
266 /// Returns the inner representation.
267 pub fn into_inner(self) -> B {
268 self.buf
269 }
270}
271
272impl<I, B> fmt::Debug for Check<I, B>
273where
274 I: Invariant,
275 B: AsRef<str> + fmt::Debug,
276{
277 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
278 fmt::Debug::fmt(&self.buf, f)
279 }
280}
281
282impl<I, B1, B2> PartialEq<Check<I, B2>> for Check<I, B1>
283where
284 I: Invariant,
285 B1: AsRef<str>,
286 B2: AsRef<str>,
287{
288 fn eq(&self, other: &Check<I, B2>) -> bool {
289 self.as_str() == other.as_str()
290 }
291}
292
293impl<I, B1, B2> PartialOrd<Check<I, B2>> for Check<I, B1>
294where
295 I: Invariant,
296 B1: AsRef<str>,
297 B2: AsRef<str>,
298{
299 fn partial_cmp(&self, other: &Check<I, B2>) -> Option<cmp::Ordering> {
300 self.as_ck().partial_cmp(other.as_ck())
301 }
302}
303
304impl<I: Invariant, B: AsRef<str>> Eq for Check<I, B> {}
305
306impl<I: Invariant, B: AsRef<str>> Ord for Check<I, B> {
307 fn cmp(&self, other: &Self) -> cmp::Ordering {
308 self.as_ck().cmp(other.as_ck())
309 }
310}
311
312impl<I: Invariant, B: AsRef<str>> hash::Hash for Check<I, B> {
313 fn hash<H: hash::Hasher>(&self, state: &mut H) {
314 self.as_str().hash(state);
315 }
316}
317
318impl<I: Invariant, B: AsRef<str>> ops::Deref for Check<I, B> {
319 type Target = Ck<I>;
320
321 fn deref(&self) -> &Self::Target {
322 self.as_ck()
323 }
324}
325
326impl<I: Invariant, B: AsRef<str>> AsRef<Ck<I>> for Check<I, B> {
327 fn as_ref(&self) -> &Ck<I> {
328 self.as_ck()
329 }
330}
331
332impl<I: Invariant, B: AsRef<str>> AsRef<str> for Check<I, B> {
333 fn as_ref(&self) -> &str {
334 self.as_str()
335 }
336}
337
338impl<I: Invariant, B: AsRef<str>> borrow::Borrow<Ck<I>> for Check<I, B> {
339 fn borrow(&self) -> &Ck<I> {
340 self.as_ck()
341 }
342}
343
344impl<I: Invariant, B: AsRef<str>> fmt::Display for Check<I, B> {
345 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
346 fmt::Display::fmt(self.as_str(), f)
347 }
348}
349
350impl<'a, I, B> From<&'a Ck<I>> for Check<I, B>
351where
352 I: Invariant,
353 B: AsRef<str> + From<&'a str>,
354{
355 fn from(check: &'a Ck<I>) -> Self {
356 check.to_check()
357 }
358}
359
360impl<I, B> str::FromStr for Check<I, B>
361where
362 I: Invariant,
363 for<'a> B: AsRef<str> + From<&'a str>,
364{
365 type Err = I::Error;
366
367 fn from_str(s: &str) -> Result<Self, Self::Err> {
368 Ok(s.ck()?.to_check())
369 }
370}
371
372// impl Ck
373
374impl<I: Invariant> Ck<I> {
375 /// Returns an `Ok` if the `&str` upholds the invariants, otherwise `Err`.
376 pub fn from_slice(slice: &str) -> Result<&Self, I::Error> {
377 I::check(slice)?;
378
379 // SAFETY: invariants are upheld, and `Ck` has the same ABI as `str` by `#[repr(transparent)]`.
380 unsafe { Ok(core::mem::transmute::<&str, &Ck<I>>(slice)) }
381 }
382
383 /// Returns an owned [`Check`] from `&self`.
384 pub fn to_check<'a, B>(&'a self) -> Check<I, B>
385 where
386 B: AsRef<str> + From<&'a str>,
387 {
388 Check {
389 _marker: marker::PhantomData,
390 buf: self.as_str().into(),
391 }
392 }
393
394 /// Returns the `&str` representation.
395 pub fn as_str(&self) -> &str {
396 &self.slice
397 }
398}
399
400impl<I: Invariant> fmt::Debug for Ck<I> {
401 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
402 fmt::Debug::fmt(&self.slice, f)
403 }
404}
405
406impl<I: Invariant> PartialEq for Ck<I> {
407 fn eq(&self, other: &Self) -> bool {
408 self.as_str() == other.as_str()
409 }
410}
411
412impl<I: Invariant> PartialOrd for Ck<I> {
413 fn partial_cmp(&self, other: &Self) -> Option<cmp::Ordering> {
414 Some(self.cmp(other))
415 }
416}
417
418impl<I: Invariant> Eq for Ck<I> {}
419
420impl<I: Invariant> Ord for Ck<I> {
421 fn cmp(&self, other: &Self) -> cmp::Ordering {
422 self.as_str().cmp(other.as_str())
423 }
424}
425
426impl<I: Invariant> hash::Hash for Ck<I> {
427 fn hash<H: hash::Hasher>(&self, state: &mut H) {
428 self.as_str().hash(state);
429 }
430}
431
432impl<I: Invariant> AsRef<str> for Ck<I> {
433 fn as_ref(&self) -> &str {
434 self.as_str()
435 }
436}
437
438impl<I: Invariant> borrow::Borrow<str> for Ck<I> {
439 fn borrow(&self) -> &str {
440 self.as_str()
441 }
442}
443
444impl<I: Invariant> ToOwned for Ck<I> {
445 type Owned = Check<I>;
446
447 fn to_owned(&self) -> Self::Owned {
448 self.to_check()
449 }
450}
451
452impl<I: Invariant> fmt::Display for Ck<I> {
453 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
454 fmt::Display::fmt(self.as_str(), f)
455 }
456}
457
458impl<'a, I: Invariant, B: AsRef<str>> From<&'a Check<I, B>> for &'a Ck<I> {
459 fn from(check: &'a Check<I, B>) -> Self {
460 check.as_ck()
461 }
462}
463
464impl<'a, I: Invariant> TryFrom<&'a str> for &'a Ck<I> {
465 type Error = I::Error;
466
467 fn try_from(slice: &'a str) -> Result<Self, Self::Error> {
468 Ck::from_slice(slice)
469 }
470}
471
472#[cfg(test)]
473mod tests {
474 use super::*;
475
476 /// Test invariant.
477 struct NoInvariant;
478
479 impl Invariant for NoInvariant {
480 type Error = core::convert::Infallible;
481
482 fn check(_slice: &str) -> Result<(), Self::Error> {
483 Ok(())
484 }
485 }
486
487 #[test]
488 fn test_debug_impl() {
489 let this = "this".ck::<NoInvariant>().unwrap();
490 let fmt_debug = format!("{this:?}");
491
492 assert_eq!(fmt_debug, "\"this\"");
493 }
494
495 #[test]
496 fn test_ck_partial_eq() {
497 let this = "this".ck::<NoInvariant>().unwrap();
498 let still_this = "this".ck::<NoInvariant>().unwrap();
499 let other = "other".ck::<NoInvariant>().unwrap();
500
501 // With other different instance
502 assert_ne!(this, other);
503 assert_ne!(this, &other);
504 assert_ne!(&this, other);
505 assert_ne!(&this, &other);
506
507 // With other equal instance
508 assert_eq!(this, still_this);
509 assert_eq!(this, &still_this);
510 assert_eq!(&this, still_this);
511 assert_eq!(&this, &still_this);
512
513 // With itself
514 assert_eq!(this, this);
515 assert_eq!(this, &this);
516 assert_eq!(&this, this);
517 assert_eq!(&this, &this);
518 }
519
520 #[test]
521 fn test_check_partial_eq() {
522 let this = "this".check::<NoInvariant>().unwrap();
523 let still_this = "this".check::<NoInvariant>().unwrap();
524 let other = "other".check::<NoInvariant>().unwrap();
525
526 // With other different instance
527 assert_ne!(this, other);
528 assert_ne!(this, &other);
529 assert_ne!(&this, other);
530 assert_ne!(&this, &other);
531
532 // With other equal instance
533 assert_eq!(this, still_this);
534 assert_eq!(this, &still_this);
535 assert_eq!(&this, still_this);
536 assert_eq!(&this, &still_this);
537
538 // With itself
539 assert_eq!(this, this);
540 assert_eq!(this, &this);
541 assert_eq!(&this, this);
542 assert_eq!(&this, &this);
543 }
544}