qubit-metadata 0.2.3

Type-safe extensible metadata model for the Qubit LLM SDK
Documentation

Qubit Metadata

CircleCI Coverage Status Crates.io Rust License 中文文档

A general-purpose, type-safe, extensible metadata model for Rust.

Overview

qubit-metadata provides a Metadata type — a structured key-value store designed for any domain that needs to attach arbitrary, typed, serializable annotations to its data models. Common use cases include:

  • Attaching contextual information to domain entities (messages, records, events)
  • Carrying provider- or adapter-specific fields without polluting core models
  • Expressing query filter conditions over annotated data (e.g. vector stores)
  • Passing through opaque metadata across service or library boundaries

It is not a plain HashMap<String, String>. It is a domain-level extensibility point with type-safe access, serde_json::Value backing, and first-class serde support.

Design Goals

  • Type Safety: Typed get/set API backed by serde_json::Value
  • Generality: No domain-specific assumptions — usable in any Rust project
  • Extensibility: Acts as a structured extension point, not a stringly-typed bag
  • Serialization: First-class serde support for JSON interchange
  • Filtering: MetadataFilter for composable predicates over Metadata

Features

🗂 Metadata

  • Ordered key-value store with String keys and serde_json::Value values
  • Two typed access layers: convenience get::<T>() / set() and explicit try_get::<T>() / try_set()
  • Lightweight JSON value type inspection via MetadataValueType
  • Merge, extend, and iterate operations
  • Full serde serialization / deserialization support
  • Debug, Clone, PartialEq, Default derives

🔍 MetadataFilter

  • Composable tree of conditions over metadata keys, evaluated with matches(&metadata)
  • Comparison, existence, and set-membership leaves; logical and, or, and not
  • Serialize / Deserialize for persisting or exchanging filter definitions as JSON
  • Useful for in-memory filtering, query predicates, or any pipeline that needs a portable metadata predicate

Installation

Add to your Cargo.toml:

[dependencies]
qubit-metadata = "0.2.0"

Usage

use qubit_metadata::Metadata;

let mut meta = Metadata::new();
meta.set("author", "alice");
meta.set("priority", 3_i64);
meta.set("reviewed", true);

// Convenience API: terse and forgiving.
let author: Option<String> = meta.get("author");
assert_eq!(author.as_deref(), Some("alice"));

// Explicit API: preserves failure reasons.
let priority = meta.try_get::<i64>("priority").unwrap();
assert_eq!(priority, 3);

MetadataFilter

MetadataFilter describes a predicate on a single Metadata value. You build a tree of conditions, then call matches to test whether a given Metadata satisfies it.

Leaf constructors

Each leaf inspects one key (values are serialized to serde_json::Value the same way as Metadata::set):

Constructor Semantics
equal, not_equal Equality / inequality of the stored JSON value
greater, greater_equal, less, less_equal Ordered comparison (numeric ordering, string lexicographic order, and mixed-type rules as implemented for your value shapes)
exists, not_exists Key present / absent
in_values, not_in_values Membership of the key’s value in a finite set

Logical combinators

  • and — all sub-filters must match. If self is already an And node, the new filter is appended (flat structure).
  • or — at least one sub-filter must match (same flattening behavior as and).
  • not — negates a filter. The ! operator is also implemented via Not for MetadataFilter.

Edge cases: an And with no children matches every Metadata; an Or with no children matches none.

Evaluation

Call filter.matches(&meta) to obtain a bool. How a missing key interacts with each leaf operator is defined on Condition (for example, equal requires the key to exist; not_equal may still match when the key is absent).

Serde

MetadataFilter implements Serialize and Deserialize, so you can store filters in configuration, databases, or JSON APIs alongside your metadata model.

Examples

AND — all conditions must hold:

use qubit_metadata::{Metadata, MetadataFilter};

let mut meta = Metadata::new();
meta.set("status", "active");
meta.set("score", 42_i64);

let filter = MetadataFilter::equal("status", "active")
    .and(MetadataFilter::greater_equal("score", 10_i64));

assert!(filter.matches(&meta));

OR, membership, and negation:

use qubit_metadata::{Metadata, MetadataFilter};

let mut meta = Metadata::new();
meta.set("region", "eu");
meta.set("tier", "pro");

let filter = MetadataFilter::in_values("region", ["eu", "us"])
    .or(MetadataFilter::equal("tier", "enterprise"));

assert!(filter.matches(&meta));

let hide_drafts = MetadataFilter::equal("status", "draft").not();
// Equivalent: !MetadataFilter::equal("status", "draft")
assert!(hide_drafts.matches(&meta));

Error Handling

When callers need to distinguish missing keys from type mismatches, prefer the explicit APIs and inspect MetadataError:

use qubit_metadata::{Metadata, MetadataError, MetadataValueType};

let mut meta = Metadata::new();
meta.set("answer", "forty-two");

match meta.try_get::<i64>("answer") {
    Err(MetadataError::DeserializationError { actual, .. }) => {
        assert_eq!(actual, MetadataValueType::String);
    }
    other => panic!("unexpected result: {other:?}"),
}

License

Licensed under the Apache License, Version 2.0.