4 releases

0.1.0 Nov 24, 2024
0.0.3 Feb 11, 2025
0.0.2 Feb 2, 2025
0.0.1 Feb 2, 2025
0.0.0 Nov 24, 2024

#797 in Encoding

Download history 154/week @ 2024-11-19 48/week @ 2024-11-26 9/week @ 2024-12-03 4/week @ 2024-12-10 179/week @ 2025-01-28 55/week @ 2025-02-04 123/week @ 2025-02-11

357 downloads per month

MIT/Apache

73KB
1.5K SLoC

Refined

Crates.io Version Release Status Crates.io License

Simple refinement types for Rust.

A basic introduction to the library is available on my blog.

For detailed information, please see the documentation on docs.rs.

Quickstart

The basic usage example on docs.rs is a minimal example that should be easy to follow.

You can also use the examples to get started. Each example is a complete cargo project of its own. They are meant to be run with cargo run so that you can view their output and reference it against the code.


lib.rs:

Basic refinement types for the Rust standard library.

Refinement in this context is the process of imbuing types with predicates, allowing maintainers to see immediately that types must be constrained with certain invariants and ensuring that those invariants hold at run time. This allows types to be "narrowed" to a subset of their possible values. For a gentle introduction, you can refer to my blog post announcing the release of the library.

In addition to the [Predicate] implementations provided for the standard library, refined also provides a simple mechanism for defining your own refinement types.

Most users will be interested primarily in the [Refinement] struct, which allows a [Predicate] to be applied to values of a type and ensures that the predicate always holds.

Examples

In addition to the examples included here, you can also refer to the examples on GitHub for complete end-to-end examples that could you easily build and run yourself.

Basic usage

use refined::{Refinement, RefinementError, boundable::unsigned::{LessThanEqual, ClosedInterval}};

type FrobnicatorName = Refinement<String, ClosedInterval<1, 10>>;

type FrobnicatorSize = Refinement<u8, LessThanEqual<100>>;

#[derive(Debug)]
struct Frobnicator {
  name: FrobnicatorName,
  size: FrobnicatorSize
}

impl Frobnicator {
  pub fn new(name: String, size: u8) -> Result<Frobnicator, RefinementError> {
    let name = FrobnicatorName::refine(name)?;
    let size = FrobnicatorSize::refine(size)?;

    Ok(Self {
      name,
      size
    })
  }
}

assert!(Frobnicator::new("Good name".to_string(), 99).is_ok());
assert_eq!(Frobnicator::new("Bad name, too long".to_string(), 99).unwrap_err().to_string(),
           "refinement violated: must be greater than or equal to 1 and must be less than or equal to 10");
assert_eq!(Frobnicator::new("Good name".to_string(), 123).unwrap_err().to_string(),
           "refinement violated: must be less than or equal to 100");

Named refinement

As you can see in the error messages above, there are two possible fields that could have led to the error in refinement, but it isn't readily apparent which field caused the error by reading the error message. While this isn't a problem when using libraries like serde_path_to_error, this can be important functionality to have in your own error messages if you're using basic serde functionality.

If this is something that you need, consider using [NamedRefinement] instead of [Refinement].

use refined::{NamedRefinement, RefinementError, boundable::unsigned::{LessThanEqual, ClosedInterval}, type_string, TypeString};

type_string!(Name, "name");
type FrobnicatorName = NamedRefinement<Name, String, ClosedInterval<1, 10>>;

type_string!(Size, "size");
type FrobnicatorSize = NamedRefinement<Size, u8, LessThanEqual<100>>;

#[derive(Debug)]
struct Frobnicator {
  name: FrobnicatorName,
  size: FrobnicatorSize
}

impl Frobnicator {
  pub fn new(name: String, size: u8) -> Result<Frobnicator, RefinementError> {
    let name = FrobnicatorName::refine(name)?;
    let size = FrobnicatorSize::refine(size)?;

    Ok(Self {
      name,
      size
    })
  }
}

assert!(Frobnicator::new("Good name".to_string(), 99).is_ok());
assert_eq!(Frobnicator::new("Bad name, too long".to_string(), 99).unwrap_err().to_string(),
           "refinement violated: name must be greater than or equal to 1 and must be less than or equal to 10");
assert_eq!(Frobnicator::new("Good name".to_string(), 123).unwrap_err().to_string(),
           "refinement violated: size must be less than or equal to 100");

Serde support

use refined::{Refinement, boundable::unsigned::LessThan};
use serde::{Serialize, Deserialize};
use serde_json::{from_str, to_string};

#[derive(Debug, Serialize, Deserialize)]
struct Example {
  name: String,
  size: Refinement<u8, LessThan<100>>
}

let good: Result<Example, _> =  from_str(r#"{"name":"Good example","size":99}"#);
assert!(good.is_ok());
let bad: Result<Example, _> =  from_str(r#"{"name":"Bad example","size":123}"#);
assert!(bad.is_err());

Implication

Note that enabling incomplete_features and generic_const_exprs is required for the [Implies] trait bounds to be met.

#![allow(incomplete_features)]
#![feature(generic_const_exprs)]

use refined::{Refinement, boundable::unsigned::LessThan, Implies};

fn takes_lt_100(value: Refinement<u8, LessThan<100>>) -> String {
  format!("{}", value)
}

let lt_50: Refinement<u8, LessThan<50>> = Refinement::refine(49).unwrap();
let ex: Refinement<u8, LessThan<51>> = lt_50.imply();
let result = takes_lt_100(lt_50.imply());
assert_eq!(result, "49");

This design leads to some interesting emergent properties; for example, the "compatibility" of range comparison over equality is enforced at compile time:

#![allow(incomplete_features)]
#![feature(generic_const_exprs)]

use refined::{Refinement, boundable::unsigned::OpenInterval, Implies};

let bigger_range: Refinement<u8, OpenInterval<1, 100>> = Refinement::refine(50).unwrap();
let smaller_range: Refinement<u8, OpenInterval<25, 75>> = Refinement::refine(50).unwrap();
let incompatible_range: Refinement<u8, OpenInterval<101, 200>> = Refinement::refine(150).unwrap();
// assert_eq!(bigger_range, smaller_range); // Fails to compile, type mismatch
// assert_eq!(bigger_ragne, incompatible_range) // Fails to compile, invalid implication
assert_eq!(bigger_range, smaller_range.imply()); // Works!

Note that the order matters here; the smaller range refinement can be implied to the larger range, but the opposite is logically invalid.

Provided refinements

refined comes packaged with a large number of refinements over commonly used std types. The refinements are grouped into modules based on the type of refinement that they provide.

Here's a quick reference of what is currently available:

  • [UnsignedBoundable]: types that can be reduced to an unsigned size so that their size can be bounded. Examples include String, u8, u64, or any std container-like type that implements a len() method
  • [SignedBoundable]: types that can be reduced to a signed size so that their size can be bounded. Examples include i8, i64, and isize
  • [boolean]: "combinator" refinements that allow other refinements to be combined with one another. Examples include And and Or
  • [character]: refinements of [char]. Examples include IsLowercase and IsWhitespace
  • [string]: refinements of any type that implements AsRef<str>. Examples include Contains and Trimmed

Features

  • serde: enabled by default; allows [Refinement] to be serialized and deserialized using the serde library. This functionality was actually my main motivation for writing the crate in the first place, but technically the serde dependency is not required for the core functionality of the trait, so it can be disabled
  • implication: enabling implication allows the use of the [Implies] trait; this is behind an off-by-default feature because it requires generic_const_exprs, which is both unstable and incomplete. The functionality is very useful, but its stability cannot be guaranteed

Dependencies

~0.3–1MB
~21K SLoC