optifier

Crates.iooptifier
lib.rsoptifier
version0.1.0-beta.3
created_at2023-04-09 09:32:47.519703+00
updated_at2026-01-15 23:55:01.5329+00
descriptionRust macros for deriving optional types
homepagehttps://github.com/kkafar/optifier
repositoryhttps://github.com/kkafar/optifier
max_upload_size
id834054
size20,535
Kacper Kafara (kkafar)

documentation

README

Optifier

Proc macro crate: Partial derive

[!note] This crate is early development phase. I use it only in my personal projects and update it as needed.

Derive Partial on a struct Foo to generate a new struct named FooPartial where every field type is wrapped in Option<T> unless it is already an Option<T>. Only structs with named fields are accepted; tuple and unit structs are not supported yet.

For every FooPartial the macro also generates:

  • an inherent merge method to combine two partial values, and
  • a TryFrom<FooPartial> for Foo implementation with a dedicated error type.

Example:

use optifier;

#[derive(optifier::Partial)]
#[optifier::partial_derive(Debug, Clone)]
pub struct Foo {
    a: i32,
    b: Option<String>,
    pub c: Vec<u8>,
}

expands to:

#[derive(Debug, Clone)]
pub struct FooPartial {
    a: Option<i32>,
    b: Option<String>, // stays as-is
    pub c: Option<Vec<u8>>,
}

// merge two partials (right-hand side only fills in missing values)
impl FooPartial {
    // generated by the macro
    pub fn merge(self, other: FooPartial) -> Self { ... }
}

// fallible conversion back to the original type
// (succeeds only if all non-optional original fields are present)
#[derive(thiserror::Error, Debug)]
pub enum FooPartialError {
    #[error("a field is missing")]
    AMissing,
    #[error("c field is missing")]
    CMissing,
}

impl TryFrom<FooPartial> for Foo {
    type Error = FooPartialError;

    fn try_from(partial: FooPartial) -> Result<Self, Self::Error> {
        Ok(Foo {
            a: partial.a.ok_or(FooPartialError::AMissing)?,
            b: partial.b,                // was already Option<_> in Foo
            c: partial.c.ok_or(FooPartialError::CMissing)?,
        })
    }
}

merge method on *Partial types

For every generated <OriginalName>Partial the macro adds a merge method:

impl FooPartial {
    pub fn merge(self, other: FooPartial) -> Self {
        Self {
            a: self.a.or(other.a),
            b: self.b.or(other.b),
            c: self.c.or(other.c),
        }
    }
}

Semantics:

  • It returns a new partial where, for each field, the value from self is used if it is Some.
  • If self.field is None, the value from other.field is used.
  • This is a shallow merge; it does not recurse into nested structs.

This is useful for layering configurations or patches:

let base: FooPartial = /* ... */;
let override_: FooPartial = /* ... */;
let merged = base.merge(override_);

Fallible conversion: TryFrom<*Partial> for Original

For each original struct Foo, the macro generates:

  • A dedicated error type: <OriginalName>PartialError (e.g. FooPartialError).

  • An implementation of TryFrom<FooPartial> for Foo:

    • The conversion succeeds only if every field that was non-Option in Foo is Some in FooPartial.
    • Fields that were already Option<...> in Foo are allowed to be None without causing an error.
    • The error type is an enum annotated with thiserror::Error, with one variant per non-optional field in the original struct.
    • Variant naming:
      • Field name is converted to PascalCase using the convert_case crate.
      • The suffix Missing is appended.
      • Examples:
        • aAMissing
        • user_idUserIdMissing

The error variants have messages of the form: "<field_name> field is missing".

Concrete example:

#[derive(optifier::Partial)]
pub struct User {
    id: i64,
    user_name: String,
    email: Option<String>,
}

Generates something equivalent to:

pub struct UserPartial {
    id: Option<i64>,
    user_name: Option<String>,
    email: Option<String>,
}

#[derive(thiserror::Error, Debug)]
pub enum UserPartialError {
    #[error("id field is missing")]
    IdMissing,
    #[error("user_name field is missing")]
    UserNameMissing,
    // no variant for `email` (it was already Option<_> on User)
}

impl TryFrom<UserPartial> for User {
    type Error = UserPartialError;

    fn try_from(partial: UserPartial) -> Result<Self, Self::Error> {
        Ok(User {
            id: partial.id.ok_or(UserPartialError::IdMissing)?,
            user_name: partial.user_name.ok_or(UserPartialError::UserNameMissing)?,
            email: partial.email, // stays Option<_>
        })
    }
}

You can then use the standard try_into API:

let partial: UserPartial = /* ... */;
let user: User = partial.try_into()?; // or User::try_from(partial)?

#[optifier::partial_derive(...)] attribute

The #[optifier::partial_derive(...)] attribute configures which traits are derived for the generated *Partial type.

  • It is applied to the original struct, together with #[derive(optifier::Partial)].
  • It accepts a comma-separated list of trait paths (e.g. Debug, Clone, PartialEq, or fully qualified paths).
  • Whatever traits you list there are emitted as a #[derive(...)] on the generated <OriginalName>Partial type.

Example:

use optifier;

#[derive(optifier::Partial)]
#[optifier::partial_derive(Debug, Clone, PartialEq)]
pub struct Foo {
    a: i32,
    b: Option<String>,
}

generates roughly:

#[derive(Debug, Clone, PartialEq)]
pub struct FooPartial {
    a: Option<i32>,
    b: Option<String>,
}
Commit count: 10

cargo fmt