1 unstable release
Uses new Rust 2024
| new 0.3.0 | Feb 4, 2026 |
|---|---|
| 0.2.1 |
|
| 0.2.0 |
|
| 0.1.2 |
|
| 0.1.1 |
|
#1475 in Rust patterns
Used in gpui-form
14KB
Unwrapped
Generate struct variants with different optionality semantics.
Unwrapped
Creates a new struct, changing each field Option<T> -> T.
#[derive(Unwrapped)]
pub struct Ab {
a : Option<Ab>,
b : u8,
#[unwrapped(skip)]
c : Option<String>,
}
->
pub struct AbUw {
a : Ab,
b : u8,
// c is not included - skip removes the field entirely
}
Fields marked with #[unwrapped(skip)] are completely removed from the generated struct. When any field has skip, From trait implementations are not generated (since conversion is impossible without all fields).
Conversions
Important: No panics, no defaults! All conversions are explicit and fallible.
Unwrapped::try_from(original)is always generated. It returnsErr(UnwrappedError)if any non-skippedOptionfield isNone.From<Unwrapped> for Originalis generated only when no fields are skipped.- With skipped fields, use
into_original(self, skipped...)to reconstruct the original type.
Converting Back with Skipped Fields
When fields are skipped, an into_original helper method is generated that allows you to reconstruct the original struct by providing values for the skipped fields:
use unwrapped::Unwrapped;
#[derive(Debug, PartialEq, Unwrapped)]
#[unwrapped(name = UserFormUw)]
struct UserForm {
name: Option<String>,
email: Option<String>,
#[unwrapped(skip)]
created_at: i64,
#[unwrapped(skip)]
id: u64,
}
// Create an unwrapped struct (without skipped fields)
let form = UserFormUw {
name: "Alice".to_string(),
email: "alice@example.com".to_string(),
};
// Convert back to original using into_original, providing skipped fields
let original = form.into_original(1234567890, 42);
assert_eq!(original.name, Some("Alice".to_string()));
assert_eq!(original.email, Some("alice@example.com".to_string()));
assert_eq!(original.created_at, 1234567890);
assert_eq!(original.id, 42);
Using bon Builders (Optional)
If the original struct uses bon::Builder (via #[derive(bon::Builder)] or #[builder(...)]) and you also use skip, the macro adds a helper on the builder:
from_unwrapped(self, uw)pre-fills the builder with the non-skipped fields.
use unwrapped::Unwrapped;
#[derive(Debug, PartialEq, Unwrapped, bon::Builder)]
#[unwrapped(name = UserFormUw)]
#[builder(on(Option<String>, into))]
struct UserForm {
name: Option<String>,
email: Option<String>,
#[unwrapped(skip)]
created_at: i64,
#[unwrapped(skip)]
id: u64,
}
let form = UserFormUw {
name: "Alice".to_string(),
email: "alice@example.com".to_string(),
};
let original = UserForm::builder()
.from_unwrapped(form)
.created_at(1234567890)
.id(42)
.build();
assert_eq!(original.name, Some("Alice".to_string()));
assert_eq!(original.email, Some("alice@example.com".to_string()));
assert_eq!(original.created_at, 1234567890);
assert_eq!(original.id, 42);
If you are not using bon, you can still destructure the unwrapped struct and pass fields manually.
Wrapped
Creates a new struct, changing each field T -> Option<T>. This is the inverse of Unwrapped.
#[derive(Wrapped)]
pub struct Config {
timeout: u64,
retries: i32,
#[wrapped(skip)]
name: String,
}
->
pub struct ConfigW {
timeout: Option<u64>,
retries: Option<i32>,
// name is not included - skip removes the field entirely
}
Fields marked with #[wrapped(skip)] are completely removed from the generated struct. When any field has skip, the From trait implementations are not generated (since conversion is impossible without all fields).
Conversions
Important: No panics, no defaults! All conversions are explicit and fallible.
From<Original> for Wrappedis generated only when no fields are skipped.Wrapped::try_from(wrapped)is generated only when no fields are skipped and returnsErr(UnwrappedError)if any required wrapped field isNone.- With skipped fields, use
into_original(self, skipped...) -> Result<Original, UnwrappedError>.
Converting Back with Skipped Fields
When fields are skipped, an into_original helper method is generated that allows you to reconstruct the original struct by providing values for the skipped fields:
use unwrapped::Wrapped;
#[derive(Debug, PartialEq, Wrapped)]
#[wrapped(name = ConfigW)]
struct Config {
timeout: u64,
retries: i32,
#[wrapped(skip)]
created_at: i64,
#[wrapped(skip)]
version: String,
}
// Create a wrapped struct (without skipped fields)
let wrapped = ConfigW {
timeout: Some(30),
retries: Some(3),
};
// Convert back to original using into_original, providing skipped fields
let original = wrapped
.into_original(1234567890, "v1.0".to_string())
.unwrap();
assert_eq!(original.timeout, 30);
assert_eq!(original.retries, 3);
assert_eq!(original.created_at, 1234567890);
assert_eq!(original.version, "v1.0".to_string());
Using bon Builders (Optional)
If the original struct uses bon::Builder (via #[derive(bon::Builder)] or #[builder(...)]) and you also use skip, the macro adds a helper on the builder:
from_wrapped(self, w)pre-fills the builder and returnsResult<Builder, UnwrappedError>.
use unwrapped::Wrapped;
#[derive(Debug, PartialEq, Wrapped, bon::Builder)]
#[wrapped(name = UserFormW)]
#[builder(on(Option<String>, into))]
struct UserForm {
name: String,
email: String,
note: Option<String>,
#[wrapped(skip)]
created_at: i64,
#[wrapped(skip)]
id: u64,
}
let wrapped = UserFormW {
name: Some("Alice".to_string()),
email: Some("alice@example.com".to_string()),
note: Some("hello".to_string()),
};
let original = UserForm::builder()
.from_wrapped(wrapped)
.unwrap()
.created_at(1234567890)
.id(42)
.build();
assert_eq!(original.name, "Alice".to_string());
assert_eq!(original.email, "alice@example.com".to_string());
assert_eq!(original.note, Some("hello".to_string()));
assert_eq!(original.created_at, 1234567890);
assert_eq!(original.id, 42);
Customizing the Generated Struct Name
You can specify a custom name for the generated struct using the unwrapped attribute.
use unwrapped::Unwrapped;
#[derive(Debug, PartialEq, Unwrapped)]
#[unwrapped(prefix = "A", name = UserUnwrapped, suffix = "c")]
struct User0;
#[allow(dead_code)]
type S0 = AUserUnwrappedc;
#[derive(Debug, PartialEq, Unwrapped)]
#[unwrapped(prefix = "Bad")]
struct User1;
#[allow(dead_code)]
type S1 = BadUser1;
#[derive(Debug, PartialEq, Unwrapped)]
#[unwrapped(suffix = "Something")]
struct User2;
#[allow(dead_code)]
type S2 = User2Something;
#[derive(Debug, PartialEq, Unwrapped)]
#[unwrapped(prefix = "Bad", suffix = "Something")]
struct User3;
#[allow(dead_code)]
type S3 = BadUser3Something;
You can specify a custom name for the generated struct using the wrapped attribute.
use unwrapped::Wrapped;
#[derive(Debug, PartialEq, Wrapped)]
#[wrapped(prefix = "A", name = UserWrapped, suffix = "c")]
struct User0;
#[allow(dead_code)]
type S0 = AUserWrappedc;
#[derive(Debug, PartialEq, Wrapped)]
#[wrapped(prefix = "Bad")]
struct User1;
#[allow(dead_code)]
type S1 = BadUser1;
#[derive(Debug, PartialEq, Wrapped)]
#[wrapped(suffix = "Something")]
struct User2;
#[allow(dead_code)]
type S2 = User2Something;
#[derive(Debug, PartialEq, Wrapped)]
#[wrapped(prefix = "Bad", suffix = "Something")]
struct User3;
#[allow(dead_code)]
type S3 = BadUser3Something;
For Proc-Macro Authors
[dependencies]
unwrapped-core = { version = "*" }
You can then use it in your own proc-macro:
use proc_macro::TokenStream;
use quote::{format_ident, quote};
use syn::DeriveInput;
use std::collections::HashMap;
use unwrapped_core::{Opts, UnwrappedProcUsageOpts, unwrapped};
#[proc_macro_derive(MyUnwrappedMacro)]
pub fn my_unwrapped_macro(input: TokenStream) -> TokenStream {
let derive_input: DeriveInput = syn::parse(input).unwrap();
let model_options = Opts::builder()
.suffix(format_ident!("FormValueHolder"))
.build();
let mut fields_to_unwrap: HashMap<String, bool> = HashMap::new();
fields_to_unwrap.insert("id".to_owned(), true);
fields_to_unwrap.insert("name".to_owned(), false);
let macro_options = UnwrappedProcUsageOpts::new(fields_to_unwrap, None);
// Generate the unwrapped data model struct with a custom suffix
let model_struct = unwrapped(&derive_input, Some(model_options), macro_options);
// ... your macro's logic ...
let expanded = quote! {
#model_struct
};
expanded.into()
}
Or for the Wrapped variant:
use proc_macro::TokenStream;
use quote::{format_ident, quote};
use syn::DeriveInput;
use std::collections::HashMap;
use unwrapped_core::{WrappedOpts, WrappedProcUsageOpts, wrapped};
#[proc_macro_derive(MyWrappedMacro)]
pub fn my_wrapped_macro(input: TokenStream) -> TokenStream {
let derive_input: DeriveInput = syn::parse(input).unwrap();
let mut fields_to_wrap: HashMap<String, bool> = HashMap::new();
fields_to_wrap.insert("id".to_owned(), true);
fields_to_wrap.insert("name".to_owned(), false);
let model_options = WrappedOpts::builder()
.suffix(format_ident!("FormValueHolder"))
.build();
let macro_options = WrappedProcUsageOpts::new(fields_to_wrap, None);
// Generate the wrapped data model struct with a custom suffix
let model_struct = wrapped(&derive_input, Some(model_options), macro_options);
// ... your macro's logic ...
let expanded = quote! {
#model_struct
};
expanded.into()
}
Dependencies
~0.7–1.2MB
~26K SLoC