Expand description
Structs in, structs out, in the spirit of Glaze.
Values are read straight into your types and written straight out of them. There is no token stream and no intermediate document model: a field’s bytes are converted exactly once, into the member that will hold them.
#[derive(Default, PartialEq, Debug)]
struct Config {
name: String,
port: u16,
hosts: Vec<String>,
}
structio::object!(Config { name, port, hosts });
let json = r#"{"name":"api","port":8080,"hosts":["a","b"]}"#;
let config: Config = structio::from_str(json).unwrap();
assert_eq!(config.port, 8080);
assert_eq!(structio::to_string(&config), json);§Two formats, one schema
object! declares a struct’s field list once. Both formats read and
write against it, so the same type round-trips through either without a
second declaration:
jsonis JSON text.beveis BEVE, a tagged binary format that keeps JSON’s shape and self-description while storing numbers as numbers and numeric arrays as contiguous blocks.
let sample = Sample { id: 7, values: vec![1.5, 2.5, 3.5] };
let text = structio::to_string(&sample);
let binary = structio::to_beve(&sample);
assert_eq!(structio::from_str::<Sample>(&text).unwrap(), sample);
assert_eq!(structio::from_beve::<Sample>(&binary).unwrap(), sample);§Objects and arrays
object! encodes a struct by key. array! encodes it by position,
for a type whose field names carry nothing a reader does not already have:
#[derive(Default, PartialEq, Debug)]
struct Vec3 { x: f64, y: f64, z: f64 }
structio::array!(Vec3 [x, y, z]);
let v = Vec3 { x: 1.0, y: 2.0, z: 3.0 };
assert_eq!(structio::to_string(&v), "[1,2,3]");Nothing is hashed and no keys go on the wire, at the cost of a schema that cannot change shape: an array of the wrong length is an error, where an object can be read with a policy that steps over a field it does not recognize.
transparent! is the third reading of a struct: a one-field wrapper
written as that field alone, for the newtype that exists to keep two
u64s apart in Rust and means nothing to either format.
#[derive(Default, PartialEq, Debug)]
struct UserId(u64);
structio::transparent!(UserId { 0 });
assert_eq!(structio::to_string(&UserId(7)), "7");§Enums
An enum’s schema is its variant names, and they go on the wire as names
rather than as positions. A variant carrying nothing is written as its
name; a variant carrying a value is written as an object of one member
keyed by that name, and is marked (_) in the declaration.
#[derive(Default, PartialEq, Debug)]
enum Shape {
#[default]
Empty,
Sides(u32),
}
structio::tagged_enum!(Shape { Empty, Sides(_) });
assert_eq!(structio::to_string(&Shape::Empty), "\"Empty\"");
assert_eq!(structio::to_string(&Shape::Sides(3)), r#"{"Sides":3}"#);
assert_eq!(structio::from_str::<Shape>(r#"{"Sides":3}"#).unwrap(), Shape::Sides(3));unit_enum! is the same declaration for an enum whose variants all carry
nothing, and will not compile if one of them does, so its wire form is a
plain string and stays one.
A variant carrying nothing reads back from either form, but one carrying a value has only the object form, and the two macros’ pages say what each error means. docs/enums.md is the long form.
The crate root re-exports the JSON entry points unqualified, because JSON
is what most callers want first. The BEVE ones carry the format in the
name at the root (to_beve, from_beve) and drop it inside the
module, so beve::to_vec and to_beve are the same function.
§Design
- No dependencies. Standard library only.
- No proc-macros required.
object!,array!andtagged_enum!aremacro_rules!macros, so there is no proc-macro crate to build and link for the host before your code can, and nothing extra when cross-compiling. It buys a smaller dependency graph rather than a faster build: expanding a declaration costs about what a derive costs. Thederivefeature adds#[derive(Structio)], a dependency-free front end that expands to the same macros, for a type you own and would rather not restate. - A declaration is checked against its type. Naming a field the struct
does not have has always been an error; leaving one out is now one too,
naming the field. End a declaration with
..where the omission is deliberate. - Keys are hashed at compile time.
KeyMap::buildruns during const evaluation and picks the cheapest perfect hash that fits your key set, from a single byte comparison up to a full key hash. Both formats look keys up in that one table. - Reads reuse what you already own. Parsing into an existing value refills its buffers instead of reallocating them.
§What it does not do
This is for statically known types. Reading a document you have no type
for is a different matter, and Value is the tree for that case: a
shape nothing declares and something walks by path, the register map a
device publishes, a setting stored under a key some plugin chose. It is a
destination like any other, not a stage every read passes through, and a
value you could have declared a type for is better read into that type. A
body you forward rather than look at is a different problem again, and a
tree is the wrong answer to it: json::Raw carries one
through as the text that spelled it. See value for what it
holds and what it costs.
A BEVE document can still be looked into without being decoded whole.
from_beve_at reads the one value a JSON Pointer names and steps over
everything else, and validate_beve checks a document is well formed
without decoding any of it. What comes back from the first is still a type
you declared.
beve_to_json hands back a BEVE document’s contents without a type
and without a tree: it rewrites the whole document as JSON in a single
walk. See transcode for what survives the trip and what does not.
§Complex numbers and matrices
BEVE’s core covers what JSON covers; its extensions cover what scientific
data needs on top of that. Complex and Matrix are those two, as
ordinary types that work in both formats:
use structio::{Complex, Matrix, MatrixLayout};
let signal = vec![Complex::new(1.0f64, 2.0), Complex::new(3.0, -4.0)];
let bytes = structio::to_beve(&signal);
assert_eq!(structio::from_beve::<Vec<Complex<f64>>>(&bytes).unwrap(), signal);
let m = Matrix::new(MatrixLayout::RowMajor, vec![2, 3], (0..6i32).collect()).unwrap();
assert_eq!(
structio::to_string(&m),
r#"{"layout":"layout_right","extents":[2,3],"value":[0,1,2,3,4,5]}"#
);
assert_eq!(structio::from_beve::<Matrix<i32>>(&structio::to_beve(&m)).unwrap(), m);A run of complex numbers is one header and one block, so a
Vec<Complex<f64>> moves in a single copy exactly as a Vec<f64> does, and
a Matrix<Complex<f64>> stores its data that way with no case of its own.
See ext.
§Options
Whether the JSON is indented, whether a member that would be null is
written at all, and whether a key nothing claims is refused, are decided at
compile time by a policy type. Every entry point has a _with
twin that takes one, and the plain one is Standard:
use structio::{Pretty, SkipNull, to_string, to_string_with};
let server = Server { port: 8080, tls: None };
assert_eq!(to_string(&server), r#"{"port":8080,"tls":null}"#);
assert_eq!(
to_string_with::<Pretty, _>(&server),
"{\n \"port\": 8080,\n \"tls\": null\n}"
);
assert_eq!(to_string_with::<SkipNull, _>(&server), r#"{"port":8080}"#);Indentation puts every element of an array on a line of its own, which is
not what a run of numbers wants. PrettyInlineArrays keeps each array on
the line it began on and indents everything else:
use structio::{PrettyInlineArrays, to_string_with};
let sample = Sample { id: 7, values: vec![1.5, 2.5, 3.5] };
assert_eq!(
to_string_with::<PrettyInlineArrays, _>(&sample),
"{\n \"id\": 7,\n \"values\": [1.5, 2.5, 3.5]\n}"
);Text that is already JSON takes the same policy from the other side.
prettify lays out a document that did not come from a Write impl, and
emits its whitespace through the same writer, so the result is what writing
the same data would have produced:
use structio::{PrettyInlineArrays, json::prettify_with, prettify};
assert_eq!(prettify(r#"{"a":[1,2]}"#).unwrap(), "{\n \"a\": [\n 1,\n 2\n ]\n}");
assert_eq!(
prettify_with::<PrettyInlineArrays>(r#"{"a":[1,2]}"#).unwrap(),
"{\n \"a\": [1, 2]\n}"
);minify goes the other way, and has no layout to agree with: it copies
the document through and drops the whitespace between its tokens. That needs
nothing but the strings located, so it neither reads a value nor checks a
bracket, which is what makes it the fastest thing here.
assert_eq!(structio::minify("{\n \"a\": [1, 2]\n}").unwrap(), r#"{"a":[1,2]}"#);Reading takes a policy the same way. It has three settings, and only one
of them is on by default: a key that no field claims is an
ErrorCode::UnknownKey, which catches a typo or the wrong document
rather than passing over it in silence. Ask for SkipUnknown to read a
subset of a larger document. The other way round, a declared field the
document leaves out is simply left as the destination had it, since reading
is into a value that already exists. Mark a field #[required] in the
declaration and its absence is an ErrorCode::MissingKey under every
policy, which is what a mixed schema wants; RequireKeys says the same of
every field at once.
use structio::{ErrorCode, SkipUnknown, from_str, from_str_with};
let doc = r#"{"port":8080,"debug":true}"#;
assert_eq!(from_str::<Server>(doc).unwrap_err().code, ErrorCode::UnknownKey);
assert_eq!(from_str_with::<SkipUnknown, Server>(doc).unwrap().port, 8080);use structio::{ErrorCode, RequireKeys, from_str, from_str_with};
let doc = r#"{"port":8080}"#;
assert_eq!(from_str::<Server>(doc).unwrap().host, "");
let e = from_str_with::<RequireKeys, Server>(doc).unwrap_err();
assert_eq!(e.code, ErrorCode::MissingKey);
// A member that is not there has no position of its own, so the offset
// names the object and the error names the member.
assert_eq!(e.key, Some("host"));The third is AllowComments, which reads JSONC: // and /* */
wherever whitespace is allowed, for the documents people edit by hand.
The setting you do not ask for costs nothing: a compact writer emits no
indentation code at all. Combinations are your own unit struct and an impl,
since every constant on Options has a default. See options.
§Streaming
Documents that do not fit, or have not fully arrived, go through
json::stream and beve::stream: to_writer drains into an
std::io::Write, Documents pulls a sequence of values out of an
std::io::Read, and Feed takes chunks pushed at it. The BEVE
counterparts are beve::to_writer, beve::Documents and
beve::Feed, and they hand out the elements of a typed array one at a
time as readily as whole records. beve_to_json_writer drains a transcode
into a sink the same way.
Re-exports§
pub use error::Error;pub use error::ErrorCode;pub use error::Result;pub use error::StreamError;pub use error::StreamResult;pub use ext::Complex;pub use ext::Matrix;pub use ext::MatrixLayout;pub use ext::MatrixRef;pub use keymap::KeyMap;pub use map::OrderedMap;pub use options::AllowComments;pub use options::Options;pub use options::Pretty;pub use options::PrettyInlineArrays;pub use options::RequireKeys;pub use options::SkipNull;pub use options::SkipUnknown;pub use options::Standard;pub use value::Number;pub use value::Object;pub use value::Value;pub use value::from_value;pub use value::from_value_with;pub use value::to_value;pub use json::Documents;pub use json::Feed;pub use json::Mode;pub use json::append;pub use json::append_with;pub use json::from_reader;pub use json::from_reader_with;pub use json::from_slice;pub use json::from_slice_with;pub use json::from_str;pub use json::from_str_with;pub use json::minify;pub use json::minify_into;pub use json::minify_into_with;pub use json::minify_with;pub use json::prettify;pub use json::prettify_into;pub use json::prettify_into_with;pub use json::prettify_with;pub use json::read_into;pub use json::read_into_with;pub use json::to_string;pub use json::to_string_with;pub use json::to_vec;pub use json::to_vec_with;pub use json::to_writer;pub use json::to_writer_buffered;pub use json::to_writer_buffered_with;pub use json::to_writer_with;pub use json::write_into;pub use json::write_into_with;pub use beve::append as append_beve;pub use beve::append_aligned as append_beve_aligned;pub use beve::append_aligned_with as append_beve_aligned_with;pub use beve::append_with as append_beve_with;pub use beve::from_reader as from_beve_reader;pub use beve::from_reader_array as from_beve_reader_array;pub use beve::from_reader_with as from_beve_reader_with;pub use beve::from_slice as from_beve;pub use beve::from_slice_at as from_beve_at;pub use beve::from_slice_at_with as from_beve_at_with;pub use beve::from_slice_with as from_beve_with;pub use beve::read_array_into as read_beve_array_into;pub use beve::read_into as read_beve_into;pub use beve::read_into_at as read_beve_into_at;pub use beve::read_into_at_with as read_beve_into_at_with;pub use beve::read_into_with as read_beve_into_with;pub use beve::size as beve_size;pub use beve::size_after as beve_size_after;pub use beve::size_after_with as beve_size_after_with;pub use beve::size_aligned as beve_size_aligned;pub use beve::size_aligned_after as beve_size_aligned_after;pub use beve::size_aligned_after_with as beve_size_aligned_after_with;pub use beve::size_aligned_with as beve_size_aligned_with;pub use beve::size_with as beve_size_with;pub use beve::slice_ref as beve_slice_ref;pub use beve::to_vec as to_beve;pub use beve::to_vec_aligned as to_beve_aligned;pub use beve::to_vec_aligned_with as to_beve_aligned_with;pub use beve::to_vec_with as to_beve_with;pub use beve::to_writer as to_beve_writer;pub use beve::to_writer_buffered as to_beve_writer_buffered;pub use beve::to_writer_buffered_with as to_beve_writer_buffered_with;pub use beve::to_writer_with as to_beve_writer_with;pub use beve::validate as validate_beve;pub use beve::validate_reader as validate_beve_reader;pub use beve::write_into as write_beve_into;pub use beve::write_into_with as write_beve_into_with;pub use transcode::beve_to_json;pub use transcode::beve_to_json_into;pub use transcode::beve_to_json_into_with;pub use transcode::beve_to_json_with;pub use transcode::beve_to_json_writer;pub use transcode::beve_to_json_writer_buffered;pub use transcode::beve_to_json_writer_buffered_with;pub use transcode::beve_to_json_writer_with;
Modules§
- beve
- BEVE: the same structs, as tagged binary.
- case
- Compile-time case conversion for keys and variant names.
- error
- Error types.
- ext
- The two BEVE extensions that carry data, as ordinary Rust types.
- json
- JSON: text in, text out.
- keymap
- Compile-time perfect hashing of object keys.
- map
- An insertion-ordered map with
Stringkeys. - options
- Compile-time options, for reading and for writing.
- transcode
- Between the formats, without a type in the middle.
- value
- A value whose shape is not known at compile time.
Macros§
- array
- Declare a struct’s schema as a positional array, for every format.
- beve_
array - Declare a struct’s schema as a positional array, for BEVE alone.
- beve_
object - Declare a struct’s schema for BEVE alone.
- beve_
tagged_ enum - Declare an enum for BEVE alone.
- beve_
transparent - Declare a one-field struct as that field alone, for BEVE only.
- json_
array - Declare a struct’s schema as a positional array, for JSON alone.
- json_
object - Declare a struct’s schema for JSON alone.
- json_
tagged_ enum - Declare an enum for JSON alone.
- json_
transparent - Declare a one-field struct as that field alone, for JSON only.
- object
- Declare a struct’s schema, for every format.
- tagged_
enum - Declare an enum, for every format.
- transparent
- Declare a one-field struct as that field alone.
- unit_
enum - Declare an enum whose variants carry nothing, for every format.
- value
- Build a
Valuefrom JSON-shaped syntax.
Structs§
- Same
- The identity adapter: read and write this position the way the type itself would.
Traits§
- Elements
- The length of a struct encoded as a positional array.
- Keys
- The key schema of a struct, and its compile-time perfect hash.
- Read
Owned - Convenience bound for a function that parses a
Tout of a document it owns: readable in every format this crate supports, from any input, and constructible. - Read
Write - Convenience bound for generic containers: readable and writable in every format this crate supports, from any input.
- Variants
- The variant names of an enum, and their compile-time perfect hash.
- Write
- Convenience bound for generic containers that are only ever written: writable in every format this crate supports.
Functions§
- assert_
tag_ not_ a_ field - Refuse an internally tagged declaration whose tag is also a field of the variant’s payload.
Derive Macros§
- Structio
- Declare a type’s schema from its definition:
#[derive(Structio)].