string_types-macro

Crates.iostring_types-macro
lib.rsstring_types-macro
version0.13.0
created_at2025-07-12 23:13:45.367361+00
updated_at2025-07-21 22:30:12.137457+00
descriptionString newtypes
homepage
repositoryhttps://codeberg.org/gwadej/string_types
max_upload_size
id1749760
size20,672
G. Wade Johnson (gwadej)

documentation

README

string_types crate

One place where many developers are likely to use primitives even when they know they should have a more specific type is strings. In a large number of systems that I have maintained, a fair amount of code is devoted to verifying that strings that pass around the insides of the system are valid. Even if they are validated on input, they continue to be passed around as strings, so they are often revalidated because you never know whether this instance of the string is valid.

Defining a type for important kinds of strings is a pretty reasonable thing to do, but it normally doesn't happen because of an annoying amount of boilerplate needed to make happen. And it is usually only helpful when something goes wrong.

This is my attempt to make creating specific kinds of strings easier to validate and make type-safe. Unlike some other refined type libraries, this crate does not try to be all things for all people or be very flexible and generic. It just handles strings.

Goals

Easy to create String-based types. The fact that the type exists and can be used throughout a system is more useful than just validation. Easy to add functionality to one of the types. The type itself is just defined by a small number of traits. Adding new functionality can easily be handled by implementing more functions on top of the current traits.

Macro support is minimal just to remove boilerplate. There is nothing preventing the user from implementing the traits explicitly.

Traits

There are two traits that basically define everything needed to make a valid string type: StringType and EnsureValid. Actually using a StringType in your code may require implementing a few other traits. At a minimum, you will want Display. You may also want to define different From<T> and/or TryFrom<T> generic traits that are useful for your specific use cases.

Let's look at those traits:

pub trait StringType: EnsureValid + FromStr + std::fmt::Display {
    /// Type of the unnamed field that stores the data for the `StringType`
    type Inner;

    /// Attempt to construct a [`StringType`] newtype from a supplied string slice.
    ///
    /// Syntactic sugar around parse.
    fn try_new(s: &str) -> Result<Self, <Self as FromStr>::Err> {
        s.parse()
    }

    /// Attempt to construct an optional [`StringType`] newtype from a supplied string slice
    fn optional(s: &str) -> Option<Self> {
        s.parse().ok()
    }

    /// Return a string slice referencing the [`StringType`] inner value.
    fn as_str(&self) -> &str;

    /// Return the inner value
    fn to_inner(self) -> Self::Inner;
}

Notice that you only need to supply the Inner type definition, and the definitions of as_str and to_inner to implement this trait. (Even that is covered if you use the string_type attribute described later.) The Inner type simplifies other definitions, without you needing to keep return types and such consistent between different parts of code. The try_new and optional methods are utility functions that might simplify your usage of these types under certain circumstances.

The core features of this trait are as_str which returns an immutable string slice of the internals of the type (which allows you to use the type as a string without re-implementing all &str methods for each StringType. Likewise, to_inner consumes the current object returning the inner data.

The other trait is the one you will definitely need to implement: EnsureValid.

pub trait EnsureValid {
    /// Define the type of error that will be reported if the string is not valid.
    type ParseErr;

    /// Validate string slice
    fn ensure_valid(s: &str) -> Result<(), Self::ParseErr>;
}

This one is pretty self-explanatory. You need to supply a type that allows you to report validation failures from the workhorse method: ensure_valid. This method validates an incoming string slice without changing it, returning an error if the string is not valid for this type. This method should be pretty straight-forward to implement if you already need to validate string data.

There are a number of standard traits you will want to derive to make the StringTypes a bit more like Strings. These include Debug, Clone, Hash, Eq, Ord, PartialEq, and PartialOrd.

Macros

Once you have defined the EnsureValid trait, most of the rest of what StringType does is pretty much boilerplate. So, this crate supplies an attribute macro and a couple of derive macros to handle as much of this boilerplate as possible.

The string_type attribute macro supplies an implementation for the StringType trait and either one (inner type of String) or two From<Type> implementations so that you don't have to. In addition, the macro add the standard traits described earlier to any StringType you give the attributes Debug and Clone. You will still need to supply the comparison traits yourself.

Example

Let's assume that we wanted a type to represent inventory items in business. Let's say that these inventory ids are always 3 uppercase letters followed by 6 digits. The following would define an appropriate type-safe InventoryId.

use string_types::{Display, FromStr, ParseError, string_type};

#[derive(Display, FromStr, Eq, PartialEq, Hash, Ord, PartialOrd)]
#[string_type]
pub struct InventoryId(NonBlankString);

impl EnsureValid for InventoryId {
    type ParseErr = ParseError;

    fn ensure_valid(s: &str) -> Result<(), Self::ParseErr> {
        // length must be 9 characters
        if s.len() != 9 { return Err(ParseError::InvalidLength(s.len())); }

        // First 3 characters
        if let Some((i, c)) = s.chars()
                .enumerate()
                .take(3)
                .find(|(_i, c)| !c.is_ascii_lowercase()) {
            return Err(ParseError::UnexpectedCharacter(i, c));
        }

        if let Some((i, c)) = s.chars()
                .enumerate()
                .skip(3)
                .find(|(_i, c)| { !c.is_ascii_digit() }) {
            return Err(ParseError::NonDigit(i, c));
        }
        Ok(());
    }
}

This example uses the ParseError enumeration, but that isn't a requirement. By using the #[string_type] attribute, we get an implementation of the StringType trait, as well as implementing From<Username> for both String and NonBlankString. By deriving FromStr we get a reasonable FromStr implementation. Deriving from Display gives the default format definition.

That allows us to use this type as follows:

let item: InventoryId = "ABC123456".parse()?;
let opt_item = InventoryId::optional("CDE654321");

if "ABC123456" == item.as_str() {
    println!("Special item");
}

let item_str: String = item.into();

Features

There are two features that you can enable on this crate: book_ids and card_ids. These enable some extra string types. Both of these have extra dependencies, so they are only available if you ask for them.

book_ids feature

The book_ids feature enables the Isbn10, Isbn13, and Lccn. These string types represent strings that match different identifiers for printed matter.

  • Isbn10: a 10 character ISBN, guaranteed to be digits (possible X as last character) with the check digit verified.
  • Isbn13: a 13 character ISBN, guaranteed to be digits with the check digit verified.
  • Lccn: a valid Library of Congress Control Number.

card_ids feature

The card_ids feature enables the CreditCard string type. Verifies only digits in the string, that the length fits within the legal lengths for credit cards, and that the checksum validates.

Different credit cards around the world have different lengths and initial digits. This string type does not check for a particular credit card type and country.

serde feature

Adds the Serialize and Deserialize derivations to the StringTypes in the crate.

More Examples

Obviously, there are more potential string types than would be reasonable to include in the crate, especially since most of them would not be useful to most users of the crate. However, more examples might be useful as starting points for your own types. Some further examples will be provided in the examples directory. These can be used to spark ideas for your own types, or if I happened to provide a type that would solve your problem, feel free to copy the example definition into your project.

Alternatives

Some other crates have also been designed with similar goals in mind.

  • refined - powerful use of generics for refining types. I find refinement by generics to be a bit complicated for the kinds of uses I normally have. Seems more useful for other types than just strings.
  • nutype - seems powerful, much more macro-driven. Support for multiple types beyond strings seems to be generalizing more than I needed.
  • deranged - Focused on ranged integers.
  • refined_type - Focus on building up an individual definition using generics like refined above. I see this as different that types for their own sake.
  • refinement-types - much like refined and refined_type.

The string_types crate is focused on the kinds of strings I have needed to validate in the past. It only tries to make these types easy and does not try to generalize beyond that point.

License

This project is licensed under the MIT License (https://opensource.org/licenses/MIT).

Commit count: 0

cargo fmt