Crates.io | string_types |
lib.rs | string_types |
version | 0.13.0 |
created_at | 2025-07-12 23:43:43.282982+00 |
updated_at | 2025-07-21 22:30:20.661049+00 |
description | String newtypes |
homepage | |
repository | https://codeberg.org/gwadej/string_types |
max_upload_size | |
id | 1749770 |
size | 125,334 |
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.
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.
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 StringType
s a bit more
like String
s. These include Debug
, Clone
, Hash
, Eq
, Ord
, PartialEq
, and PartialOrd
.
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.
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();
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.
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.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.
Adds the Serialize
and Deserialize
derivations to the StringType
s in the crate.
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.
Some other crates have also been designed with similar goals in mind.
refined
above. I see this as different that types for their own
sake.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.
This project is licensed under the MIT License (https://opensource.org/licenses/MIT).