Skip to content

NexRX/restructed

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

53 Commits
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

restructed


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.

Features

  • Reduce boilerplate: Generate multiple related structs from a single definition
  • Flexible field selection: Include or exclude specific fields with fields() and omit()
  • 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.

Installation

Add restructed to your Cargo.toml:

[dependencies]
restructed = "0.2"

Or run this command in your project directory:

cargo add restructed

Quick Start

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 with id, username, and email fields
  • UserUpdate struct with Option<String> fields for username and email
  • Automatic From implementations for conversions

Model Types

#[view] - Field Subsets

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

#[patch] - Optional Field Wrappers

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

#[model] - Base Configuration

Defines default arguments applied to all generated models. This attribute doesn't generate structs itself but configures other model generators.

Sub-attributes:

base - Non-overridable Defaults

Arguments that are always applied and cannot be overridden by individual models.

#[model(base(derive(Debug, Clone)))]  // All models MUST derive Debug and Clone

defaults - Overridable Defaults

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

Advanced Features

Presets

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
  • "read" (requires 'openapi' feature): For readable fields
    • Removes #[oai(write_only)] fields
    • Uses MaybeUndefined for patch option type

Attribute Inheritance

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

Complete Example

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

Feature Flags

openapi - Poem OpenAPI Integration

Enables integration with the poem-openapi crate:

  • Use MaybeUndefined<T> instead of Option<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,
}

Limitations

  • 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!

License

See the project repository for license information.

About

Quickly derive subsets of your structs

Topics

Resources

Stars

Watchers

Forks

Packages

No packages published

Languages