cu29-value 1.3.0

This is a fork of Value with custom types added for Copper like the time and units.
Documentation

copper-value

license-badge

copper-value provides a way to capture serialization value trees for later processing. Customizations are made to enable a more compact representation for the structured logging of copper.

Native payload descriptions

Enable cu29/self-describing-logs to describe native Encode bytes and decode them into a Value tree offline. The feature enables std and reflection. Encode derives supply the ValueDecode companion automatically; payload authors keep their usual Reflect derive.

use cu29::bincode::{Decode, Encode};
use cu29::prelude::*;

#[derive(Clone, Debug, Default, Serialize, Deserialize, Encode, Decode, Reflect)]
struct Sample {
    ticks: u32,
    valid: bool,
}

let description = ValueDecodeDescription::from_type::<Sample>()?;
let config = cu29::bincode::config::standard();
let mut bytes = [0; 32];
let len = cu29::bincode::encode_into_slice(
    Sample { ticks: 42, valid: true }, &mut bytes, config,
)?;
let (tree, consumed) = description.decode(
    &bytes[..len], config, ValueDecodeLimits::default(),
)?;

A description implements native bincode Encode and Decode. After transporting it as bytes, an offline reader uses only the description, payload bytes, and the producer's codec configuration. Decoding returns the exact number of consumed bytes, allowing sequential values to be read from one buffer.

Descriptions preserve field names, original type identities, scalar widths, and quantity storage units. Each type declares a static, typed ValueDecode::METADATA slice using Copper's metadata vocabulary, shared through cu29-value-types. All supported quantities retain their coherent storage unit in both scalar widths: length is m, velocity is m·s⁻¹, and mass is kg. Copper clock values retain nanosecond storage. Unit symbols are presentation output from typed storage units.

A handwritten type can attach existing Copper metadata:

use cu29_value::{QuantityMetadata, TimeStorageUnit, ValueMetadata};
use bincode::{ValueDecode, ValueDecodeSpec};

struct Elapsed(u64);
impl ValueDecode for Elapsed {
    const DECODE: &'static ValueDecodeSpec = <u64 as ValueDecode>::DECODE;
    const METADATA: &'static [ValueMetadata] = &[
        ValueMetadata::Quantity(
            QuantityMetadata::time(TimeStorageUnit::Nanosecond),
        ),
    ];
}

Portable schemas contain metadata entries. schema.quantity() returns recognized quantity metadata; entry.known() returns recognized typed metadata. Entries use permanent numeric kind IDs and length-delimited bodies. Quantity kind 1 contains exactly eight bytes: a little-endian u32 quantity ID followed by a little-endian u32 storage alternative (1 for coherent storage, 2 for nanoseconds). The outer kind and body length use the description's bincode configuration. Copper assigns new IDs when its vocabulary grows and retains existing meanings.

An older reader preserves unknown kinds, quantities, and storage alternatives verbatim. It exposes the decoded payload value and leaves unsupported units uninterpreted. Invalid known combinations and malformed or truncated entries return a decoding error. Metadata framing supports evolving metadata alongside wire operations the reader already understands.

For a handwritten encoder, implement ValueDecode by delegating to the type actually written. An opaque reflected wrapper encoding [f32; 4] declares:

impl ValueDecode for Orientation {
    const DECODE: &'static ValueDecodeSpec = <[f32; 4] as ValueDecode>::DECODE;
}

Structurally reflected encoded fields must be present in reflection with the same native type. Tuple fields require reflection to retain every declaration position. Hidden encoded fields and ambiguous tuple mappings return a description-building error. For opaque reflection, the static encoding recipe supplies the encoded field names, declaration positions, and enum variants. Skipped fields are excluded from the wire description. Missing nested descriptions and custom codec descriptions fail compilation with self-describing-logs enabled.

Description construction and value decoding allocate in offline tooling. Native message encoding keeps its existing byte layout and encoding pass. The portable IR is experimental. Enable cu29-value/decode-catalog for offline catalog readers. Applications enable cu29/self-describing-logs to generate and save the shared compressed catalog only at startup, before resources. It occupies one static section beside application metadata and contains ordered slot maps for every compiled mission. Native recipes and names are borrowed from the application image; sizing and compression use bounded working memory. See examples/cu_self_describing_logs and doc/self-describing-logs.md. Run just self-describing-logs-check at the Copper workspace root to verify this API.

Python Feature

With the python feature enabled, this crate also provides conversion helpers between cu29_value::Value and Python objects via PyO3.

That bridge is used in two places:

  • cu-python-task, where Copper temporarily converts task inputs/state/outputs so a Python function can mutate them
  • cu29-export, where Copper data is exposed to Python for offline analysis

Conversion behavior is intentionally simple:

  • None maps to Value::Unit
  • Python lists and tuples map to Value::Seq
  • Python dicts map to Value::Map
  • Python integers are accepted up to 128-bit signed/unsigned range
  • Python bytes maps to Value::Bytes

Coherent storage symbols use conventional Unicode SI notation, such as V, T, N·m, and m·s⁻¹, from the shared cu29-value-types catalogue. JSON and RON metadata include a typed quantity and its presentation symbol. Readers interpret the permanent IDs and retain unknown metadata bodies.