string_types 0.13.0

String newtypes
Documentation
# 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:

```rust
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`.

```rust
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`.

## 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`.

```rust
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:

```rust
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 `StringType`s 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]https://crates.io/crates/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]https://github.com/greyblake/nutype - seems powerful, much more macro-driven. Support
  for multiple types beyond strings seems to be generalizing more than I needed.
- [deranged]https://github.com/jhpratt/deranged - Focused on ranged integers.
- [refined_type]https://crates.io/crates/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]https://crates.io/crates/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](./LICENSE.txt) (https://opensource.org/licenses/MIT).