Files
mize/docs/part-system.md
2026-10-14 01:50:08 +02:00

14 KiB

Mize Part System (LLM generated guide)

Overview

Mize is built around a parts architecture that allows you to compose functionality modularly. Each part is a self-contained unit that can:

  • Store and manage its own state
  • Expose methods to other parts
  • Register itself with the Mize instance
  • Define configuration options

Parts are the primary way to extend Mize's functionality. The system is designed so that parts can depend on each other, and users can enable/disable parts as needed.


Core Concepts

The MizePart Trait

All parts implement the MizePart trait, which provides:

pub trait MizePart: MizePartGenerated {
    fn name(&self) -> &'static str;      // Part identifier
    fn get_mize(&mut self) -> &mut Mize;  // Access to the Mize instance
    fn init(&mut self, mize: &mut Mize) -> MizeResult<()>;  // Initialization
    fn run(&mut self, mize: &mut Mize) -> MizeResult<()>;    // Execution
    fn deps(&self) -> &'static [&'static str];             // Dependencies
    fn opts(&self, mize: &mut Mize);                      // Config options
    // ... async variants
}

Part Registration

Parts register themselves with the Mize instance using:

  • mize.register_part(Box::new(MyPart { ... })) - For parts with state and methods
  • mize.add_name_only_part("name") - For simple parts that only need to be known by name (and have no state)

Part Access

Parts can access other parts using:

// Get a part by name (returns a guard that ensures the part is returned)
let mut part = mize.get_part("part_name")?;

// Get a typed part (for parts with structs)
let mut typed_part = mize.get_part_native::<OtherPart>("other_part")?;

Important: Users must ensure that dependent parts are enabled before the parts that depend on them. There's no automatic dependency resolution.


Current Implementation (Compiled-in Parts)

Currently, all parts are compiled into the marts crate and registered via the compile_in_parts! macro in the application's main.rs.

Example: apps/os/src/main.rs

compile_in_parts! {
    mize,
    "cli": marts::cli,
    "auth": marts::part_client_auth,
    "spacetime": marts::spacetime,
    "ppc.server": marts::ppc::server::part_ppc_server,
    "c2vi": marts::part_c2vi,
    "sci": marts::sci::part_sci,
    "sci.timetrack": marts::sci::timetrack::part_timetrack,
    "sci.inbox": marts::sci::inbox::part_sci_inbox,
};

Each entry maps a part name to a function that registers the part with the Mize instance.

How It Works

  1. The macro reads the enable config option (a list of part names)
  2. For each enabled part, it calls the corresponding registration function
  3. Each registration function creates the part and calls mize.register_part()

Creating a New Part

Option 1: Simple Name-Only Part

For parts that don't need state or methods:

// In your part module
pub fn part_my_simple_part(mize: &mut Mize) -> MizeResult<()> {
    // Just register the name
    mize.add_name_only_part("my_simple_part");
    Ok(())
}

Option 2: Part with State and Methods (Using mize_part Macro)

For parts that need to store state and expose methods:

use mize::{mize_part, Mize, MizePart, MizePartCreate, MizeResult};

#[mize_part]  // or #[mize_part("custom_name")] for custom name
#[derive(Default)]
pub struct MyPart {
    mize: Mize,           // Required field
    counter: u32,        // Your custom state
    api_key: String,     // More state
}

// Implement MizePartCreate for parts that need custom initialization
impl MizePartCreate for MyPart {
    // Optional: override create_generated for custom initialization
}

impl MizePart for MyPart {
    // Optional: override name if not using the struct name
    fn name(&self) -> &'static str {
        "my_part"  // Default would be "mypart" from struct name
    }
    
    // Optional: define config options this part needs
    fn opts(&self, mize: &mut Mize) {
        mize.new_opt("my_part.api_key");
        mize.new_opt("my_part.counter_initial");
    }
    
    // Optional: initialization logic
    fn init(&mut self, mize: &mut Mize) -> MizeResult<()> {
        self.counter = mize.get_config("my_part.counter_initial")?.value_u32()?;
        self.api_key = mize.get_config("my_part.api_key")?.value_string()?;
        Ok(())
    }
    
    // Optional: define dependencies on other parts
    fn deps(&self) -> &'static [&'static str] {
        &["auth"]  // This part requires "auth" to be loaded first
    }
}

// The registration function
pub fn part_my_part(mize: &mut Mize) -> MizeResult<()> {
    mize.register_part(Box::new(MyPart {
        mize: mize.clone(),
        counter: 0,
        api_key: String::new(),
    }))?;
    Ok(())
}

// You can add methods to your part struct
impl MyPart {
    pub fn increment(&mut self) -> u32 {
        self.counter += 1;
        self.counter
    }
    
    pub fn get_api_key(&self) -> &str {
        &self.api_key
    }
}

Using the Part from Another Part

pub fn part_another_part(mize: &mut Mize) -> MizeResult<()> {
    // Get the MyPart instance
    let mut my_part = mize.get_part_native::<MyPart>("my_part")?;
    
    // Call its methods
    let count = my_part.increment();
    let api_key = my_part.get_api_key();
    
    // ... your logic
    
    Ok(())
}

The mize_part Macro

The #[mize_part] procedural macro simplifies part creation by automatically implementing the required traits.

What It Generates

For a struct like:

#[mize_part]
#[derive(Default)]
pub struct MyPart {
    mize: Mize,
    // ... your fields
}

The macro generates:

  1. MizePartGenerated implementation - Provides default implementations for the generated trait methods
  2. MizePartCreateGenerated implementation - Provides a default create_generated method
  3. MizePartCreateThisSucks implementation - Compatibility trait
  4. Extern "C" function - Named make_mize_part_<name> for dynamic loading

Custom Part Name

#[mize_part("custom_name")]
pub struct MyPart {
    mize: Mize,
}

This will use "custom_name" instead of the derived "mypart".

Requirements

  • The struct must have a field named mize: Mize
  • The struct should derive Default or implement it

Configuration

Defining Config Options

(about to be deprecad, does nothing)

Parts declare what config options they need in their opts() method:

impl MizePart for MyPart {
    fn opts(&self, mize: &mut Mize) {
        mize.new_opt("my_part.api_url");
        mize.new_opt("my_part.timeout");
    }
}

Using Config Options

Parts retrieve config using mize.get_config():

fn init(&mut self, mize: &mut Mize) -> MizeResult<()> {
    let api_url = mize.get_config("my_part.api_url")?.value_string()?;
    let timeout = mize.get_config("my_part.timeout")?.value_u64()?;
    Ok(())
}

User Configuration

Users configure parts via TOML files or environment variables. Example config.toml:

[my_part]
api_url = "https://api.example.com"
timeout = 30

[enable]
# List of parts to enable
my_part = true
auth = true
spacetime = true

Or via command line:

mize --opt my_part.api_url=https://api.example.com run

Part Dependencies

Declaring Dependencies

(about to be deprecad, does nothing)

Parts declare their dependencies in the deps() method:

impl MizePart for MyPart {
    fn deps(&self) -> &'static [&'static str] {
        &["auth", "spacetime"]
    }
}

Note: This is currently informational only. Users must manually ensure dependencies are enabled.

Accessing Dependencies

Use get_part_native() to access other parts:

fn run(&mut self, mize: &mut Mize) -> MizeResult<()> {
    // Get the auth part
    let mut auth = mize.get_part_native::<ClientAuth>("client_auth")?;
    
    // Use it
    let token = auth.id_token()?;
    
    // Get spacetime
    let mut spacetime = mize.get_part_native::<SpacetimePart>("spacetime")?;
    
    Ok(())
}

Future Vision: Separate Crates

The goal is to move each part to its own crate following the naming convention mize-part-<name>. when creating a part make a new Cargo project like this already.

Structure of a Part Crate

mize-part-discord/
├── Cargo.toml
└── src/
    └── lib.rs

Example: mize-part-discord/Cargo.toml

[package]
name = "mize-part-discord"
version = "0.1.0"
edition = "2021"

[lib]
crate-type = ["cdylib", "rlib"]

[dependencies]
mize = { path = "../mize" }
serenity = "0.12"

Example: mize-part-discord/src/lib.rs

use mize::{mize_part, Mize, MizePart, MizePartCreate, MizeResult};
use serenity::Client;

#[mize_part]
#[derive(Default)]
pub struct DiscordPart {
    mize: Mize,
    client: Option<Client>,
}

impl MizePartCreate for DiscordPart {}

impl MizePart for DiscordPart {
    fn name(&self) -> &'static str {
        "discord"
    }
    
    fn opts(&self, mize: &mut Mize) {
        mize.new_opt("discord.token");
    }
    
    fn init(&mut self, mize: &mut Mize) -> MizeResult<()> {
        let token = mize.get_config("discord.token")?.value_string()?;
        // Initialize serenity client...
        Ok(())
    }
}

// The registration function - THIS IS WHAT MATTERS
pub fn part_discord(mize: &mut Mize) -> MizeResult<()> {
    mize.register_part(Box::new(DiscordPart {
        mize: mize.clone(),
        client: None,
    }))?;
    Ok(())
}

Using Separate Part Crates

In your application's Cargo.toml:

[dependencies]
mize = { path = "../mize" }
mize-part-discord = { path = "../mize-part-discord" }
mize-part-auth = { path = "../mize-part-auth" }

In your application's main.rs:

compile_in_parts! {
    mize,
    "discord": mize_part_discord::part_discord,
    "auth": mize_part_auth::part_auth,
};

Complete Example: Creating a New Part

Let's create a complete example of a new part called mize-part-greeting.

Step 1: Create the Crate

cargo new --lib mize-part-greeting
cd mize-part-greeting

Step 2: Edit Cargo.toml

[package]
name = "mize-part-greeting"
version = "0.1.0"
edition = "2021"

[lib]
crate-type = ["cdylib", "rlib"]

[dependencies]
mize = { path = "../mize" }

Step 3: Create the Part (src/lib.rs)

use mize::{mize_part, Mize, MizePart, MizePartCreate, MizeResult};

/// A simple greeting part that stores a name and can generate greetings
#[mize_part]
#[derive(Default)]
pub struct GreetingPart {
    mize: Mize,
    name: String,
}

impl MizePartCreate for GreetingPart {}

impl MizePart for GreetingPart {
    fn name(&self) -> &'static str {
        "greeting"
    }
    
    fn opts(&self, mize: &mut Mize) {
        mize.new_opt("greeting.name");
        mize.new_opt("greeting.formal");
    }
    
    fn init(&mut self, mize: &mut Mize) -> MizeResult<()> {
        self.name = mize
            .get_config("greeting.name")?
            .value_string()
            .unwrap_or_else(|_| "World".to_string());
        Ok(())
    }
}

impl GreetingPart {
    /// Generate a greeting message
    pub fn greet(&self) -> String {
        let formal = self
            .mize
            .get_config("greeting.formal")
            .map(|v| v.value_bool().unwrap_or(false))
            .unwrap_or(false);
        
        if formal {
            format!("Good day, {}!", self.name)
        } else {
            format!("Hello, {}!", self.name)
        }
    }
    
    /// Set the name
    pub fn set_name(&mut self, name: String) {
        self.name = name;
    }
    
    /// Get the current name
    pub fn get_name(&self) -> &str {
        &self.name
    }
}

/// Registration function - this is what the compile_in_parts! macro will call
pub fn part_greeting(mize: &mut Mize) -> MizeResult<()> {
    mize.register_part(Box::new(GreetingPart {
        mize: mize.clone(),
        name: String::new(),
    }))?;
    Ok(())
}

Step 4: Add to Your Application

In your application's Cargo.toml:

[dependencies]
mize = { path = "../mize" }
mize-part-greeting = { path = "../mize-part-greeting" }

In your application's main.rs:

compile_in_parts! {
    mize,
    "greeting": mize_part_greeting::part_greeting,
};

Step 5: Use the Part

// In another part or in your CLI
let mut greeting = mize.get_part_native::<GreetingPart>("greeting")?;
println!("{}", greeting.greet());

// Change the name
greeting.set_name("Alice".to_string());
println!("{}", greeting.greet());

Step 6: Configure It

Create a config.toml:

[greeting]
name = "Bob"
formal = true

[enable]
greeting = true

Run with:

mize --config config.toml run

Best Practices

  1. Naming: Use mize-part-<name> for crate names and part_<name> for registration functions
  2. State: Store part-specific state in the part struct
  3. Dependencies: Declare dependencies in deps() and document them
  4. Config: Declare all config options in opts()
  5. Error Handling: Use MizeResult and mize_err! for errors
  6. Documentation: Document your part's purpose, config options, and methods

Summary

  • Parts are modular units of functionality in Mize
  • Each part implements the MizePart trait
  • Use #[mize_part] macro for parts with state and methods
  • Register parts with mize.register_part() or mize.add_name_only_part()
  • Parts access config via mize.get_config()
  • Parts access other parts via mize.get_part_native::<T>()
  • Users enable parts via the enable config option
  • Future: Each part will be its own crate named mize-part-<name>

Quick Reference

Common Patterns

Register a part:

mize.register_part(Box::new(MyPart { mize: mize.clone(), ... }))?;

Get a part:

let mut part = mize.get_part_native::<MyPart>("my_part")?;

Get config:

let value = mize.get_config("my_part.setting")?.value_string()?;

Declare config option:

mize.new_opt("my_part.setting");

Define a part struct:

#[mize_part]
#[derive(Default)]
pub struct MyPart {
    mize: Mize,
    // your fields...
}

Registration function:

pub fn part_my_part(mize: &mut Mize) -> MizeResult<()> {
    mize.register_part(Box::new(MyPart { mize: mize.clone(), ... }))?;
    Ok(())
}