typex
typex is a lightweight Rust library for inspecting, traversing, comparing and
mutating values at runtime without knowing their concrete types. It supports
common Rust types, such as u8 and Vec<T>, as well as custom structs and
enums using #[derive(Meta)]. Values can be wrapped in an Object, which
provides reflection operations. typex supports no_std.
Why typex?
typex builds on dyn Any and provides an ergonomic API on top of it.
bevy_reflect is a broader reflection
framework used by Bevy. It covers structural type information, dynamic
reflected values, instantiation and serialisation. typex has a smaller surface
and focuses on structural access, mutation and equality of runtime objects.
Instantiation, deserialisation and instance-independent type descriptors are
out of scope.
Contents
- Why typex?
- Features
- Installation
- Supported types
- Quick start
- Core concepts
- Derive macros
- Mutation
- Reflective patching
- Staged mutation batches
- Thread-safe objects
- Workspace structure
- Licence
Features
- Runtime type introspection of objects
- Structural access, traversal, mutation and equality (fields, keys, items)
- Typed paths for nested access
- Patch operations with batching and rollback
- Type-keyed maps for registries and dispatch tables
- Support for common scalar, container and collection types
- Derive macros for structs and enums
- MSRV: Rust 1.85 (edition 2024)
- Compatible with
no_std+alloc - No runtime dependencies
- No
unsafecode - Small implementation: ~4k lines of Rust code
Installation
[]
= { = "0.1.0", = ["derive"] }
The derive feature re-exports the Meta and MetaMut derive macros from
typex_derive. Leave it out to use the reflection traits without the
derives.
The std feature is enabled by default. It adds HashMap support and
Error implementations
for the crate's error types. Disable default features to use typex in
no_std environments with alloc:
[]
= { = "0.1.0", = false }
You can read the full API docs on docs.rs.
Supported types
- Scalars:
bool,char,i8/i16/i32/i64/i128/isize,u8/u16/u32/u64/u128/usize,f32/f64,&'static str,String - Containers:
Option,Result, tuples (up to 8 elements), arrays of any length,Box,Rc,Arc - Collections:
Vec,VecDeque,LinkedList,BTreeSet,BinaryHeap,BTreeMap,HashMap(requiresstd) - Custom structs and enums via
#[derive(Meta)]. Mutable reflection is opt-in via the separateMetaMutderive.
Quick start
Enable the derive feature when using the derive macros.
let object = new;
assert!;
assert_eq!;
assert_eq!;
assert_eq!;
let scope_name = ;
assert_eq!;
// Use typed paths to traverse fields without an explicit downcast
assert_eq!;
assert_eq!;
Core concepts
Borrowed object views
Object owns a boxed value with reflective access. ObjectRef is the
corresponding read-only view, which borrows the value without allocating. Use it
when the value already exists elsewhere, or when passing a borrowed view of the
value to another function:
use ObjectRef;
let values = vec!;
let view = new;
assert!;
assert_eq!;
assert_eq!;
assert_eq!;
You do not need to construct an ObjectRef explicitly when starting with an
owned Object. The read-only navigation methods on Object return borrowed
ObjectRef values:
let object = new;
let item = object.item.unwrap;
assert_eq!;
ObjectRef is returned by field(), item(), key() and raw path traversal.
Use ObjectRefMut when the underlying value must be changed.
Container access
Objects expose indexed, named and keyed access through item(), field() and
key():
let option = new;
let list = new;
let record = new;
let map = new;
assert_eq!;
assert_eq!;
// Tuple fields use string names, so "0" refers to the first field.
assert_eq!;
assert_eq!;
Map access
key() looks up string-like keys. key_typed() looks up keys using their
concrete key type. visit_map_entries() visits arbitrary keys through a
MapEntryVisitor callback without converting them to strings.
len() reports the number of exposed structural items, including keyed entries
for maps. Map implementations must provide that count and expose their entries
through visit_map_entries() for structural equality. Maps with non-string
keys must override Meta::eq_dyn.
use ;
let numbers = from;
assert_eq!;
let mut mutable_numbers = numbers.clone;
mutable_numbers
.key_typed_mut
.unwrap
.set
.unwrap;
assert_eq!;
let object = new;
let mut seen = Vecnew;
assert!;
assert_eq!;
Type shapes
kind() reports the structural shape of an object. access_kind() reports how
the value can be traversed.
let scalar = new;
let shape = ;
assert_eq!;
assert_eq!;
assert_eq!;
assert_eq!;
assert_eq!;
AccessKind::ItemKey is reserved for types that combine keyed and indexed
access, such as indexmap, using
operations such as key() and item() together.
Option<T> always reports ValueKind::Option as its own shape. For
Some, field() and key() forward to the inner value, while item(0) and
option_value() expose that value as an ObjectRef. access_kind() follows
the inner value's access mode. For None, these operations return no inner
value or forwarded access.
Type information
is(), type_info() and type_name() expose runtime type information. Owned
values should be created explicitly with Object::new or
ObjectMut::from_clone when an owned mutable clone is required. An
ObjectMut keeps the value mutable while an Object is immutable:
use ObjectMut;
let object = new;
assert!;
assert_eq!;
assert_eq!;
let mutable = from_clone;
assert_eq!;
Object conversions
ObjectOps provides conversions to borrowed or owned values. to_ref()
borrows the value. to() consumes an owned value and performs an exact-type
downcast without requiring Clone. Use Rust's From or TryFrom traits for
conversions between known concrete types.
assert_eq!;
assert_eq!;
Equality
Meta::eq_dyn() compares two Meta values and is also available as == on
ObjectRef, ObjectRefMut and &dyn Meta. Mismatched concrete types are
never equal. Structural values compare recursively through exposed fields, map
entries or indexed items. Derived types can opt into their own PartialEq
implementation with #[typex(partial_eq)]; see
Equality options.
Hand-written Meta implementations can implement eq_dyn() using PartialEq,
as #[typex(partial_eq)] does:
;
let a = ManualId;
let b = ManualId;
assert!;
Equality dispatches on Meta::kind(), so a struct or map with zero exposed
fields compares equal to itself:
;
let a = EmptyState;
let b = EmptyState;
assert!;
Paths
FieldPath traverses several hops in one call using either raw
&[PathSegment] values or typed paths. Typed item steps support Vec, arrays,
VecDeque and LinkedList. Typed key steps support BTreeMap and HashMap
with String or &'static str keys. BTreeSet and BinaryHeap support
read-only indexed reflection through raw paths, but do not support typed item
steps or mutable item access.
Raw paths return an ObjectRef to downcast explicitly, while typed paths
return a reference to the target:
use BTreeMap;
let mut payload = Payload ;
assert_eq!;
assert_eq!;
assert_eq!;
Dispatch tables
TypeMap<T> maps type identities to values of type T. It is useful for
registries and dispatch tables. For example, a router could be represented as
follows:
;
;
let mut routes = new;
routes.;
routes.;
assert_eq!;
assert!;
Similarly, a dispatch table can bind marker types to handlers:
;
;
let mut handlers = new;
handlers.;
handlers.;
let handler = handlers..unwrap;
assert_eq!;
Derive macros
With the derive feature, typex re-exports these macros from typex_derive:
#[derive(Meta)]: provides structural read-only access for structs and enums. Add#[typex(opaque)]to hide fields and items.#[derive(MetaMut)]: adds structural mutation and whole-value replacement;Metamust be derived separately.
Opaque types
By default, derived structs expose their fields and enums expose their active
variant and payload fields. Add #[typex(opaque)] when the representation
should remain hidden or child types do not implement the required reflection
trait:
;
For enums, opaque hides both the active variant and its payload fields:
Opaque values still support replacement through
ObjectRefMut::set; see Mutation.
Equality options
#[typex(opaque)] and #[typex(partial_eq)] control different aspects of a
derived type:
#[typex(opaque)]hides fields and items. Withoutpartial_eq,eq_dyn()has no fields or items to compare and returnsfalse, even for the same value.#[typex(partial_eq)]uses the type'sPartialEqimplementation instead of structural equality. It can be used with or withoutopaque.#[typex(opaque, partial_eq)]hides the structure and usesPartialEq.
An opaque type without partial_eq is unequal even to itself:
;
let id = OpaqueId;
assert!;
Combining opaque with partial_eq restores equality via PartialEq:
;
let id = UserId;
assert!;
Capabilities and bounds
Field types must implement Meta. The crate provides implementations for the
supported types.
For generic types, the derives preserve the declared generics. They add a
Meta or MetaMut bound for each field type that uses a type parameter.
Bounding a field type that contains the derived type would overflow trait
resolution. For such a field, the derives bound only the parts that use a type
parameter without the derived type. Here, Tree<T> requires T: Meta for
value, but no bound on Vec<Tree<T>>:
let tree = Tree ;
assert_eq!;
Opaque types expose no child paths, so they add no child capability bounds.
Meta and MetaMut do not require Clone or Debug for ordinary reflective
access. Use ObjectMut::from_clone when an owned clone is needed. Wrapper
Debug implementations report type metadata and shape, so they do not require
the underlying value to implement Debug.
Mutation
ObjectRefMut::set replaces the whole value with an owned Object when
the concrete types match. This also works for opaque values. MetaMut then
provides structural mutation through fields, items and keys:
;
let mut id = UserId;
new
.set
.unwrap;
assert_eq!;
*payload.field_mut.unwrap..unwrap = 4;
assert_eq!;
The derive macro defines constants for type-safe access. Compose them with
then for nested fields, item for sequence items and key for map entries:
let mut root = new;
*root
.field_path_mut
.unwrap = "secondary";
root
.set_field_path
.unwrap;
drop;
assert_eq!;
assert_eq!;
ObjectRefMut also provides collection operations (item_mut,
key_mut, push_item, insert_key). Each returns a mutable view that supports
set to replace the value or to_mut to access the underlying value.
use BTreeMap;
use ReflectiveError;
let mut values = vec!;
let mut values_ref = new;
values_ref
.item_mut
.unwrap
.set
.unwrap;
*values_ref
.push_item
.unwrap
.
.unwrap += 1;
drop;
assert_eq!;
let mut map = from;
let mut map_ref = new;
map_ref.key_mut.unwrap.set.unwrap;
map_ref.insert_key.unwrap;
drop;
assert_eq!;
assert!;
Reflective patching
A patch groups mutating operations that target one or more paths and applies them in order. Use it to batch fine-grained updates.
use BTreeMap;
use PatchOperation;
let mut config = ServiceConfig ;
let patch = vec!;
new.apply.unwrap;
assert!;
assert_eq!;
assert_eq!;
assert_eq!;
let path_patch = vec!;
new.apply.unwrap;
assert!;
assert_eq!;
PatchOperation also provides remove_key, insert_item, push_item,
remove_item and move_item. ObjectRefMut::apply applies operations in
order, stops at the first failure and leaves earlier successful operations
applied. Use a MutationBatch when earlier changes must be undone after a
later operation fails.
MetaMut::move_item defaults to MoveItemError::Unsupported. Sequential types
override it with a native move operation.
Staged mutation batches
Use a MutationBatch when a group of changes must be committed
transactionally. A batch owns its operations without borrowing or changing the
target. commit consumes the batch, applies operations in order against the
target's current state and records owned inverses for successful operations. If
a later operation fails, it replays those inverses in reverse order to roll
back the earlier changes.
use ;
let mut values = vec!;
let batch = from;
let rollback = batch.commit.unwrap;
assert_eq!;
rollback.undo.unwrap;
assert_eq!;
Call MutationRollback::undo immediately or retain the rollback to undo the
changes later. A later batch can then stage replacement and structural
operations in its execution order:
use ;
let mut settings = Settings ;
let name_path = ;
let batch = from;
batch
.commit
.unwrap
.undo
.unwrap;
assert_eq!;
let batch = from;
let rollback = batch.commit.unwrap;
assert_eq!;
rollback.undo.unwrap;
assert_eq!;
Structural operations affect the paths and indexes used by later operations.
Rollback replays an inverse for each operation instead of restoring a
whole-value snapshot, and staged values become visible only after commit.
Custom MetaMut implementations must implement MetaMut::replace correctly
when whole-value inverse operations are required.
A custom MetaMut implementation can also make a commit fail in a way that
leaves the target inconsistent. CommitError::RollbackFailed reports an
inverse that could not be applied; CommitOperationError::ItemLost reports a
move that could not restore its item. In both cases, treat the target as
invalid for transactional purposes.
Thread-safe objects
Object is not Send. Use SendObject to move a boxed object across thread
boundaries. SendMeta is the backing trait for SendObject; normal callers
should use the owned wrapper:
;
let job = new;
spawn
.join
.unwrap;
Workspace structure
The workspace contains two crates:
typex: runtime reflection, mutation, object APIstypex_derive: derive macros forMetaandMetaMut, re-exported by thederivefeature oftypex
Licence
Licensed under the Apache License, Version 2.0.