Skip to main content

Crate optionize

Crate optionize 

Source
Expand description

optionize generates structs for partial configuration, updates, and builders. The complete struct is the subject; its partial representation is the object. By default, #[optionized] generates SubjectOptional, wrapping each field in Option<Value> and preserving the subject’s visibility. The library supports no_std with alloc.

§Applying, merging, and converting patches

Import the traits for the operations you use. PartialOptionized provides optionize, patch, and merge on objects; Optionizable provides load and downgrade on subjects. Optionized adds validation and upgrading.

use optionize::{optionized, Optionizable, Optionized, PartialOptionized};

#[optionized]
#[derive(Debug, PartialEq)]
struct Config {
    host: String,
    port: u16,
}

let mut config = Config { host: "localhost".into(), port: 8080 };
let mut patch = ConfigOptional { host: Some("gateway".into()), port: None };
patch.merge(ConfigOptional { host: None, port: Some(0) });
// Incoming None preserves the existing update; Some(0) is a real update.
assert_eq!(patch.host.as_deref(), Some("gateway"));
patch.patch(&mut config);
assert_eq!(config, Config { host: "gateway".into(), port: 0 });

// Loading is the subject-side spelling of patching.
config.load(ConfigOptional { host: None, port: Some(9090) });
assert_eq!(config.host, "gateway");

// Downgrading consumes the subject and fills every managed field.
let patch: ConfigOptional = config.downgrade();
assert_eq!(patch.port, Some(9090));
assert!(patch.validate().is_ok());
let config: Config = patch.upgrade().unwrap();
assert_eq!(config.port, 9090);

// The object-side constructor performs the same conversion as downgrade().
let patch = ConfigOptional::optionize(config);
assert_eq!(patch.host.as_deref(), Some("gateway"));

These operations move values; neither the subject nor its fields need Clone. For generic helpers and objects with several subjects, see Optionized.

§Missing, clearing, and setting values

A nullable subject field Option<Value> becomes Option<Option<Value>>. The outer option records whether an update was supplied:

Patch valueEffect on the subjectComplete enough to upgrade?
NoneLeave the field unchangedNo
Some(None)Clear the fieldYes
Some(Some(value))Set the fieldYes
use optionize::{optionized, Optionizable, Optionized};

#[optionized]
struct Config { socket_mark: Option<u32> }

let mut config = Config { socket_mark: Some(7) };
config.load(ConfigOptional { socket_mark: None });
assert_eq!(config.socket_mark, Some(7));
config.load(ConfigOptional { socket_mark: Some(None) });
assert_eq!(config.socket_mark, None);
config.load(ConfigOptional { socket_mark: Some(Some(0)) });
assert_eq!(config.socket_mark, Some(0));

assert!(ConfigOptional { socket_mark: None }.validate().is_err());
assert!(ConfigOptional { socket_mark: Some(None) }.upgrade().unwrap().socket_mark.is_none());

Ordinary fields have no implicit defaults during upgrading: None is missing even when the field’s type implements Default, unless explicitly configured with #[optionize(default)] or default = callback. Deriving Default for an object makes wrapped fields None; flattened fields retain their type’s default values and still participate in updates. It does not fill the subject’s defaults.

§Upgrade defaults

default uses the subject field’s Default::default() when its update is absent. default = callback instead accepts a function or non-capturing closure with signature fn(&Object) -> FieldType. The parameter is the actual object, including when object = ... or subject = ... selects an existing type. Callbacks receive an immutable reference and never require the object to be cloned.

use optionize::{optionized, Optionized};

#[optionized]
struct Config {
    name: String,
    #[optionize(default = |object| object.name.as_ref().unwrap().len())]
    name_length: usize,
    #[optionize(default = reusable)]
    reusable: bool,
    #[optionize(default)]
    retries: u32,
}
fn reusable(_: &ConfigOptional) -> bool { true }

let patch = ConfigOptional {
    name: Some("node".into()), name_length: None, reusable: Some(false), retries: None,
};
patch.validate().unwrap(); // Does not execute the callbacks.
let config = patch.upgrade().unwrap();
assert_eq!(config.name_length, 4);
assert!(!config.reusable); // A supplied false is preserved.
assert_eq!(config.retries, 0);

Upgrading first validates the supplied fields. If validation fails, no default callbacks run. Otherwise, required defaults are evaluated once in declaration order, before any fields are moved out of the object. Every callback sees the original patch: defaults computed for earlier fields are not written back into it. Callbacks must handle other omitted defaulted fields themselves.

Defaults only apply during upgrading. They do not change patch, load, merge, retain, Serde behavior, or the object’s Default implementation. A supplied Some(None) is an explicit clear and does not trigger a default:

use optionize::{optionized, Optionized};
#[optionized]
struct Config {
    #[optionize(default = |_| Some("fallback".into()))]
    note: Option<String>,
}
assert_eq!(ConfigOptional { note: None }.upgrade().unwrap().note.as_deref(), Some("fallback"));
assert_eq!(ConfigOptional { note: Some(None) }.upgrade().unwrap().note, None);

A nested default returns the complete child subject, not its patch. It runs only when the whole child is absent; a supplied child still validates normally:

use optionize::{optionized, Optionized};
#[optionized]
struct Endpoint { port: u16 }
#[optionized]
struct Config {
    #[optionize(nest = EndpointOptional, default = |_| Endpoint { port: 8080 })]
    endpoint: Endpoint,
}
assert_eq!(ConfigOptional { endpoint: None }.upgrade().unwrap().endpoint.port, 8080);
assert!(ConfigOptional { endpoint: Some(EndpointOptional { port: None }) }.upgrade().is_err());

default cannot be combined with flatten, because flattened fields are always supplied. Standard defaults add FieldType: Default only to the upgrade implementation; other operations remain available without that bound. Callbacks are infallible functions, but may panic. They should not invalidate nested objects through shared interior state: nested values are checked again when consumed, and invalidation during construction causes a panic.

§Names and inherited attributes

name = "{}Patch" changes the generated type name; {} expands to the subject’s name. On a named field, {} expands to that field’s name. Tuple fields cannot be renamed. Attributes after #[optionized] are inherited by default.

attrs(...) specifies attributes on the generated type or field. With ordinary attributes only, it replaces the inherited attributes; attrs() clears them. Repeated lists are combined, so an empty list does not reset other lists. The original subject’s attributes remain in place.

use optionize::{optionized, Optionized};

#[optionized]
#[optionize(name = "{}Patch", attrs(derive(Debug, Default)), attrs(), attrs(derive(Clone)))]
#[derive(PartialEq)]
struct Config {
    /// The listening port on the complete configuration.
    #[optionize(name = "set_{}", attrs(doc = "An optional port update."))]
    port: u16,
}

let mut patch = ConfigPatch::default();
assert_eq!(patch.set_port, None);
patch.set_port = Some(8080);
assert_eq!(patch.clone().upgrade().unwrap().port, 8080);

To reuse original attributes, include selectors alongside ordinary attributes:

ItemMeaning
+pathInherit original attributes with this exact path.
..Inherit all original attributes.
-pathExclude matching attributes from the inherited selection.
Ordinary attributeAppend this attribute after the inherited selection.

Selectors operate only on the original attributes: -derive does not remove an explicitly added derive(...). Inherited attributes keep their original order; repeated selectors do not copy the same attribute twice, while multiple original doc attributes are all preserved. Explicit attributes follow in their written order across all lists and are not merged or deduplicated. Paths match exactly, without name resolution; a selector that matches nothing has no effect. -path alone does not imply inheriting everything else. Generated implementations allow access to deprecated fields and types, while direct accesses in user code still follow the user’s lint settings. Deprecation attributes are inherited normally. An enclosing forbid(deprecated) cannot be overridden by the generated allow(deprecated).

This example retains the type’s documentation while replacing its entire derive list, and retains the field’s documentation while appending another line:

use optionize::optionized;

#[optionized]
/// Connection settings.
#[derive(Clone)]
#[optionize(attrs(.., -derive, derive(Debug, Default)))]
struct Config {
    /// The listening port.
    #[optionize(attrs(+doc, doc = "Omit to keep the current port."))]
    port: u16,
}

let patch = ConfigOptional::default();
assert_eq!(format!("{patch:?}"), "ConfigOptional { port: None }");
let config = Config { port: 8080 };
assert_eq!(config.clone().port, 8080);

Each selector accepts an attribute path only. +derive or -derive selects or excludes whole derive(...) attributes; selecting individual derived traits is not supported. Replace the whole list as above instead:

ⓘ
use optionize::optionized;
#[optionized]
#[derive(Debug, Clone)]
#[optionize(attrs(.., -derive(Clone)))]
struct Config { port: u16 }

Place derives after #[optionized] if the object should inherit them. A derive that has already run before the macro is not inherited:

ⓘ
use optionize::optionized;
#[derive(Clone)]
#[optionized]
struct Config { port: u16 }
fn needs_clone<Value: Clone>() {}
needs_clone::<ConfigOptional>();

§Partial structs and skipped fields

By default, objects implement both PartialOptionized and Optionized. partial disables validation and upgrading. It also permits skip, which removes a field from the object and leaves that subject field untouched when patching. A skipped field need not implement Default when upgrading is disabled.

use optionize::{optionized, Optionizable};

struct Connection(u32);
#[optionized]
#[optionize(partial)]
struct Config {
    port: u16,
    #[optionize(skip)]
    connection: Connection,
}

let mut config = Config { port: 8080, connection: Connection(7) };
config.load(ConfigOptional { port: Some(9090) });
assert_eq!(config.port, 9090);
assert_eq!(config.connection.0, 7);

A partial-only object cannot be upgraded:

ⓘ
use optionize::{optionized, Optionized};
#[optionized]
#[optionize(partial)]
struct Config { port: u16 }
ConfigOptional { port: Some(8080) }.upgrade();

Use partial(upgradable) to retain upgrading while allowing skipped fields. During upgrading, a skipped field uses Default::default() or the callback supplied by #[optionize(skip, default = callback)]. It follows the same initialization rules as ordinary upgrade defaults and can inspect the original object. skip can be combined with default, but not other field options.

use optionize::{optionized, Optionized};

#[optionized]
#[optionize(partial(upgradable))]
struct Config {
    port: u16,
    #[optionize(skip)]
    connections: Vec<u32>,
    #[optionize(skip, default = |_| 3)]
    retries: u8,
}

let patch = ConfigOptional { port: Some(8080) };
assert!(patch.validate().is_ok());
let config = patch.upgrade().unwrap();
assert!(config.connections.is_empty());
assert_eq!(config.retries, 3);

§Generics, markers, tuples, and units

Field cfg and cfg_attr conditions are evaluated before mapping fields, including attributes enabled by cfg_attr. Removed fields need no available type and do not contribute validation or equality bounds. Tuple indices are determined after conditional fields are removed.

use optionize::{optionized, Optionized};
#[optionized]
struct Config {
    #[cfg(any())]
    platform: UnavailableType,
    #[cfg_attr(all(), optionize(name = "active"))]
    enabled: bool,
}
let config = ConfigOptional { active: Some(true) }.upgrade().unwrap();
assert!(config.enabled);

Generated objects preserve the subject’s generic parameters and bounds, including lifetimes and const parameters. Borrows may refer to local values:

use optionize::{optionized, Optionized};

#[optionized]
struct Window<'v, Value, const SIZE: usize> {
    values: &'v [Value; SIZE],
}
let values = [1, 2, 3];
let patch = WindowOptional { values: Some(&values) };
assert_eq!(patch.upgrade().unwrap().values, &values);

If skipping fields leaves a type or lifetime parameter unused, add partial(marked) to inject a PhantomData field. Named structs use an available _marker name by default; marked(name = ...) chooses one explicitly. marked(attrs(...)) uses the same attribute-selection rules, starting from the marker’s default #[doc(hidden)] attribute. For example, marked(attrs(.., allow(dead_code))) keeps it and adds another attribute.

use core::marker::PhantomData;
use optionize::{optionized, Optionized};

#[optionized]
#[optionize(partial(upgradable, marked(name = data, attrs(doc = "Tracks the data type."))))]
struct Config<Data: Default> {
    port: u16,
    #[optionize(skip)]
    cache: Data,
}

let patch = ConfigOptional::<Vec<u8>> { port: Some(8080), data: PhantomData };
assert!(patch.upgrade().unwrap().cache.is_empty());

Tuple fields keep their order, with skipped fields removed. A marker is appended to a tuple object and cannot be given a name. Unit subjects produce unit objects unless marked: then the object has a tuple marker, or a named marker when marked(name = ...) is specified.

use core::marker::PhantomData;
use optionize::{optionized, Optionized};

#[optionized]
struct Address(String, u16);
#[optionized]
struct Empty;
#[optionized]
#[optionize(partial(upgradable, marked))]
struct Tagged;

let address = AddressOptional(Some("localhost".into()), Some(8080)).upgrade().unwrap();
assert_eq!((address.0.as_str(), address.1), ("localhost", 8080));
let _: Empty = EmptyOptional.upgrade().unwrap();
let _: Tagged = TaggedOptional(PhantomData).upgrade().unwrap();

§Flattened fields

flatten omits the extra Option wrapper. Such fields always participate in patching and merging, including when their own value is None. They cannot represent an omitted update and do not need a presence check during upgrading.

use optionize::{optionized, Optionizable, PartialOptionized};

#[optionized]
struct Config {
    #[optionize(flatten)]
    socket_mark: Option<u32>,
    port: u16,
}
let mut config = Config { socket_mark: Some(7), port: 8080 };
let mut patch = ConfigOptional { socket_mark: Some(9), port: None };
patch.merge(ConfigOptional { socket_mark: None, port: None });
config.load(patch);
assert_eq!(config.socket_mark, None);
assert_eq!(config.port, 8080);

§Nested patches

nest = ChildOptional delegates operations to the nested object’s traits. Strings such as nest = "ChildOptional" are also accepted. Without nest, a supplied child replaces the whole child. With nest, merging and patching preserve omitted child fields. flatten and nest can be combined: the child patch is always present, but it still applies its own partial-update rules.

use optionize::{optionized, Optionizable, PartialOptionized, Retain};

#[optionized]
struct Endpoint { host: String, port: u16 }
#[optionized]
struct Config {
    #[optionize(nest = EndpointOptional)]
    primary: Endpoint,
    #[optionize(flatten, nest = "EndpointOptional")]
    fallback: Endpoint,
}

let mut config = Config {
    primary: Endpoint { host: "primary".into(), port: 8080 },
    fallback: Endpoint { host: "fallback".into(), port: 8081 },
};
let mut patch = ConfigOptional {
    primary: Some(EndpointOptional { host: Some("gateway".into()), port: None }),
    fallback: EndpointOptional { host: None, port: Some(9091) },
};
patch.merge(ConfigOptional {
    primary: Some(EndpointOptional { host: None, port: Some(9090) }),
    fallback: EndpointOptional { host: None, port: None },
});
config.load(patch);
assert_eq!(config.primary.host, "gateway");
assert_eq!(config.primary.port, 9090);
assert_eq!(config.fallback.host, "fallback");
assert_eq!(config.fallback.port, 9091);

// Comparison also recurses; Endpoint itself does not need PartialEq.
let mut patch = ConfigOptional {
    primary: Some(EndpointOptional { host: None, port: Some(9090) }),
    fallback: EndpointOptional { host: None, port: Some(9091) },
};
assert!(!patch.retain(&config));
assert!(patch.primary.is_none());
assert!(patch.fallback.port.is_none());

Upgrading validates every supplied child recursively. An absent optional child is a missing field; a flattened child is always validated. In a partial baseline, an absent child is unknown, so even an explicitly supplied empty child patch is retained when compared with it.

§Converted fields

as = Type maps a field to a different type in the generated object, such as a plain representation of a subject-only type. The two types must convert into each other with From, so no conversion can fail: downgrading converts the subject’s value into the object’s type, while patching and upgrading convert it back. Because PartialOptionized performs both directions, mapping a foreign type to another foreign type needs an existing implementation or a local wrapper.

A converted field combines with flatten and with default, but not with nest, which already determines the object’s field type. Under subject = ..., as names the subject’s field type, following nest.

use core::time::Duration;
use optionize::{optionized, Optionizable, Optionized, Retain};

/// The object representation of a duration.
#[derive(Debug, PartialEq)]
struct Seconds(u64);

impl From<Duration> for Seconds {
    fn from(value: Duration) -> Self {
        Self(value.as_secs())
    }
}

impl From<Seconds> for Duration {
    fn from(value: Seconds) -> Self {
        Self::from_secs(value.0)
    }
}

#[optionized]
struct Config {
    #[optionize(as = Seconds)]
    timeout: Duration,
}

let patch: ConfigOptional = Config { timeout: Duration::from_secs(30) }.downgrade();
assert_eq!(patch.timeout, Some(Seconds(30)));

let mut config = Config { timeout: Duration::ZERO };
config.load(ConfigOptional { timeout: Some(Seconds(7)) });
assert_eq!(config.timeout, Duration::from_secs(7));
assert_eq!(
    ConfigOptional { timeout: Some(Seconds(9)) }.upgrade().unwrap().timeout,
    Duration::from_secs(9)
);

// Converted fields have no comparable view entry: their updates are kept.
let mut patch = ConfigOptional { timeout: Some(Seconds(7)) };
assert!(patch.retain(&config));
assert_eq!(patch.timeout, Some(Seconds(7)));

The generated object stores the mapped type, so the subject’s type needs no PartialEq or Clone for comparison. Against any baseline, a converted field borrows as unknown in the shared Schema view, which keeps its update rather than removing it, exactly like a baseline field the compared type cannot know.

§Existing objects and external subjects

At least one side of a mapping must be local to the crate defining it. The external side needs public accessible fields and no optionize annotations. This supports generated protobuf structs in either direction. The modules below stand in for types that can instead be imported from another crate.

§Using an existing object

object = ... generates trait implementations for an existing object instead of generating another struct. Its fields must match the mapping’s types, names, flatten, skip, and nest choices. It cannot be combined with struct-level name, attrs, subject, or partial(marked).

object and subject both accept an unquoted type path or a string template. In strings, {} is replaced with the annotated struct’s name. Generic arguments must be explicit: unquoted paths use ::<Value> because attribute values are parsed as expressions; strings may use <Value>.

use optionize::{optionized, Optionizable, Optionized};
mod wire {
    pub struct ConfigPatch<Value> { pub value: Option<Value> }
}

// A reusable pattern also works when this attribute is applied by another macro.
#[optionized]
#[optionize(object = "wire::{}Patch<Value>")]
struct Config<Value> { value: Value }

let patch: wire::ConfigPatch<String> = Config { value: "node".into() }.downgrade();
let config: Config<String> = patch.upgrade().unwrap();
assert_eq!(config.value, "node");

The direct path object = wire::ConfigPatch::<Value> selects the same type. Lifetimes and const arguments must likewise be written in an existing type path; only generated object types inherit those arguments automatically. nest accepts the same quoted or direct type-path syntax, but no {} substitution.

§Using an external subject

Put the macro on the local object and specify subject = .... Declare ordinary object fields as Option<Value> yourself; type aliases for Option also work. Flattened fields retain their declared types. In this direction, a field’s name selects the subject field, and nest names the nested subject type.

use optionize::{optionized, Optionizable, Optionized, Retain};
mod model {
    pub struct Endpoint { pub port: u16 }
    pub struct Config<Value> { pub enabled: bool, pub data: Value, pub endpoint: Endpoint }
}

#[optionized]
#[optionize(subject = model::Endpoint)]
struct EndpointPatch { port: Option<u16> }

#[optionized]
#[optionize(subject = model::Config::<Value>)]
struct ConfigPatch<Value> {
    #[optionize(name = "enabled")]
    active: Option<bool>,
    data: Option<Value>,
    #[optionize(nest = model::Endpoint)]
    endpoint: Option<EndpointPatch>,
}

let mut config = model::Config {
    enabled: false,
    data: String::from("node"),
    endpoint: model::Endpoint { port: 8080 },
};
config.load(ConfigPatch { active: Some(true), data: None, endpoint: None });
assert!(config.enabled);
assert_eq!(config.endpoint.port, 8080);
let mut patch = ConfigPatch { active: Some(true), data: None, endpoint: None };
assert!(!patch.retain(&config));
let patch: ConfigPatch<String> = config.downgrade();
assert_eq!(patch.upgrade().unwrap().data, "node");

With subject, a skip field declares an unmanaged subject field using its subject type; the macro removes it from the local object. The same partial and upgrade-default rules apply. subject cannot be combined with object, struct-level name, or struct-level attrs. The object alone implements PartialOptionized; the subject receives Optionizable methods and a Schema for borrowed comparisons.

§Retaining changes

Retain::retain removes updates already represented by a complete or partial baseline, borrowing values without requiring Clone. It returns true when updates remain, or false when the entire patch can be omitted relative to that baseline. Unknown baseline fields do not remove updates.

use optionize::{optionized, Retain};

#[optionized]
struct Config { enabled: bool, socket_mark: Option<u32> }

let current = Config { enabled: true, socket_mark: Some(7) };
let mut patch = ConfigOptional { enabled: Some(true), socket_mark: Some(None) };
assert!(patch.retain(&current));
assert_eq!(patch.enabled, None);
assert_eq!(patch.socket_mark, Some(None)); // Still needs to clear the current value.

let unknown = ConfigOptional { enabled: None, socket_mark: None };
assert!(patch.retain(&unknown));
assert_eq!(patch.socket_mark, Some(None)); // Unknown does not mean already cleared.

let cleared = ConfigOptional { enabled: None, socket_mark: Some(None) };
assert!(!patch.retain(&cleared));
assert_eq!(patch.socket_mark, None);

The macro provides Retain automatically when the compared fields implement PartialEq. The subject itself need not implement PartialEq. Nested patches compare their fields recursively, and skipped fields do not participate. Equality follows each field’s PartialEq implementation; for example, a floating point NaN compares unequal to itself and remains in the patch.

Flattened fields stay stored because they cannot represent absence. Their equality still affects the return value: false does not imply that every stored field is None.

use optionize::{optionized, Retain};
#[optionized]
struct Config {
    #[optionize(flatten)]
    enabled: bool,
}
let mut patch = ConfigOptional { enabled: false };
assert!(!patch.retain(&Config { enabled: false }));
assert!(!patch.enabled); // The flattened value is still present.
assert!(patch.retain(&Config { enabled: true }));

A generic helper can accept either a full or partial baseline through Schema<Subject, Patch>. The macro generates both schemas. Neither callers nor generic helpers need to construct views:

use optionize::{optionized, Retain, Schema};

fn retain_changes<Patch, Subject, Baseline>(patch: &mut Patch, baseline: &Baseline) -> bool
where
    Patch: Retain<Subject>,
    Baseline: Schema<Subject, Patch>,
{
    patch.retain(baseline)
}

#[derive(PartialEq)]
struct Token(String); // Deliberately has no Clone implementation.
#[optionized]
struct Config { token: Token }

let current = Config { token: Token("same".into()) };
let mut patch = ConfigOptional { token: Some(Token("same".into())) };
assert!(!retain_changes(&mut patch, &current));
let mut patch = ConfigOptional { token: Some(Token("new".into())) };
assert!(retain_changes(&mut patch, &ConfigOptional { token: None }));
assert_eq!(patch.token.unwrap().0, "new");

A different baseline type may implement Schema<Subject, Patch> without implementing patch operations. Its view must be the one owned by Patch; sharing the subject type alone does not make two object schemas interchangeable. Views borrow the known fields and recursively construct nested views. For a custom read-only baseline, start with Default::default() (all fields unknown) and fill the known v_<subject_field> entries. View fields have the same visibility as the annotated fields; tuple fields use v_0, v_1, and so on.

use optionize::{optionized, Retain, Schema};

#[optionized]
struct Config { enabled: bool, port: u16 }
struct Status { active: bool }

impl Schema<Config, ConfigOptional> for Status {
    type View<'v> = <ConfigOptional as Schema<Config>>::View<'v>;
    fn view<'s>(&'s self) -> <ConfigOptional as Schema<Config>>::View<'s>
    where
        Config: 's,
    {
        let mut view: <ConfigOptional as Schema<Config>>::View<'s> = Default::default();
        view.v_enabled = Some(&self.active);
        view
    }
}

let mut patch = ConfigOptional { enabled: Some(true), port: Some(8080) };
assert!(patch.retain(&Status { active: true }));
assert_eq!(patch.enabled, None);
assert_eq!(patch.port, Some(8080)); // Status knows nothing about the port.

Comparison requirements do not restrict other operations:

use optionize::{optionized, Optionized};
struct Connection;
#[optionized]
struct Config { connection: Connection }
let _: Config = ConfigOptional { connection: Some(Connection) }.upgrade().unwrap();

Calling retain still requires comparison support:

ⓘ
use optionize::{optionized, Retain};
struct Connection;
#[optionized]
struct Config { connection: Connection }
let mut patch = ConfigOptional { connection: Some(Connection) };
patch.retain(&Config { connection: Connection });

This also applies to nested fields, even when a particular patch omits that child:

ⓘ
use optionize::{optionized, Retain};
struct Connection;
#[optionized]
struct Child { connection: Connection }
#[optionized]
struct Parent {
    #[optionize(nest = ChildOptional)]
    child: Child,
}
let mut patch = ParentOptional { child: None };
patch.retain(&Parent { child: Child { connection: Connection } });

§Validation and errors

validate() borrows the patch and collects all missing fields and nested validation failures in an ErrorCollection. upgrade() validates and then consumes the patch on success or failure. Validate first when an invalid patch must remain available for correction. upgrade_unchecked() is only safe when validation would succeed; ordinary callers should use upgrade().

use optionize::{optionized, Error, FieldInfo, Optionized, PartialOptionized};

#[optionized]
struct Endpoint { port: u16 }
#[optionized]
struct Config {
    #[optionize(name = "label")]
    name: String,
    note: Option<String>,
    #[optionize(nest = EndpointOptional)]
    endpoint: Endpoint,
}

let mut patch = ConfigOptional {
    label: None,
    note: Some(None), // Explicitly cleared: this nullable field is complete.
    endpoint: Some(EndpointOptional { port: None }),
};
let errors = patch.validate().unwrap_err();
assert_eq!(errors.len(), 2);
assert!(errors.iter().any(|error| matches!(error,
    Error::Missing { field: FieldInfo::Renamed { original: "name", optionized: "label" }, .. }
)));
let nested = errors.iter().find(|error| matches!(error, Error::Nested { .. })).unwrap();
assert!(std::error::Error::source(nested).is_some());
// Display groups errors by mapping; iteration permits structured reporting.
assert!(errors.to_string().contains("Missing required field"));

patch.merge(ConfigOptional {
    label: Some("node".into()),
    note: None,
    endpoint: Some(EndpointOptional { port: Some(8080) }),
});
let config = patch.upgrade().unwrap();
assert_eq!(config.name, "node");
assert_eq!(config.note, None);
assert_eq!(config.endpoint.port, 8080);

Each error identifies the subject/object mapping through TypeInfo and the field through FieldInfo, including both names when renamed. A nested error wraps an individual child error accessible through core::error::Error::source.

§Crate paths

Renamed Cargo dependencies are detected automatically. When using a re-export or another custom path, provide it to optionized, not the optionize helper:

use optionize as patches;
use patches::Optionized;

#[patches::optionized(crate = patches)]
struct Config { port: u16 }

assert_eq!(ConfigOptional { port: Some(8080) }.upgrade().unwrap().port, 8080);

Structs§

ErrorCollection
TypeInfo

Enums§

Error
FieldInfo

Traits§

Optionizable
Provides extension methods on the original subject struct to easily work with its PartialOptionized counterpart. The object type is usually inferred from the supplied patch or expected return type.
Optionized
Validates a partial object’s completeness and upgrades it to the full subject.
PartialOptionized
Relates a partial object to its complete subject. Allows extracting partial data from a full struct, applying partial data to a full struct, and merging two partial structs together.
Retain
Removes updates already represented by a borrowed baseline.
Schema
Reads borrowed fields from complete subjects and partial representations.

Attribute Macros§

optionized
Generates an optionized version of a struct, replacing its fields with Option<Value> where applicable, and implements conversion and merge logic for partial updates and builders.