subtype_rs

Crates.iosubtype_rs
lib.rssubtype_rs
version0.2.1
created_at2025-07-30 19:55:12.939726+00
updated_at2025-10-14 12:24:16.825306+00
descriptionAda‑style subtype newtype library
homepagehttps://github.com/Feralthedogg/subtype_rs
repositoryhttps://github.com/Feralthedogg/subtype_rs
max_upload_size
id1773979
size13,991
(Feralthedogg)

documentation

https://github.com/Feralthedogg/subtype_rs

README

subtype_rs

License: MIT

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(..)].

Installation

[dependencies]
subtype_rs = "0.2.1"
cargo add subtype_rs

Usage

1) Basic (tuple newtypes over primitive integers)

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);
}

2) TryFrom & From

The 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);
}

3) Custom error type (struct mode)

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);
}

4) Negative bounds / explicit suffixes

You can pass negative or suffixed literals using strings (parsed as expressions):

#[subtype(min = "-40i32", max = "120i32")]
pub struct Temperature(i32);

5) Generics allowed (field must be concrete)

#[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);

6) C-like Enums (new in v0.2.1)

Attach #[subtype] to an enum that has an explicit primitive integer #[repr(..)].

6.1 Contiguous literal discriminants (optimized fast path)

If all discriminants are integer literals forming a contiguous range, the macro:

  • Exposes const MIN / const MAX (of the enum’s repr),
  • Implements 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());
}

6.2 Sparse or non-literal discriminants (match-based path)

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());
}

6.3 Custom error type (enum mode)

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 }
    }
}

What the macro generates (summary)

Struct mode

  • Associated constants: pub const MIN: Inner, pub const MAX: Inner
  • Const range check: compile-time error if MIN > MAX
  • impl 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 Inner
  • Deref<Target = Inner>, AsRef<Inner>, Borrow<Inner>
  • Display (delegates to inner)
  • Layout: #[repr(transparent)] is auto-inserted if missing (v0.2.1+)

Enum mode (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>:

    • Contiguous literal enums: const MIN, const MAX + bounds check + optimized transmute
    • Otherwise: explicit match over known discriminants
  • Optional custom error: error = "path::Err" / error(path::Err); otherwise a small <EnumName>TryFromError is emitted

Constraints

Struct mode

  • Inner field type must be a primitive integer (u8..u128, i8..i128, usize, isize).
  • Use proper suffixes on min/max to match the inner type (e.g., 0u32, -5i32), or use the string form.
  • If you specify a custom error type, implement From<SubtypeError<Inner>> for it.
  • #[repr(transparent)] is recommended and auto-inserted if missing (v0.2.1+).

Enum mode

  • The enum must have an explicit primitive integer #[repr(..)] (e.g., #[repr(u8)]).
  • For the fast path, all discriminants must be integer literals forming a contiguous range.

Safety notes

  • Enum fast path: 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).
  • Struct layout/FFI: #[repr(transparent)] (auto from v0.2.1) improves FFI/ABI stability for newtypes over primitives.

Changelog

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.

License

This project is licensed under the MIT License.
See the LICENSE file for details.

Commit count: 0

cargo fmt