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