| Crates.io | subtype_rs |
| lib.rs | subtype_rs |
| version | 0.2.1 |
| created_at | 2025-07-30 19:55:12.939726+00 |
| updated_at | 2025-10-14 12:24:16.825306+00 |
| description | Ada‑style subtype newtype library |
| homepage | https://github.com/Feralthedogg/subtype_rs |
| repository | https://github.com/Feralthedogg/subtype_rs |
| max_upload_size | |
| id | 1773979 |
| size | 13,991 |
A lightweight, no-std Rust crate for defining numeric subtypes with enforced minimum and maximum bounds—validated at compile time (constants/type-fit, range sanity) and runtime (constructor checks). As of v0.2.1, the #[subtype] attribute also supports C-like enums with an explicit integer #[repr(..)].
[dependencies]
subtype_rs = "0.2.1"
cargo add subtype_rs
Apply the attribute to a tuple struct with a single primitive integer field.
Note: As of v0.2.1, the macro auto-adds
#[repr(transparent)]if you forgot it.
use subtype_rs::subtype;
#[subtype(min = 0u8, max = 100u8)]
#[repr(transparent)]
#[derive(Copy, Clone, Eq, PartialEq, Debug)]
pub struct Percentage(u8);
fn main() {
// Runtime-checked constructor
let p = Percentage::new(85).unwrap();
println!("{}%", p);
// Out of bounds -> error
let err = Percentage::new(150).unwrap_err();
println!("Error: {:?}", err);
// Constants are type-checked at compile time
assert_eq!(Percentage::MIN, 0u8);
assert_eq!(Percentage::MAX, 100u8);
}
TryFrom & FromThe macro implements TryFrom<Inner> and From<Subtype> for you:
use std::convert::TryFrom;
use subtype_rs::{subtype, SubtypeError};
#[subtype(min = 0u32, max = 100u32)]
pub struct Percent(u32);
fn main() {
let ok = Percent::try_from(42u32);
let bad: Result<Percent, SubtypeError<u32>> = Percent::try_from(200u32);
let inner: u32 = Percent::new(50).unwrap().into();
assert_eq!(inner, 50);
}
Use either error(MyErrorPath) or error = "my_crate::MyErrorPath".
Your error type must implement From<subtype_rs::SubtypeError<Inner>>.
use subtype_rs::{subtype, SubtypeError};
#[derive(Debug)]
pub enum MyError {
TooSmall(u32),
TooLarge(u32),
}
impl From<SubtypeError<u32>> for MyError {
fn from(e: SubtypeError<u32>) -> Self {
match e {
SubtypeError::BelowMinimum(v) => MyError::TooSmall(v),
SubtypeError::AboveMaximum(v) => MyError::TooLarge(v),
}
}
}
#[subtype(min = 0u32, max = 100u32, error(MyError))]
pub struct Percent(u32);
fn main() {
let e = Percent::new(150).unwrap_err(); // MyError::TooLarge(150)
println!("{:?}", e);
}
You can pass negative or suffixed literals using strings (parsed as expressions):
#[subtype(min = "-40i32", max = "120i32")]
pub struct Temperature(i32);
#[subtype(min = 0u32, max = 999u32)]
pub struct Tagged<T>(u32); // OK: concrete inner type
// ❌ Not allowed (bare type parameter as field):
// #[subtype(min = 0, max = 100)]
// pub struct Invalid<T>(T);
Attach #[subtype] to an enum that has an explicit primitive integer #[repr(..)].
If all discriminants are integer literals forming a contiguous range, the macro:
const MIN / const MAX (of the enum’s repr),TryFrom<repr> using a bounds check, then an optimized unsafe transmute (sound after the check).use subtype_rs::subtype;
#[repr(u8)]
#[subtype]
enum Code { A = 1, B = 2, C = 3 }
fn main() {
assert_eq!(u8::from(Code::B), 2);
assert_eq!(Code::B.to_repr(), 2);
assert_eq!(format!("{}", Code::B), "2");
use core::convert::TryFrom;
assert_eq!(Code::MIN, 1u8);
assert_eq!(Code::MAX, 3u8);
assert!(Code::try_from(2u8).is_ok());
assert!(Code::try_from(5u8).is_err());
}
If the enum has holes or non-literal expressions, TryFrom<repr> matches against known discriminants.
use subtype_rs::subtype;
#[repr(u16)]
#[subtype]
enum Flag { One = 1, Four = 4, Eight = 8 }
fn main() {
use core::convert::TryFrom;
assert_eq!(u16::from(Flag::One), 1);
assert_eq!(Flag::One.to_repr(), 1);
assert!(Flag::try_from(1u16).is_ok());
assert!(Flag::try_from(5u16).is_err());
}
Provide error = "path::Err" or error(path::Err). If omitted, a lightweight <EnumName>TryFromError is generated.
use subtype_rs::subtype;
#[derive(Debug)]
pub struct EnumErr { value: u8 }
#[repr(u8)]
#[subtype(error = "EnumErr")]
enum Code { A = 10, B = 11 }
impl From<subtype_rs::SubtypeError<u8>> for EnumErr {
fn from(e: subtype_rs::SubtypeError<u8>) -> Self {
// Map BelowMinimum/AboveMaximum however you like.
let v = match e { subtype_rs::SubtypeError::BelowMinimum(v)
| subtype_rs::SubtypeError::AboveMaximum(v) => v };
EnumErr { value: v }
}
}
pub const MIN: Inner, pub const MAX: InnerMIN > MAXimpl Newtype { fn new(inner) -> Result<Self, E>; fn into_inner(self) -> Inner; fn min_value(); fn max_value(); }TryFrom<Inner> for Newtype (with E: From<SubtypeError<Inner>>)From<Newtype> for InnerDeref<Target = Inner>, AsRef<Inner>, Borrow<Inner>Display (delegates to inner)#[repr(transparent)] is auto-inserted if missing (v0.2.1+)Requires explicit primitive #[repr(..)] on the enum (u8..u128, i8..i128, usize/isize)
Display (numeric), From<Enum> for repr, to_repr()
TryFrom<repr>:
const MIN, const MAX + bounds check + optimized transmutematch over known discriminantsOptional custom error: error = "path::Err" / error(path::Err); otherwise a small <EnumName>TryFromError is emitted
u8..u128, i8..i128, usize, isize).min/max to match the inner type (e.g., 0u32, -5i32), or use the string form.From<SubtypeError<Inner>> for it.#[repr(transparent)] is recommended and auto-inserted if missing (v0.2.1+).#[repr(..)] (e.g., #[repr(u8)]).unsafe transmute is used only after verifying the input is within [MIN, MAX], making it sound (every integer in the range maps to a valid variant).#[repr(transparent)] (auto from v0.2.1) improves FFI/ABI stability for newtypes over primitives.See the latest changelog in the repository README: github.com/Feralthedogg/subtype_macro/blob/main/README.md
If you want, I can also provide a minimal diff patch instead of the full paste.
This project is licensed under the MIT License.
See the LICENSE file for details.