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:
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.
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, Clone, Hash, Eq, Ord, PartialEq, and PartialOrd.
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 ;
;
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 = optional;
if "ABC123456" == item.as_str
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.
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
refinedabove. I see this as different that types for their own sake. - refinement-types - much like
refinedandrefined_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).