A quick and easy way to create derivative models of your existing types without repeating yourself. Reduce boilerplate and automatically generate related structs with custom field subsets and transformations.
- Reduce boilerplate: Generate multiple related structs from a single definition
- Flexible field selection: Include or exclude specific fields with
fields()
andomit()
- Automatic trait generation:
From<T>
implementations between original and generated structs - Derive support: Apply derives to generated structs
- Multiple model types: Views, patches, and custom transformations
New features and roadmap are available here on GitHub.
Add restructed
to your Cargo.toml
:
[dependencies]
restructed = "0.2"
Or run this command in your project directory:
cargo add restructed
Add the derive macro to your struct:
use restructed::Models;
#[derive(restructed::Models)]
struct User {
id: i32,
username: String,
email: String,
password: String,
}
Then add attributes for each model you want to generate:
#[derive(restructed::Models)]
#[view(UserProfile, omit(password))] // Subset without sensitive fields
#[patch(UserUpdate, fields(username, email))] // Optional fields for updates
struct User {
id: i32,
username: String,
email: String,
password: String,
}
This generates:
UserProfile
struct withid
,username
, andemail
fieldsUserUpdate
struct withOption<String>
fields forusername
andemail
- Automatic
From
implementations for conversions
Creates a struct containing a subset of the original fields. Perfect for API responses, database views, or public representations.
Arguments:
Name | Description | Required | Type | Example |
---|---|---|---|---|
name |
Name of the generated struct | Yes (first) | Identifier | UserProfile |
fields |
Fields to include | No | List | fields(id, username) |
omit |
Fields to exclude | No | List | omit(password, secret) |
derive |
Traits to derive | No | List | derive(Debug, Clone) |
preset |
Behavior preset to apply | No | String | preset = "read" |
attributes_with |
Attributes to inherit | No | String | attributes_with = "all" |
Note: Use either fields
OR omit
, not both.
Example:
#[derive(Clone, restructed::Models)]
#[view(UserProfile, omit(id, password))]
struct User {
id: i32, // Not in UserProfile
username: String, // In UserProfile
email: String, // In UserProfile
bio: String, // In UserProfile
password: String, // Not in UserProfile
}
// Usage
let user = User {
id: 1,
username: "alice".to_string(),
email: "[email protected]".to_string(),
bio: "Rustacean".to_string(),
password: "super_secret".to_string(),
};
let profile: UserProfile = user.into();
Creates a struct where each field is wrapped in Option<T>
. Ideal for partial updates, PATCH endpoints, or optional modifications.
Arguments:
Name | Description | Required | Type | Example |
---|---|---|---|---|
name |
Name of the generated struct | Yes (first) | Identifier | UserUpdate |
fields |
Fields to include | No | List | fields(username, email) |
omit |
Fields to exclude | No | List | omit(id, created_at) |
derive |
Traits to derive | No | List | derive(Debug, Serialize) |
preset |
Behavior preset to apply | No | String | preset = "write" |
attributes_with |
Attributes to inherit | No | String | attributes_with = "oai" |
option |
Alternative to Option<T> |
No | Type | option = MaybeUndefined |
skip_serializing_double_option |
Skip serializing None for Option<Option<T>> |
No | Boolean | skip_serializing_double_option = true |
Example:
#[derive(Clone, restructed::Models)]
#[patch(UserUpdate, omit(id))]
struct User {
id: i32, // Not in UserUpdate
username: String, // Option<String> in UserUpdate
email: String, // Option<String> in UserUpdate
bio: Option<String>, // Option<Option<String>> in UserUpdate
}
// Usage
let update = UserUpdate {
username: Some("new_username".to_string()),
email: None, // Don't update email
bio: Some(Some("New bio".to_string())), // Set bio
};
Defines default arguments applied to all generated models. This attribute doesn't generate structs itself but configures other model generators.
Sub-attributes:
Arguments that are always applied and cannot be overridden by individual models.
#[model(base(derive(Debug, Clone)))] // All models MUST derive Debug and Clone
Arguments applied only when not specified by individual models.
#[model(defaults(derive(Serialize), preset = "read"))] // Applied unless overridden
Example:
#[derive(restructed::Models)]
#[model(
base(derive(Debug)), // All models derive Debug
defaults(derive(Clone), preset = "read") // Default unless overridden
)]
#[view(UserView)] // Inherits Debug + Clone + preset="read"
#[patch(UserPatch, preset = "write")] // Inherits Debug + Clone, overrides preset
struct User {
id: i32,
username: String,
password: String,
}
Presets apply common configurations automatically:
"none"
(default): No special behavior"write"
(requires 'openapi' feature): For writable fields- Removes
#[oai(read_only)]
fields - Uses
MaybeUndefined
for patch option type
- Removes
"read"
(requires 'openapi' feature): For readable fields- Removes
#[oai(write_only)]
fields - Uses
MaybeUndefined
for patch option type
- Removes
Control which attributes are copied to generated structs:
-
"none"
(default): No attributes copied -
"oai"
(requires 'openapi' feature): Copy OpenAPI attributes -
"deriveless"
: Copy all attributes except derives -
"all"
: Copy all attributes, even dervies but they'll need to be on their own line (See below example) i.e.#[derive(restructed::Models)] #[model(defaults(attributes_with = "all"))] #[derive(Clone)]
#[derive(restructed::Models, Clone)]
#[view(UserProfile, omit(password, internal_id))]
#[view(UserSummary, fields(id, username))]
#[patch(UserUpdate, omit(id, internal_id, created_at), preset = "write")]
struct User {
id: i32,
internal_id: String,
username: String,
email: String,
password: String,
created_at: String,
}
fn example_usage() {
let user = User {
id: 1,
internal_id: "internal_123".to_string(),
username: "alice".to_string(),
email: "[email protected]".to_string(),
password: "secret".to_string(),
created_at: "2024-01-01".to_string(),
};
// Convert to different views
let profile: UserProfile = user.clone().into();
let summary: UserSummary = user.clone().into();
// Create update struct
let update = UserUpdate {
username: Some("new_alice".to_string()),
email: None, // Don't update
password: Some("new_secret".to_string()),
};
}
Enables integration with the poem-openapi crate:
- Use
MaybeUndefined<T>
instead ofOption<T>
in patch models - Copy
#[oai(...)]
attributes to generated structs - Respect
read_only
/write_only
attributes in presets
use restructed::Models;
#[derive(poem_openapi::Object, Models)]
#[oai(skip_serializing_if_is_none, rename_all = "camelCase")]
#[model(
base(derive(poem_openapi::Object, Debug)),
defaults(preset = "read")
)]
#[patch(UserUpdate, preset = "write")]
#[view(UserProfile)]
#[view(UserNames, fields(username, name, surname))]
pub struct User {
#[oai(read_only)]
pub id: u32,
#[oai(validator(min_length = 3, max_length = 16, pattern = r"^[a-zA-Z0-9_]*$"))]
pub username: String,
#[oai(validator(min_length = 5, max_length = 1024), write_only)]
pub password: String,
#[oai(validator(min_length = 2, max_length = 16, pattern = r"^[a-zA-Z\s]*$"))]
pub name: Option<String>,
#[oai(validator(min_length = 2, max_length = 16, pattern = r"^[a-zA-Z\s]*$"))]
pub surname: Option<String>,
#[oai(read_only)]
pub joined: u64,
}
- Generic types: Currently doesn't support generic structs or enums (e.g.,
Struct<T>
) - Enum support: Only works with structs, not enums
Contributions for these features are welcome!
See the project repository for license information.