| Crates.io | shrinkwrap |
| lib.rs | shrinkwrap |
| version | 0.5.0 |
| created_at | 2025-07-19 22:43:01.479993+00 |
| updated_at | 2025-10-09 15:58:07.951614+00 |
| description | Automates the wrapping and nesting of additional variations for some or all of a struct's fields. |
| homepage | |
| repository | https://github.com/kylekingcdn/shrinkwrap-rs |
| max_upload_size | |
| id | 1760584 |
| size | 26,929 |
shrinkwrapShrinkwrap makes it easy to render additional variations for a given set (or subset) of data.
A common use-case is providing human-readable variations of some coded format in an API response (e.g. timestamps, numerics, currency balances, unit conversions).
Consider the following struct which is used in an API response.
pub struct UserResponse {
id: Uuid,
username: String,
balance: i64, // balance in USD cents
last_login: DateTime<Utc>,
}
Suppose you wanted to provide API clients with balance and timestamps ready for display, e.g. $123.45 USD.
Shrinkwrap allows for single-call conversion from
{
"id": "ab7aa843-ae3b-4120-b63e-e9d962120f9c",
"username": "johndoe",
"balance": 27468,
"last_login": 1754497944
}
To
{
"id": "ab7aa843-ae3b-4120-b63e-e9d962120f9c",
"username": "johndoe",
"balance": 27468,
"last_login": 1754497944,
"extra": {
"text": {
"balance": "$274.68",
"last_login": "Thu, 07 Aug 2025 02:14:44 GMT"
}
}
}
Shrinkwrap will provide conversion when you add #[derive(Wrap)] and a few required attributes to your
struct, plus a function to handle the actual conversion.
Alternatively, you could...
Newtype wrap your fields, providing a Display impl to override the original display format.
Or, use #[serde(with = ...)] to provide custom serialization for the field.
If these suit your needs, great!
But sometimes you need more control.
shrinkwrap can do:[!NOTE] Alternatively, you could add fields for each variation directly into your main struct. e.g.
idusernamebalancebalance_textbalance_localbalance_local_textlast_loginlast_login_textHowever, this clearly becomes unweildy:
Your structs become bloated with this extra baggage and conversions are being done all over the place.
To accomplish the example from the Overview section:
Define your transform struct. This is the type that will be used to handle conversions.
use shrinkwrap::Transform;
struct MyTransformOpts {
// no run-time options required in this example
}
struct MyTransform {}
impl Transform for MyTransform {
type Options = MyTransformOpts;
}
Annotate your data struct, specifying the transform type from step 1, the nest definition, and fields to include in the nest
use shrinkwrap::Wrap;
#[derive(Debug, Clone, Serialize, Wrap)]
#[shrinkwrap(transform = MyTransform)] // associate the wrapper/extra/nest conversion to your transform
#[shrinkwrap(nest(id = "text", field_type = String))] // define a variant group (nest) for text repr
pub struct UserResponse { // + specify the return type for all fields within the nest
id: Uuid,
username: String,
#[shrinkwrap(nests("text"))] // fields are opt-in and must be selected for each nest
balance: i64,
#[shrinkwrap(nests("text"))]
last_login: DateTime<Utc>,
}
Add the conversion impl
use shrinkwrap::TransformToNest;
impl TransformToNest<UserResponseNestedText> for MyTransform {
type Data = UserResponse;
fn transform_to_nest(&self, data: &UserResponse, _options: &MyTransformOpts) -> UserResponseNestedText {
UserResponseNestedText {
balance: format!("${:.2} USD", data.balance as f32 / 100.0),
last_login: data.last_login.format("%Y-%m-%d%l:%M%P").to_string(),
}
}
}
Use your derived impls + structs
use shrinkwrap::ToWrappedWith;
// -- snip -- //
let transform = MyTransform {};
let transform_opts = MyTransformOpts {};
let data = UserResponse {
id: Uuid::new_v4(),
username: "johndoe".into(),
balance: 27468,
last_login: DateTime::from_timestamp(1754497944, 0).ok_or("Timestamp parse failed")?
};
// generate the wrapper with your mapped data nested under 'extra'.
let wrapped = data.to_wrapped_with(&transform, &transform_opts);
println!("Generated wrapper struct debug output:\n{wrapped:#?}\n");
println!("Generated wrapper json: {}", serde_json::to_string_pretty(&wrapped)?);
The above example will output the following:
Generated wrapper struct debug output:
UserResponseWrapper {
data: UserResponse {
id: 2330f8a8-2f6b-4ed4-81a2-a4500db6ac33,
username: "johndoe",
balance: 27468,
last_login: 2025-08-06T16:32:24Z,
},
extra: UserResponseExtra {
text: UserResponseNestedText {
balance: "$274.68 USD",
last_login: "2025-08-06 4:32pm",
},
},
}
Generated wrapper json:
{
"id": "2330f8a8-2f6b-4ed4-81a2-a4500db6ac33",
"username": "johndoe",
"balance": 27468,
"last_login": "2025-08-06T16:32:24Z",
"extra": {
"text": {
"balance": "$274.68 USD",
"last_login": "2025-08-06 4:32pm"
}
}
}
[!NOTE] This example can be viewed and compiled in full at
examples/readme
shrinkwrap hierarchyShrinkwrap generates the following:
Nest struct for each defined variation setExtra struct that contains all associated nestsWrapper struct containing the original data struct, and the Extra struct.
#[serde(flatten)]
applied to it, giving the appearance of inlined data. This is done to reduce excessive nesting for consumers.Tree diagram:
- wrapper
- data (original data struct, which gets inlined into the wrapper)
* field1
* field2
- extra
- nest1:
* field1
* field2
- nest2:
* field1
* field2
Variations are placed in a dedicated struct (the nests) to avoid polluting source data sets. Each data set can support multiple nests, where each provides a distinct variation of a subset of the source data's fields.
Nests can be branched off one another, allowing for chained variations. An example of such flow:
balance in USD centsbalance in local currencybalance in human readabable local currencyContinuing with the first JSON example, where we are chaining:
balance in USD cents
balance in USD in a human-readable formatbalance in local currency
balance in local currency in a human-readable format{
"id": "ab7aa843-ae3b-4120-b63e-e9d962120f9c",
"username": "johndoe",
"balance": 27468,
"last_login": 1754532884,
"extra": {
"text": {
"balance": "$274.68",
"last_login": "Thu, 07 Aug 2025 02:14:44 GMT"
},
"local_value": {
"balance": 37737,
"extra": {
"text": {
"balance": "$377.37 CAD"
}
}
}
}
}
[!CAUTION] Work in progress - will be finished in the next patch
This project is licensed under the MIT license.