Skip to content
/ sval Public

A lightweight, no-std, object-safe, serialization-only framework for Rust

License

Apache-2.0, MIT licenses found

Licenses found

Apache-2.0
LICENSE-APACHE
MIT
LICENSE-MIT
Notifications You must be signed in to change notification settings

sval-rs/sval

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

sval

Rust Latest version Documentation Latest

What is it?

sval is a serialization-only framework for Rust. It has a simple, but expressive design that can express any Rust data structure, plus some it can't yet. It was originally designed as a niche framework for structured logging, targeting serialization to JSON, protobuf, and Rust's Debug-style formatting. The project has evolved beyond this point into a fully general and capable framework for introspecting runtime data.

The core of sval is the Stream trait. It defines the data model and features of the framework. sval makes a few different API design decisions compared to serde, the de-facto choice, to better accommodate the needs of Rust diagnostic frameworks:

  1. Simple API. The Stream trait has only a few required members that all values are forwarded to. This makes it easy to write bespoke handling for specific data types without needing to implement unrelated methods.
  2. dyn-friendly. The Stream trait is internally mutable, so is trivial to make dyn-compatible without intermediate boxing, making it possible to use in no-std environments.
  3. Buffer-friendly. The Stream trait is non-recursive, so values can be buffered as a flat stream of tokens and replayed later.
  4. Borrowing as an optimization. The Stream trait may accept borrowed text or binary fragments for a specific lifetime 'sval, but is also required to accept temporary ones too. This makes it possible to optimize away allocations where possible, but still force them if it's required.
  5. Broadly compatible. sval imposes very few constraints of its own, so it can trivially translate implementations of serde::Serialize into implementations of sval::Value.

sval's data model takes inspiration from CBOR, specifically:

  1. Small core. The base data model of sval is small. The required members of the Stream trait only includes nulls, booleans, text, 64-bit signed integers, and sequences. All other types, like arbitrary-precision floating point numbers, records, and tuples, are representable in the base model.
  2. Extensible tags. Users can define tags that extend sval's data model with new semantics. Examples of tags include Rust's Some and None variants, constant-sized arrays, text that doesn't require JSON escaping, and anything else you might need.

Getting started

This section is a high-level guided tour of sval's design and API. To get started, add sval to your Cargo.toml:

[dependencies.sval]
version = "2.16.0"
features = ["derive"]

Serializing values

As a quick example, here's how you can use sval to serialize a runtime value as JSON.

First, add sval_json to your Cargo.toml:

[dependencies.sval_json]
version = "2.16.0"
features = ["std"]

Next, derive the Value trait on the type you want to serialize, including on any other types it uses in its fields:

#[derive(sval::Value)]
pub struct MyRecord<'a> {
    field_0: i32,
    field_1: bool,
    field_2: &'a str,
}

Finally, use stream_to_string to serialize an instance of your type as JSON:

let my_record = MyRecord {
    field_0: 1,
    field_1: true,
    field_2: "some text",
};

// Produces:
//
// {"field_0":1,"field_1":true,"field_2":"some text"}
let json: String = sval_json::stream_to_string(my_record)?;

The Value trait

The previous example didn't reveal a lot of detail about how sval works, only that there's a Value trait involved, and it somehow allows us to convert an instance of the MyRecord struct into a JSON object. Using cargo expand, we can peek behind the covers and see what the Value trait does. The previous example expands to something like this:

impl<'a> sval::Value for MyRecord<'a> {
    fn stream<'sval, S: sval::Stream<'sval> + ?Sized>(&'sval self, stream: &mut S) -> sval::Result {
        let type_label = Some(&sval::Label::new("MyRecord").with_tag(&sval::tags::VALUE_IDENT));
        let type_index = None;

        stream.record_tuple_begin(None, type_label, type_index, Some(3))?;

        let mut field_index = 0;

        // field_0
        {
            field_index += 1;

            let field_index = &sval::Index::from(field_index - 1).with_tag(&sval::tags::VALUE_OFFSET);
            let field_label = &sval::Label::new("field_0").with_tag(&sval::tags::VALUE_IDENT);

            stream.record_tuple_value_begin(None, field_label, field_index)?;
            stream.value(&self.field_0)?;
            stream.record_tuple_value_end(None, field_label, field_index)?;
        }

        // field_1
        {
            field_index += 1;

            let field_index = &sval::Index::from(field_index - 1).with_tag(&sval::tags::VALUE_OFFSET);
            let field_label = &sval::Label::new("field_1").with_tag(&sval::tags::VALUE_IDENT);

            stream.record_tuple_value_begin(None, field_label, field_index)?;
            stream.value(&self.field_1)?;
            stream.record_tuple_value_end(None, field_label, field_index)?;
        }

        // field_2
        {
            field_index += 1;

            let field_index = &sval::Index::from(field_index - 1).with_tag(&sval::tags::VALUE_OFFSET);
            let field_label = &sval::Label::new("field_2").with_tag(&sval::tags::VALUE_IDENT);
            
            stream.record_tuple_value_begin(None, field_label, field_index)?;
            stream.value(&self.field_2)?;
            stream.record_tuple_value_end(None, field_label, field_index)?;
        }

        stream.record_tuple_end(None, type_label, type_index)
    }
}

The Value trait has a single required method, stream, which is responsible for driving an instance of a Stream with its fields. The Stream trait defines sval's data model and the mechanics of how data is described in it. In this example, the MyRecord struct is represented as a record tuple, a type that can be either a record with fields named by a Label, or a tuple with fields indexed by an Index. Labels and indexes can be annotated with a Tag which add user-defined semantics to them. In this case, the labels carry the VALUE_IDENT tag meaning they're valid Rust identifiers, and the indexes carry the VALUE_OFFSET tag meanings they're zero-indexed field offsets. The specific type of Stream can decide whether to treat the MyRecord type as either a record (in the case of JSON) or a tuple (in the case of protobuf), and whether it understands that tags it sees or not.

The Stream trait

Something to notice about the Stream API in the expanded MyRecord example is that it is flat. The call to record_tuple_begin doesn't return a new type like serde's serialize_struct does. The implementor of Value is responsible for issuing the correct sequence of Stream calls as it works through its structure. The Stream can then rely on markers like record_tuple_value_begin and record_tuple_value_end to know what position within a value it is without needing to track that state itself. The flat API makes dyn-compatibility and buffering simpler, but makes implementing non-trivial streams more difficult, because you can't rely on recursive to manage state.

Recall the way MyRecord was converted into JSON earlier:

let json: String = sval_json::stream_to_string(my_record)?;

Internally, stream_to_string uses an instance of Stream that writes JSON tokens for each piece of the value it encounters. For example, record_tuple_begin and record_tuple_end will emit the corresponding { } characters for a JSON object.

sval's data model is layered. The required methods on Stream represent the base data model that more expressive constructs map down to. Here's what a minimal Stream that just formats values in sval's base data model looks like:

pub struct MyStream;

impl<'sval> sval::Stream<'sval> for MyStream {
    fn null(&mut self) -> sval::Result {
        print!("null");
        Ok(())
    }

    fn bool(&mut self, v: bool) -> sval::Result {
        print!("{}", v);
        Ok(())
    }

    fn i64(&mut self, v: i64) -> sval::Result {
        print!("{}", v);
        Ok(())
    }

    fn text_begin(&mut self, _: Option<usize>) -> sval::Result {
        print!("\"");
        Ok(())
    }

    fn text_fragment_computed(&mut self, fragment: &str) -> sval::Result {
        print!("{}", fragment.escape_debug());

        Ok(())
    }

    fn text_end(&mut self) -> sval::Result {
        print!("\"");
        Ok(())
    }

    fn seq_begin(&mut self, _: Option<usize>) -> sval::Result {
        print!("[");
        Ok(())
    }

    fn seq_value_begin(&mut self) -> sval::Result {
        Ok(())
    }

    fn seq_value_end(&mut self) -> sval::Result {
        print!(",");
        Ok(())
    }

    fn seq_end(&mut self) -> sval::Result {
        print!("]");
        Ok(())
    }
}

let my_record = MyRecord {
    field_0: 1,
    field_1: true,
    field_2: "some text",
};

// Prints:
//
// [["field_0",1,],["field_1",true,],["field_2","some text",],],
sval::stream(&mut MyStream, my_record);

Recall that the MyRecord struct mapped to a record_tuple in sval's data model. record_tuples in turn are represented in the base data model as a sequence of 2-dimensional sequences where the first element is the field label and the second is its value.

Streams aren't limited to just serializing data into interchange formats. They can manipulate or interrogate a value any way it likes. Here's an example of a Stream that attempts to extract a specific field of a value as an i32:

pub fn get_i32<'sval>(field: &str, value: impl sval::Value) -> Option<i32> {
    struct Extract<'a> {
        depth: usize,
        field: &'a str,
        matched_field: bool,
        extracted: Option<i32>,
    }

    impl<'a, 'sval> sval::Stream<'sval> for Extract<'a> {
        // Rust structs that derive `Value`, like `MyRecord` from earlier, are records in `sval`'s data model.
        // Each field of the record starts with a call to `record_value_begin` with its name.
        fn record_value_begin(&mut self, _: Option<&sval::Tag>, label: &sval::Label) -> sval::Result {
            self.matched_field = label.as_str() == self.field;
            Ok(())
        }

        fn record_value_end(&mut self, _: Option<&sval::Tag>, _: &sval::Label) -> sval::Result {
            Ok(())
        }

        // We're looking for an `i32`, so will attempt to cast an integer we find.
        // `sval` will forward any convertible integer to `i64` by default.
        // We could also override the `i32` method here.
        fn i64(&mut self, v: i64) -> sval::Result {
            if self.matched_field {
                self.extracted = v.try_into().ok();
            }

            Ok(())
        }

        // `sval` will forward all complex types as sequences by default.
        // We're only interested in top-level fields of records here, so whenever
        // we encounter a sequence we increment/decrement our depth to tell how
        // deeply nested we are.
        fn seq_begin(&mut self, _: Option<usize>) -> sval::Result {
            self.depth += 1;
            Ok(())
        }
    
        fn seq_value_begin(&mut self) -> sval::Result {
            Ok(())
        }
    
        fn seq_value_end(&mut self) -> sval::Result {
            Ok(())
        }
    
        fn seq_end(&mut self) -> sval::Result {
            self.depth -= 1;
            Ok(())
        }

        // These other methods are required members of `Stream`.
        // We're not interested in them in this example.
        fn null(&mut self) -> sval::Result {
            Ok(())
        }
    
        fn bool(&mut self, _: bool) -> sval::Result {
            Ok(())
        }
    
        fn text_begin(&mut self, _: Option<usize>) -> sval::Result {
            Ok(())
        }
    
        fn text_fragment_computed(&mut self, _: &str) -> sval::Result {
            Ok(())
        }
    
        fn text_end(&mut self) -> sval::Result {
            Ok(())
        }
    }

    let mut stream = Extract {
        depth: 0,
        field,
        matched_field: false,
        extracted: None,
    };

    sval::stream(&mut stream, &value).ok()?;
    stream.extracted
}

let my_record = MyRecord {
    field_0: 1,
    field_1: true,
    field_2: "some text",
};

assert_eq!(Some(1), get_i32("field_0", &my_record));

Streaming text

Strings in sval don't need to be streamed in a single call. As an example, say we have a template type like this:

enum Part<'a> {
    Literal(&'a str),
    Property(&'a str),
}

pub struct Template<'a>(&'a [Part<'a>]);

If we wanted to serialize Template to a string, we could implement Value, handling each literal and property as a separate fragment:

impl<'a> sval::Value for Template<'a> {
    fn stream<'sval, S: sval::Stream<'sval> + ?Sized>(&'sval self, stream: &mut S) -> sval::Result {
        stream.text_begin(None)?;

        for part in self.0 {
            match part {
                Part::Literal(lit) => stream.text_fragment(lit)?,
                Part::Property(prop) => {
                    stream.text_fragment("{")?;
                    stream.text_fragment(prop)?;
                    stream.text_fragment("}")?;
                }
            }
        }

        stream.text_end()
    }
}

When streamed as JSON, Template would produce something like this:

let template = Template(&[
    Part::Literal("some literal text and "),
    Part::Property("x"),
    Part::Literal(" and more literal text"),
]);

// Produces:
//
// "some literal text and {x} and more literal text"
let json = sval_json::stream_to_string(template)?;

Borrowed data

The Stream trait carries a 'sval lifetime it can use to accept borrowed text and binary values. Borrowing in sval is an optimization. Even if a Stream uses a concrete 'sval lifetime, it still needs to handle computed values. Here's an example of a Stream that attempts to extract a borrowed string from a value by making use of the 'sval lifetime:

pub fn to_text(value: &(impl Value + ?Sized)) -> Option<&str> {
    struct Extract<'sval> {
        extracted: Option<&'sval str>,
        seen_fragment: bool,
    }

    impl<'sval> Stream<'sval> for Extract<'sval> {
        fn text_begin(&mut self, _: Option<usize>) -> Result {
            Ok(())
        }

        // `text_fragment` accepts a string borrowed for `'sval`.
        //
        // Implementations of `Value` will send borrowed data if they can.
        fn text_fragment(&mut self, fragment: &'sval str) -> Result {
            // Allow either independent strings, or fragments of a single borrowed string.
            if !self.seen_fragment {
                self.extracted = Some(fragment);
                self.seen_fragment = true;
            } else {
                self.extracted = None;
            }

            Ok(())
        }

        // `text_fragment_computed` accepts a string for an arbitrarily short lifetime.
        //
        // The fragment can't be borrowed outside of the function call, so would need to
        // be buffered.
        fn text_fragment_computed(&mut self, _: &str) -> Result {
            self.extracted = None;
            self.seen_fragment = true;

            sval::error()
        }

        fn text_end(&mut self) -> Result {
            Ok(())
        }

        fn null(&mut self) -> Result {
            sval::error()
        }

        fn bool(&mut self, _: bool) -> Result {
            sval::error()
        }

        fn i64(&mut self, _: i64) -> Result {
            sval::error()
        }

        fn seq_begin(&mut self, _: Option<usize>) -> Result {
            sval::error()
        }

        fn seq_value_begin(&mut self) -> Result {
            sval::error()
        }

        fn seq_value_end(&mut self) -> Result {
            sval::error()
        }

        fn seq_end(&mut self) -> Result {
            sval::error()
        }
    }

    let mut extract = Extract {
        extracted: None,
        seen_fragment: false,
    };

    value.stream(&mut extract).ok()?;
    extract.extracted
}

Implementations of Value should provide a Stream with borrowed data where possible, and only compute it if it needs to.

Error handling

sval's Error type doesn't carry any state of its own. It only signals early termination of the Stream which may be because its job is done, or because it failed. It's up to the Stream to carry whatever state it needs to provide meaningful errors.

Data model

This section descibes sval's data model in detail using examples in Rust syntax. Some types in sval's model aren't representable in Rust yet, so they use pseudo syntax.

Base model

Nulls

null
stream.null()?;

Booleans

bool
stream.bool(true)?;

64bit signed integers

i64
stream.i64(-1)?;

Text

[str]
stream.text_begin(None)?;

stream.text_fragment("Hello, ")?;
stream.text_fragment("World")?;

stream.text_end()?;

Note that sval text is an array of strings.

Sequences

[dyn T]
stream.seq_begin(None)?;

stream.seq_value_begin()?;
stream.i64(-1)?;
stream.seq_value_end()?;

stream.seq_value_begin()?;
stream.bool(true)?;
stream.seq_value_end()?;

stream.seq_end()?;

Note that Rust arrays are homogeneous, but sval sequences are heterogeneous.

Extended model

8bit unsigned integers

u8
stream.u8(1)?;

8bit unsigned integers reduce to 64bit signed integers in the base model.

16bit unsigned integers

u16
stream.u16(1)?;

16bit unsigned integers reduce to 64bit signed integers in the base model.

32bit unsigned integers

u32
stream.u32(1)?;

32bit unsigned integers reduce to 64bit signed integers in the base model.

64bit unsigned integers

u64
stream.u64(1)?;

64bit unsigned integers reduce to 64bit signed integers in the base data model if they fit, or base10 ASCII text if they don't.

128bit unsigned integers

u128
stream.u128(1)?;

128bit unsigned integers reduce to 64bit signed integers in the base data model if they fit, or base10 ASCII text if they don't.

8bit signed integers

i8
stream.i8(1)?;

8bit signed integers reduce to 64bit signed integers in the base model.

16bit signed integers

i16
stream.i16(1)?;

16bit signed integers reduce to 64bit signed integers in the base model.

32bit signed integers

i32
stream.i32(1)?;

32bit signed integers reduce to 64bit signed integers in the base model.

128bit signed integers

i128
stream.i128(1)?;

128bit signed integers reduce to 64bit signed integers in the base data model if they fit, or base10 ASCII text if they don't.

32bit binary floating point numbers

f32
stream.f32(1)?;

32bit binary floating point numbers reduce to base10 ASCII text in the base model.

64bit binary floating point numbers

f64
stream.f64(1)?;

64bit binary floating point numbers reduce to base10 ASCII text in the base model.

Binary

[[u8]]
stream.binary_begin(None)?;

stream.binary_fragment(b"Hello, ")?;
stream.binary_fragment(b"World")?;

stream.binary_end()?;

Binary values reduce to sequences of numbers in the base model.

Maps

[(dyn K, dyn V)]
stream.map_begin(None)?;

stream.map_key_begin()?;
stream.i64(0)?;
stream.map_key_end()?;

stream.map_value_begin()?;
stream.bool(false)?;
stream.map_value_end()?;

stream.map_key_begin()?;
stream.i64(1)?;
stream.map_key_end()?;

stream.map_value_begin()?;
stream.bool(true)?;
stream.map_value_end()?;

stream.map_end()?;

Note that most Rust maps are homogeneous, but sval maps are heterogeneous.

Maps reduce to a sequence of 2D sequences in the base model.

Tags

struct Tag
stream.tag(None, Some(&sval::Label::new("Tag")), None)?;

Tags reduce to null in the base model.

Tagged values

struct Tagged(i64);
stream.tagged_begin(None, Some(&sval::Label::new("Tagged")), None)?;
stream.i64(1)?;
stream.tagged_end(None, Some(&sval::Label::new("Tagged")), None)?;

Tagged values reduce to their wrapped value in the base model.

Tuples

struct Tuple(i64, bool)
stream.tuple_begin(None, Some(&sval::Label::new("Tuple")), None, None)?;

stream.tuple_value_begin(None, &sval::Index::new(0))?;
stream.i64(1)?;
stream.tuple_value_end(None, &sval::Index::new(0))?;

stream.tuple_value_begin(None, &sval::Index::new(1))?;
stream.bool(true)?;
stream.tuple_value_end(None, &sval::Index::new(1))?;

stream.tuple_end(None, Some(&sval::Label::new("Tuple")), None)?;

sval tuples may also be unnamed:

(i64, bool)
stream.tuple_begin(None, None, None, None)?;

stream.tuple_value_begin(None, &sval::Index::new(0))?;
stream.i64(1)?;
stream.tuple_value_end(None, &sval::Index::new(0))?;

stream.tuple_value_begin(None, &sval::Index::new(1))?;
stream.bool(true)?;
stream.tuple_value_end(None, &sval::Index::new(1))?;

stream.tuple_end(None, None, None)?;

Tuples reduce to sequences in the base model.

Records

struct Record { a: i64, b: bool }
stream.record_begin(None, Some(&sval::Label::new("Record")), None, None)?;

stream.record_value_begin(None, &sval::Label::new("a"))?;
stream.i64(1)?;
stream.record_value_end(None, &sval::Label::new("a"))?;

stream.record_value_begin(None, &sval::Label::new("b"))?;
stream.bool(true)?;
stream.record_value_end(None, &sval::Label::new("b"))?;

stream.record_end(None, Some(&sval::Label::new("Record")), None)?;

sval records may also be unnamed:

{ a: i64, b: bool }
stream.record_begin(None, None, None, None)?;

stream.record_value_begin(None, &sval::Label::new("a"))?;
stream.i64(1)?;
stream.record_value_end(None, &sval::Label::new("a"))?;

stream.record_value_begin(None, &sval::Label::new("b"))?;
stream.bool(true)?;
stream.record_value_end(None, &sval::Label::new("b"))?;

stream.record_end(None, None, None)?;

Records reduce to a sequence of 2D sequences in the base model.

Enums

sval enums wrap a variant, which may be any of the following types:

  • Tags
  • Tagged values
  • Records
  • Tuples
  • Enums
Enum::Tag
stream.enum_begin(None, Some(&sval::Label::new("Enum")), None)?;

stream.tag(None, Some(&sval::Label::new("Tag")), Some(&sval::Index::new(0)))?;

stream.enum_end(None, Some(&sval::Label::new("Enum")), None)?;
Enum::Tagged(i64)
stream.enum_begin(None, Some(&sval::Label::new("Enum")), None)?;

stream.tagged_begin(None, Some(&sval::Label::new("Tagged")), Some(&sval::Index::new(1)))?;
stream.i64(1)?;
stream.tagged_end(None, Some(&sval::Label::new("Tagged")), Some(&sval::Index::new(1)))?;

stream.enum_end(None, Some(&sval::Label::new("Enum")), None)?;
Enum::Tuple(i64, bool)
stream.enum_begin(None, Some(&sval::Label::new("Enum")), None)?;

stream.tuple_begin(None, Some(&sval::Label::new("Tuple")), Some(&sval::Index::new(2)), None)?;

stream.tuple_value_begin(None, &sval::Index::new(0))?;
stream.i64(1)?;
stream.tuple_value_end(None, &sval::Index::new(0))?;

stream.tuple_value_begin(None, &sval::Index::new(1))?;
stream.bool(true)?;
stream.tuple_value_end(None, &sval::Index::new(1))?;

stream.tuple_end(None, Some(&sval::Label::new("Tuple")), Some(&sval::Index::new(2)))?;

stream.enum_end(None, Some(&sval::Label::new("Enum")), None)?;
Enum::Record { a: i64, b: bool }
stream.enum_begin(None, Some(&sval::Label::new("Enum")), None)?;

stream.record_begin(None, Some(&sval::Label::new("Record")), Some(&sval::Index::new(3)), None)?;

stream.record_value_begin(None, &sval::Label::new("a"))?;
stream.i64(1)?;
stream.record_value_end(None, &sval::Label::new("a"))?;

stream.record_value_begin(None, &sval::Label::new("b"))?;
stream.bool(true)?;
stream.record_value_end(None, &sval::Label::new("b"))?;

stream.record_end(None, Some(&sval::Label::new("Record")), Some(&sval::Index::new(3)))?;

stream.enum_end(None, Some(&sval::Label::new("Enum")), None)?;

sval enum variants may also be unnamed:

Enum::<i32>
stream.enum_begin(None, Some(&sval::Label::new("Enum")), None)?;

stream.tagged_begin(None, None, None)?;
stream.i64(1)?;
stream.tagged_end(None, None, None)?;

stream.enum_end(None, Some(&sval::Label::new("Enum")), None)?;
Enum::<(i64, bool)>
stream.enum_begin(None, Some(&sval::Label::new("Enum")), None)?;

stream.tuple_begin(None, None, None, None)?;

stream.tuple_value_begin(None, &sval::Index::new(0))?;
stream.i64(1)?;
stream.tuple_value_end(None, &sval::Index::new(0))?;

stream.tuple_value_begin(None, &sval::Index::new(1))?;
stream.bool(true)?;
stream.tuple_value_end(None, &sval::Index::new(1))?;

stream.tuple_end(None, None, None)?;

stream.enum_end(None, Some(&sval::Label::new("Enum")), None)?;
Enum::<{ a: i64, b: bool }>
stream.enum_begin(None, Some(&sval::Label::new("Enum")), None)?;

stream.record_begin(None, None, None, None)?;

stream.record_value_begin(None, &sval::Label::new("a"))?;
stream.i64(1)?;
stream.record_value_end(None, &sval::Label::new("a"))?;

stream.record_value_begin(None, &sval::Label::new("b"))?;
stream.bool(true)?;
stream.record_value_end(None, &sval::Label::new("b"))?;

stream.record_end(None, None, None)?;

stream.enum_end(None, Some(&sval::Label::new("Enum")), None)?;

sval enum variants may be other enums:

Enum::Inner::Tagged(i64)
stream.enum_begin(None, Some(&sval::Label::new("Enum")), None)?;

stream.enum_begin(None, Some(&sval::Label::new("Inner")), Some(&sval::Index::new(0)))?;

stream.tagged_begin(None, Some(&sval::Label::new("Tagged")), Some(&sval::Index::new(1)))?;
stream.i64(1)?;
stream.tagged_end(None, Some(&sval::Label::new("Tagged")), Some(&sval::Index::new(1)))?;

stream.enum_end(None, Some(&sval::Label::new("Inner")), Some(&sval::Index::new(0)))?;

stream.enum_end(None, Some(&sval::Label::new("Enum")), None)?;

User-defined tags

sval tags, tagged values, records, tuples, enums, and their values can carry a user-defined Tag that alters their semantics. A Stream may understand a Tag and treat its annotated value differently, or it may ignore them. An example of a Tag is NUMBER, which is for text that encodes an arbitrary-precision decimal floating point number with a standardized format. A Stream may parse these numbers and encode them differently to regular text.

Here's an example of a user-defined Tag for treating integers as Unix timestamps, and a Stream that understands them:

// Define a tag as a constant.
//
// Tags are expected to have unique names.
//
// The rules of our tag are that 64bit unsigned integers that carry it are seconds since
// the Unix epoch.
pub const UNIX_TIMESTAMP: sval::Tag = sval::Tag::new("unixts");

// Derive `Value` on a type, annotating it with our tag.
//
// We could also implement `Value` manually using `stream.tagged_begin(Some(&UNIX_TIMESTAMP), ..)`.
#[derive(Value)]
#[sval(tag = "UNIX_TIMESTAMP")]
pub struct Timestamp(u64);

// Here's an example of a `Stream` that understands our tag.
pub struct MyStream {
    is_unix_ts: bool,
}

impl<'sval> sval::Stream<'sval> for MyStream {
    fn tagged_begin(
        &mut self,
        tag: Option<&sval::Tag>,
        _: Option<&sval::Label>,
        _: Option<&sval::Index>,
    ) -> sval::Result {
        // When beginning a tagged value, check to see if it's a tag we understand.
        if let Some(&UNIX_TIMESTAMP) = tag {
            self.is_unix_ts = true;
        }

        Ok(())
    }

    fn tagged_end(
        &mut self,
        tag: Option<&sval::Tag>,
        _: Option<&sval::Label>,
        _: Option<&sval::Index>,
    ) -> sval::Result {
        if let Some(&UNIX_TIMESTAMP) = tag {
            self.is_unix_ts = false;
        }

        Ok(())
    }

    fn u64(&mut self, v: u64) -> sval::Result {
        // If the value is tagged as a Unix timestamp then print it using a human-readable RFC3339 format.
        if self.is_unix_ts {
            print!(
                "{}",
                humantime::format_rfc3339(
                    std::time::SystemTime::UNIX_EPOCH + std::time::Duration::from_secs(v)
                )
            );
        }

        Ok(())
    }

    fn null(&mut self) -> sval::Result {
        Ok(())
    }

    fn bool(&mut self, _: bool) -> sval::Result {
        Ok(())
    }

    fn i64(&mut self, _: i64) -> sval::Result {

        Ok(())
    }

    fn text_begin(&mut self, _: Option<usize>) -> sval::Result {
        Ok(())
    }

    fn text_fragment_computed(&mut self, fragment: &str) -> sval::Result {
        Ok(())
    }

    fn text_end(&mut self) -> sval::Result {
        Ok(())
    }

    fn seq_begin(&mut self, _: Option<usize>) -> sval::Result {
        Ok(())
    }

    fn seq_value_begin(&mut self) -> sval::Result {
    }

    fn seq_value_end(&mut self) -> sval::Result {
        Ok(())
    }

    fn seq_end(&mut self) -> sval::Result {
        Ok(())
    }
}

The Label and Index types can also carry a Tag. An example of a Tag you might use on a Label is VALUE_IDENT, for labels that hold a valid Rust identifier.

Type system

sval has an implicit structural type system based on the sequence of calls a Stream receives, and the values of any Label, Index, or Tag on them, with the following exceptions:

  • Text type does not depend on the composition of fragments, or on their length.
  • Binary type does not depend on the composition of fragments, or on their length.
  • Sequences are untyped. Their type doesn't depend on the types of their elements, or on their length.
  • Maps are untyped. Their type doesn't depend on the types of their keys or values, or on their length.
  • Enums holding differently typed variants have the same type.

These rules may be better formalized in the future.

Ecosystem

sval is a general framework with specific serialization formats and utilities provided as external libraries:

  • sval_fmt: Colorized Rust-style debug formatting.
  • sval_json: Serialize values as JSON in a serde-compatible format.
  • sval_protobuf: Serialize values as protobuf messages.
  • sval_serde: Convert between serde and sval.
  • sval_buffer: Losslessly buffers any Value into an owned, thread-safe variant.
  • sval_flatten: Flatten the fields of a value onto its parent, like #[serde(flatten)].
  • sval_nested: Buffer sval's flat Stream API into a recursive one like serde's. For types that #[derive(Value)], the translation is non-allocating.
  • sval_ref: A variant of Value for types that are internally borrowed (like MyType<'a>) instead of externally (like &'a MyType).

About

A lightweight, no-std, object-safe, serialization-only framework for Rust

Resources

License

Apache-2.0, MIT licenses found

Licenses found

Apache-2.0
LICENSE-APACHE
MIT
LICENSE-MIT

Code of conduct

Stars

Watchers

Forks

Packages

No packages published

Contributors 5

Languages